arcly_http_core/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 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(headers: &HeaderMap, container: &FrozenDiContainer) -> Option<Arc<Claims>> {
35 decode_bearer_token(headers, container).or_else(|| {
36 let cookie = container.try_get::<CookieService>()?;
37 let jwt = container.try_get::<JwtService>()?;
38 let value = cookie.extract(headers)?;
39 jwt.decode_access(&value)
40 })
41}
42
43/// Extract claims **and** load the server-side session.
44///
45/// Used by the HTTP boundary and plugin routes. Session loading is skipped
46/// entirely (no store round-trip) when no `SessionManager` is registered.
47pub async fn extract_auth(headers: &HeaderMap, container: &FrozenDiContainer) -> AuthExtraction {
48 let claims = extract_claims(headers, container);
49
50 let session = match container.try_get::<SessionManager>() {
51 Some(sm) => sm.load_from_headers(headers).await,
52 None => None,
53 };
54
55 AuthExtraction { claims, session }
56}