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