Skip to main content

arcly_http/auth/
extract.rs

1//! The single credential-extraction pipeline shared by every request boundary.
2//!
3//! Order of precedence:
4//! 1. `Authorization: Bearer <token>` header (decoded as an **access** JWT).
5//! 2. HMAC-signed JWT cookie (`CookieService`) — same `decode_access` path,
6//!    so the security properties are identical to the Bearer path.
7//! 3. Server-side session (`SessionManager`) — loaded independently of claims;
8//!    a request can carry both a JWT identity and a mutable session.
9//!
10//! HTTP boundary, plugin routes, and the WebSocket handshake all call into
11//! this module, so a security fix or a new credential source lands everywhere
12//! at once — there is deliberately no second copy of this logic anywhere.
13
14use std::sync::Arc;
15
16use axum::http::HeaderMap;
17
18use crate::auth::cookie::CookieService;
19use crate::auth::jwt::{decode_bearer_token, JwtService};
20use crate::auth::session::{Session, SessionManager};
21use crate::core::engine::FrozenDiContainer;
22use crate::web::context::Claims;
23
24/// Everything the boundaries need to authenticate one request.
25pub struct AuthExtraction {
26    pub claims: Option<Arc<Claims>>,
27    pub session: Option<Arc<Session>>,
28}
29
30/// Extract JWT claims only (Bearer → signed-cookie fallback).
31///
32/// Used directly by the WebSocket handshake, which authenticates once at
33/// upgrade time and never loads a server-side session.
34pub fn extract_claims(
35    headers: &HeaderMap,
36    container: &'static FrozenDiContainer,
37) -> Option<Arc<Claims>> {
38    decode_bearer_token(headers, container).or_else(|| {
39        let cookie = container.try_get::<CookieService>()?;
40        let jwt = container.try_get::<JwtService>()?;
41        let value = cookie.extract(headers)?;
42        jwt.decode_access(&value)
43    })
44}
45
46/// Extract claims **and** load the server-side session.
47///
48/// Used by the HTTP boundary and plugin routes. Session loading is skipped
49/// entirely (no store round-trip) when no `SessionManager` is registered.
50pub async fn extract_auth(
51    headers: &HeaderMap,
52    container: &'static FrozenDiContainer,
53) -> AuthExtraction {
54    let claims = extract_claims(headers, container);
55
56    let session = match container.try_get::<SessionManager>() {
57        Some(sm) => sm.load_from_headers(headers).await,
58        None => None,
59    };
60
61    AuthExtraction { claims, session }
62}