Skip to main content

cratefield_auth_client/
extractor.rs

1//! The axum extractor a consuming app's handlers use.
2//!
3//! A handler that takes [`Authenticated`] cannot run for an unverified
4//! request: there is no way to obtain the claims except through
5//! verification. That is the point — it makes "did I remember to check
6//! the token here?" a question the compiler answers.
7
8use std::sync::Arc;
9
10use axum::extract::FromRequestParts;
11use axum::http::request::Parts;
12use cratefield_core::Problem;
13
14use crate::jwks::AuthClient;
15use crate::verify::{Claims, token_fingerprint};
16
17/// Router state carrying the verifier.
18///
19/// A consuming app either uses this as its state or implements
20/// `FromRef` so the extractor can reach the client.
21#[derive(Clone)]
22pub struct AuthState(pub Arc<AuthClient>);
23
24/// A verified caller.
25///
26/// ```ignore
27/// async fn me(Authenticated(claims): Authenticated) -> String {
28///     claims.sub
29/// }
30/// ```
31#[derive(Debug, Clone)]
32pub struct Authenticated(pub Claims);
33
34impl<S> FromRequestParts<S> for Authenticated
35where
36    AuthState: axum::extract::FromRef<S>,
37    S: Send + Sync,
38{
39    type Rejection = Problem;
40
41    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
42        let AuthState(client) = axum::extract::FromRef::from_ref(state);
43        let token = bearer(parts).ok_or_else(unauthorized)?;
44        match client.verify(token).await {
45            Ok(claims) => Ok(Self(claims)),
46            Err(err) => {
47                // The reason is logged, never returned: telling a caller
48                // which check failed tells them how to get closer.
49                tracing::info!(
50                    reason = %err,
51                    token = %token_fingerprint(token),
52                    "rejected a bearer token"
53                );
54                Err(unauthorized())
55            }
56        }
57    }
58}
59
60/// The `Authorization: Bearer` value, if the header is well formed.
61fn bearer(parts: &Parts) -> Option<&str> {
62    let value = parts
63        .headers
64        .get(axum::http::header::AUTHORIZATION)?
65        .to_str()
66        .ok()?;
67    let rest = value
68        .strip_prefix("Bearer ")
69        .or_else(|| value.strip_prefix("bearer "))?;
70    (!rest.is_empty()).then_some(rest)
71}
72
73/// The one refusal this crate ever returns.
74///
75/// Missing header, malformed token, unknown key, wrong audience,
76/// expired: all the same. A caller learns that it is not authenticated
77/// and nothing else, because each distinguishable answer is a hint
78/// about how to get closer.
79pub const UNAUTHENTICATED: cratefield_core::ProblemDef = cratefield_core::SLUGS.unauthenticated;
80
81fn unauthorized() -> Problem {
82    Problem::new(&UNAUTHENTICATED)
83}