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}