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