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}