this repo has no description

JWT token refresh good error

lewis ec1f430c f95bdcae

Changed files
+170 -42
src
+1
src/api/error.rs
··· 192 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown, 193 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError, 194 crate::auth::TokenValidationError::AuthenticationFailed => Self::AuthenticationFailed, 195 } 196 } 197 }
··· 192 crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown, 193 crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError, 194 crate::auth::TokenValidationError::AuthenticationFailed => Self::AuthenticationFailed, 195 + crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken, 196 } 197 } 198 }
+18 -2
src/auth/extractor.rs
··· 19 MissingToken, 20 InvalidFormat, 21 AuthenticationFailed, 22 AccountDeactivated, 23 AccountTakedown, 24 AdminRequired, ··· 39 ), 40 AuthError::AuthenticationFailed => ( 41 StatusCode::UNAUTHORIZED, 42 - "AuthenticationFailed", 43 - "Invalid or expired token", 44 ), 45 AuthError::AccountDeactivated => ( 46 StatusCode::UNAUTHORIZED, ··· 174 Ok(user) => Ok(BearerAuth(user)), 175 Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 176 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 177 Err(_) => Err(AuthError::AuthenticationFailed), 178 } 179 } else { ··· 181 Ok(user) => Ok(BearerAuth(user)), 182 Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 183 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 184 Err(_) => Err(AuthError::AuthenticationFailed), 185 } 186 } ··· 224 { 225 Ok(user) => Ok(BearerAuthAllowDeactivated(user)), 226 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 227 Err(_) => Err(AuthError::AuthenticationFailed), 228 } 229 } else { ··· 236 { 237 Ok(user) => Ok(BearerAuthAllowDeactivated(user)), 238 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 239 Err(_) => Err(AuthError::AuthenticationFailed), 240 } 241 } ··· 284 Err(TokenValidationError::AccountTakedown) => { 285 return Err(AuthError::AccountTakedown); 286 } 287 Err(_) => return Err(AuthError::AuthenticationFailed), 288 } 289 } else { ··· 294 } 295 Err(TokenValidationError::AccountTakedown) => { 296 return Err(AuthError::AccountTakedown); 297 } 298 Err(_) => return Err(AuthError::AuthenticationFailed), 299 }
··· 19 MissingToken, 20 InvalidFormat, 21 AuthenticationFailed, 22 + TokenExpired, 23 AccountDeactivated, 24 AccountTakedown, 25 AdminRequired, ··· 40 ), 41 AuthError::AuthenticationFailed => ( 42 StatusCode::UNAUTHORIZED, 43 + "InvalidToken", 44 + "Token could not be verified", 45 + ), 46 + AuthError::TokenExpired => ( 47 + StatusCode::UNAUTHORIZED, 48 + "ExpiredToken", 49 + "Token has expired", 50 ), 51 AuthError::AccountDeactivated => ( 52 StatusCode::UNAUTHORIZED, ··· 180 Ok(user) => Ok(BearerAuth(user)), 181 Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 182 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 183 + Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired), 184 Err(_) => Err(AuthError::AuthenticationFailed), 185 } 186 } else { ··· 188 Ok(user) => Ok(BearerAuth(user)), 189 Err(TokenValidationError::AccountDeactivated) => Err(AuthError::AccountDeactivated), 190 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 191 + Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired), 192 Err(_) => Err(AuthError::AuthenticationFailed), 193 } 194 } ··· 232 { 233 Ok(user) => Ok(BearerAuthAllowDeactivated(user)), 234 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 235 + Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired), 236 Err(_) => Err(AuthError::AuthenticationFailed), 237 } 238 } else { ··· 245 { 246 Ok(user) => Ok(BearerAuthAllowDeactivated(user)), 247 Err(TokenValidationError::AccountTakedown) => Err(AuthError::AccountTakedown), 248 + Err(TokenValidationError::TokenExpired) => Err(AuthError::TokenExpired), 249 Err(_) => Err(AuthError::AuthenticationFailed), 250 } 251 } ··· 294 Err(TokenValidationError::AccountTakedown) => { 295 return Err(AuthError::AccountTakedown); 296 } 297 + Err(TokenValidationError::TokenExpired) => { 298 + return Err(AuthError::TokenExpired); 299 + } 300 Err(_) => return Err(AuthError::AuthenticationFailed), 301 } 302 } else { ··· 307 } 308 Err(TokenValidationError::AccountTakedown) => { 309 return Err(AuthError::AccountTakedown); 310 + } 311 + Err(TokenValidationError::TokenExpired) => { 312 + return Err(AuthError::TokenExpired); 313 } 314 Err(_) => return Err(AuthError::AuthenticationFailed), 315 }
+51 -40
src/auth/mod.rs
··· 28 create_service_token, 29 }; 30 pub use verify::{ 31 - get_did_from_token, get_jti_from_token, verify_access_token, verify_refresh_token, verify_token, 32 }; 33 34 const KEY_CACHE_TTL_SECS: u64 = 300; ··· 40 AccountTakedown, 41 KeyDecryptionFailed, 42 AuthenticationFailed, 43 } 44 45 impl fmt::Display for TokenValidationError { ··· 49 Self::AccountTakedown => write!(f, "AccountTakedown"), 50 Self::KeyDecryptionFailed => write!(f, "KeyDecryptionFailed"), 51 Self::AuthenticationFailed => write!(f, "AuthenticationFailed"), 52 } 53 } 54 } ··· 193 return Err(TokenValidationError::AccountTakedown); 194 } 195 196 - if let Ok(token_data) = verify_access_token(token, &decrypted_key) { 197 - let jti = &token_data.claims.jti; 198 - let session_cache_key = format!("auth:session:{}:{}", did, jti); 199 - let mut session_valid = false; 200 201 - if let Some(c) = cache { 202 - if let Some(cached_value) = c.get(&session_cache_key).await { 203 - session_valid = cached_value == "1"; 204 - crate::metrics::record_auth_cache_hit("session"); 205 - } else { 206 - crate::metrics::record_auth_cache_miss("session"); 207 } 208 - } 209 210 - if !session_valid { 211 - let session_exists = sqlx::query_scalar!( 212 - "SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()", 213 - did, 214 - jti 215 - ) 216 - .fetch_optional(db) 217 - .await 218 - .ok() 219 - .flatten(); 220 221 - session_valid = session_exists.is_some(); 222 223 - if session_valid && let Some(c) = cache { 224 - let _ = c 225 - .set( 226 - &session_cache_key, 227 - "1", 228 - Duration::from_secs(SESSION_CACHE_TTL_SECS), 229 - ) 230 - .await; 231 } 232 } 233 - 234 - if session_valid { 235 - return Ok(AuthenticatedUser { 236 - did: did.clone(), 237 - key_bytes: Some(decrypted_key), 238 - is_oauth: false, 239 - is_admin, 240 - scope: None, 241 - }); 242 } 243 } 244 } 245 } ··· 283 is_admin: oauth_token.is_admin, 284 scope: oauth_info.scope, 285 }); 286 } 287 } 288
··· 28 create_service_token, 29 }; 30 pub use verify::{ 31 + TokenVerifyError, get_did_from_token, get_jti_from_token, verify_access_token, 32 + verify_access_token_typed, verify_refresh_token, verify_token, 33 }; 34 35 const KEY_CACHE_TTL_SECS: u64 = 300; ··· 41 AccountTakedown, 42 KeyDecryptionFailed, 43 AuthenticationFailed, 44 + TokenExpired, 45 } 46 47 impl fmt::Display for TokenValidationError { ··· 51 Self::AccountTakedown => write!(f, "AccountTakedown"), 52 Self::KeyDecryptionFailed => write!(f, "KeyDecryptionFailed"), 53 Self::AuthenticationFailed => write!(f, "AuthenticationFailed"), 54 + Self::TokenExpired => write!(f, "ExpiredToken"), 55 } 56 } 57 } ··· 196 return Err(TokenValidationError::AccountTakedown); 197 } 198 199 + match verify_access_token_typed(token, &decrypted_key) { 200 + Ok(token_data) => { 201 + let jti = &token_data.claims.jti; 202 + let session_cache_key = format!("auth:session:{}:{}", did, jti); 203 + let mut session_valid = false; 204 205 + if let Some(c) = cache { 206 + if let Some(cached_value) = c.get(&session_cache_key).await { 207 + session_valid = cached_value == "1"; 208 + crate::metrics::record_auth_cache_hit("session"); 209 + } else { 210 + crate::metrics::record_auth_cache_miss("session"); 211 + } 212 } 213 214 + if !session_valid { 215 + let session_exists = sqlx::query_scalar!( 216 + "SELECT 1 as one FROM session_tokens WHERE did = $1 AND access_jti = $2 AND access_expires_at > NOW()", 217 + did, 218 + jti 219 + ) 220 + .fetch_optional(db) 221 + .await 222 + .ok() 223 + .flatten(); 224 225 + session_valid = session_exists.is_some(); 226 + 227 + if session_valid && let Some(c) = cache { 228 + let _ = c 229 + .set( 230 + &session_cache_key, 231 + "1", 232 + Duration::from_secs(SESSION_CACHE_TTL_SECS), 233 + ) 234 + .await; 235 + } 236 + } 237 238 + if session_valid { 239 + return Ok(AuthenticatedUser { 240 + did: did.clone(), 241 + key_bytes: Some(decrypted_key), 242 + is_oauth: false, 243 + is_admin, 244 + scope: None, 245 + }); 246 } 247 } 248 + Err(verify::TokenVerifyError::Expired) => { 249 + return Err(TokenValidationError::TokenExpired); 250 } 251 + Err(verify::TokenVerifyError::Invalid) => {} 252 } 253 } 254 } ··· 292 is_admin: oauth_token.is_admin, 293 scope: oauth_info.scope, 294 }); 295 + } else { 296 + return Err(TokenValidationError::TokenExpired); 297 } 298 } 299
+100
src/auth/verify.rs
··· 10 use hmac::{Hmac, Mac}; 11 use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier}; 12 use sha2::Sha256; 13 use subtle::ConstantTimeEq; 14 15 type HmacSha256 = Hmac<Sha256>; 16 17 pub fn get_did_from_token(token: &str) -> Result<String, String> { 18 let parts: Vec<&str> = token.split('.').collect(); ··· 228 let token_scope = claims.scope.as_deref().unwrap_or(""); 229 if !scopes.contains(&token_scope) { 230 return Err(anyhow!("Invalid token scope: {}", token_scope)); 231 } 232 } 233
··· 10 use hmac::{Hmac, Mac}; 11 use k256::ecdsa::{Signature, SigningKey, VerifyingKey, signature::Verifier}; 12 use sha2::Sha256; 13 + use std::fmt; 14 use subtle::ConstantTimeEq; 15 16 type HmacSha256 = Hmac<Sha256>; 17 + 18 + #[derive(Debug, Clone, Copy, PartialEq, Eq)] 19 + pub enum TokenVerifyError { 20 + Expired, 21 + Invalid, 22 + } 23 + 24 + impl fmt::Display for TokenVerifyError { 25 + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 + match self { 27 + Self::Expired => write!(f, "Token expired"), 28 + Self::Invalid => write!(f, "Token invalid"), 29 + } 30 + } 31 + } 32 + 33 + impl std::error::Error for TokenVerifyError {} 34 35 pub fn get_did_from_token(token: &str) -> Result<String, String> { 36 let parts: Vec<&str> = token.split('.').collect(); ··· 246 let token_scope = claims.scope.as_deref().unwrap_or(""); 247 if !scopes.contains(&token_scope) { 248 return Err(anyhow!("Invalid token scope: {}", token_scope)); 249 + } 250 + } 251 + 252 + Ok(TokenData { claims }) 253 + } 254 + 255 + pub fn verify_access_token_typed( 256 + token: &str, 257 + key_bytes: &[u8], 258 + ) -> Result<TokenData<Claims>, TokenVerifyError> { 259 + verify_token_typed_internal( 260 + token, 261 + key_bytes, 262 + Some(TOKEN_TYPE_ACCESS), 263 + Some(&[SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED]), 264 + ) 265 + } 266 + 267 + fn verify_token_typed_internal( 268 + token: &str, 269 + key_bytes: &[u8], 270 + expected_typ: Option<&str>, 271 + allowed_scopes: Option<&[&str]>, 272 + ) -> Result<TokenData<Claims>, TokenVerifyError> { 273 + let parts: Vec<&str> = token.split('.').collect(); 274 + if parts.len() != 3 { 275 + return Err(TokenVerifyError::Invalid); 276 + } 277 + 278 + let header_b64 = parts[0]; 279 + let claims_b64 = parts[1]; 280 + let signature_b64 = parts[2]; 281 + 282 + let Ok(header_bytes) = URL_SAFE_NO_PAD.decode(header_b64) else { 283 + return Err(TokenVerifyError::Invalid); 284 + }; 285 + 286 + let Ok(header) = serde_json::from_slice::<Header>(&header_bytes) else { 287 + return Err(TokenVerifyError::Invalid); 288 + }; 289 + 290 + if let Some(expected) = expected_typ 291 + && header.typ != expected 292 + { 293 + return Err(TokenVerifyError::Invalid); 294 + } 295 + 296 + let Ok(signature_bytes) = URL_SAFE_NO_PAD.decode(signature_b64) else { 297 + return Err(TokenVerifyError::Invalid); 298 + }; 299 + 300 + let Ok(signature) = Signature::from_slice(&signature_bytes) else { 301 + return Err(TokenVerifyError::Invalid); 302 + }; 303 + 304 + let Ok(signing_key) = SigningKey::from_slice(key_bytes) else { 305 + return Err(TokenVerifyError::Invalid); 306 + }; 307 + let verifying_key = VerifyingKey::from(&signing_key); 308 + 309 + let message = format!("{}.{}", header_b64, claims_b64); 310 + if verifying_key.verify(message.as_bytes(), &signature).is_err() { 311 + return Err(TokenVerifyError::Invalid); 312 + } 313 + 314 + let Ok(claims_bytes) = URL_SAFE_NO_PAD.decode(claims_b64) else { 315 + return Err(TokenVerifyError::Invalid); 316 + }; 317 + 318 + let Ok(claims) = serde_json::from_slice::<Claims>(&claims_bytes) else { 319 + return Err(TokenVerifyError::Invalid); 320 + }; 321 + 322 + let now = Utc::now().timestamp() as usize; 323 + if claims.exp < now { 324 + return Err(TokenVerifyError::Expired); 325 + } 326 + 327 + if let Some(scopes) = allowed_scopes { 328 + let token_scope = claims.scope.as_deref().unwrap_or(""); 329 + if !scopes.contains(&token_scope) { 330 + return Err(TokenVerifyError::Invalid); 331 } 332 } 333