this repo has no description
1use super::helpers::{create_access_token_with_delegation, verify_pkce};
2use super::types::{TokenGrant, TokenResponse, ValidatedTokenRequest};
3use crate::config::AuthConfig;
4use crate::delegation;
5use crate::oauth::{
6 AuthFlowState, ClientAuth, OAuthError, RefreshToken, TokenData, TokenId,
7 client::{ClientMetadataCache, verify_client_auth},
8 db::{self, RefreshTokenLookup},
9 dpop::DPoPVerifier,
10};
11use crate::state::AppState;
12use axum::Json;
13use axum::http::HeaderMap;
14use chrono::{Duration, Utc};
15
16const ACCESS_TOKEN_EXPIRY_SECONDS: i64 = 300;
17const REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL: i64 = 60;
18const REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC: i64 = 14;
19
20pub async fn handle_authorization_code_grant(
21 state: AppState,
22 _headers: HeaderMap,
23 request: ValidatedTokenRequest,
24 dpop_proof: Option<String>,
25) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
26 let (code, code_verifier, redirect_uri) = match request.grant {
27 TokenGrant::AuthorizationCode { code, code_verifier, redirect_uri } => {
28 (code, code_verifier, redirect_uri)
29 }
30 _ => return Err(OAuthError::InvalidRequest("Expected authorization_code grant".to_string())),
31 };
32 let auth_request = db::consume_authorization_request_by_code(&state.db, &code)
33 .await?
34 .ok_or_else(|| OAuthError::InvalidGrant("Invalid or expired code".to_string()))?;
35
36 let flow_state = AuthFlowState::from_request_data(&auth_request);
37 if flow_state.is_expired() {
38 return Err(OAuthError::InvalidGrant(
39 "Authorization code has expired".to_string(),
40 ));
41 }
42 if !flow_state.can_exchange() {
43 return Err(OAuthError::InvalidGrant(
44 "Authorization not completed".to_string(),
45 ));
46 }
47
48 if let Some(request_client_id) = &request.client_auth.client_id
49 && request_client_id != &auth_request.client_id
50 {
51 return Err(OAuthError::InvalidGrant("client_id mismatch".to_string()));
52 }
53 let did = flow_state.did().unwrap().to_string();
54 let client_metadata_cache = ClientMetadataCache::new(3600);
55 let client_metadata = client_metadata_cache.get(&auth_request.client_id).await?;
56 let client_auth = if let (Some(assertion), Some(assertion_type)) =
57 (&request.client_auth.client_assertion, &request.client_auth.client_assertion_type)
58 {
59 if assertion_type != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
60 return Err(OAuthError::InvalidClient(
61 "Unsupported client_assertion_type".to_string(),
62 ));
63 }
64 ClientAuth::PrivateKeyJwt {
65 client_assertion: assertion.clone(),
66 }
67 } else if let Some(secret) = &request.client_auth.client_secret {
68 ClientAuth::SecretPost {
69 client_secret: secret.clone(),
70 }
71 } else {
72 ClientAuth::None
73 };
74 verify_client_auth(&client_metadata_cache, &client_metadata, &client_auth).await?;
75 verify_pkce(&auth_request.parameters.code_challenge, &code_verifier)?;
76 if let Some(req_redirect_uri) = &redirect_uri
77 && req_redirect_uri != &auth_request.parameters.redirect_uri
78 {
79 return Err(OAuthError::InvalidGrant(
80 "redirect_uri mismatch".to_string(),
81 ));
82 }
83 let dpop_jkt = if let Some(proof) = &dpop_proof {
84 let config = AuthConfig::get();
85 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
86 let pds_hostname =
87 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
88 let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
89 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?;
90 if !db::check_and_record_dpop_jti(&state.db, &result.jti).await? {
91 return Err(OAuthError::InvalidDpopProof(
92 "DPoP proof has already been used".to_string(),
93 ));
94 }
95 if let Some(expected_jkt) = &auth_request.parameters.dpop_jkt
96 && result.jkt.as_str() != expected_jkt
97 {
98 return Err(OAuthError::InvalidDpopProof(
99 "DPoP key binding mismatch".to_string(),
100 ));
101 }
102 Some(result.jkt.as_str().to_string())
103 } else if auth_request.parameters.dpop_jkt.is_some() || client_metadata.requires_dpop() {
104 return Err(OAuthError::UseDpopNonce(
105 crate::oauth::dpop::DPoPVerifier::new(AuthConfig::get().dpop_secret().as_bytes())
106 .generate_nonce(),
107 ));
108 } else {
109 None
110 };
111 if let Err(e) = db::revoke_tokens_for_client(&state.db, &did, &auth_request.client_id).await {
112 tracing::warn!("Failed to revoke previous tokens for client: {:?}", e);
113 }
114 let token_id = TokenId::generate();
115 let refresh_token = RefreshToken::generate();
116 let now = Utc::now();
117
118 let (final_scope, controller_did) = if let Some(ref controller) = auth_request.controller_did {
119 let grant = delegation::get_delegation(&state.db, &did, controller)
120 .await
121 .ok()
122 .flatten();
123 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default();
124 let requested = auth_request
125 .parameters
126 .scope
127 .as_deref()
128 .unwrap_or("atproto");
129 let intersected = delegation::intersect_scopes(requested, &granted_scopes);
130 (Some(intersected), Some(controller.clone()))
131 } else {
132 (auth_request.parameters.scope.clone(), None)
133 };
134
135 let access_token = create_access_token_with_delegation(
136 &token_id.0,
137 &did,
138 dpop_jkt.as_deref(),
139 final_scope.as_deref(),
140 controller_did.as_deref(),
141 )?;
142 let stored_client_auth = auth_request.client_auth.unwrap_or(ClientAuth::None);
143 let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) {
144 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC
145 } else {
146 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
147 };
148 let mut stored_parameters = auth_request.parameters.clone();
149 stored_parameters.dpop_jkt = dpop_jkt.clone();
150 let token_data = TokenData {
151 did: did.clone(),
152 token_id: token_id.0.clone(),
153 created_at: now,
154 updated_at: now,
155 expires_at: now + Duration::days(refresh_expiry_days),
156 client_id: auth_request.client_id.clone(),
157 client_auth: stored_client_auth,
158 device_id: auth_request.device_id,
159 parameters: stored_parameters,
160 details: None,
161 code: None,
162 current_refresh_token: Some(refresh_token.0.clone()),
163 scope: final_scope.clone(),
164 controller_did: controller_did.clone(),
165 };
166 db::create_token(&state.db, &token_data).await?;
167 tokio::spawn({
168 let pool = state.db.clone();
169 let did_clone = did.clone();
170 async move {
171 if let Err(e) = db::enforce_token_limit_for_user(&pool, &did_clone).await {
172 tracing::warn!("Failed to enforce token limit for user: {:?}", e);
173 }
174 }
175 });
176 let mut response_headers = HeaderMap::new();
177 let config = AuthConfig::get();
178 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
179 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
180 Ok((
181 response_headers,
182 Json(TokenResponse {
183 access_token,
184 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
185 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
186 refresh_token: Some(refresh_token.0),
187 scope: final_scope,
188 sub: Some(did),
189 }),
190 ))
191}
192
193pub async fn handle_refresh_token_grant(
194 state: AppState,
195 _headers: HeaderMap,
196 request: ValidatedTokenRequest,
197 dpop_proof: Option<String>,
198) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
199 let refresh_token_str = match request.grant {
200 TokenGrant::RefreshToken { refresh_token } => refresh_token,
201 _ => return Err(OAuthError::InvalidRequest("Expected refresh_token grant".to_string())),
202 };
203 let token_prefix = &refresh_token_str[..std::cmp::min(16, refresh_token_str.len())];
204 tracing::info!(
205 refresh_token_prefix = %token_prefix,
206 has_dpop = dpop_proof.is_some(),
207 "Refresh token grant requested"
208 );
209
210 let lookup = db::lookup_refresh_token(&state.db, &refresh_token_str).await?;
211 let token_state = lookup.state();
212 tracing::debug!(state = %token_state, "Refresh token state");
213
214 let (db_id, token_data) = match lookup {
215 RefreshTokenLookup::Valid { db_id, token_data } => (db_id, token_data),
216 RefreshTokenLookup::InGracePeriod { db_id: _, token_data, rotated_at } => {
217 tracing::info!(
218 refresh_token_prefix = %token_prefix,
219 rotated_at = %rotated_at,
220 "Refresh token reuse within grace period, returning existing tokens"
221 );
222 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref();
223 let access_token = create_access_token_with_delegation(
224 &token_data.token_id,
225 &token_data.did,
226 dpop_jkt,
227 token_data.scope.as_deref(),
228 token_data.controller_did.as_deref(),
229 )?;
230 let mut response_headers = HeaderMap::new();
231 let config = AuthConfig::get();
232 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
233 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
234 return Ok((
235 response_headers,
236 Json(TokenResponse {
237 access_token,
238 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
239 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
240 refresh_token: token_data.current_refresh_token,
241 scope: token_data.scope,
242 sub: Some(token_data.did),
243 }),
244 ));
245 }
246 RefreshTokenLookup::Used { original_token_id } => {
247 tracing::warn!(
248 refresh_token_prefix = %token_prefix,
249 "Refresh token reuse detected, revoking token family"
250 );
251 db::delete_token_family(&state.db, original_token_id).await?;
252 return Err(OAuthError::InvalidGrant(
253 "Refresh token reuse detected, token family revoked".to_string(),
254 ));
255 }
256 RefreshTokenLookup::Expired { db_id } => {
257 tracing::warn!(refresh_token_prefix = %token_prefix, "Refresh token has expired");
258 db::delete_token_family(&state.db, db_id).await?;
259 return Err(OAuthError::InvalidGrant(
260 "Refresh token has expired".to_string(),
261 ));
262 }
263 RefreshTokenLookup::NotFound => {
264 tracing::warn!(refresh_token_prefix = %token_prefix, "Refresh token not found");
265 return Err(OAuthError::InvalidGrant("Invalid refresh token".to_string()));
266 }
267 };
268 let dpop_jkt = if let Some(proof) = &dpop_proof {
269 let config = AuthConfig::get();
270 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
271 let pds_hostname =
272 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
273 let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
274 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?;
275 if !db::check_and_record_dpop_jti(&state.db, &result.jti).await? {
276 return Err(OAuthError::InvalidDpopProof(
277 "DPoP proof has already been used".to_string(),
278 ));
279 }
280 if let Some(expected_jkt) = &token_data.parameters.dpop_jkt
281 && result.jkt.as_str() != expected_jkt
282 {
283 return Err(OAuthError::InvalidDpopProof(
284 "DPoP key binding mismatch".to_string(),
285 ));
286 }
287 Some(result.jkt.as_str().to_string())
288 } else if token_data.parameters.dpop_jkt.is_some() {
289 return Err(OAuthError::InvalidRequest(
290 "DPoP proof required".to_string(),
291 ));
292 } else {
293 None
294 };
295 let new_token_id = TokenId::generate();
296 let new_refresh_token = RefreshToken::generate();
297 let refresh_expiry_days = if matches!(token_data.client_auth, ClientAuth::None) {
298 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC
299 } else {
300 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
301 };
302 let new_expires_at = Utc::now() + Duration::days(refresh_expiry_days);
303 db::rotate_token(
304 &state.db,
305 db_id,
306 &new_token_id.0,
307 &new_refresh_token.0,
308 new_expires_at,
309 )
310 .await?;
311 tracing::info!(
312 did = %token_data.did,
313 new_expires_at = %new_expires_at,
314 "Refresh token rotated successfully"
315 );
316 let access_token = create_access_token_with_delegation(
317 &new_token_id.0,
318 &token_data.did,
319 dpop_jkt.as_deref(),
320 token_data.scope.as_deref(),
321 token_data.controller_did.as_deref(),
322 )?;
323 let mut response_headers = HeaderMap::new();
324 let config = AuthConfig::get();
325 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
326 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
327 Ok((
328 response_headers,
329 Json(TokenResponse {
330 access_token,
331 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
332 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
333 refresh_token: Some(new_refresh_token.0),
334 scope: token_data.scope,
335 sub: Some(token_data.did),
336 }),
337 ))
338}