Skip to main content

kellnr_auth/
token.rs

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