Skip to main content

cratefield_auth_client/
port.rs

1//! [`AuthClient`] as the harness's [`Auth`] port (issue #153).
2//!
3//! The extractor in this crate needs `AuthState` in the router's state,
4//! and a module's state is an `Arc<ModuleContext>` — so a module could
5//! not use it, and a module serving somebody's rows had no way to learn
6//! whose they were. Implementing the port is what makes this verifier
7//! reachable from a module that declared `Port::Auth`.
8
9use async_trait::async_trait;
10use cratefield_core::{Auth, AuthError, Caller, Subject};
11use http::HeaderMap;
12
13use crate::jwks::AuthClient;
14use crate::verify::token_fingerprint;
15
16#[async_trait]
17impl Auth for AuthClient {
18    /// Identifies the caller from an `Authorization: Bearer` header.
19    ///
20    /// No header is [`Caller::Anonymous`]. A header that is present and
21    /// does not verify is [`AuthError::NotVerified`] — never anonymity,
22    /// which would let an expired token read a public table and tell its
23    /// holder nothing was wrong.
24    ///
25    /// [`AuthError::Unavailable`] is never returned here, and that is a
26    /// property of the verifier rather than an omission: a JWKS fetch
27    /// that fails leaves the previously fetched keys in place, so an
28    /// unreachable auth service does not invalidate a running app's
29    /// sessions. The one case it cannot cover is a cold start that has
30    /// never fetched a key set, where every token is an unknown key and
31    /// so a refusal.
32    async fn identify(&self, headers: &HeaderMap) -> Result<Caller, AuthError> {
33        let Some(presented) = headers.get(http::header::AUTHORIZATION) else {
34            return Ok(Caller::Anonymous);
35        };
36        let Some(token) = bearer(presented) else {
37            // Present and not a bearer token: a different scheme, an
38            // empty value, non-ASCII bytes. The caller meant to
39            // authenticate and did not, which is a refusal — reading it
40            // as anonymous would serve a public route to someone whose
41            // credentials are broken and tell them nothing is wrong.
42            tracing::info!("rejected an Authorization header that is not a bearer token");
43            return Err(AuthError::NotVerified);
44        };
45        match self.verify(token).await {
46            Ok(claims) => Ok(Caller::Subject(
47                Subject::new(claims.sub).session(claims.sid).email(
48                    // Only an address the issuer says it verified.
49                    claims
50                        .email
51                        .filter(|_| claims.email_verified.unwrap_or(false)),
52                ),
53            )),
54            Err(err) => {
55                // The reason is logged and never returned, for the reason
56                // the extractor gives: each answer a caller can tell apart
57                // is a hint about how to get closer.
58                tracing::info!(
59                    reason = %err,
60                    token = %token_fingerprint(token),
61                    "rejected a bearer token"
62                );
63                Err(AuthError::NotVerified)
64            }
65        }
66    }
67}
68
69/// The token inside an `Authorization: Bearer` value, if it is one.
70///
71/// `None` means the header is present and is not a bearer token, which
72/// the caller turns into a refusal rather than into anonymity.
73fn bearer(presented: &http::HeaderValue) -> Option<&str> {
74    let value = presented.to_str().ok()?;
75    let rest = value
76        .strip_prefix("Bearer ")
77        .or_else(|| value.strip_prefix("bearer "))?;
78    (!rest.is_empty()).then_some(rest)
79}