Skip to main content

chopin_auth/
extractor.rs

1//! [`Auth<C>`] request extractor and global [`JwtManager`] initialisation.
2//!
3//! Call [`init_jwt_manager`] once at server startup to install the global manager,
4//! then use `Auth<C>` as a parameter in any route handler:
5//!
6//! ```rust,ignore
7//! use chopin_auth::{Auth, init_jwt_manager, JwtManager, StandardClaims};
8//! use chopin_core::{get, Context, Response};
9//!
10//! // Once at startup:
11//! init_jwt_manager(JwtManager::new(b"secret"));
12//!
13//! #[get("/me")]
14//! fn me(ctx: Context) -> Response {
15//!     let auth = match ctx.extract::<Auth<StandardClaims>>() {
16//!         Ok(a)  => a,
17//!         Err(r) => return r,   // 401 Unauthorized
18//!     };
19//!     Response::text(&auth.claims.sub)
20//! }
21//! ```
22//!
23//! To customise error responses, register a [`ErrorHandler`] with [`set_error_handler`].
24// src/extractor.rs
25use std::sync::OnceLock;
26
27use crate::jwt::{AuthError, HasJti, JwtManager};
28use chopin_core::extract::FromRequest;
29use chopin_core::http::{Context, Response};
30use serde::Deserialize;
31
32// ─── ErrorHandler ───────────────────────────────────────────────────────────
33
34/// Convert an [`AuthError`] into an HTTP [`Response`].
35///
36/// A default implementation is provided that returns an empty 401 for
37/// token errors and an empty 500 for internal errors, preserving
38/// backward-compatible behaviour.
39///
40/// Register a custom handler once at startup with [`set_error_handler`].
41///
42/// # Example
43/// ```rust,ignore
44/// use chopin_auth::extractor::{ErrorHandler, set_error_handler};
45/// use chopin_auth::jwt::AuthError;
46/// use chopin_core::http::Response;
47///
48/// struct JsonErrors;
49/// impl ErrorHandler for JsonErrors {
50///     fn handle(&self, err: AuthError) -> Response {
51///         let body = format!(r#"{{"error":"{err}"}}");
52///         Response::json(401, &body)
53///     }
54/// }
55///
56/// set_error_handler(JsonErrors);
57/// ```
58pub trait ErrorHandler: Send + Sync {
59    /// Convert `err` into an HTTP response that will be returned to the client.
60    fn handle(&self, err: AuthError) -> Response;
61}
62
63struct DefaultErrorHandler;
64impl ErrorHandler for DefaultErrorHandler {
65    fn handle(&self, err: AuthError) -> Response {
66        match err {
67            AuthError::Expired | AuthError::Revoked | AuthError::InvalidToken(_) => {
68                Response::new(401)
69            }
70            _ => Response::server_error(),
71        }
72    }
73}
74
75static GLOBAL_ERROR_HANDLER: OnceLock<Box<dyn ErrorHandler>> = OnceLock::new();
76
77/// Register a custom [`ErrorHandler`] used by all [`Auth`] extractors.
78///
79/// Call this **once** before starting the server, after (or alongside)
80/// [`init_jwt_manager`]. If never called, the default handler returns empty
81/// 401/500 responses.
82///
83/// Panics if called more than once.
84///
85/// # Example
86/// ```rust,ignore
87/// use chopin_auth::extractor::set_error_handler;
88/// set_error_handler(MyJsonErrorHandler);
89/// ```
90pub fn set_error_handler(handler: impl ErrorHandler + 'static) {
91    if GLOBAL_ERROR_HANDLER.set(Box::new(handler)).is_err() {
92        panic!("ErrorHandler already set — call set_error_handler only once");
93    }
94}
95
96#[inline]
97fn dispatch_error(err: AuthError) -> Response {
98    match GLOBAL_ERROR_HANDLER.get() {
99        Some(h) => h.handle(err),
100        None => DefaultErrorHandler.handle(err),
101    }
102}
103
104// ─── Global manager ──────────────────────────────────────────────────────────
105
106/// The global [`JwtManager`] shared across all threads.
107///
108/// Initialise it once at startup with [`init_jwt_manager`] before the server
109/// starts accepting requests.
110pub static GLOBAL_JWT_MANAGER: OnceLock<JwtManager> = OnceLock::new();
111
112/// Initialise the global [`JwtManager`].
113///
114/// Call this **once** before starting the server. Panics if called more than once.
115///
116/// # Example
117/// ```rust,ignore
118/// use chopin_auth::{JwtManager, init_jwt_manager};
119/// init_jwt_manager(JwtManager::new(b"my-secret"));
120/// ```
121pub fn init_jwt_manager(manager: JwtManager) {
122    if GLOBAL_JWT_MANAGER.set(manager).is_err() {
123        panic!("JwtManager already initialised — call init_jwt_manager only once");
124    }
125}
126
127// ─── Auth extractor ─────────────────────────────────────────────────────────
128
129/// A request extractor that validates the `Authorization: Bearer <token>` header
130/// and resolves to the decoded claims `T`.
131///
132/// `T` must implement both [`Deserialize`] and [`HasJti`]. Types that do not use
133/// revocation can satisfy [`HasJti`] with a one-line empty impl.
134///
135/// # Responses on failure
136/// - `401` – missing header, invalid/expired/revoked token.
137/// - `500` – the global [`JwtManager`] was not initialised.
138pub struct Auth<T> {
139    pub claims: T,
140}
141
142impl<'a, T> FromRequest<'a> for Auth<T>
143where
144    T: for<'de> Deserialize<'de> + HasJti + 'static,
145{
146    type Error = Response;
147
148    // `Response` is intentionally the error type here (HTTP 401/500 short-circuits).
149    #[allow(clippy::result_large_err)]
150    fn from_request(ctx: &'a Context<'a>) -> Result<Self, Self::Error> {
151        // Extract the Authorization header.
152        let auth_header = (0..ctx.req.header_count as usize).find_map(|i| {
153            let (k, v) = ctx.req.headers[i];
154            k.eq_ignore_ascii_case("Authorization").then_some(v)
155        });
156
157        let token = auth_header
158            .and_then(|v| v.strip_prefix("Bearer "))
159            .ok_or_else(|| Response::new(401))?;
160
161        let manager = GLOBAL_JWT_MANAGER
162            .get()
163            .ok_or_else(Response::server_error)?;
164
165        let claims = manager.decode::<T>(token).map_err(dispatch_error)?;
166
167        Ok(Auth { claims })
168    }
169}