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