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