Skip to main content

kellnr_http/
token.rs

1use std::iter;
2use std::sync::Arc;
3use std::time::Duration;
4
5use axum::http::{HeaderMap, StatusCode};
6use base64::Engine;
7use base64::engine::general_purpose::STANDARD;
8use kellnr_common::account_name::UserName;
9use kellnr_common::plain_token::PlainToken;
10use kellnr_db::DbProvider;
11use kellnr_db::error::DbError;
12use kellnr_db::password::hash_token;
13use rand::distr::Alphanumeric;
14use rand::{RngExt, rng};
15use serde::Deserialize;
16use tokio::time::sleep;
17use tracing::warn;
18use utoipa::ToSchema;
19
20use crate::password::{PasswordAuthError, authenticate_password};
21use crate::state::AppStateData;
22use crate::token_cache::CachedTokenData;
23
24/// The user of the cargo token or basic auth credentials of a request.
25#[derive(Debug)]
26pub(crate) struct Token {
27    /// The auth token, `None` for basic auth with a password
28    pub value: Option<PlainToken>,
29    pub user: UserName,
30    pub is_admin: bool,
31    pub is_read_only: bool,
32}
33
34pub fn generate_token() -> PlainToken {
35    let mut rng = rng();
36    iter::repeat(())
37        .map(|()| rng.sample(Alphanumeric))
38        .map(char::from)
39        .take(32)
40        .collect::<String>()
41        .into()
42}
43
44impl Token {
45    /// The user of the `Authorization` header. Without the header, the
46    /// result is 401.
47    pub(crate) async fn from_header(
48        headers: &HeaderMap,
49        state: &AppStateData,
50    ) -> Result<Token, StatusCode> {
51        let (db, cache) = (&state.db, &state.token_cache);
52        let mut token = headers
53            .get("Authorization")
54            .ok_or(StatusCode::UNAUTHORIZED)?
55            .to_str()
56            .map_err(|_| StatusCode::BAD_REQUEST)?;
57
58        // Handle basic authentication
59        if let Some(credentials) = strip_scheme(token, "Basic") {
60            if state.settings.oauth2.enforced {
61                return Err(StatusCode::FORBIDDEN);
62            }
63
64            let decoded = STANDARD
65                .decode(credentials)
66                .map_err(|_| StatusCode::BAD_REQUEST)?;
67            let decoded_str = String::from_utf8(decoded).map_err(|_| StatusCode::BAD_REQUEST)?;
68            let (user, pwd) = decoded_str.split_once(':').ok_or(StatusCode::BAD_REQUEST)?;
69
70            return authenticate_basic(user, pwd, state).await;
71        }
72
73        // Handle bearer authentication (uses token cache)
74        if let Some(bearer) = strip_scheme(token, "Bearer") {
75            token = bearer;
76        }
77
78        // Check cache first
79        if let Some(cached) = cache.get(token).await {
80            return Ok(Token {
81                value: Some(PlainToken::from(token)),
82                user: cached.user,
83                is_admin: cached.is_admin,
84                is_read_only: cached.is_read_only,
85            });
86        }
87
88        // Cache miss - query DB with retry logic. The generation is read
89        // before the lookup so a concurrent invalidation prevents caching a
90        // result that may already be outdated.
91        let generation = cache.generation();
92        let Ok(user) = get_user_with_retry(
93            db,
94            &PlainToken::from(token),
95            state.settings.registry.token_db_retry_count,
96            state.settings.registry.token_db_retry_delay_ms,
97        )
98        .await
99        else {
100            return Err(StatusCode::FORBIDDEN);
101        };
102
103        // Insert into cache on successful DB lookup
104        cache
105            .insert(
106                token.to_string(),
107                CachedTokenData {
108                    user: user.name.clone(),
109                    is_admin: user.is_admin,
110                    is_read_only: user.is_read_only,
111                },
112                generation,
113            )
114            .await;
115
116        Ok(Token {
117            value: Some(PlainToken::from(token)),
118            user: user.name,
119            is_admin: user.is_admin,
120            is_read_only: user.is_read_only,
121        })
122    }
123}
124
125/// Cache key for verified basic auth credentials. Only a hash of the
126/// credentials is kept in memory, and the prefix keeps the key space apart
127/// from bearer tokens.
128fn basic_auth_cache_key(user: &str, pwd: &str) -> String {
129    format!("basic:{}", hash_token(&format!("{user}:{pwd}")))
130}
131
132/// Verifies basic auth credentials. Cargo sends them with every request and
133/// password hashing is deliberately slow, so successful verifications are
134/// cached like bearer tokens and dropped by the same invalidations. A cache
135/// hit implies correct credentials, so it bypasses the attempt throttle and
136/// keeps CI working while an attacker locks out the username.
137async fn authenticate_basic(
138    user: &str,
139    pwd: &str,
140    state: &AppStateData,
141) -> Result<Token, StatusCode> {
142    let cache = &state.token_cache;
143    let cache_key = basic_auth_cache_key(user, pwd);
144    if let Some(cached) = cache.get(&cache_key).await {
145        return Ok(Token {
146            value: None,
147            user: cached.user,
148            is_admin: cached.is_admin,
149            is_read_only: cached.is_read_only,
150        });
151    }
152
153    // An empty user name cannot exist, so it fails like a wrong password.
154    let user = UserName::new(user).map_err(|_| StatusCode::FORBIDDEN)?;
155    let generation = cache.generation();
156    let user = authenticate_password(state.db.as_ref(), &state.login_throttle, &user, pwd)
157        .await
158        .map_err(|e| match e {
159            PasswordAuthError::Throttled => StatusCode::TOO_MANY_REQUESTS,
160            PasswordAuthError::Invalid => StatusCode::FORBIDDEN,
161        })?;
162
163    cache
164        .insert(
165            cache_key,
166            CachedTokenData {
167                user: user.name.clone(),
168                is_admin: user.is_admin,
169                is_read_only: user.is_read_only,
170            },
171            generation,
172        )
173        .await;
174
175    Ok(Token {
176        value: None,
177        user: user.name,
178        is_admin: user.is_admin,
179        is_read_only: user.is_read_only,
180    })
181}
182
183async fn get_user_with_retry(
184    db: &Arc<dyn DbProvider>,
185    token: &PlainToken,
186    max_retries: u32,
187    delay_ms: u64,
188) -> Result<kellnr_db::User, DbError> {
189    let mut attempts = 0;
190
191    loop {
192        match db.get_user_from_token(token).await {
193            Ok(user) => return Ok(user),
194            Err(e) => {
195                // Do not retry on "not found" errors - these are definitive
196                if matches!(e, DbError::TokenNotFound | DbError::UserNotFound(_)) {
197                    return Err(e);
198                }
199
200                attempts += 1;
201                if attempts > max_retries {
202                    warn!(
203                        "Failed to get user from token after {} retries: {}",
204                        max_retries, e
205                    );
206                    return Err(e);
207                }
208
209                warn!(
210                    "Transient DB error on attempt {}/{}, retrying in {}ms: {}",
211                    attempts,
212                    max_retries + 1,
213                    delay_ms,
214                    e
215                );
216                sleep(Duration::from_millis(delay_ms)).await;
217            }
218        }
219    }
220}
221
222#[derive(Deserialize, ToSchema)]
223pub struct NewTokenReqData {
224    pub name: String,
225}
226
227/// The credentials of an `Authorization` header value with the given auth
228/// `scheme`, which is compared case-insensitively as required by RFC 9110.
229fn strip_scheme<'a>(value: &'a str, scheme: &str) -> Option<&'a str> {
230    let (prefix, rest) = value.split_at_checked(scheme.len())?;
231    let credentials = rest.strip_prefix(' ')?;
232    prefix.eq_ignore_ascii_case(scheme).then_some(credentials)
233}
234
235#[cfg(test)]
236mod tests {
237    use std::sync::atomic::{AtomicU32, Ordering};
238
239    use kellnr_db::error::DbError;
240    use kellnr_db::mock::MockDb;
241    use kellnr_db::{User, UserFlags};
242    use kellnr_settings::Settings;
243    use kellnr_test_db::TestDb;
244    use mockall::predicate::*;
245
246    use super::*;
247    use crate::login_throttle::LoginThrottle;
248    use crate::test_support::sqlite_test_state;
249    use crate::token_cache::TokenCacheManager;
250
251    fn test_user() -> User {
252        User {
253            id: kellnr_db::UserId::new(1),
254            name: UserName::new("test_user").unwrap(),
255            pwd: String::new(),
256            salt: String::new(),
257            is_admin: false,
258            is_read_only: false,
259            created: String::new(),
260        }
261    }
262
263    fn test_user_name() -> UserName {
264        UserName::new("test_user").unwrap()
265    }
266
267    fn test_settings() -> Arc<Settings> {
268        let mut settings = kellnr_settings::test_settings();
269        settings.registry.token_cache_enabled = true;
270        settings.registry.token_cache_ttl_seconds = 60;
271        settings.registry.token_cache_max_capacity = 100;
272        settings.registry.token_db_retry_count = 3;
273        settings.registry.token_db_retry_delay_ms = 1;
274        Arc::new(settings)
275    }
276
277    fn cache() -> Arc<TokenCacheManager> {
278        Arc::new(TokenCacheManager::new(true, Duration::from_secs(60), 100))
279    }
280
281    /// App state on a database with the user `test_user`, its password
282    /// `correct_password` and its cargo token `valid_token`.
283    async fn db_state(
284        cache: &Arc<TokenCacheManager>,
285        settings: Arc<Settings>,
286    ) -> (AppStateData, TestDb) {
287        let (state, db) = sqlite_test_state().await;
288        db.add_user(&test_user_name(), "correct_password", UserFlags::default())
289            .await
290            .unwrap();
291        db.add_auth_token("test", &PlainToken::from("valid_token"), &test_user_name())
292            .await
293            .unwrap();
294        let state = AppStateData {
295            token_cache: cache.clone(),
296            settings,
297            ..state
298        };
299        (state, db)
300    }
301
302    /// Settings with SSO-only login enforced.
303    fn sso_enforced_settings() -> Arc<Settings> {
304        let mut settings = Settings::clone(&test_settings());
305        settings.oauth2.enabled = true;
306        settings.oauth2.enforced = true;
307        Arc::new(settings)
308    }
309
310    fn basic_auth_header(user: &str, pwd: &str) -> HeaderMap {
311        let encoded = STANDARD.encode(format!("{user}:{pwd}"));
312        let mut headers = HeaderMap::new();
313        headers.insert("Authorization", format!("Basic {encoded}").parse().unwrap());
314        headers
315    }
316
317    fn bearer_header(token: &str) -> HeaderMap {
318        let mut headers = HeaderMap::new();
319        headers.insert("Authorization", format!("Bearer {token}").parse().unwrap());
320        headers
321    }
322
323    async fn change_pwd(db: &TestDb, pwd: &str) {
324        db.change_pwd(&test_user_name(), pwd).await.unwrap();
325    }
326
327    // ===================
328    // Retry Logic Tests
329    // ===================
330
331    #[tokio::test]
332    async fn test_retry_succeeds_on_first_attempt() {
333        let (state, _db) = db_state(&cache(), test_settings()).await;
334
335        let result = get_user_with_retry(&state.db, &PlainToken::from("valid_token"), 3, 10).await;
336
337        assert_eq!(result.unwrap().name, "test_user");
338    }
339
340    // The tests below inject transient database errors and count the
341    // attempts, so they need a mock database.
342
343    #[tokio::test]
344    async fn test_retry_succeeds_after_transient_error() {
345        // DB fails once with transient error, then succeeds
346        let call_count = Arc::new(AtomicU32::new(0));
347        let call_count_clone = call_count.clone();
348
349        let mut mock_db = MockDb::new();
350        mock_db
351            .expect_get_user_from_token()
352            .with(eq(PlainToken::from("token")))
353            .times(2)
354            .returning(move |_| {
355                let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
356                if count == 0 {
357                    Err(DbError::PostgresError(sea_orm::DbErr::ConnectionAcquire(
358                        sea_orm::error::ConnAcquireErr::Timeout,
359                    )))
360                } else {
361                    Ok(test_user())
362                }
363            });
364
365        let db: Arc<dyn DbProvider> = Arc::new(mock_db);
366        let result = get_user_with_retry(&db, &PlainToken::from("token"), 3, 1).await;
367
368        assert!(result.is_ok());
369        assert_eq!(call_count.load(Ordering::SeqCst), 2);
370    }
371
372    /// An unknown token is final, so the lookup must not wait for a retry.
373    #[tokio::test]
374    async fn test_no_retry_on_token_not_found() {
375        let (state, _db) = db_state(&cache(), test_settings()).await;
376
377        let result = tokio::time::timeout(
378            Duration::from_secs(10),
379            get_user_with_retry(&state.db, &PlainToken::from("invalid_token"), 3, 60_000),
380        )
381        .await
382        .expect("lookup was retried");
383
384        assert!(matches!(result.unwrap_err(), DbError::TokenNotFound));
385    }
386
387    #[tokio::test]
388    async fn test_no_retry_on_user_not_found() {
389        // UserNotFound should NOT trigger retries
390        let mut mock_db = MockDb::new();
391        mock_db
392            .expect_get_user_from_token()
393            .with(eq(PlainToken::from("orphan_token")))
394            .times(1)
395            .returning(|_| Err(DbError::UserNotFound("orphan".to_string())));
396
397        let db: Arc<dyn DbProvider> = Arc::new(mock_db);
398        let result = get_user_with_retry(&db, &PlainToken::from("orphan_token"), 3, 10).await;
399
400        assert!(result.is_err());
401        assert!(matches!(result.unwrap_err(), DbError::UserNotFound(_)));
402    }
403
404    #[tokio::test]
405    async fn test_exhausts_retries_on_persistent_error() {
406        // DB fails all retries with transient error
407        let call_count = Arc::new(AtomicU32::new(0));
408        let call_count_clone = call_count.clone();
409
410        let mut mock_db = MockDb::new();
411        mock_db
412            .expect_get_user_from_token()
413            .with(eq(PlainToken::from("token")))
414            .times(4) // 1 initial + 3 retries
415            .returning(move |_| {
416                call_count_clone.fetch_add(1, Ordering::SeqCst);
417                Err(DbError::PostgresError(sea_orm::DbErr::ConnectionAcquire(
418                    sea_orm::error::ConnAcquireErr::Timeout,
419                )))
420            });
421
422        let db: Arc<dyn DbProvider> = Arc::new(mock_db);
423        let result = get_user_with_retry(&db, &PlainToken::from("token"), 3, 1).await;
424
425        assert!(result.is_err());
426        assert_eq!(call_count.load(Ordering::SeqCst), 4);
427    }
428
429    #[tokio::test]
430    async fn test_zero_retries_only_attempts_once() {
431        // With max_retries = 0, should only attempt once
432        let call_count = Arc::new(AtomicU32::new(0));
433        let call_count_clone = call_count.clone();
434
435        let mut mock_db = MockDb::new();
436        mock_db
437            .expect_get_user_from_token()
438            .with(eq(PlainToken::from("token")))
439            .times(1)
440            .returning(move |_| {
441                call_count_clone.fetch_add(1, Ordering::SeqCst);
442                Err(DbError::PostgresError(sea_orm::DbErr::ConnectionAcquire(
443                    sea_orm::error::ConnAcquireErr::Timeout,
444                )))
445            });
446
447        let db: Arc<dyn DbProvider> = Arc::new(mock_db);
448        let result = get_user_with_retry(&db, &PlainToken::from("token"), 0, 1).await;
449
450        assert!(result.is_err());
451        assert_eq!(call_count.load(Ordering::SeqCst), 1);
452    }
453
454    // =====================================
455    // Token Extraction with Cache Tests
456    // =====================================
457
458    /// The cached token does not exist in the database, so only the cache
459    /// can know it.
460    #[tokio::test]
461    async fn test_cache_hit_returns_cached_token() {
462        let cache = cache();
463        cache
464            .insert(
465                "cached_token".to_string(),
466                CachedTokenData {
467                    user: UserName::new("cached_user").unwrap(),
468                    is_admin: true,
469                    is_read_only: false,
470                },
471                cache.generation(),
472            )
473            .await;
474        let (state, _db) = db_state(&cache, test_settings()).await;
475
476        let token = Token::from_header(&bearer_header("cached_token"), &state)
477            .await
478            .unwrap();
479
480        assert_eq!(token.user, "cached_user");
481        assert!(token.is_admin);
482    }
483
484    #[tokio::test]
485    async fn test_cache_miss_queries_db_and_caches() {
486        let cache = cache();
487        let (state, db) = db_state(&cache, test_settings()).await;
488        let db_user = UserName::new("db_user").unwrap();
489        let flags = UserFlags {
490            read_only: true,
491            ..UserFlags::default()
492        };
493        db.add_user(&db_user, "pwd", flags).await.unwrap();
494        db.add_auth_token("test", &PlainToken::from("new_token"), &db_user)
495            .await
496            .unwrap();
497
498        let token = Token::from_header(&bearer_header("new_token"), &state)
499            .await
500            .unwrap();
501
502        assert_eq!(token.user, "db_user");
503        assert!(token.is_read_only);
504        let cached = cache.get("new_token").await;
505        assert_eq!(cached.unwrap().user, "db_user");
506    }
507
508    /// A token revoked while its DB lookup is in flight must not end up in
509    /// the cache, otherwise it would stay usable for the whole TTL. The mock
510    /// revokes it in the middle of the lookup.
511    #[tokio::test]
512    async fn test_invalidation_during_lookup_is_not_cached() {
513        let cache = cache();
514        let cache_in_db = cache.clone();
515
516        let mut mock_db = MockDb::new();
517        mock_db
518            .expect_get_user_from_token()
519            .with(eq(PlainToken::from("racy_token")))
520            .times(1)
521            .returning(move |_| {
522                cache_in_db.invalidate_all();
523                Ok(test_user())
524            });
525
526        let state = AppStateData {
527            db: Arc::new(mock_db),
528            token_cache: cache.clone(),
529            settings: test_settings(),
530            ..crate::test_support::test_state()
531        };
532
533        let token = Token::from_header(&bearer_header("racy_token"), &state)
534            .await
535            .unwrap();
536
537        assert_eq!(token.user, "test_user");
538        assert!(cache.get("racy_token").await.is_none());
539    }
540
541    #[tokio::test]
542    async fn test_cache_miss_with_invalid_token_returns_forbidden() {
543        let cache = cache();
544        let (state, _db) = db_state(&cache, test_settings()).await;
545
546        let result = Token::from_header(&bearer_header("bad_token"), &state).await;
547
548        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
549        assert!(cache.get("bad_token").await.is_none());
550    }
551
552    // ==========================
553    // SSO Enforcement Tests
554    // ==========================
555
556    /// Basic auth carries a username and password, so it must be rejected
557    /// outright when SSO is the only permitted login method, even with the
558    /// correct password.
559    #[tokio::test]
560    async fn test_basic_auth_rejected_when_sso_enforced() {
561        let (state, _db) = db_state(&cache(), sso_enforced_settings()).await;
562        let headers = basic_auth_header("test_user", "correct_password");
563
564        let result = Token::from_header(&headers, &state).await;
565
566        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
567    }
568
569    /// The lowercase spelling of the scheme must not slip past the check.
570    #[tokio::test]
571    async fn test_lowercase_basic_auth_rejected_when_sso_enforced() {
572        let (state, _db) = db_state(&cache(), sso_enforced_settings()).await;
573        let encoded = STANDARD.encode("test_user:correct_password");
574        let mut headers = HeaderMap::new();
575        headers.insert("Authorization", format!("basic {encoded}").parse().unwrap());
576
577        let result = Token::from_header(&headers, &state).await;
578
579        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
580    }
581
582    /// Enforcement only targets passwords. Cargo tokens are independent of the
583    /// login method and must keep working so publishing does not break.
584    #[tokio::test]
585    async fn test_bearer_token_still_works_when_sso_enforced() {
586        let (state, _db) = db_state(&cache(), sso_enforced_settings()).await;
587
588        let token = Token::from_header(&bearer_header("valid_token"), &state)
589            .await
590            .expect("bearer tokens must remain valid under SSO enforcement");
591
592        assert_eq!(token.user, "test_user");
593    }
594
595    /// Without enforcement, basic auth keeps authenticating against the DB.
596    #[tokio::test]
597    async fn test_basic_auth_allowed_when_sso_not_enforced() {
598        let (state, _db) = db_state(&cache(), test_settings()).await;
599        let headers = basic_auth_header("test_user", "correct_password");
600
601        let token = Token::from_header(&headers, &state)
602            .await
603            .expect("basic auth should work when SSO is not enforced");
604
605        assert_eq!(token.user, "test_user");
606    }
607
608    // ==========================
609    // Basic Auth Cache Tests
610    // ==========================
611
612    /// Password hashing is slow by design, so cargo requests with the same
613    /// credentials must not verify the password again. The password changes
614    /// in the database after the first request, so the later requests can
615    /// only succeed from the cache.
616    #[tokio::test]
617    async fn test_basic_auth_success_is_cached() {
618        let (state, db) = db_state(&cache(), test_settings()).await;
619        let headers = basic_auth_header("test_user", "correct_password");
620
621        Token::from_header(&headers, &state).await.unwrap();
622        change_pwd(&db, "new_password").await;
623
624        for _ in 0..2 {
625            let token = Token::from_header(&headers, &state).await.unwrap();
626            assert_eq!(token.user, "test_user");
627        }
628    }
629
630    /// A failed login is checked again, so it succeeds once the password is
631    /// valid.
632    #[tokio::test]
633    async fn test_basic_auth_failure_is_not_cached() {
634        let (state, db) = db_state(&cache(), test_settings()).await;
635        let headers = basic_auth_header("test_user", "new_password");
636
637        let result = Token::from_header(&headers, &state).await;
638        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
639        change_pwd(&db, "new_password").await;
640
641        let token = Token::from_header(&headers, &state).await.unwrap();
642        assert_eq!(token.user, "test_user");
643    }
644
645    /// A different password for a cached user must still be verified.
646    #[tokio::test]
647    async fn test_basic_auth_cache_is_keyed_by_password() {
648        let (state, _db) = db_state(&cache(), test_settings()).await;
649
650        let ok = basic_auth_header("test_user", "correct_password");
651        Token::from_header(&ok, &state).await.unwrap();
652
653        let wrong = basic_auth_header("test_user", "wrong_password");
654        let result = Token::from_header(&wrong, &state).await;
655        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
656    }
657
658    /// Password changes invalidate the whole cache, after which the old
659    /// credentials must be verified against the DB again.
660    #[tokio::test]
661    async fn test_basic_auth_cache_respects_invalidation() {
662        let cache = cache();
663        let (state, db) = db_state(&cache, test_settings()).await;
664        let headers = basic_auth_header("test_user", "correct_password");
665
666        Token::from_header(&headers, &state).await.unwrap();
667        change_pwd(&db, "new_password").await;
668        cache.invalidate_all();
669
670        let result = Token::from_header(&headers, &state).await;
671        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
672    }
673
674    /// Brute forcing a password through cargo basic auth is throttled per
675    /// username, and the password is no longer checked once locked.
676    #[tokio::test]
677    async fn test_basic_auth_failures_are_throttled() {
678        let (state, _db) = db_state(&cache(), test_settings()).await;
679        let state = AppStateData {
680            login_throttle: Arc::new(LoginThrottle::new(2, Duration::from_mins(5))),
681            ..state
682        };
683        let guess = basic_auth_header("test_user", "guess");
684
685        for _ in 0..2 {
686            let result = Token::from_header(&guess, &state).await;
687            assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
688        }
689        let valid = basic_auth_header("test_user", "correct_password");
690        let result = Token::from_header(&valid, &state).await;
691        assert_eq!(result.unwrap_err(), StatusCode::TOO_MANY_REQUESTS);
692    }
693
694    /// Credentials verified before the lockout keep working from the cache,
695    /// so an attacker cannot lock out CI that uses basic auth.
696    #[tokio::test]
697    async fn test_cached_basic_auth_bypasses_throttle() {
698        let (state, _db) = db_state(&cache(), test_settings()).await;
699        let state = AppStateData {
700            login_throttle: Arc::new(LoginThrottle::new(1, Duration::from_mins(5))),
701            ..state
702        };
703        let valid = basic_auth_header("test_user", "correct_password");
704        Token::from_header(&valid, &state).await.unwrap();
705
706        let guess = basic_auth_header("test_user", "guess");
707        assert_eq!(
708            Token::from_header(&guess, &state).await.unwrap_err(),
709            StatusCode::FORBIDDEN
710        );
711        assert_eq!(
712            Token::from_header(&guess, &state).await.unwrap_err(),
713            StatusCode::TOO_MANY_REQUESTS
714        );
715
716        let token = Token::from_header(&valid, &state).await.unwrap();
717        assert_eq!(token.user, "test_user");
718    }
719
720    #[test]
721    fn test_basic_auth_cache_key_does_not_contain_password() {
722        let key = basic_auth_cache_key("test_user", "secret");
723
724        assert!(key.starts_with("basic:"));
725        assert!(!key.contains("secret"));
726        assert_ne!(key, basic_auth_cache_key("test_user", "other"));
727    }
728
729    #[tokio::test]
730    async fn test_no_authorization_header_returns_unauthorized() {
731        let state = crate::test_support::test_state();
732
733        let result = Token::from_header(&HeaderMap::new(), &state).await;
734
735        assert_eq!(result.unwrap_err(), StatusCode::UNAUTHORIZED);
736    }
737
738    /// Without the cache, a revoked token stops working at once.
739    #[tokio::test]
740    async fn test_disabled_cache_always_queries_db() {
741        let cache = Arc::new(TokenCacheManager::new(false, Duration::from_secs(60), 100));
742        let (state, db) = db_state(&cache, test_settings()).await;
743        let headers = bearer_header("valid_token");
744
745        Token::from_header(&headers, &state).await.unwrap();
746        let tokens = db.get_auth_tokens(&test_user_name()).await.unwrap();
747        db.delete_auth_token(&test_user_name(), tokens[0].id)
748            .await
749            .unwrap();
750
751        let result = Token::from_header(&headers, &state).await;
752        assert_eq!(result.unwrap_err(), StatusCode::FORBIDDEN);
753    }
754
755    #[tokio::test]
756    async fn test_lowercase_bearer_prefix_works() {
757        let (state, _db) = db_state(&cache(), test_settings()).await;
758        let mut headers = HeaderMap::new();
759        headers.insert("Authorization", "bearer valid_token".parse().unwrap());
760
761        let token = Token::from_header(&headers, &state).await.unwrap();
762
763        assert_eq!(token.user, "test_user");
764    }
765
766    #[test]
767    fn auth_scheme_is_case_insensitive() {
768        for value in ["Bearer abc", "bearer abc", "BEARER abc", "bEaReR abc"] {
769            assert_eq!(strip_scheme(value, "Bearer"), Some("abc"), "{value}");
770        }
771        assert_eq!(strip_scheme("BASIC abc", "Basic"), Some("abc"));
772        assert_eq!(strip_scheme("Bearerabc", "Bearer"), None);
773        assert_eq!(strip_scheme("abc", "Bearer"), None);
774        assert_eq!(strip_scheme("Bäarer abc", "Bearer"), None);
775    }
776}