+131
crates/jacquard-axum/src/service_auth.rs
+131
crates/jacquard-axum/src/service_auth.rs
···
242
242
/// ```
243
243
pub struct ExtractServiceAuth(pub VerifiedServiceAuth<'static>);
244
244
245
+
/// Axum extractor for optional service authentication.
246
+
///
247
+
/// Like `ExtractServiceAuth`, but returns `None` if no Authorization header
248
+
/// is present. If a header IS present but invalid, returns an error.
249
+
///
250
+
/// Use this for endpoints that work for both authenticated and anonymous users,
251
+
/// but show different content based on auth status.
252
+
///
253
+
/// # Example
254
+
///
255
+
/// ```no_run
256
+
/// use axum::{Router, routing::get};
257
+
/// use jacquard_axum::service_auth::{ServiceAuthConfig, ExtractOptionalServiceAuth};
258
+
/// use jacquard_identity::JacquardResolver;
259
+
/// use jacquard_identity::resolver::ResolverOptions;
260
+
/// use jacquard_common::types::string::Did;
261
+
///
262
+
/// async fn handler(
263
+
/// ExtractOptionalServiceAuth(auth): ExtractOptionalServiceAuth,
264
+
/// ) -> String {
265
+
/// match auth {
266
+
/// Some(a) => format!("Authenticated as {}", a.did()),
267
+
/// None => "Anonymous request".to_string(),
268
+
/// }
269
+
/// }
270
+
///
271
+
/// #[tokio::main]
272
+
/// async fn main() {
273
+
/// let resolver = JacquardResolver::new(
274
+
/// reqwest::Client::new(),
275
+
/// ResolverOptions::default(),
276
+
/// );
277
+
/// let config = ServiceAuthConfig::new(
278
+
/// Did::new_static("did:web:example.com").unwrap(),
279
+
/// resolver,
280
+
/// );
281
+
///
282
+
/// let app = Router::new()
283
+
/// .route("/xrpc/com.example.getData", get(handler))
284
+
/// .with_state(config);
285
+
///
286
+
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
287
+
/// .await
288
+
/// .unwrap();
289
+
/// axum::serve(listener, app).await.unwrap();
290
+
/// }
291
+
/// ```
292
+
pub struct ExtractOptionalServiceAuth(pub Option<VerifiedServiceAuth<'static>>);
293
+
245
294
/// Errors that can occur during service auth verification.
246
295
#[derive(Debug, Error, miette::Diagnostic)]
247
296
pub enum ServiceAuthError {
···
409
458
lxm: claims.lxm.as_ref().map(|l| l.clone().into_static()),
410
459
jti: claims.jti.as_ref().map(|j| j.clone().into_static()),
411
460
}))
461
+
}
462
+
}
463
+
}
464
+
465
+
impl<S> FromRequestParts<S> for ExtractOptionalServiceAuth
466
+
where
467
+
S: ServiceAuth + Send + Sync,
468
+
S::Resolver: Send + Sync,
469
+
{
470
+
type Rejection = ServiceAuthError;
471
+
472
+
fn from_request_parts(
473
+
parts: &mut Parts,
474
+
state: &S,
475
+
) -> impl std::future::Future<Output = Result<Self, Self::Rejection>> + Send {
476
+
async move {
477
+
// Check for Authorization header - if missing, return None (not an error)
478
+
let auth_header = match parts.headers.get(header::AUTHORIZATION) {
479
+
Some(h) => h,
480
+
None => return Ok(ExtractOptionalServiceAuth(None)),
481
+
};
482
+
483
+
// Header is present - now we MUST validate it (bad auth = error)
484
+
let auth_str = auth_header
485
+
.to_str()
486
+
.map_err(|_| ServiceAuthError::InvalidAuthHeader)?;
487
+
488
+
let token = auth_str
489
+
.strip_prefix("Bearer ")
490
+
.ok_or(ServiceAuthError::InvalidAuthHeader)?;
491
+
492
+
// Parse JWT
493
+
let parsed = service_auth::parse_jwt(token)?;
494
+
495
+
// Get claims for DID resolution
496
+
let claims = parsed.claims();
497
+
498
+
// Resolve DID to get signing key
499
+
let did_doc = state
500
+
.resolver()
501
+
.resolve_did_doc(&claims.iss)
502
+
.await
503
+
.map_err(|e| ServiceAuthError::DidResolutionFailed {
504
+
did: claims.iss.clone().into_static(),
505
+
source: Box::new(e),
506
+
})?;
507
+
508
+
// Parse the DID document response to get verification methods
509
+
let doc = did_doc
510
+
.parse()
511
+
.map_err(|e| ServiceAuthError::DidResolutionFailed {
512
+
did: claims.iss.clone().into_static(),
513
+
source: Box::new(e),
514
+
})?;
515
+
516
+
// Extract signing key from DID document
517
+
let verification_methods = doc
518
+
.verification_method
519
+
.as_deref()
520
+
.ok_or_else(|| ServiceAuthError::NoSigningKey(claims.iss.clone().into_static()))?;
521
+
522
+
let signing_key = extract_signing_key(verification_methods)
523
+
.ok_or_else(|| ServiceAuthError::NoSigningKey(claims.iss.clone().into_static()))?;
524
+
525
+
// Verify signature FIRST - if this fails, nothing else matters
526
+
service_auth::verify_signature(&parsed, &signing_key)?;
527
+
528
+
// Now validate claims (audience, expiration, etc.)
529
+
claims.validate(state.service_did())?;
530
+
531
+
// Check method binding if required
532
+
if state.require_lxm() && claims.lxm.is_none() {
533
+
return Err(ServiceAuthError::MethodBindingRequired);
534
+
}
535
+
536
+
// All checks passed - return verified auth
537
+
Ok(ExtractOptionalServiceAuth(Some(VerifiedServiceAuth {
538
+
did: claims.iss.clone().into_static(),
539
+
aud: claims.aud.clone().into_static(),
540
+
lxm: claims.lxm.as_ref().map(|l| l.clone().into_static()),
541
+
jti: claims.jti.as_ref().map(|j| j.clone().into_static()),
542
+
})))
412
543
}
413
544
}
414
545
}