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}