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