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