Skip to content

Commit 30d505b

Browse files
committed
feat: client assertion and JWKS validation improvements
1 parent 17dc13a commit 30d505b

File tree

10 files changed

+801
-52
lines changed

10 files changed

+801
-52
lines changed

crates/infera-management-api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ base64 = { workspace = true }
2323
chrono = { workspace = true }
2424
jsonwebtoken = { workspace = true }
2525
metrics-exporter-prometheus = { workspace = true }
26+
once_cell = "1.20"
27+
pem = "3.0"
2628
rand = { workspace = true }
29+
reqwest = { workspace = true }
2730
serde = { workspace = true }
2831
serde_json = { workspace = true }
2932
serde_yaml = { workspace = true }

crates/infera-management-api/src/handlers/tokens.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ pub async fn generate_vault_token(
153153
vault_id,
154154
vault_role,
155155
access_ttl,
156+
&state.config.auth.jwt_issuer,
157+
&state.config.auth.jwt_audience,
156158
);
157159

158160
// Sign the access token
@@ -302,6 +304,8 @@ pub async fn refresh_vault_token(
302304
old_token.vault_id,
303305
old_token.vault_role,
304306
access_ttl,
307+
&state.config.auth.jwt_issuer,
308+
&state.config.auth.jwt_audience,
305309
);
306310

307311
let access_token = signer.sign_vault_token(&claims, &certificate)?;
@@ -577,6 +581,8 @@ pub async fn client_assertion_authenticate(
577581
vault_id,
578582
requested_role,
579583
access_ttl,
584+
&state.config.auth.jwt_issuer,
585+
&state.config.auth.jwt_audience,
580586
);
581587

582588
let access_token = signer.sign_vault_token(&vault_claims, &certificate)?;

crates/infera-management-api/src/handlers/vaults.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,33 @@ pub async fn get_vault(
230230
Ok(Json(vault_to_response(vault)))
231231
}
232232

233+
/// Get a specific vault by ID (server-to-server endpoint)
234+
///
235+
/// GET /v1/vaults/:vault
236+
/// Auth: Session or Server JWT (dual authentication)
237+
///
238+
/// This endpoint is used by the Server API to verify vault ownership and metadata.
239+
/// Unlike the organization-scoped endpoint, this uses the vault ID directly without
240+
/// requiring organization context.
241+
pub async fn get_vault_by_id(
242+
State(state): State<AppState>,
243+
Path(vault_id): Path<i64>,
244+
) -> Result<Json<VaultResponse>> {
245+
let repos = RepositoryContext::new((*state.storage).clone());
246+
let vault = repos
247+
.vault
248+
.get(vault_id)
249+
.await?
250+
.ok_or_else(|| CoreError::NotFound("Vault not found".to_string()))?;
251+
252+
// Don't return deleted vaults
253+
if vault.is_deleted() {
254+
return Err(CoreError::NotFound("Vault not found".to_string()).into());
255+
}
256+
257+
Ok(Json(vault_to_response(vault)))
258+
}
259+
233260
/// Update a vault
234261
///
235262
/// PATCH /v1/organizations/:org/vaults/:vault
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use axum::{
2+
extract::{Request, State},
3+
middleware::Next,
4+
response::Response,
5+
};
6+
use axum_extra::extract::cookie::CookieJar;
7+
8+
use crate::handlers::auth::{ApiError, AppState, SESSION_COOKIE_NAME};
9+
use infera_management_core::error::Error as CoreError;
10+
11+
use super::{require_server_jwt, require_session, ServerContext, SessionContext};
12+
13+
/// Dual authentication middleware
14+
///
15+
/// Accepts EITHER session authentication OR server JWT authentication.
16+
/// This allows both user requests (via session cookies/tokens) and
17+
/// server-to-server requests (via JWT) to access the same endpoints.
18+
///
19+
/// Attaches either SessionContext or ServerContext to the request extensions.
20+
pub async fn require_session_or_server_jwt(
21+
State(state): State<AppState>,
22+
jar: CookieJar,
23+
request: Request,
24+
next: Next,
25+
) -> Result<Response, ApiError> {
26+
// Check if this looks like a JWT request (Bearer token in Authorization header)
27+
let has_bearer_token = request
28+
.headers()
29+
.get("authorization")
30+
.and_then(|h| h.to_str().ok())
31+
.map(|s| s.starts_with("Bearer "))
32+
.unwrap_or(false);
33+
34+
// Check if this looks like a session request (session cookie or numeric Bearer token)
35+
let has_session = jar.get(SESSION_COOKIE_NAME).is_some()
36+
|| request
37+
.headers()
38+
.get("authorization")
39+
.and_then(|h| h.to_str().ok())
40+
.and_then(|s| s.strip_prefix("Bearer "))
41+
.and_then(|token| token.parse::<i64>().ok())
42+
.is_some();
43+
44+
// If we have a Bearer token and it's NOT a session (numeric), try JWT auth first
45+
if has_bearer_token && !has_session {
46+
return require_server_jwt(State(state), request, next).await;
47+
}
48+
49+
// Otherwise, try session auth first
50+
if has_session {
51+
return require_session(State(state), jar, request, next).await;
52+
}
53+
54+
// No recognizable auth present
55+
Err(CoreError::Auth(
56+
"Authentication required: provide either a session token or server JWT".to_string(),
57+
)
58+
.into())
59+
}
60+
61+
/// Extract either session context or server context from request
62+
///
63+
/// Returns SessionContext if session auth was used, or converts ServerContext to a
64+
/// compatible format if server JWT was used.
65+
pub fn extract_dual_auth_context(request: &Request) -> Result<AuthContextType, ApiError> {
66+
// Try to extract session context first
67+
if let Some(session_ctx) = request.extensions().get::<SessionContext>() {
68+
return Ok(AuthContextType::Session(session_ctx.clone()));
69+
}
70+
71+
// Try to extract server context
72+
if let Some(server_ctx) = request.extensions().get::<ServerContext>() {
73+
return Ok(AuthContextType::Server(server_ctx.clone()));
74+
}
75+
76+
Err(CoreError::Auth("No authentication context found in request".to_string()).into())
77+
}
78+
79+
/// Enum representing the type of authentication used
80+
#[derive(Debug, Clone)]
81+
pub enum AuthContextType {
82+
/// Session-based authentication (user request)
83+
Session(SessionContext),
84+
/// Server JWT authentication (server-to-server)
85+
Server(ServerContext),
86+
}
87+
88+
impl AuthContextType {
89+
/// Get user ID if this is a session context, None otherwise
90+
pub fn user_id(&self) -> Option<i64> {
91+
match self {
92+
AuthContextType::Session(ctx) => Some(ctx.user_id),
93+
AuthContextType::Server(_) => None,
94+
}
95+
}
96+
97+
/// Get server ID if this is a server context, None otherwise
98+
pub fn server_id(&self) -> Option<&str> {
99+
match self {
100+
AuthContextType::Session(_) => None,
101+
AuthContextType::Server(ctx) => Some(&ctx.server_id),
102+
}
103+
}
104+
105+
/// Check if this is a server authentication
106+
pub fn is_server_auth(&self) -> bool {
107+
matches!(self, AuthContextType::Server(_))
108+
}
109+
110+
/// Check if this is a session authentication
111+
pub fn is_session_auth(&self) -> bool {
112+
matches!(self, AuthContextType::Session(_))
113+
}
114+
}

crates/infera-management-api/src/middleware/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
pub mod dual_auth;
12
pub mod logging;
23
pub mod organization;
34
pub mod permission;
45
pub mod ratelimit;
6+
pub mod server_auth;
57
pub mod session;
68
pub mod vault;
79

10+
pub use dual_auth::{extract_dual_auth_context, require_session_or_server_jwt, AuthContextType};
811
pub use logging::logging_middleware;
912
pub use organization::{
1013
require_admin_or_owner, require_member, require_organization_member, require_owner,
@@ -14,6 +17,7 @@ pub use permission::{
1417
get_user_permissions, has_organization_permission, require_organization_permission,
1518
};
1619
pub use ratelimit::{login_rate_limit, registration_rate_limit};
20+
pub use server_auth::{extract_server_context, require_server_jwt, ServerContext};
1721
pub use session::{extract_session_context, require_session, SessionContext};
1822
pub use vault::{
1923
get_user_vault_role, require_admin, require_manager, require_reader, require_vault_access,

0 commit comments

Comments
 (0)