1use std::sync::Arc;
10
11use axum::Json;
12use axum::extract::{FromRef, FromRequestParts};
13use axum::http::StatusCode;
14use axum::http::request::Parts;
15use axum::response::{IntoResponse, Response};
16use axum_extra::extract::cookie::CookieJar;
17use chrono::Utc;
18use ironflow_store::entities::ApiKeyScope;
19use ironflow_store::store::Store;
20use serde_json::json;
21use tracing::error;
22use uuid::Uuid;
23
24use crate::cookies::AUTH_COOKIE_NAME;
25use crate::jwt::{AccessToken, AccessTokenClaims, JwtConfig};
26use crate::password;
27
28#[derive(Debug, Clone)]
48pub struct AuthenticatedUser {
49 pub user_id: Uuid,
51 pub username: String,
53 pub is_admin: bool,
55}
56
57impl<S> FromRequestParts<S> for AuthenticatedUser
58where
59 S: Send + Sync,
60 Arc<JwtConfig>: FromRef<S>,
61 Arc<dyn Store>: FromRef<S>,
62{
63 type Rejection = AuthRejection;
64
65 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
66 let jwt_config = Arc::<JwtConfig>::from_ref(state);
67 let store = Arc::<dyn Store>::from_ref(state);
68
69 let jar = CookieJar::from_headers(&parts.headers);
70 let token = jar
71 .get(AUTH_COOKIE_NAME)
72 .map(|c| c.value().to_string())
73 .or_else(|| {
74 parts
75 .headers
76 .get("authorization")
77 .and_then(|v| v.to_str().ok())
78 .and_then(|v| v.strip_prefix("Bearer "))
79 .map(|t| t.to_string())
80 });
81
82 let token = token.ok_or(AuthRejection {
83 status: StatusCode::UNAUTHORIZED,
84 code: "MISSING_TOKEN",
85 message: "No authentication token provided",
86 })?;
87
88 let claims = verify_access_token(&token, &jwt_config, store.as_ref()).await?;
89
90 Ok(AuthenticatedUser {
91 user_id: claims.user_id,
92 username: claims.username,
93 is_admin: claims.is_admin,
94 })
95 }
96}
97
98pub(crate) async fn verify_access_token(
107 token: &str,
108 jwt_config: &JwtConfig,
109 store: &dyn Store,
110) -> Result<AccessTokenClaims, AuthRejection> {
111 let mut claims = AccessToken::decode(token, jwt_config).map_err(|_| AuthRejection {
112 status: StatusCode::UNAUTHORIZED,
113 code: "INVALID_TOKEN",
114 message: "Invalid or expired authentication token",
115 })?;
116
117 let user = store.find_user_by_id(claims.user_id).await.map_err(|e| {
118 error!(user_id = %claims.user_id, error = %e, "failed to look up user for token");
119 AuthRejection {
120 status: StatusCode::INTERNAL_SERVER_ERROR,
121 code: "INTERNAL_ERROR",
122 message: "Failed to look up user",
123 }
124 })?;
125
126 let user = user
127 .filter(|user| user.token_version == claims.ver)
128 .ok_or(AuthRejection {
129 status: StatusCode::UNAUTHORIZED,
130 code: "TOKEN_REVOKED",
131 message: "Authentication token has been revoked",
132 })?;
133 claims.is_admin = user.is_admin;
134 claims.username = user.username;
135
136 Ok(claims)
137}
138
139pub struct AuthRejection {
141 status: StatusCode,
142 code: &'static str,
143 message: &'static str,
144}
145
146impl IntoResponse for AuthRejection {
147 fn into_response(self) -> Response {
148 let body = json!({
149 "error": {
150 "code": self.code,
151 "message": self.message,
152 }
153 });
154 (self.status, Json(body)).into_response()
155 }
156}
157
158pub const API_KEY_PREFIX: &str = "irfl_";
164
165pub const API_KEY_SUFFIX_LEN: usize = 8;
167
168#[derive(Debug, Clone)]
182pub struct ApiKeyAuth {
183 pub key_id: Uuid,
185 pub user_id: Uuid,
187 pub key_name: String,
189 pub scopes: Vec<ApiKeyScope>,
191 pub owner_is_admin: bool,
193}
194
195impl ApiKeyAuth {
196 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
198 ApiKeyScope::has_permission(&self.scopes, required)
199 }
200}
201
202pub struct ApiKeyRejection {
204 status: StatusCode,
205 code: &'static str,
206 message: &'static str,
207}
208
209impl IntoResponse for ApiKeyRejection {
210 fn into_response(self) -> Response {
211 let body = json!({
212 "error": {
213 "code": self.code,
214 "message": self.message,
215 }
216 });
217 (self.status, Json(body)).into_response()
218 }
219}
220
221impl<S> FromRequestParts<S> for ApiKeyAuth
222where
223 S: Send + Sync,
224 Arc<dyn Store>: FromRef<S>,
225{
226 type Rejection = ApiKeyRejection;
227
228 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
229 let store = Arc::<dyn Store>::from_ref(state);
230
231 let token = parts
232 .headers
233 .get("authorization")
234 .and_then(|v| v.to_str().ok())
235 .and_then(|v| v.strip_prefix("Bearer "))
236 .ok_or(ApiKeyRejection {
237 status: StatusCode::UNAUTHORIZED,
238 code: "MISSING_TOKEN",
239 message: "No authentication token provided",
240 })?;
241
242 if !token.starts_with(API_KEY_PREFIX) {
243 return Err(ApiKeyRejection {
244 status: StatusCode::UNAUTHORIZED,
245 code: "INVALID_TOKEN",
246 message: "Expected API key (irfl_...) in Authorization header",
247 });
248 }
249
250 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(API_KEY_SUFFIX_LEN);
251 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
252
253 let api_key = store
254 .find_api_key_by_prefix(prefix)
255 .await
256 .map_err(|_| ApiKeyRejection {
257 status: StatusCode::INTERNAL_SERVER_ERROR,
258 code: "INTERNAL_ERROR",
259 message: "Failed to look up API key",
260 })?
261 .ok_or(ApiKeyRejection {
262 status: StatusCode::UNAUTHORIZED,
263 code: "INVALID_TOKEN",
264 message: "Invalid API key",
265 })?;
266
267 if !api_key.is_active {
268 return Err(ApiKeyRejection {
269 status: StatusCode::UNAUTHORIZED,
270 code: "KEY_DISABLED",
271 message: "API key is disabled",
272 });
273 }
274
275 if let Some(expires_at) = api_key.expires_at
276 && expires_at < Utc::now()
277 {
278 return Err(ApiKeyRejection {
279 status: StatusCode::UNAUTHORIZED,
280 code: "KEY_EXPIRED",
281 message: "API key has expired",
282 });
283 }
284
285 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
286 status: StatusCode::INTERNAL_SERVER_ERROR,
287 code: "INTERNAL_ERROR",
288 message: "Failed to verify API key",
289 })?;
290
291 if !valid {
292 return Err(ApiKeyRejection {
293 status: StatusCode::UNAUTHORIZED,
294 code: "INVALID_TOKEN",
295 message: "Invalid API key",
296 });
297 }
298
299 let _ = store.touch_api_key(api_key.id).await;
300
301 let owner = store
302 .find_user_by_id(api_key.user_id)
303 .await
304 .map_err(|_| ApiKeyRejection {
305 status: StatusCode::INTERNAL_SERVER_ERROR,
306 code: "INTERNAL_ERROR",
307 message: "Failed to look up API key owner",
308 })?;
309 let owner_is_admin = owner.map(|u| u.is_admin).unwrap_or(false);
310
311 Ok(ApiKeyAuth {
312 key_id: api_key.id,
313 user_id: api_key.user_id,
314 key_name: api_key.name,
315 scopes: api_key.scopes,
316 owner_is_admin,
317 })
318 }
319}
320
321#[derive(Debug, Clone)]
340pub struct Authenticated {
341 pub user_id: Uuid,
343 pub method: AuthMethod,
345}
346
347#[derive(Debug, Clone)]
349pub enum AuthMethod {
350 Jwt {
352 username: String,
354 is_admin: bool,
356 },
357 ApiKey {
359 key_id: Uuid,
361 key_name: String,
363 scopes: Vec<ApiKeyScope>,
365 owner_is_admin: bool,
367 },
368}
369
370impl Authenticated {
371 pub fn is_admin(&self) -> bool {
379 match &self.method {
380 AuthMethod::Jwt { is_admin, .. } => *is_admin,
381 AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
382 }
383 }
384}
385
386impl<S> FromRequestParts<S> for Authenticated
387where
388 S: Send + Sync,
389 Arc<JwtConfig>: FromRef<S>,
390 Arc<dyn Store>: FromRef<S>,
391{
392 type Rejection = AuthRejection;
393
394 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
395 let jar = CookieJar::from_headers(&parts.headers);
396 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
397
398 let header_token = parts
399 .headers
400 .get("authorization")
401 .and_then(|v| v.to_str().ok())
402 .and_then(|v| v.strip_prefix("Bearer "))
403 .map(|t| t.to_string());
404
405 if let Some(ref token) = header_token
407 && token.starts_with(API_KEY_PREFIX)
408 {
409 let api_key_auth =
410 ApiKeyAuth::from_request_parts(parts, state)
411 .await
412 .map_err(|_| AuthRejection {
413 status: StatusCode::UNAUTHORIZED,
414 code: "INVALID_TOKEN",
415 message: "Invalid or expired authentication token",
416 })?;
417 return Ok(Authenticated {
418 user_id: api_key_auth.user_id,
419 method: AuthMethod::ApiKey {
420 key_id: api_key_auth.key_id,
421 key_name: api_key_auth.key_name,
422 scopes: api_key_auth.scopes,
423 owner_is_admin: api_key_auth.owner_is_admin,
424 },
425 });
426 }
427
428 let token = cookie_token.or(header_token).ok_or(AuthRejection {
430 status: StatusCode::UNAUTHORIZED,
431 code: "MISSING_TOKEN",
432 message: "No authentication token provided",
433 })?;
434
435 let jwt_config = Arc::<JwtConfig>::from_ref(state);
436 let store = Arc::<dyn Store>::from_ref(state);
437 let claims = verify_access_token(&token, &jwt_config, store.as_ref()).await?;
438
439 Ok(Authenticated {
440 user_id: claims.user_id,
441 method: AuthMethod::Jwt {
442 username: claims.username,
443 is_admin: claims.is_admin,
444 },
445 })
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use std::sync::Arc;
452
453 use axum::body::Body;
454 use axum::extract::FromRef;
455 use axum::http::{Request, StatusCode};
456 use axum::routing::get;
457 use axum::{Json, Router};
458 use http_body_util::BodyExt;
459 use ironflow_store::entities::NewUser;
460 use ironflow_store::entities::{ApiKeyScope, NewApiKey};
461 use ironflow_store::memory::InMemoryStore;
462 use ironflow_store::store::Store;
463 use serde_json::Value;
464 use tower::ServiceExt;
465 use uuid::Uuid;
466
467 use crate::jwt::{AccessToken, JwtConfig};
468 use crate::password;
469
470 use super::*;
471
472 #[derive(Clone)]
473 struct TestState {
474 jwt_config: Arc<JwtConfig>,
475 store: Arc<dyn Store>,
476 }
477
478 impl FromRef<TestState> for Arc<JwtConfig> {
479 fn from_ref(state: &TestState) -> Self {
480 state.jwt_config.clone()
481 }
482 }
483
484 impl FromRef<TestState> for Arc<dyn Store> {
485 fn from_ref(state: &TestState) -> Self {
486 state.store.clone()
487 }
488 }
489
490 fn test_jwt_config() -> Arc<JwtConfig> {
491 Arc::new(JwtConfig {
492 secret: "test-secret-key-for-extractor-tests".to_string(),
493 access_token_ttl_secs: 900,
494 refresh_token_ttl_secs: 604800,
495 cookie_domain: None,
496 cookie_secure: false,
497 })
498 }
499
500 fn test_state() -> TestState {
501 TestState {
502 jwt_config: test_jwt_config(),
503 store: Arc::new(InMemoryStore::new()),
504 }
505 }
506
507 async fn response_json(resp: axum::http::Response<Body>) -> Value {
508 let body = resp.into_body().collect().await.unwrap().to_bytes();
509 serde_json::from_slice(&body).unwrap()
510 }
511
512 #[tokio::test]
517 async fn jwt_extractor_from_bearer_header() {
518 let state = test_state();
519 let user_id = stored_user(&state.store, "alice", false).await;
520 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
521
522 let app = Router::new()
523 .route(
524 "/me",
525 get(|user: AuthenticatedUser| async move {
526 Json(json!({
527 "user_id": user.user_id,
528 "username": user.username,
529 "is_admin": user.is_admin
530 }))
531 }),
532 )
533 .with_state(state);
534
535 let req = Request::builder()
536 .uri("/me")
537 .header("authorization", format!("Bearer {}", token.0))
538 .body(Body::empty())
539 .unwrap();
540
541 let resp = app.oneshot(req).await.unwrap();
542 assert_eq!(resp.status(), StatusCode::OK);
543
544 let json = response_json(resp).await;
545 assert_eq!(json["user_id"], user_id.to_string());
546 assert_eq!(json["username"], "alice");
547 assert_eq!(json["is_admin"], false);
548 }
549
550 #[tokio::test]
551 async fn jwt_extractor_from_cookie() {
552 let state = test_state();
553 let user_id = stored_user(&state.store, "bob", true).await;
554 let token = AccessToken::for_user(user_id, "bob", true, &state.jwt_config).unwrap();
555
556 let app = Router::new()
557 .route(
558 "/me",
559 get(|user: AuthenticatedUser| async move {
560 Json(json!({ "username": user.username, "is_admin": user.is_admin }))
561 }),
562 )
563 .with_state(state);
564
565 let req = Request::builder()
566 .uri("/me")
567 .header("cookie", format!("{}={}", AUTH_COOKIE_NAME, token.0))
568 .body(Body::empty())
569 .unwrap();
570
571 let resp = app.oneshot(req).await.unwrap();
572 assert_eq!(resp.status(), StatusCode::OK);
573
574 let json = response_json(resp).await;
575 assert_eq!(json["username"], "bob");
576 assert_eq!(json["is_admin"], true);
577 }
578
579 #[tokio::test]
580 async fn jwt_extractor_rejects_missing_token() {
581 let app = Router::new()
582 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
583 .with_state(test_state());
584
585 let req = Request::builder().uri("/me").body(Body::empty()).unwrap();
586
587 let resp = app.oneshot(req).await.unwrap();
588 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
589
590 let json = response_json(resp).await;
591 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
592 }
593
594 #[tokio::test]
595 async fn jwt_extractor_rejects_invalid_token() {
596 let app = Router::new()
597 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
598 .with_state(test_state());
599
600 let req = Request::builder()
601 .uri("/me")
602 .header("authorization", "Bearer invalid.token.here")
603 .body(Body::empty())
604 .unwrap();
605
606 let resp = app.oneshot(req).await.unwrap();
607 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
608
609 let json = response_json(resp).await;
610 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
611 }
612
613 async fn setup_api_key(store: &Arc<dyn Store>) -> (Uuid, String) {
618 let user = store
619 .create_user(NewUser {
620 email: "key-owner@test.com".to_string(),
621 username: "keyowner".to_string(),
622 password_hash: password::hash("pass123").unwrap(),
623 is_admin: Some(true),
624 })
625 .await
626 .unwrap();
627
628 let raw_key = "irfl_abcdef12rest-of-secret-key";
629 let key_hash = password::hash(raw_key).unwrap();
630 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
631
632 store
633 .create_api_key(NewApiKey {
634 user_id: user.id,
635 name: "test-key".to_string(),
636 key_hash,
637 key_prefix: prefix.to_string(),
638 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
639 expires_at: None,
640 rate_limit_override: None,
641 })
642 .await
643 .unwrap();
644
645 (user.id, raw_key.to_string())
646 }
647
648 #[tokio::test]
649 async fn api_key_extractor_valid_key() {
650 let state = test_state();
651 let (user_id, raw_key) = setup_api_key(&state.store).await;
652
653 let app = Router::new()
654 .route(
655 "/check",
656 get(|key: ApiKeyAuth| async move {
657 Json(json!({
658 "user_id": key.user_id,
659 "key_name": key.key_name,
660 "scopes": key.scopes,
661 "owner_is_admin": key.owner_is_admin
662 }))
663 }),
664 )
665 .with_state(state);
666
667 let req = Request::builder()
668 .uri("/check")
669 .header("authorization", format!("Bearer {raw_key}"))
670 .body(Body::empty())
671 .unwrap();
672
673 let resp = app.oneshot(req).await.unwrap();
674 assert_eq!(resp.status(), StatusCode::OK);
675
676 let json = response_json(resp).await;
677 assert_eq!(json["user_id"], user_id.to_string());
678 assert_eq!(json["key_name"], "test-key");
679 assert_eq!(json["owner_is_admin"], true);
680 }
681
682 #[tokio::test]
683 async fn api_key_extractor_rejects_missing_header() {
684 let app = Router::new()
685 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
686 .with_state(test_state());
687
688 let req = Request::builder()
689 .uri("/check")
690 .body(Body::empty())
691 .unwrap();
692
693 let resp = app.oneshot(req).await.unwrap();
694 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
695
696 let json = response_json(resp).await;
697 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
698 }
699
700 #[tokio::test]
701 async fn api_key_extractor_rejects_non_irfl_token() {
702 let app = Router::new()
703 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
704 .with_state(test_state());
705
706 let req = Request::builder()
707 .uri("/check")
708 .header("authorization", "Bearer not-an-api-key")
709 .body(Body::empty())
710 .unwrap();
711
712 let resp = app.oneshot(req).await.unwrap();
713 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
714
715 let json = response_json(resp).await;
716 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
717 }
718
719 #[tokio::test]
720 async fn api_key_extractor_rejects_unknown_key() {
721 let app = Router::new()
722 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
723 .with_state(test_state());
724
725 let req = Request::builder()
726 .uri("/check")
727 .header("authorization", "Bearer irfl_unknown1rest-of-key")
728 .body(Body::empty())
729 .unwrap();
730
731 let resp = app.oneshot(req).await.unwrap();
732 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
733
734 let json = response_json(resp).await;
735 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
736 }
737
738 #[test]
743 fn has_scope_returns_true_for_granted_scope() {
744 let auth = ApiKeyAuth {
745 key_id: Uuid::now_v7(),
746 user_id: Uuid::now_v7(),
747 key_name: "k".to_string(),
748 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
749 owner_is_admin: false,
750 };
751 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
752 assert!(auth.has_scope(&ApiKeyScope::WorkflowsRead));
753 }
754
755 #[test]
756 fn has_scope_returns_false_for_missing_scope() {
757 let auth = ApiKeyAuth {
758 key_id: Uuid::now_v7(),
759 user_id: Uuid::now_v7(),
760 key_name: "k".to_string(),
761 scopes: vec![ApiKeyScope::RunsRead],
762 owner_is_admin: false,
763 };
764 assert!(!auth.has_scope(&ApiKeyScope::RunsWrite));
765 assert!(!auth.has_scope(&ApiKeyScope::Admin));
766 }
767
768 #[test]
769 fn has_scope_admin_grants_everything() {
770 let auth = ApiKeyAuth {
771 key_id: Uuid::now_v7(),
772 user_id: Uuid::now_v7(),
773 key_name: "k".to_string(),
774 scopes: vec![ApiKeyScope::Admin],
775 owner_is_admin: true,
776 };
777 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
778 assert!(auth.has_scope(&ApiKeyScope::RunsWrite));
779 assert!(auth.has_scope(&ApiKeyScope::StatsRead));
780 }
781
782 #[tokio::test]
787 async fn authenticated_via_jwt() {
788 let state = test_state();
789 let user_id = stored_user(&state.store, "alice", true).await;
790 let token = AccessToken::for_user(user_id, "alice", true, &state.jwt_config).unwrap();
791
792 let app = Router::new()
793 .route(
794 "/auth",
795 get(|auth: Authenticated| async move {
796 Json(json!({
797 "user_id": auth.user_id,
798 "is_admin": auth.is_admin(),
799 "method": match auth.method {
800 AuthMethod::Jwt { .. } => "jwt",
801 AuthMethod::ApiKey { .. } => "api_key",
802 }
803 }))
804 }),
805 )
806 .with_state(state);
807
808 let req = Request::builder()
809 .uri("/auth")
810 .header("authorization", format!("Bearer {}", token.0))
811 .body(Body::empty())
812 .unwrap();
813
814 let resp = app.oneshot(req).await.unwrap();
815 assert_eq!(resp.status(), StatusCode::OK);
816
817 let json = response_json(resp).await;
818 assert_eq!(json["user_id"], user_id.to_string());
819 assert_eq!(json["is_admin"], true);
820 assert_eq!(json["method"], "jwt");
821 }
822
823 #[tokio::test]
824 async fn authenticated_via_api_key() {
825 let state = test_state();
826 let (user_id, raw_key) = setup_api_key(&state.store).await;
827
828 let app = Router::new()
829 .route(
830 "/auth",
831 get(|auth: Authenticated| async move {
832 Json(json!({
833 "user_id": auth.user_id,
834 "is_admin": auth.is_admin(),
835 "method": match auth.method {
836 AuthMethod::Jwt { .. } => "jwt",
837 AuthMethod::ApiKey { .. } => "api_key",
838 }
839 }))
840 }),
841 )
842 .with_state(state);
843
844 let req = Request::builder()
845 .uri("/auth")
846 .header("authorization", format!("Bearer {raw_key}"))
847 .body(Body::empty())
848 .unwrap();
849
850 let resp = app.oneshot(req).await.unwrap();
851 assert_eq!(resp.status(), StatusCode::OK);
852
853 let json = response_json(resp).await;
854 assert_eq!(json["user_id"], user_id.to_string());
855 assert_eq!(json["is_admin"], true);
856 assert_eq!(json["method"], "api_key");
857 }
858
859 #[tokio::test]
860 async fn authenticated_rejects_missing_token() {
861 let app = Router::new()
862 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
863 .with_state(test_state());
864
865 let req = Request::builder().uri("/auth").body(Body::empty()).unwrap();
866
867 let resp = app.oneshot(req).await.unwrap();
868 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
869
870 let json = response_json(resp).await;
871 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
872 }
873
874 #[test]
879 fn is_admin_jwt_true() {
880 let auth = Authenticated {
881 user_id: Uuid::now_v7(),
882 method: AuthMethod::Jwt {
883 username: "admin".to_string(),
884 is_admin: true,
885 },
886 };
887 assert!(auth.is_admin());
888 }
889
890 #[test]
891 fn is_admin_jwt_false() {
892 let auth = Authenticated {
893 user_id: Uuid::now_v7(),
894 method: AuthMethod::Jwt {
895 username: "user".to_string(),
896 is_admin: false,
897 },
898 };
899 assert!(!auth.is_admin());
900 }
901
902 #[test]
903 fn is_admin_api_key_true() {
904 let auth = Authenticated {
905 user_id: Uuid::now_v7(),
906 method: AuthMethod::ApiKey {
907 key_id: Uuid::now_v7(),
908 key_name: "k".to_string(),
909 scopes: vec![],
910 owner_is_admin: true,
911 },
912 };
913 assert!(auth.is_admin());
914 }
915
916 #[test]
917 fn is_admin_api_key_false() {
918 let auth = Authenticated {
919 user_id: Uuid::now_v7(),
920 method: AuthMethod::ApiKey {
921 key_id: Uuid::now_v7(),
922 key_name: "k".to_string(),
923 scopes: vec![],
924 owner_is_admin: false,
925 },
926 };
927 assert!(!auth.is_admin());
928 }
929
930 #[tokio::test]
935 async fn auth_rejection_into_response() {
936 let rejection = AuthRejection {
937 status: StatusCode::UNAUTHORIZED,
938 code: "TEST_CODE",
939 message: "test message",
940 };
941 let resp = rejection.into_response();
942 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
943
944 let body = resp.into_body().collect().await.unwrap().to_bytes();
945 let json: Value = serde_json::from_slice(&body).unwrap();
946 assert_eq!(json["error"]["code"], "TEST_CODE");
947 assert_eq!(json["error"]["message"], "test message");
948 }
949
950 #[tokio::test]
951 async fn api_key_rejection_into_response() {
952 let rejection = ApiKeyRejection {
953 status: StatusCode::FORBIDDEN,
954 code: "KEY_DISABLED",
955 message: "API key is disabled",
956 };
957 let resp = rejection.into_response();
958 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
959
960 let body = resp.into_body().collect().await.unwrap().to_bytes();
961 let json: Value = serde_json::from_slice(&body).unwrap();
962 assert_eq!(json["error"]["code"], "KEY_DISABLED");
963 assert_eq!(json["error"]["message"], "API key is disabled");
964 }
965
966 async fn stored_user(store: &Arc<dyn Store>, username: &str, is_admin: bool) -> Uuid {
971 store
972 .create_user(NewUser {
973 email: format!("{username}@test.com"),
974 username: username.to_string(),
975 password_hash: "argon2hash".to_string(),
976 is_admin: Some(is_admin),
977 })
978 .await
979 .unwrap()
980 .id
981 }
982
983 fn bearer(uri: &str, token: &str) -> Request<Body> {
984 Request::builder()
985 .uri(uri)
986 .header("authorization", format!("Bearer {token}"))
987 .body(Body::empty())
988 .unwrap()
989 }
990
991 #[tokio::test]
992 async fn jwt_extractor_accepts_token_at_current_version() {
993 let state = test_state();
994 let user_id = stored_user(&state.store, "alice", false).await;
995 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
996 let app = Router::new()
997 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
998 .with_state(state);
999
1000 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1001 assert_eq!(resp.status(), StatusCode::OK);
1002 }
1003
1004 #[tokio::test]
1005 async fn jwt_extractor_rejects_token_after_session_revoked() {
1006 let state = test_state();
1007 let user_id = stored_user(&state.store, "alice", false).await;
1008 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1009 state.store.revoke_user_sessions(user_id).await.unwrap();
1010
1011 let app = Router::new()
1012 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1013 .with_state(state);
1014
1015 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1016 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1017 let json = response_json(resp).await;
1018 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1019 }
1020
1021 #[tokio::test]
1022 async fn jwt_extractor_accepts_token_issued_at_new_version() {
1023 let state = test_state();
1024 let user_id = stored_user(&state.store, "alice", false).await;
1025 let v = state.store.revoke_user_sessions(user_id).await.unwrap();
1026 let jwt = state.jwt_config.clone();
1027 let token = AccessToken::for_user_with_version(user_id, "alice", false, v, &jwt).unwrap();
1028
1029 let app = Router::new()
1030 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1031 .with_state(state);
1032
1033 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1034 assert_eq!(resp.status(), StatusCode::OK);
1035 }
1036
1037 #[tokio::test]
1038 async fn dual_auth_rejects_jwt_after_session_revoked() {
1039 let state = test_state();
1040 let user_id = stored_user(&state.store, "alice", false).await;
1041 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1042 state.store.revoke_user_sessions(user_id).await.unwrap();
1043
1044 let app = Router::new()
1045 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
1046 .with_state(state);
1047
1048 let resp = app.oneshot(bearer("/auth", &token.0)).await.unwrap();
1049 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1050 let json = response_json(resp).await;
1051 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1052 }
1053
1054 #[tokio::test]
1055 async fn jwt_extractor_rejects_token_of_deleted_user() {
1056 let state = test_state();
1057 let user_id = stored_user(&state.store, "alice", false).await;
1058 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1059 state.store.delete_user(user_id).await.unwrap();
1060
1061 let app = Router::new()
1062 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1063 .with_state(state);
1064
1065 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1066 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1067 let json = response_json(resp).await;
1068 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1069 }
1070
1071 #[tokio::test]
1072 async fn jwt_extractor_rejects_token_of_unknown_user() {
1073 let state = test_state();
1074 let token =
1075 AccessToken::for_user(Uuid::now_v7(), "ghost", true, &state.jwt_config).unwrap();
1076
1077 let app = Router::new()
1078 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1079 .with_state(state);
1080
1081 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1082 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1083 let json = response_json(resp).await;
1084 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1085 }
1086
1087 #[tokio::test]
1088 async fn dual_auth_rejects_jwt_of_deleted_user() {
1089 let state = test_state();
1090 let user_id = stored_user(&state.store, "alice", false).await;
1091 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1092 state.store.delete_user(user_id).await.unwrap();
1093
1094 let app = Router::new()
1095 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
1096 .with_state(state);
1097
1098 let resp = app.oneshot(bearer("/auth", &token.0)).await.unwrap();
1099 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1100 let json = response_json(resp).await;
1101 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1102 }
1103
1104 #[tokio::test]
1105 async fn demoted_admin_token_is_rejected() {
1106 let state = test_state();
1107 let admin_id = stored_user(&state.store, "root", true).await;
1108 let token = AccessToken::for_user(admin_id, "root", true, &state.jwt_config).unwrap();
1109 state.store.update_user_role(admin_id, false).await.unwrap();
1110
1111 let app = Router::new()
1112 .route("/admin", get(|_auth: Authenticated| async { "ok" }))
1113 .with_state(state);
1114
1115 let resp = app.oneshot(bearer("/admin", &token.0)).await.unwrap();
1116 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1117 let json = response_json(resp).await;
1118 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1119 }
1120
1121 #[tokio::test]
1122 async fn jwt_is_admin_comes_from_store() {
1123 let state = test_state();
1124 let user_id = stored_user(&state.store, "member", false).await;
1125 let token = AccessToken::for_user(user_id, "forged", true, &state.jwt_config).unwrap();
1127
1128 let app = Router::new()
1129 .route(
1130 "/auth",
1131 get(|auth: Authenticated| async move {
1132 let username = match &auth.method {
1133 AuthMethod::Jwt { username, .. } => username.clone(),
1134 AuthMethod::ApiKey { .. } => String::new(),
1135 };
1136 Json(json!({ "is_admin": auth.is_admin(), "username": username }))
1137 }),
1138 )
1139 .route(
1140 "/me",
1141 get(|user: AuthenticatedUser| async move {
1142 Json(json!({ "is_admin": user.is_admin, "username": user.username }))
1143 }),
1144 )
1145 .with_state(state);
1146
1147 let resp = app
1148 .clone()
1149 .oneshot(bearer("/auth", &token.0))
1150 .await
1151 .unwrap();
1152 assert_eq!(resp.status(), StatusCode::OK);
1153 let json = response_json(resp).await;
1154 assert_eq!(json["is_admin"], false);
1155 assert_eq!(json["username"], "member");
1156
1157 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1158 assert_eq!(resp.status(), StatusCode::OK);
1159 let json = response_json(resp).await;
1160 assert_eq!(json["is_admin"], false);
1161 assert_eq!(json["username"], "member");
1162 }
1163}