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}