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#[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#[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
88pub 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}