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