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