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