this repo has no description
at main 18 kB view raw
1use serde::{Deserialize, Serialize}; 2use sqlx::PgPool; 3use std::fmt; 4use std::time::Duration; 5 6use crate::AccountStatus; 7use crate::cache::Cache; 8use crate::oauth::scopes::ScopePermissions; 9use crate::types::Did; 10 11pub mod extractor; 12pub mod scope_check; 13pub mod service; 14pub mod verification_token; 15pub mod webauthn; 16 17pub use extractor::{ 18 AuthError, BearerAuth, BearerAuthAdmin, BearerAuthAllowDeactivated, ExtractedToken, 19 extract_auth_token_from_header, extract_bearer_token_from_header, 20}; 21pub use service::{ServiceTokenClaims, ServiceTokenVerifier, is_service_token}; 22 23pub use tranquil_auth::{ 24 ActClaim, Claims, Header, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, 25 SCOPE_REFRESH, TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, TokenData, 26 TokenVerifyError, TokenWithMetadata, UnsafeClaims, create_access_token, 27 create_access_token_hs256, create_access_token_hs256_with_metadata, 28 create_access_token_with_delegation, create_access_token_with_metadata, 29 create_access_token_with_scope_metadata, create_refresh_token, create_refresh_token_hs256, 30 create_refresh_token_hs256_with_metadata, create_refresh_token_with_metadata, 31 create_service_token, create_service_token_hs256, generate_backup_codes, 32 generate_qr_png_base64, generate_totp_secret, generate_totp_uri, get_algorithm_from_token, 33 get_did_from_token, get_jti_from_token, hash_backup_code, is_backup_code_format, 34 verify_access_token, verify_access_token_hs256, verify_access_token_typed, verify_backup_code, 35 verify_refresh_token, verify_refresh_token_hs256, verify_token, verify_totp_code, 36}; 37 38pub fn encrypt_totp_secret(secret: &[u8]) -> Result<Vec<u8>, String> { 39 crate::config::encrypt_key(secret) 40} 41 42pub fn decrypt_totp_secret(encrypted: &[u8], version: i32) -> Result<Vec<u8>, String> { 43 crate::config::decrypt_key(encrypted, Some(version)) 44} 45 46const KEY_CACHE_TTL_SECS: u64 = 300; 47const SESSION_CACHE_TTL_SECS: u64 = 60; 48const USER_STATUS_CACHE_TTL_SECS: u64 = 60; 49 50#[derive(Serialize, Deserialize)] 51struct CachedUserStatus { 52 deactivated: bool, 53 takendown: bool, 54 is_admin: bool, 55} 56 57#[derive(Debug, Clone, Copy, PartialEq, Eq)] 58pub enum TokenValidationError { 59 AccountDeactivated, 60 AccountTakedown, 61 KeyDecryptionFailed, 62 AuthenticationFailed, 63 TokenExpired, 64 OAuthTokenExpired, 65} 66 67impl fmt::Display for TokenValidationError { 68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 match self { 70 Self::AccountDeactivated => write!(f, "AccountDeactivated"), 71 Self::AccountTakedown => write!(f, "AccountTakedown"), 72 Self::KeyDecryptionFailed => write!(f, "KeyDecryptionFailed"), 73 Self::AuthenticationFailed => write!(f, "AuthenticationFailed"), 74 Self::TokenExpired | Self::OAuthTokenExpired => write!(f, "ExpiredToken"), 75 } 76 } 77} 78 79pub struct AuthenticatedUser { 80 pub did: Did, 81 pub key_bytes: Option<Vec<u8>>, 82 pub is_oauth: bool, 83 pub is_admin: bool, 84 pub status: AccountStatus, 85 pub scope: Option<String>, 86 pub controller_did: Option<Did>, 87} 88 89impl AuthenticatedUser { 90 pub fn permissions(&self) -> ScopePermissions { 91 if let Some(ref scope) = self.scope 92 && scope != SCOPE_ACCESS 93 { 94 return ScopePermissions::from_scope_string(Some(scope)); 95 } 96 if !self.is_oauth { 97 return ScopePermissions::from_scope_string(Some("atproto")); 98 } 99 ScopePermissions::from_scope_string(self.scope.as_deref()) 100 } 101 102 pub fn is_takendown(&self) -> bool { 103 self.status.is_takendown() 104 } 105} 106 107pub async fn validate_bearer_token( 108 db: &PgPool, 109 token: &str, 110) -> Result<AuthenticatedUser, TokenValidationError> { 111 validate_bearer_token_with_options_internal(db, None, token, false, false).await 112} 113 114pub async fn validate_bearer_token_allow_deactivated( 115 db: &PgPool, 116 token: &str, 117) -> Result<AuthenticatedUser, TokenValidationError> { 118 validate_bearer_token_with_options_internal(db, None, token, true, false).await 119} 120 121pub async fn validate_bearer_token_cached( 122 db: &PgPool, 123 cache: &dyn Cache, 124 token: &str, 125) -> Result<AuthenticatedUser, TokenValidationError> { 126 validate_bearer_token_with_options_internal(db, Some(cache), token, false, false).await 127} 128 129pub async fn validate_bearer_token_cached_allow_deactivated( 130 db: &PgPool, 131 cache: &dyn Cache, 132 token: &str, 133) -> Result<AuthenticatedUser, TokenValidationError> { 134 validate_bearer_token_with_options_internal(db, Some(cache), token, true, false).await 135} 136 137pub async fn validate_bearer_token_for_service_auth( 138 db: &PgPool, 139 token: &str, 140) -> Result<AuthenticatedUser, TokenValidationError> { 141 validate_bearer_token_with_options_internal(db, None, token, true, true).await 142} 143 144pub async fn validate_bearer_token_allow_takendown( 145 db: &PgPool, 146 token: &str, 147) -> Result<AuthenticatedUser, TokenValidationError> { 148 validate_bearer_token_with_options_internal(db, None, token, false, true).await 149} 150 151async fn validate_bearer_token_with_options_internal( 152 db: &PgPool, 153 cache: Option<&dyn Cache>, 154 token: &str, 155 allow_deactivated: bool, 156 allow_takendown: bool, 157) -> Result<AuthenticatedUser, TokenValidationError> { 158 let did_from_token = get_did_from_token(token).ok(); 159 160 if let Some(ref did) = did_from_token { 161 let key_cache_key = format!("auth:key:{}", did); 162 let mut cached_key: Option<Vec<u8>> = None; 163 164 if let Some(c) = cache { 165 cached_key = c.get_bytes(&key_cache_key).await; 166 if cached_key.is_some() { 167 crate::metrics::record_auth_cache_hit("key"); 168 } else { 169 crate::metrics::record_auth_cache_miss("key"); 170 } 171 } 172 173 let (decrypted_key, deactivated_at, takedown_ref, is_admin) = if let Some(key) = cached_key 174 { 175 let status_cache_key = format!("auth:status:{}", did); 176 let cached_status: Option<CachedUserStatus> = if let Some(c) = cache { 177 c.get(&status_cache_key) 178 .await 179 .and_then(|s| serde_json::from_str(&s).ok()) 180 } else { 181 None 182 }; 183 184 if let Some(status) = cached_status { 185 ( 186 Some(key), 187 if status.deactivated { 188 Some(chrono::Utc::now()) 189 } else { 190 None 191 }, 192 if status.takendown { 193 Some("takendown".to_string()) 194 } else { 195 None 196 }, 197 status.is_admin, 198 ) 199 } else { 200 let user_status = sqlx::query!( 201 "SELECT deactivated_at, takedown_ref, is_admin FROM users WHERE did = $1", 202 did 203 ) 204 .fetch_optional(db) 205 .await 206 .ok() 207 .flatten(); 208 209 match user_status { 210 Some(status) => { 211 if let Some(c) = cache { 212 let cached = CachedUserStatus { 213 deactivated: status.deactivated_at.is_some(), 214 takendown: status.takedown_ref.is_some(), 215 is_admin: status.is_admin, 216 }; 217 if let Ok(json) = serde_json::to_string(&cached) { 218 let _ = c 219 .set( 220 &status_cache_key, 221 &json, 222 Duration::from_secs(USER_STATUS_CACHE_TTL_SECS), 223 ) 224 .await; 225 } 226 } 227 ( 228 Some(key), 229 status.deactivated_at, 230 status.takedown_ref, 231 status.is_admin, 232 ) 233 } 234 None => (None, None, None, false), 235 } 236 } 237 } else if let Some(user) = sqlx::query!( 238 "SELECT k.key_bytes, k.encryption_version, u.deactivated_at, u.takedown_ref, u.is_admin 239 FROM users u 240 JOIN user_keys k ON u.id = k.user_id 241 WHERE u.did = $1", 242 did 243 ) 244 .fetch_optional(db) 245 .await 246 .ok() 247 .flatten() 248 { 249 let key = crate::config::decrypt_key(&user.key_bytes, user.encryption_version) 250 .map_err(|_| TokenValidationError::KeyDecryptionFailed)?; 251 252 if let Some(c) = cache { 253 let _ = c 254 .set_bytes( 255 &key_cache_key, 256 &key, 257 Duration::from_secs(KEY_CACHE_TTL_SECS), 258 ) 259 .await; 260 261 let status_cache_key = format!("auth:status:{}", did); 262 let cached = CachedUserStatus { 263 deactivated: user.deactivated_at.is_some(), 264 takendown: user.takedown_ref.is_some(), 265 is_admin: user.is_admin, 266 }; 267 if let Ok(json) = serde_json::to_string(&cached) { 268 let _ = c 269 .set( 270 &status_cache_key, 271 &json, 272 Duration::from_secs(USER_STATUS_CACHE_TTL_SECS), 273 ) 274 .await; 275 } 276 } 277 278 ( 279 Some(key), 280 user.deactivated_at, 281 user.takedown_ref, 282 user.is_admin, 283 ) 284 } else { 285 (None, None, None, false) 286 }; 287 288 if let Some(decrypted_key) = decrypted_key { 289 if !allow_deactivated && deactivated_at.is_some() { 290 return Err(TokenValidationError::AccountDeactivated); 291 } 292 293 if !allow_takendown && takedown_ref.is_some() { 294 return Err(TokenValidationError::AccountTakedown); 295 } 296 297 match verify_access_token_typed(token, &decrypted_key) { 298 Ok(token_data) => { 299 let jti = &token_data.claims.jti; 300 let session_cache_key = format!("auth:session:{}:{}", did, jti); 301 let mut session_valid = false; 302 303 if let Some(c) = cache { 304 if let Some(cached_value) = c.get(&session_cache_key).await { 305 session_valid = cached_value == "1"; 306 crate::metrics::record_auth_cache_hit("session"); 307 } else { 308 crate::metrics::record_auth_cache_miss("session"); 309 } 310 } 311 312 if !session_valid { 313 let session_row = sqlx::query!( 314 "SELECT access_expires_at FROM session_tokens WHERE did = $1 AND access_jti = $2", 315 did, 316 jti 317 ) 318 .fetch_optional(db) 319 .await 320 .ok() 321 .flatten(); 322 323 if let Some(row) = session_row { 324 if row.access_expires_at > chrono::Utc::now() { 325 session_valid = true; 326 if let Some(c) = cache { 327 let _ = c 328 .set( 329 &session_cache_key, 330 "1", 331 Duration::from_secs(SESSION_CACHE_TTL_SECS), 332 ) 333 .await; 334 } 335 } else { 336 return Err(TokenValidationError::TokenExpired); 337 } 338 } 339 } 340 341 if session_valid { 342 let controller_did = token_data 343 .claims 344 .act 345 .as_ref() 346 .map(|a| Did::new_unchecked(a.sub.clone())); 347 let status = 348 AccountStatus::from_db_fields(takedown_ref.as_deref(), deactivated_at); 349 return Ok(AuthenticatedUser { 350 did: Did::new_unchecked(did.clone()), 351 key_bytes: Some(decrypted_key), 352 is_oauth: false, 353 is_admin, 354 status, 355 scope: token_data.claims.scope.clone(), 356 controller_did, 357 }); 358 } 359 } 360 Err(TokenVerifyError::Expired) => { 361 return Err(TokenValidationError::TokenExpired); 362 } 363 Err(TokenVerifyError::Invalid) => {} 364 } 365 } 366 } 367 368 if let Ok(oauth_info) = crate::oauth::verify::extract_oauth_token_info(token) 369 && let Some(oauth_token) = sqlx::query!( 370 r#"SELECT t.did, t.expires_at, u.deactivated_at, u.takedown_ref, u.is_admin, 371 k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 372 FROM oauth_token t 373 JOIN users u ON t.did = u.did 374 LEFT JOIN user_keys k ON u.id = k.user_id 375 WHERE t.token_id = $1"#, 376 oauth_info.token_id 377 ) 378 .fetch_optional(db) 379 .await 380 .ok() 381 .flatten() 382 { 383 let status = AccountStatus::from_db_fields( 384 oauth_token.takedown_ref.as_deref(), 385 oauth_token.deactivated_at, 386 ); 387 388 if !allow_deactivated && status.is_deactivated() { 389 return Err(TokenValidationError::AccountDeactivated); 390 } 391 392 if !allow_takendown && status.is_takendown() { 393 return Err(TokenValidationError::AccountTakedown); 394 } 395 396 let now = chrono::Utc::now(); 397 if oauth_token.expires_at > now { 398 let key_bytes = if let (Some(kb), Some(ev)) = 399 (&oauth_token.key_bytes, oauth_token.encryption_version) 400 { 401 crate::config::decrypt_key(kb, Some(ev)).ok() 402 } else { 403 None 404 }; 405 return Ok(AuthenticatedUser { 406 did: Did::new_unchecked(oauth_token.did), 407 key_bytes, 408 is_oauth: true, 409 is_admin: oauth_token.is_admin, 410 status, 411 scope: oauth_info.scope, 412 controller_did: oauth_info.controller_did.map(Did::new_unchecked), 413 }); 414 } else { 415 return Err(TokenValidationError::TokenExpired); 416 } 417 } 418 419 Err(TokenValidationError::AuthenticationFailed) 420} 421 422pub async fn invalidate_auth_cache(cache: &dyn Cache, did: &str) { 423 let key_cache_key = format!("auth:key:{}", did); 424 let status_cache_key = format!("auth:status:{}", did); 425 let _ = cache.delete(&key_cache_key).await; 426 let _ = cache.delete(&status_cache_key).await; 427} 428 429#[allow(clippy::too_many_arguments)] 430pub async fn validate_token_with_dpop( 431 db: &PgPool, 432 token: &str, 433 is_dpop_token: bool, 434 dpop_proof: Option<&str>, 435 http_method: &str, 436 http_uri: &str, 437 allow_deactivated: bool, 438 allow_takendown: bool, 439) -> Result<AuthenticatedUser, TokenValidationError> { 440 if !is_dpop_token { 441 if allow_takendown { 442 return validate_bearer_token_allow_takendown(db, token).await; 443 } else if allow_deactivated { 444 return validate_bearer_token_allow_deactivated(db, token).await; 445 } else { 446 return validate_bearer_token(db, token).await; 447 } 448 } 449 match crate::oauth::verify::verify_oauth_access_token( 450 db, 451 token, 452 dpop_proof, 453 http_method, 454 http_uri, 455 ) 456 .await 457 { 458 Ok(result) => { 459 let user_info = sqlx::query!( 460 r#"SELECT u.deactivated_at, u.takedown_ref, u.is_admin, 461 k.key_bytes as "key_bytes?", k.encryption_version as "encryption_version?" 462 FROM users u 463 LEFT JOIN user_keys k ON u.id = k.user_id 464 WHERE u.did = $1"#, 465 result.did 466 ) 467 .fetch_optional(db) 468 .await 469 .ok() 470 .flatten(); 471 let Some(user_info) = user_info else { 472 return Err(TokenValidationError::AuthenticationFailed); 473 }; 474 let status = AccountStatus::from_db_fields( 475 user_info.takedown_ref.as_deref(), 476 user_info.deactivated_at, 477 ); 478 if !allow_deactivated && status.is_deactivated() { 479 return Err(TokenValidationError::AccountDeactivated); 480 } 481 if !allow_takendown && status.is_takendown() { 482 return Err(TokenValidationError::AccountTakedown); 483 } 484 let key_bytes = if let (Some(kb), Some(ev)) = 485 (&user_info.key_bytes, user_info.encryption_version) 486 { 487 crate::config::decrypt_key(kb, Some(ev)).ok() 488 } else { 489 None 490 }; 491 Ok(AuthenticatedUser { 492 did: Did::new_unchecked(result.did), 493 key_bytes, 494 is_oauth: true, 495 is_admin: user_info.is_admin, 496 status, 497 scope: result.scope, 498 controller_did: None, 499 }) 500 } 501 Err(crate::oauth::OAuthError::ExpiredToken(_)) => { 502 Err(TokenValidationError::OAuthTokenExpired) 503 } 504 Err(_) => Err(TokenValidationError::AuthenticationFailed), 505 } 506}