this repo has no description
1use axum::{ 2 Json, 3 extract::FromRequestParts, 4 http::{StatusCode, request::Parts}, 5 response::{IntoResponse, Response}, 6}; 7use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 8use hmac::{Hmac, Mac}; 9use serde_json::json; 10use sha2::Sha256; 11use sqlx::PgPool; 12use subtle::ConstantTimeEq; 13 14use super::OAuthError; 15use super::db; 16use super::dpop::DPoPVerifier; 17use crate::config::AuthConfig; 18use crate::state::AppState; 19 20pub struct OAuthTokenInfo { 21 pub did: String, 22 pub token_id: String, 23 pub client_id: String, 24 pub scope: Option<String>, 25 pub dpop_jkt: Option<String>, 26} 27 28pub struct VerifyResult { 29 pub did: String, 30 pub token_id: String, 31 pub client_id: String, 32 pub scope: Option<String>, 33} 34 35pub async fn verify_oauth_access_token( 36 pool: &PgPool, 37 access_token: &str, 38 dpop_proof: Option<&str>, 39 http_method: &str, 40 http_uri: &str, 41) -> Result<VerifyResult, OAuthError> { 42 let token_info = extract_oauth_token_info(access_token)?; 43 let token_data = db::get_token_by_id(pool, &token_info.token_id) 44 .await? 45 .ok_or_else(|| OAuthError::InvalidToken("Token not found or revoked".to_string()))?; 46 let now = chrono::Utc::now(); 47 if token_data.expires_at < now { 48 return Err(OAuthError::InvalidToken("Token has expired".to_string())); 49 } 50 if let Some(expected_jkt) = &token_data.parameters.dpop_jkt { 51 let proof = dpop_proof 52 .ok_or_else(|| OAuthError::UseDpopNonce("DPoP proof required".to_string()))?; 53 let config = AuthConfig::get(); 54 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 55 let access_token_hash = compute_ath(access_token); 56 let result = 57 verifier.verify_proof(proof, http_method, http_uri, Some(&access_token_hash))?; 58 if !db::check_and_record_dpop_jti(pool, &result.jti).await? { 59 return Err(OAuthError::InvalidDpopProof( 60 "DPoP proof has already been used".to_string(), 61 )); 62 } 63 if &result.jkt != expected_jkt { 64 return Err(OAuthError::InvalidDpopProof( 65 "DPoP key binding mismatch".to_string(), 66 )); 67 } 68 } 69 Ok(VerifyResult { 70 did: token_data.did, 71 token_id: token_info.token_id, 72 client_id: token_data.client_id, 73 scope: token_data.scope, 74 }) 75} 76 77pub fn extract_oauth_token_info(token: &str) -> Result<OAuthTokenInfo, OAuthError> { 78 let parts: Vec<&str> = token.split('.').collect(); 79 if parts.len() != 3 { 80 return Err(OAuthError::InvalidToken("Invalid token format".to_string())); 81 } 82 let header_bytes = URL_SAFE_NO_PAD 83 .decode(parts[0]) 84 .map_err(|_| OAuthError::InvalidToken("Invalid token encoding".to_string()))?; 85 let header: serde_json::Value = serde_json::from_slice(&header_bytes) 86 .map_err(|_| OAuthError::InvalidToken("Invalid token header".to_string()))?; 87 if header.get("typ").and_then(|t| t.as_str()) != Some("at+jwt") { 88 return Err(OAuthError::InvalidToken( 89 "Not an OAuth access token".to_string(), 90 )); 91 } 92 if header.get("alg").and_then(|a| a.as_str()) != Some("HS256") { 93 return Err(OAuthError::InvalidToken( 94 "Unsupported algorithm".to_string(), 95 )); 96 } 97 let config = AuthConfig::get(); 98 let secret = config.jwt_secret(); 99 let signing_input = format!("{}.{}", parts[0], parts[1]); 100 let provided_sig = URL_SAFE_NO_PAD 101 .decode(parts[2]) 102 .map_err(|_| OAuthError::InvalidToken("Invalid signature encoding".to_string()))?; 103 type HmacSha256 = Hmac<Sha256>; 104 let mut mac = HmacSha256::new_from_slice(secret.as_bytes()) 105 .map_err(|_| OAuthError::ServerError("HMAC initialization failed".to_string()))?; 106 mac.update(signing_input.as_bytes()); 107 let expected_sig = mac.finalize().into_bytes(); 108 if !bool::from(expected_sig.ct_eq(&provided_sig)) { 109 return Err(OAuthError::InvalidToken( 110 "Invalid token signature".to_string(), 111 )); 112 } 113 let payload_bytes = URL_SAFE_NO_PAD 114 .decode(parts[1]) 115 .map_err(|_| OAuthError::InvalidToken("Invalid payload encoding".to_string()))?; 116 let payload: serde_json::Value = serde_json::from_slice(&payload_bytes) 117 .map_err(|_| OAuthError::InvalidToken("Invalid token payload".to_string()))?; 118 let exp = payload 119 .get("exp") 120 .and_then(|e| e.as_i64()) 121 .ok_or_else(|| OAuthError::InvalidToken("Missing exp claim".to_string()))?; 122 let now = chrono::Utc::now().timestamp(); 123 if exp < now { 124 return Err(OAuthError::InvalidToken("Token has expired".to_string())); 125 } 126 let token_id = payload 127 .get("jti") 128 .and_then(|j| j.as_str()) 129 .ok_or_else(|| OAuthError::InvalidToken("Missing jti claim".to_string()))? 130 .to_string(); 131 let did = payload 132 .get("sub") 133 .and_then(|s| s.as_str()) 134 .ok_or_else(|| OAuthError::InvalidToken("Missing sub claim".to_string()))? 135 .to_string(); 136 let scope = payload 137 .get("scope") 138 .and_then(|s| s.as_str()) 139 .map(|s| s.to_string()); 140 let dpop_jkt = payload 141 .get("cnf") 142 .and_then(|c| c.get("jkt")) 143 .and_then(|j| j.as_str()) 144 .map(|s| s.to_string()); 145 let client_id = payload 146 .get("client_id") 147 .and_then(|c| c.as_str()) 148 .map(|s| s.to_string()) 149 .unwrap_or_default(); 150 Ok(OAuthTokenInfo { 151 did, 152 token_id, 153 client_id, 154 scope, 155 dpop_jkt, 156 }) 157} 158 159fn compute_ath(access_token: &str) -> String { 160 use sha2::Digest; 161 let mut hasher = Sha256::new(); 162 hasher.update(access_token.as_bytes()); 163 let hash = hasher.finalize(); 164 URL_SAFE_NO_PAD.encode(hash) 165} 166 167pub fn generate_dpop_nonce() -> String { 168 let config = AuthConfig::get(); 169 let verifier = DPoPVerifier::new(config.dpop_secret().as_bytes()); 170 verifier.generate_nonce() 171} 172 173pub struct OAuthUser { 174 pub did: String, 175 pub client_id: Option<String>, 176 pub scope: Option<String>, 177 pub is_oauth: bool, 178} 179 180pub struct OAuthAuthError { 181 pub status: StatusCode, 182 pub error: String, 183 pub message: String, 184 pub dpop_nonce: Option<String>, 185} 186 187impl IntoResponse for OAuthAuthError { 188 fn into_response(self) -> Response { 189 let mut response = ( 190 self.status, 191 Json(json!({ 192 "error": self.error, 193 "message": self.message 194 })), 195 ) 196 .into_response(); 197 if let Some(nonce) = self.dpop_nonce { 198 response 199 .headers_mut() 200 .insert("DPoP-Nonce", nonce.parse().unwrap()); 201 } 202 response 203 } 204} 205 206impl FromRequestParts<AppState> for OAuthUser { 207 type Rejection = OAuthAuthError; 208 209 async fn from_request_parts( 210 parts: &mut Parts, 211 state: &AppState, 212 ) -> Result<Self, Self::Rejection> { 213 let auth_header = parts 214 .headers 215 .get("Authorization") 216 .and_then(|v| v.to_str().ok()) 217 .ok_or_else(|| OAuthAuthError { 218 status: StatusCode::UNAUTHORIZED, 219 error: "AuthenticationRequired".to_string(), 220 message: "Authorization header required".to_string(), 221 dpop_nonce: None, 222 })?; 223 let auth_header_trimmed = auth_header.trim(); 224 let (token, is_dpop_token) = if auth_header_trimmed.len() >= 7 225 && auth_header_trimmed[..7].eq_ignore_ascii_case("bearer ") 226 { 227 (auth_header_trimmed[7..].trim(), false) 228 } else if auth_header_trimmed.len() >= 5 229 && auth_header_trimmed[..5].eq_ignore_ascii_case("dpop ") 230 { 231 (auth_header_trimmed[5..].trim(), true) 232 } else { 233 return Err(OAuthAuthError { 234 status: StatusCode::UNAUTHORIZED, 235 error: "InvalidRequest".to_string(), 236 message: "Invalid authorization scheme".to_string(), 237 dpop_nonce: None, 238 }); 239 }; 240 let dpop_proof = parts.headers.get("DPoP").and_then(|v| v.to_str().ok()); 241 if let Ok(result) = try_legacy_auth(&state.db, token).await { 242 return Ok(OAuthUser { 243 did: result.did, 244 client_id: None, 245 scope: None, 246 is_oauth: false, 247 }); 248 } 249 let http_method = parts.method.as_str(); 250 let http_uri = parts.uri.to_string(); 251 match verify_oauth_access_token(&state.db, token, dpop_proof, http_method, &http_uri).await 252 { 253 Ok(result) => Ok(OAuthUser { 254 did: result.did, 255 client_id: Some(result.client_id), 256 scope: result.scope, 257 is_oauth: true, 258 }), 259 Err(OAuthError::UseDpopNonce(nonce)) => Err(OAuthAuthError { 260 status: StatusCode::UNAUTHORIZED, 261 error: "use_dpop_nonce".to_string(), 262 message: "DPoP nonce required".to_string(), 263 dpop_nonce: Some(nonce), 264 }), 265 Err(OAuthError::InvalidDpopProof(msg)) => { 266 let nonce = generate_dpop_nonce(); 267 Err(OAuthAuthError { 268 status: StatusCode::UNAUTHORIZED, 269 error: "invalid_dpop_proof".to_string(), 270 message: msg, 271 dpop_nonce: Some(nonce), 272 }) 273 } 274 Err(e) => { 275 let nonce = if is_dpop_token { 276 Some(generate_dpop_nonce()) 277 } else { 278 None 279 }; 280 Err(OAuthAuthError { 281 status: StatusCode::UNAUTHORIZED, 282 error: "AuthenticationFailed".to_string(), 283 message: format!("{:?}", e), 284 dpop_nonce: nonce, 285 }) 286 } 287 } 288 } 289} 290 291struct LegacyAuthResult { 292 did: String, 293} 294 295async fn try_legacy_auth(pool: &PgPool, token: &str) -> Result<LegacyAuthResult, ()> { 296 match crate::auth::validate_bearer_token(pool, token).await { 297 Ok(user) if !user.is_oauth => Ok(LegacyAuthResult { did: user.did }), 298 _ => Err(()), 299 } 300}