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 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}