Skip to main content

nythos_core/auth/
refresh.rs

1use std::time::{Duration, SystemTime};
2
3use uuid::Uuid;
4
5use crate::{
6    AccessToken, AuthError, Claims, NythosResult, RefreshToken, RefreshTokenRotation,
7    RevocationChecker, Role, RoleRepository, Session, SessionStore, TokenSigner,
8};
9
10/// Input for the refresh orchestration flow.
11#[derive(Debug, Clone)]
12pub struct RefreshInput {
13    refresh_token: String,
14    issued_at: SystemTime,
15    access_token_ttl: Duration,
16}
17
18impl RefreshInput {
19    pub fn new(refresh_token: String, issued_at: SystemTime, access_token_ttl: Duration) -> Self {
20        Self {
21            refresh_token,
22            issued_at,
23            access_token_ttl,
24        }
25    }
26
27    pub fn refresh_token(&self) -> &str {
28        &self.refresh_token
29    }
30
31    pub const fn issued_at(&self) -> SystemTime {
32        self.issued_at
33    }
34
35    pub const fn access_token_ttl(&self) -> Duration {
36        self.access_token_ttl
37    }
38}
39
40/// Fresh auth material returned by a successful refresh flow.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct RefreshAuthMaterial {
43    session: Session,
44    roles: Vec<Role>,
45    refresh_token: RefreshToken,
46    access_token: AccessToken,
47    claims: Claims,
48}
49
50impl RefreshAuthMaterial {
51    pub fn new(
52        session: Session,
53        roles: Vec<Role>,
54        refresh_token: RefreshToken,
55        access_token: AccessToken,
56        claims: Claims,
57    ) -> Self {
58        Self {
59            session,
60            roles,
61            refresh_token,
62            access_token,
63            claims,
64        }
65    }
66
67    pub fn session(&self) -> &Session {
68        &self.session
69    }
70
71    pub fn roles(&self) -> &[Role] {
72        &self.roles
73    }
74
75    pub fn refresh_token(&self) -> &RefreshToken {
76        &self.refresh_token
77    }
78
79    pub fn access_token(&self) -> &AccessToken {
80        &self.access_token
81    }
82
83    pub fn claims(&self) -> &Claims {
84        &self.claims
85    }
86}
87
88/// Refresh orchestration service.
89///
90/// This flow:
91/// - looks up session state by opaque refresh token
92/// - rejects missing, revoked, or expired sessions
93/// - reloads tenant-scoped roles for fresh auth material
94/// - issues a fresh access token
95/// - rotates the refresh token through `SessionStore`
96pub struct RefreshService<'a, S, R, T, C> {
97    session_store: &'a S,
98    role_repository: &'a R,
99    token_signer: &'a T,
100    revocation_checker: &'a C,
101}
102
103impl<'a, S, R, T, C> RefreshService<'a, S, R, T, C>
104where
105    S: SessionStore,
106    R: RoleRepository,
107    T: TokenSigner,
108    C: RevocationChecker,
109{
110    pub fn new(
111        session_store: &'a S,
112        role_repository: &'a R,
113        token_signer: &'a T,
114        revocation_checker: &'a C,
115    ) -> Self {
116        Self {
117            session_store,
118            role_repository,
119            token_signer,
120            revocation_checker,
121        }
122    }
123
124    pub async fn refresh(&self, input: RefreshInput) -> NythosResult<RefreshAuthMaterial> {
125        let previous_refresh = RefreshToken::new(input.refresh_token().to_owned())?;
126
127        let record = self
128            .session_store
129            .find_by_refresh_token(&previous_refresh)
130            .await?
131            .ok_or(AuthError::InvalidCredentials)?;
132
133        let session = record.session().clone();
134
135        self.ensure_session_can_refresh(&session, input.issued_at())
136            .await?;
137
138        let roles = self
139            .role_repository
140            .get_roles_for_user(session.tenant_id(), session.user_id())
141            .await?;
142
143        let claims = Claims::access(
144            session.user_id(),
145            session.tenant_id(),
146            input.issued_at(),
147            input.access_token_ttl(),
148        )?;
149
150        let access_token = self.token_signer.sign(&claims).await?;
151        let next_refresh = RefreshToken::new(Uuid::new_v4().to_string())?;
152
153        self.session_store
154            .rotate_refresh_token(RefreshTokenRotation::new(
155                session.id(),
156                previous_refresh,
157                next_refresh.clone(),
158            ))
159            .await?;
160
161        Ok(RefreshAuthMaterial::new(
162            session,
163            roles,
164            next_refresh,
165            access_token,
166            claims,
167        ))
168    }
169
170    async fn ensure_session_can_refresh(
171        &self,
172        session: &Session,
173        now: SystemTime,
174    ) -> NythosResult<()> {
175        if session.is_revoked() || self.revocation_checker.is_revoked(session.id()).await? {
176            return Err(AuthError::SessionRevoked);
177        }
178
179        if session.is_expired_at(now) {
180            return Err(AuthError::SessionExpired);
181        }
182
183        Ok(())
184    }
185}