Skip to main content

did_hosting_server/auth/
backend.rs

1//! did-hosting-server `AuthBackend` impl.
2//!
3//! DIDComm-only (no SIOPv2 surface). Same JwtKeys + storage as
4//! did-hosting-control; the canonical handler in vti-common
5//! does the heavy lifting. Differences vs control:
6//!
7//! - Per-DID rate limit applies via the canonical handler's
8//!   default (10), backed by `count_pending_challenges` —
9//!   server doesn't have a separate O(1) tracker, so this is
10//!   the rate-limit mechanism. Closes the H3 gap from the
11//!   May 2026 security review (server's O(N) inline scan is
12//!   replaced by the canonical handler's count + cap).
13//! - DIDComm freshness window: 60s (canonical default), matching
14//!   the previous behaviour where the route handler checked
15//!   `created_time` against `now - challenge_ttl`.
16
17use async_trait::async_trait;
18use std::sync::Arc;
19
20use did_hosting_common::server::auth::DidHostingSessionStore;
21use did_hosting_common::server::auth::jwt::JwtKeys;
22use vti_common::auth::backend::{AuthBackend, RoleResolution};
23
24use crate::acl::Role;
25use crate::error::AppError;
26use crate::server::AppState;
27
28pub struct DidHostingServerAuthBackend {
29    state: Arc<AppState>,
30    sessions: DidHostingSessionStore,
31    jwt_keys: Arc<JwtKeys>,
32}
33
34impl DidHostingServerAuthBackend {
35    pub fn from_state(state: &AppState) -> Result<Self, AppError> {
36        let jwt_keys = state
37            .jwt_keys
38            .clone()
39            .ok_or_else(|| AppError::Internal("JWT keys not configured".into()))?;
40        let sessions = DidHostingSessionStore::new(state.sessions_ks.clone());
41        Ok(Self {
42            state: Arc::new(state.clone()),
43            sessions,
44            jwt_keys,
45        })
46    }
47}
48
49#[async_trait]
50impl AuthBackend for DidHostingServerAuthBackend {
51    type Store = DidHostingSessionStore;
52    type Error = AppError;
53    type Role = Role;
54
55    fn sessions(&self) -> &Self::Store {
56        &self.sessions
57    }
58
59    async fn mint_access_token(
60        &self,
61        subject: &str,
62        session_id: &str,
63        role: &Self::Role,
64        _contexts: &[String],
65        amr: &[String],
66        acr: &str,
67        _tee_attested: bool,
68        ttl_secs: u64,
69        jti: &str,
70    ) -> Result<String, Self::Error> {
71        let mut claims = JwtKeys::new_claims(
72            subject.to_string(),
73            session_id.to_string(),
74            role.to_string(),
75            ttl_secs,
76        );
77        claims.amr = amr.to_vec();
78        claims.acr = acr.to_string();
79        // Override `new_claims`' own random `jti`: the caller pins this nonce
80        // to the session's `token_id`, and the `AuthClaims` extractor rejects
81        // any token whose `jti` doesn't match it.
82        claims.jti = jti.to_string();
83        self.jwt_keys
84            .encode(&claims)
85            .map_err(|e| AppError::Internal(format!("jwt encode failed: {e:?}")))
86    }
87
88    async fn check_acl(&self, did: &str) -> Result<RoleResolution<Self::Role>, Self::Error> {
89        let role = crate::acl::check_acl(&self.state.acl_ks, did).await?;
90        Ok(RoleResolution::new(role))
91    }
92
93    fn challenge_ttl(&self) -> u64 {
94        self.state.config.auth.challenge_ttl
95    }
96
97    fn access_token_ttl(&self) -> u64 {
98        self.state.config.auth.access_token_expiry
99    }
100
101    fn refresh_token_ttl(&self) -> u64 {
102        self.state.config.auth.refresh_token_expiry
103    }
104}