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() {
98 return Err(OAuthError::InvalidRequest(
99 "DPoP proof required for this authorization".to_string(),
100 ));
101 } else {
102 None
103 };
104 if let Err(e) = db::revoke_tokens_for_client(&state.db, &did, &auth_request.client_id).await {
105 tracing::warn!("Failed to revoke previous tokens for client: {:?}", e);
106 }
107 let token_id = TokenId::generate();
108 let refresh_token = RefreshToken::generate();
109 let now = Utc::now();
110
111 let (final_scope, controller_did) = if let Some(ref controller) = auth_request.controller_did {
112 let grant = delegation::get_delegation(&state.db, &did, controller)
113 .await
114 .ok()
115 .flatten();
116 let granted_scopes = grant.map(|g| g.granted_scopes).unwrap_or_default();
117 let requested = auth_request
118 .parameters
119 .scope
120 .as_deref()
121 .unwrap_or("atproto");
122 let intersected = delegation::intersect_scopes(requested, &granted_scopes);
123 (Some(intersected), Some(controller.clone()))
124 } else {
125 (auth_request.parameters.scope.clone(), None)
126 };
127
128 let access_token = create_access_token_with_delegation(
129 &token_id.0,
130 &did,
131 dpop_jkt.as_deref(),
132 final_scope.as_deref(),
133 controller_did.as_deref(),
134 )?;
135 let stored_client_auth = auth_request.client_auth.unwrap_or(ClientAuth::None);
136 let refresh_expiry_days = if matches!(stored_client_auth, ClientAuth::None) {
137 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC
138 } else {
139 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
140 };
141 let token_data = TokenData {
142 did: did.clone(),
143 token_id: token_id.0.clone(),
144 created_at: now,
145 updated_at: now,
146 expires_at: now + Duration::days(refresh_expiry_days),
147 client_id: auth_request.client_id.clone(),
148 client_auth: stored_client_auth,
149 device_id: auth_request.device_id,
150 parameters: auth_request.parameters.clone(),
151 details: None,
152 code: None,
153 current_refresh_token: Some(refresh_token.0.clone()),
154 scope: final_scope.clone(),
155 controller_did: controller_did.clone(),
156 };
157 db::create_token(&state.db, &token_data).await?;
158 tokio::spawn({
159 let pool = state.db.clone();
160 let did_clone = did.clone();
161 async move {
162 if let Err(e) = db::enforce_token_limit_for_user(&pool, &did_clone).await {
163 tracing::warn!("Failed to enforce token limit for user: {:?}", e);
164 }
165 }
166 });
167 let mut response_headers = HeaderMap::new();
168 let config = AuthConfig::get();
169 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
170 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
171 Ok((
172 response_headers,
173 Json(TokenResponse {
174 access_token,
175 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
176 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
177 refresh_token: Some(refresh_token.0),
178 scope: final_scope,
179 sub: Some(did),
180 }),
181 ))
182}
183
184pub async fn handle_refresh_token_grant(
185 state: AppState,
186 _headers: HeaderMap,
187 request: TokenRequest,
188 dpop_proof: Option<String>,
189) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
190 let refresh_token_str = request
191 .refresh_token
192 .ok_or_else(|| OAuthError::InvalidRequest("refresh_token is required".to_string()))?;
193 tracing::info!(
194 refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
195 has_dpop = dpop_proof.is_some(),
196 "Refresh token grant requested"
197 );
198 if let Some(token_id) = db::check_refresh_token_used(&state.db, &refresh_token_str).await? {
199 if let Some((_db_id, token_data)) =
200 db::get_token_by_previous_refresh_token(&state.db, &refresh_token_str).await?
201 {
202 tracing::info!(
203 refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
204 "Refresh token reuse within grace period, returning existing tokens"
205 );
206 let dpop_jkt = token_data.parameters.dpop_jkt.as_deref();
207 let access_token = create_access_token_with_delegation(
208 &token_data.token_id,
209 &token_data.did,
210 dpop_jkt,
211 token_data.scope.as_deref(),
212 token_data.controller_did.as_deref(),
213 )?;
214 let mut response_headers = HeaderMap::new();
215 let config = AuthConfig::get();
216 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
217 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
218 return Ok((
219 response_headers,
220 Json(TokenResponse {
221 access_token,
222 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
223 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
224 refresh_token: token_data.current_refresh_token,
225 scope: token_data.scope,
226 sub: Some(token_data.did),
227 }),
228 ));
229 }
230 tracing::warn!(
231 refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
232 "Refresh token reuse detected, revoking token family"
233 );
234 db::delete_token_family(&state.db, token_id).await?;
235 return Err(OAuthError::InvalidGrant(
236 "Refresh token reuse detected, token family revoked".to_string(),
237 ));
238 }
239 let (db_id, token_data) = db::get_token_by_refresh_token(&state.db, &refresh_token_str)
240 .await?
241 .ok_or_else(|| {
242 tracing::warn!(
243 refresh_token_prefix = %&refresh_token_str[..std::cmp::min(16, refresh_token_str.len())],
244 "Refresh token not found in database"
245 );
246 OAuthError::InvalidGrant("Invalid refresh token".to_string())
247 })?;
248 if token_data.expires_at < Utc::now() {
249 tracing::warn!(
250 did = %token_data.did,
251 expired_at = %token_data.expires_at,
252 "Refresh token has expired"
253 );
254 db::delete_token_family(&state.db, db_id).await?;
255 return Err(OAuthError::InvalidGrant(
256 "Refresh token has expired".to_string(),
257 ));
258 }
259 let dpop_jkt = if let Some(proof) = &dpop_proof {
260 let config = AuthConfig::get();
261 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
262 let pds_hostname =
263 std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
264 let token_endpoint = format!("https://{}/oauth/token", pds_hostname);
265 let result = verifier.verify_proof(proof, "POST", &token_endpoint, None)?;
266 if !db::check_and_record_dpop_jti(&state.db, &result.jti).await? {
267 return Err(OAuthError::InvalidDpopProof(
268 "DPoP proof has already been used".to_string(),
269 ));
270 }
271 if let Some(expected_jkt) = &token_data.parameters.dpop_jkt
272 && &result.jkt != expected_jkt
273 {
274 return Err(OAuthError::InvalidDpopProof(
275 "DPoP key binding mismatch".to_string(),
276 ));
277 }
278 Some(result.jkt)
279 } else if token_data.parameters.dpop_jkt.is_some() {
280 return Err(OAuthError::InvalidRequest(
281 "DPoP proof required".to_string(),
282 ));
283 } else {
284 None
285 };
286 let new_token_id = TokenId::generate();
287 let new_refresh_token = RefreshToken::generate();
288 let refresh_expiry_days = if matches!(token_data.client_auth, ClientAuth::None) {
289 REFRESH_TOKEN_EXPIRY_DAYS_PUBLIC
290 } else {
291 REFRESH_TOKEN_EXPIRY_DAYS_CONFIDENTIAL
292 };
293 let new_expires_at = Utc::now() + Duration::days(refresh_expiry_days);
294 db::rotate_token(
295 &state.db,
296 db_id,
297 &new_token_id.0,
298 &new_refresh_token.0,
299 new_expires_at,
300 )
301 .await?;
302 tracing::info!(
303 did = %token_data.did,
304 new_expires_at = %new_expires_at,
305 "Refresh token rotated successfully"
306 );
307 let access_token = create_access_token_with_delegation(
308 &new_token_id.0,
309 &token_data.did,
310 dpop_jkt.as_deref(),
311 token_data.scope.as_deref(),
312 token_data.controller_did.as_deref(),
313 )?;
314 let mut response_headers = HeaderMap::new();
315 let config = AuthConfig::get();
316 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes());
317 response_headers.insert("DPoP-Nonce", verifier.generate_nonce().parse().unwrap());
318 Ok((
319 response_headers,
320 Json(TokenResponse {
321 access_token,
322 token_type: if dpop_jkt.is_some() { "DPoP" } else { "Bearer" }.to_string(),
323 expires_in: ACCESS_TOKEN_EXPIRY_SECONDS as u64,
324 refresh_token: Some(new_refresh_token.0),
325 scope: token_data.scope,
326 sub: Some(token_data.did),
327 }),
328 ))
329}