Skip to main content

soma_auth/
auth_context.rs

1//! Auth context injected into request extensions by [`crate::middleware::AuthLayer`].
2//!
3//! Downstream handlers can read this when they need caller identity or scope
4//! checks, but not every route consumes it yet.
5
6use axum::http::request::Parts;
7use std::sync::Arc;
8
9/// Stored in request extensions by the HTTP auth middleware (see
10/// [`crate::middleware::AuthLayer`]).
11#[derive(Debug, Clone)]
12pub struct AuthContext {
13    /// JWT `sub` claim (or `"static-bearer"` for static-token requests).
14    pub sub: String,
15    /// Optional opaque actor key (lab-specific observability hook); produced
16    /// by the [`crate::middleware::ActorKeyDeriver`] closure when one is
17    /// installed on the layer. Consumers without an actor-key concept
18    /// (cortex etc.) leave this `None`.
19    pub actor_key: Option<Arc<str>>,
20    /// Effective scopes for this request.
21    pub scopes: Vec<String>,
22    /// JWT `iss` claim (or `"local"` / `"browser-session"` sentinel).
23    pub issuer: String,
24    /// `true` when the request was authenticated via the browser session
25    /// cookie rather than a bearer token.
26    pub via_session: bool,
27    /// Browser-session CSRF token, when the request was authenticated via
28    /// session cookie. Echoed back to handlers that need to mint a fresh
29    /// `x-csrf-token` for follow-up state-changing requests.
30    pub csrf_token: Option<String>,
31    /// Verified Google email tied to the browser session, when known.
32    pub email: Option<String>,
33}
34
35/// Build the value of an `WWW-Authenticate: Bearer ...` response header
36/// pointing browsers/agents at the protected-resource metadata document.
37///
38/// When `scope` is `Some` and non-empty, a `scope="..."` parameter (RFC 6750
39/// Section 3) is appended so clients get immediate guidance on which scopes
40/// to request during authorization, per the MCP spec's `WWW-Authenticate`
41/// guidance. `scope` is expected to be a space-joined scope list (e.g.
42/// `"syslog:read syslog:admin"`).
43#[must_use]
44pub fn www_authenticate_value(resource_url: &str, scope: Option<&str>) -> String {
45    let mut value = format!(
46        "Bearer resource_metadata=\"{}/.well-known/oauth-protected-resource\"",
47        resource_url.trim_end_matches('/')
48    );
49    if let Some(scope) = scope
50        && !scope.is_empty()
51    {
52        value.push_str(&format!(", scope=\"{scope}\""));
53    }
54    value
55}
56
57/// Convenience accessor for handlers that have already split a request into
58/// [`Parts`].
59#[must_use]
60pub fn auth_context(parts: &Parts) -> Option<&AuthContext> {
61    parts.extensions.get::<AuthContext>()
62}
63
64#[cfg(test)]
65mod tests {
66    use super::www_authenticate_value;
67
68    #[test]
69    fn www_authenticate_value_appends_metadata_path_and_strips_trailing_slash() {
70        assert_eq!(
71            www_authenticate_value("https://lab.example.com/", None),
72            "Bearer resource_metadata=\"https://lab.example.com/.well-known/oauth-protected-resource\""
73        );
74        assert_eq!(
75            www_authenticate_value("https://lab.example.com", None),
76            "Bearer resource_metadata=\"https://lab.example.com/.well-known/oauth-protected-resource\""
77        );
78    }
79
80    #[test]
81    fn www_authenticate_value_appends_scope_when_present_and_omits_when_absent() {
82        assert_eq!(
83            www_authenticate_value("https://lab.example.com", Some("syslog:read syslog:admin")),
84            "Bearer resource_metadata=\"https://lab.example.com/.well-known/oauth-protected-resource\", scope=\"syslog:read syslog:admin\""
85        );
86        // `None` and empty-string scopes are both treated as "nothing to
87        // offer" and must not append a `scope=` param.
88        assert_eq!(
89            www_authenticate_value("https://lab.example.com", None),
90            "Bearer resource_metadata=\"https://lab.example.com/.well-known/oauth-protected-resource\""
91        );
92        assert_eq!(
93            www_authenticate_value("https://lab.example.com", Some("")),
94            "Bearer resource_metadata=\"https://lab.example.com/.well-known/oauth-protected-resource\""
95        );
96    }
97}