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}