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