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,
194}
195
196impl ApiKeyAuth {
197 pub fn has_scope(&self, required: &ApiKeyScope) -> bool {
199 ApiKeyScope::has_permission(&self.scopes, required)
200 }
201}
202
203pub struct ApiKeyRejection {
205 status: StatusCode,
206 code: &'static str,
207 message: &'static str,
208}
209
210impl IntoResponse for ApiKeyRejection {
211 fn into_response(self) -> Response {
212 let body = json!({
213 "error": {
214 "code": self.code,
215 "message": self.message,
216 }
217 });
218 (self.status, Json(body)).into_response()
219 }
220}
221
222impl<S> FromRequestParts<S> for ApiKeyAuth
223where
224 S: Send + Sync,
225 Arc<dyn Store>: FromRef<S>,
226{
227 type Rejection = ApiKeyRejection;
228
229 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
230 let store = Arc::<dyn Store>::from_ref(state);
231
232 let token = parts
233 .headers
234 .get("authorization")
235 .and_then(|v| v.to_str().ok())
236 .and_then(|v| v.strip_prefix("Bearer "))
237 .ok_or(ApiKeyRejection {
238 status: StatusCode::UNAUTHORIZED,
239 code: "MISSING_TOKEN",
240 message: "No authentication token provided",
241 })?;
242
243 if !token.starts_with(API_KEY_PREFIX) {
244 return Err(ApiKeyRejection {
245 status: StatusCode::UNAUTHORIZED,
246 code: "INVALID_TOKEN",
247 message: "Expected API key (irfl_...) in Authorization header",
248 });
249 }
250
251 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(API_KEY_SUFFIX_LEN);
252 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
253
254 let api_key = store
255 .find_api_key_by_prefix(prefix)
256 .await
257 .map_err(|_| ApiKeyRejection {
258 status: StatusCode::INTERNAL_SERVER_ERROR,
259 code: "INTERNAL_ERROR",
260 message: "Failed to look up API key",
261 })?
262 .ok_or(ApiKeyRejection {
263 status: StatusCode::UNAUTHORIZED,
264 code: "INVALID_TOKEN",
265 message: "Invalid API key",
266 })?;
267
268 if !api_key.is_active {
269 return Err(ApiKeyRejection {
270 status: StatusCode::UNAUTHORIZED,
271 code: "KEY_DISABLED",
272 message: "API key is disabled",
273 });
274 }
275
276 if let Some(expires_at) = api_key.expires_at
277 && expires_at < Utc::now()
278 {
279 return Err(ApiKeyRejection {
280 status: StatusCode::UNAUTHORIZED,
281 code: "KEY_EXPIRED",
282 message: "API key has expired",
283 });
284 }
285
286 let valid = password::verify(token, &api_key.key_hash).map_err(|_| ApiKeyRejection {
287 status: StatusCode::INTERNAL_SERVER_ERROR,
288 code: "INTERNAL_ERROR",
289 message: "Failed to verify API key",
290 })?;
291
292 if !valid {
293 return Err(ApiKeyRejection {
294 status: StatusCode::UNAUTHORIZED,
295 code: "INVALID_TOKEN",
296 message: "Invalid API key",
297 });
298 }
299
300 let owner = store
303 .find_user_by_id(api_key.user_id)
304 .await
305 .map_err(|_| ApiKeyRejection {
306 status: StatusCode::INTERNAL_SERVER_ERROR,
307 code: "INTERNAL_ERROR",
308 message: "Failed to look up API key owner",
309 })?
310 .ok_or(ApiKeyRejection {
311 status: StatusCode::UNAUTHORIZED,
312 code: "TOKEN_REVOKED",
313 message: "API key owner no longer exists",
314 })?;
315
316 let _ = store.touch_api_key(api_key.id).await;
317
318 Ok(ApiKeyAuth {
319 key_id: api_key.id,
320 user_id: api_key.user_id,
321 key_name: api_key.name,
322 scopes: api_key.scopes,
323 owner_is_admin: owner.is_admin,
324 })
325 }
326}
327
328#[derive(Debug, Clone)]
347pub struct Authenticated {
348 pub user_id: Uuid,
350 pub method: AuthMethod,
352}
353
354#[derive(Debug, Clone)]
356pub enum AuthMethod {
357 Jwt {
359 username: String,
361 is_admin: bool,
363 },
364 ApiKey {
366 key_id: Uuid,
368 key_name: String,
370 scopes: Vec<ApiKeyScope>,
372 owner_is_admin: bool,
374 },
375}
376
377impl Authenticated {
378 pub fn is_admin(&self) -> bool {
386 match &self.method {
387 AuthMethod::Jwt { is_admin, .. } => *is_admin,
388 AuthMethod::ApiKey { owner_is_admin, .. } => *owner_is_admin,
389 }
390 }
391}
392
393impl<S> FromRequestParts<S> for Authenticated
394where
395 S: Send + Sync,
396 Arc<JwtConfig>: FromRef<S>,
397 Arc<dyn Store>: FromRef<S>,
398{
399 type Rejection = AuthRejection;
400
401 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
402 let jar = CookieJar::from_headers(&parts.headers);
403 let cookie_token = jar.get(AUTH_COOKIE_NAME).map(|c| c.value().to_string());
404
405 let header_token = parts
406 .headers
407 .get("authorization")
408 .and_then(|v| v.to_str().ok())
409 .and_then(|v| v.strip_prefix("Bearer "))
410 .map(|t| t.to_string());
411
412 if let Some(ref token) = header_token
414 && token.starts_with(API_KEY_PREFIX)
415 {
416 let api_key_auth =
417 ApiKeyAuth::from_request_parts(parts, state)
418 .await
419 .map_err(|_| AuthRejection {
420 status: StatusCode::UNAUTHORIZED,
421 code: "INVALID_TOKEN",
422 message: "Invalid or expired authentication token",
423 })?;
424 return Ok(Authenticated {
425 user_id: api_key_auth.user_id,
426 method: AuthMethod::ApiKey {
427 key_id: api_key_auth.key_id,
428 key_name: api_key_auth.key_name,
429 scopes: api_key_auth.scopes,
430 owner_is_admin: api_key_auth.owner_is_admin,
431 },
432 });
433 }
434
435 let token = cookie_token.or(header_token).ok_or(AuthRejection {
437 status: StatusCode::UNAUTHORIZED,
438 code: "MISSING_TOKEN",
439 message: "No authentication token provided",
440 })?;
441
442 let jwt_config = Arc::<JwtConfig>::from_ref(state);
443 let store = Arc::<dyn Store>::from_ref(state);
444 let claims = verify_access_token(&token, &jwt_config, store.as_ref()).await?;
445
446 Ok(Authenticated {
447 user_id: claims.user_id,
448 method: AuthMethod::Jwt {
449 username: claims.username,
450 is_admin: claims.is_admin,
451 },
452 })
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use std::sync::Arc;
459
460 use axum::body::Body;
461 use axum::extract::FromRef;
462 use axum::http::{Request, StatusCode};
463 use axum::routing::get;
464 use axum::{Json, Router};
465 use http_body_util::BodyExt;
466 use ironflow_store::entities::NewUser;
467 use ironflow_store::entities::{ApiKeyScope, NewApiKey};
468 use ironflow_store::memory::InMemoryStore;
469 use ironflow_store::store::Store;
470 use serde_json::Value;
471 use tower::ServiceExt;
472 use uuid::Uuid;
473
474 use crate::jwt::{AccessToken, JwtConfig};
475 use crate::password;
476
477 use super::*;
478
479 #[derive(Clone)]
480 struct TestState {
481 jwt_config: Arc<JwtConfig>,
482 store: Arc<dyn Store>,
483 }
484
485 impl FromRef<TestState> for Arc<JwtConfig> {
486 fn from_ref(state: &TestState) -> Self {
487 state.jwt_config.clone()
488 }
489 }
490
491 impl FromRef<TestState> for Arc<dyn Store> {
492 fn from_ref(state: &TestState) -> Self {
493 state.store.clone()
494 }
495 }
496
497 fn test_jwt_config() -> Arc<JwtConfig> {
498 Arc::new(JwtConfig {
499 secret: "test-secret-key-for-extractor-tests".to_string(),
500 access_token_ttl_secs: 900,
501 refresh_token_ttl_secs: 604800,
502 cookie_domain: None,
503 cookie_secure: false,
504 })
505 }
506
507 fn test_state() -> TestState {
508 TestState {
509 jwt_config: test_jwt_config(),
510 store: Arc::new(InMemoryStore::new()),
511 }
512 }
513
514 async fn response_json(resp: axum::http::Response<Body>) -> Value {
515 let body = resp.into_body().collect().await.unwrap().to_bytes();
516 serde_json::from_slice(&body).unwrap()
517 }
518
519 #[tokio::test]
524 async fn jwt_extractor_from_bearer_header() {
525 let state = test_state();
526 let user_id = stored_user(&state.store, "alice", false).await;
527 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
528
529 let app = Router::new()
530 .route(
531 "/me",
532 get(|user: AuthenticatedUser| async move {
533 Json(json!({
534 "user_id": user.user_id,
535 "username": user.username,
536 "is_admin": user.is_admin
537 }))
538 }),
539 )
540 .with_state(state);
541
542 let req = Request::builder()
543 .uri("/me")
544 .header("authorization", format!("Bearer {}", token.0))
545 .body(Body::empty())
546 .unwrap();
547
548 let resp = app.oneshot(req).await.unwrap();
549 assert_eq!(resp.status(), StatusCode::OK);
550
551 let json = response_json(resp).await;
552 assert_eq!(json["user_id"], user_id.to_string());
553 assert_eq!(json["username"], "alice");
554 assert_eq!(json["is_admin"], false);
555 }
556
557 #[tokio::test]
558 async fn jwt_extractor_from_cookie() {
559 let state = test_state();
560 let user_id = stored_user(&state.store, "bob", true).await;
561 let token = AccessToken::for_user(user_id, "bob", true, &state.jwt_config).unwrap();
562
563 let app = Router::new()
564 .route(
565 "/me",
566 get(|user: AuthenticatedUser| async move {
567 Json(json!({ "username": user.username, "is_admin": user.is_admin }))
568 }),
569 )
570 .with_state(state);
571
572 let req = Request::builder()
573 .uri("/me")
574 .header("cookie", format!("{}={}", AUTH_COOKIE_NAME, token.0))
575 .body(Body::empty())
576 .unwrap();
577
578 let resp = app.oneshot(req).await.unwrap();
579 assert_eq!(resp.status(), StatusCode::OK);
580
581 let json = response_json(resp).await;
582 assert_eq!(json["username"], "bob");
583 assert_eq!(json["is_admin"], true);
584 }
585
586 #[tokio::test]
587 async fn jwt_extractor_rejects_missing_token() {
588 let app = Router::new()
589 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
590 .with_state(test_state());
591
592 let req = Request::builder().uri("/me").body(Body::empty()).unwrap();
593
594 let resp = app.oneshot(req).await.unwrap();
595 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
596
597 let json = response_json(resp).await;
598 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
599 }
600
601 #[tokio::test]
602 async fn jwt_extractor_rejects_invalid_token() {
603 let app = Router::new()
604 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
605 .with_state(test_state());
606
607 let req = Request::builder()
608 .uri("/me")
609 .header("authorization", "Bearer invalid.token.here")
610 .body(Body::empty())
611 .unwrap();
612
613 let resp = app.oneshot(req).await.unwrap();
614 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
615
616 let json = response_json(resp).await;
617 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
618 }
619
620 async fn setup_api_key(store: &Arc<dyn Store>) -> (Uuid, String) {
625 let user = store
626 .create_user(NewUser {
627 email: "key-owner@test.com".to_string(),
628 username: "keyowner".to_string(),
629 password_hash: password::hash("pass123").unwrap(),
630 is_admin: Some(true),
631 })
632 .await
633 .unwrap();
634
635 let raw_key = "irfl_abcdef12rest-of-secret-key";
636 let key_hash = password::hash(raw_key).unwrap();
637 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
638
639 store
640 .create_api_key(NewApiKey {
641 user_id: user.id,
642 name: "test-key".to_string(),
643 key_hash,
644 key_prefix: prefix.to_string(),
645 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
646 expires_at: None,
647 rate_limit_override: None,
648 })
649 .await
650 .unwrap();
651
652 (user.id, raw_key.to_string())
653 }
654
655 #[tokio::test]
656 async fn api_key_extractor_valid_key() {
657 let state = test_state();
658 let (user_id, raw_key) = setup_api_key(&state.store).await;
659
660 let app = Router::new()
661 .route(
662 "/check",
663 get(|key: ApiKeyAuth| async move {
664 Json(json!({
665 "user_id": key.user_id,
666 "key_name": key.key_name,
667 "scopes": key.scopes,
668 "owner_is_admin": key.owner_is_admin
669 }))
670 }),
671 )
672 .with_state(state);
673
674 let req = Request::builder()
675 .uri("/check")
676 .header("authorization", format!("Bearer {raw_key}"))
677 .body(Body::empty())
678 .unwrap();
679
680 let resp = app.oneshot(req).await.unwrap();
681 assert_eq!(resp.status(), StatusCode::OK);
682
683 let json = response_json(resp).await;
684 assert_eq!(json["user_id"], user_id.to_string());
685 assert_eq!(json["key_name"], "test-key");
686 assert_eq!(json["owner_is_admin"], true);
687 }
688
689 async fn setup_orphan_api_key(store: &Arc<dyn Store>) -> String {
691 let raw_key = "irfl_orphan12rest-of-secret-key";
692 let key_hash = password::hash(raw_key).unwrap();
693 let prefix = &raw_key[..API_KEY_PREFIX.len() + API_KEY_SUFFIX_LEN];
694
695 store
696 .create_api_key(NewApiKey {
697 user_id: Uuid::now_v7(),
698 name: "orphan-key".to_string(),
699 key_hash,
700 key_prefix: prefix.to_string(),
701 scopes: vec![ApiKeyScope::Admin],
702 expires_at: None,
703 rate_limit_override: None,
704 })
705 .await
706 .unwrap();
707
708 raw_key.to_string()
709 }
710
711 #[tokio::test]
712 async fn api_key_extractor_rejects_key_of_deleted_owner() {
713 let state = test_state();
714 let (user_id, raw_key) = setup_api_key(&state.store).await;
715 state.store.delete_user(user_id).await.unwrap();
716
717 let app = Router::new()
718 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
719 .with_state(state);
720
721 let resp = app.oneshot(bearer("/check", &raw_key)).await.unwrap();
722 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
723 }
724
725 #[tokio::test]
726 async fn api_key_extractor_rejects_key_whose_owner_is_missing() {
727 let state = test_state();
728 let raw_key = setup_orphan_api_key(&state.store).await;
729
730 let app = Router::new()
731 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
732 .with_state(state.clone());
733
734 let resp = app.oneshot(bearer("/check", &raw_key)).await.unwrap();
735 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
736
737 let json = response_json(resp).await;
738 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
739
740 let key = state
742 .store
743 .find_api_key_by_prefix("irfl_orphan12")
744 .await
745 .unwrap()
746 .unwrap();
747 assert!(key.last_used_at.is_none());
748 }
749
750 #[tokio::test]
751 async fn api_key_dual_auth_rejects_key_whose_owner_is_missing() {
752 let state = test_state();
753 let raw_key = setup_orphan_api_key(&state.store).await;
754
755 let app = Router::new()
756 .route("/check", get(|_auth: Authenticated| async { "ok" }))
757 .with_state(state);
758
759 let resp = app.oneshot(bearer("/check", &raw_key)).await.unwrap();
760 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
761 }
762
763 #[tokio::test]
764 async fn api_key_extractor_rejects_missing_header() {
765 let app = Router::new()
766 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
767 .with_state(test_state());
768
769 let req = Request::builder()
770 .uri("/check")
771 .body(Body::empty())
772 .unwrap();
773
774 let resp = app.oneshot(req).await.unwrap();
775 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
776
777 let json = response_json(resp).await;
778 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
779 }
780
781 #[tokio::test]
782 async fn api_key_extractor_rejects_non_irfl_token() {
783 let app = Router::new()
784 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
785 .with_state(test_state());
786
787 let req = Request::builder()
788 .uri("/check")
789 .header("authorization", "Bearer not-an-api-key")
790 .body(Body::empty())
791 .unwrap();
792
793 let resp = app.oneshot(req).await.unwrap();
794 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
795
796 let json = response_json(resp).await;
797 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
798 }
799
800 #[tokio::test]
801 async fn api_key_extractor_rejects_unknown_key() {
802 let app = Router::new()
803 .route("/check", get(|_key: ApiKeyAuth| async { "ok" }))
804 .with_state(test_state());
805
806 let req = Request::builder()
807 .uri("/check")
808 .header("authorization", "Bearer irfl_unknown1rest-of-key")
809 .body(Body::empty())
810 .unwrap();
811
812 let resp = app.oneshot(req).await.unwrap();
813 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
814
815 let json = response_json(resp).await;
816 assert_eq!(json["error"]["code"], "INVALID_TOKEN");
817 }
818
819 #[test]
824 fn has_scope_returns_true_for_granted_scope() {
825 let auth = ApiKeyAuth {
826 key_id: Uuid::now_v7(),
827 user_id: Uuid::now_v7(),
828 key_name: "k".to_string(),
829 scopes: vec![ApiKeyScope::RunsRead, ApiKeyScope::WorkflowsRead],
830 owner_is_admin: false,
831 };
832 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
833 assert!(auth.has_scope(&ApiKeyScope::WorkflowsRead));
834 }
835
836 #[test]
837 fn has_scope_returns_false_for_missing_scope() {
838 let auth = ApiKeyAuth {
839 key_id: Uuid::now_v7(),
840 user_id: Uuid::now_v7(),
841 key_name: "k".to_string(),
842 scopes: vec![ApiKeyScope::RunsRead],
843 owner_is_admin: false,
844 };
845 assert!(!auth.has_scope(&ApiKeyScope::RunsWrite));
846 assert!(!auth.has_scope(&ApiKeyScope::Admin));
847 }
848
849 #[test]
850 fn has_scope_admin_grants_everything() {
851 let auth = ApiKeyAuth {
852 key_id: Uuid::now_v7(),
853 user_id: Uuid::now_v7(),
854 key_name: "k".to_string(),
855 scopes: vec![ApiKeyScope::Admin],
856 owner_is_admin: true,
857 };
858 assert!(auth.has_scope(&ApiKeyScope::RunsRead));
859 assert!(auth.has_scope(&ApiKeyScope::RunsWrite));
860 assert!(auth.has_scope(&ApiKeyScope::StatsRead));
861 }
862
863 #[tokio::test]
868 async fn authenticated_via_jwt() {
869 let state = test_state();
870 let user_id = stored_user(&state.store, "alice", true).await;
871 let token = AccessToken::for_user(user_id, "alice", true, &state.jwt_config).unwrap();
872
873 let app = Router::new()
874 .route(
875 "/auth",
876 get(|auth: Authenticated| async move {
877 Json(json!({
878 "user_id": auth.user_id,
879 "is_admin": auth.is_admin(),
880 "method": match auth.method {
881 AuthMethod::Jwt { .. } => "jwt",
882 AuthMethod::ApiKey { .. } => "api_key",
883 }
884 }))
885 }),
886 )
887 .with_state(state);
888
889 let req = Request::builder()
890 .uri("/auth")
891 .header("authorization", format!("Bearer {}", token.0))
892 .body(Body::empty())
893 .unwrap();
894
895 let resp = app.oneshot(req).await.unwrap();
896 assert_eq!(resp.status(), StatusCode::OK);
897
898 let json = response_json(resp).await;
899 assert_eq!(json["user_id"], user_id.to_string());
900 assert_eq!(json["is_admin"], true);
901 assert_eq!(json["method"], "jwt");
902 }
903
904 #[tokio::test]
905 async fn authenticated_via_api_key() {
906 let state = test_state();
907 let (user_id, raw_key) = setup_api_key(&state.store).await;
908
909 let app = Router::new()
910 .route(
911 "/auth",
912 get(|auth: Authenticated| async move {
913 Json(json!({
914 "user_id": auth.user_id,
915 "is_admin": auth.is_admin(),
916 "method": match auth.method {
917 AuthMethod::Jwt { .. } => "jwt",
918 AuthMethod::ApiKey { .. } => "api_key",
919 }
920 }))
921 }),
922 )
923 .with_state(state);
924
925 let req = Request::builder()
926 .uri("/auth")
927 .header("authorization", format!("Bearer {raw_key}"))
928 .body(Body::empty())
929 .unwrap();
930
931 let resp = app.oneshot(req).await.unwrap();
932 assert_eq!(resp.status(), StatusCode::OK);
933
934 let json = response_json(resp).await;
935 assert_eq!(json["user_id"], user_id.to_string());
936 assert_eq!(json["is_admin"], true);
937 assert_eq!(json["method"], "api_key");
938 }
939
940 #[tokio::test]
941 async fn authenticated_rejects_missing_token() {
942 let app = Router::new()
943 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
944 .with_state(test_state());
945
946 let req = Request::builder().uri("/auth").body(Body::empty()).unwrap();
947
948 let resp = app.oneshot(req).await.unwrap();
949 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
950
951 let json = response_json(resp).await;
952 assert_eq!(json["error"]["code"], "MISSING_TOKEN");
953 }
954
955 #[test]
960 fn is_admin_jwt_true() {
961 let auth = Authenticated {
962 user_id: Uuid::now_v7(),
963 method: AuthMethod::Jwt {
964 username: "admin".to_string(),
965 is_admin: true,
966 },
967 };
968 assert!(auth.is_admin());
969 }
970
971 #[test]
972 fn is_admin_jwt_false() {
973 let auth = Authenticated {
974 user_id: Uuid::now_v7(),
975 method: AuthMethod::Jwt {
976 username: "user".to_string(),
977 is_admin: false,
978 },
979 };
980 assert!(!auth.is_admin());
981 }
982
983 #[test]
984 fn is_admin_api_key_true() {
985 let auth = Authenticated {
986 user_id: Uuid::now_v7(),
987 method: AuthMethod::ApiKey {
988 key_id: Uuid::now_v7(),
989 key_name: "k".to_string(),
990 scopes: vec![],
991 owner_is_admin: true,
992 },
993 };
994 assert!(auth.is_admin());
995 }
996
997 #[test]
998 fn is_admin_api_key_false() {
999 let auth = Authenticated {
1000 user_id: Uuid::now_v7(),
1001 method: AuthMethod::ApiKey {
1002 key_id: Uuid::now_v7(),
1003 key_name: "k".to_string(),
1004 scopes: vec![],
1005 owner_is_admin: false,
1006 },
1007 };
1008 assert!(!auth.is_admin());
1009 }
1010
1011 #[tokio::test]
1016 async fn auth_rejection_into_response() {
1017 let rejection = AuthRejection {
1018 status: StatusCode::UNAUTHORIZED,
1019 code: "TEST_CODE",
1020 message: "test message",
1021 };
1022 let resp = rejection.into_response();
1023 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1024
1025 let body = resp.into_body().collect().await.unwrap().to_bytes();
1026 let json: Value = serde_json::from_slice(&body).unwrap();
1027 assert_eq!(json["error"]["code"], "TEST_CODE");
1028 assert_eq!(json["error"]["message"], "test message");
1029 }
1030
1031 #[tokio::test]
1032 async fn api_key_rejection_into_response() {
1033 let rejection = ApiKeyRejection {
1034 status: StatusCode::FORBIDDEN,
1035 code: "KEY_DISABLED",
1036 message: "API key is disabled",
1037 };
1038 let resp = rejection.into_response();
1039 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
1040
1041 let body = resp.into_body().collect().await.unwrap().to_bytes();
1042 let json: Value = serde_json::from_slice(&body).unwrap();
1043 assert_eq!(json["error"]["code"], "KEY_DISABLED");
1044 assert_eq!(json["error"]["message"], "API key is disabled");
1045 }
1046
1047 async fn stored_user(store: &Arc<dyn Store>, username: &str, is_admin: bool) -> Uuid {
1052 store
1053 .create_user(NewUser {
1054 email: format!("{username}@test.com"),
1055 username: username.to_string(),
1056 password_hash: "argon2hash".to_string(),
1057 is_admin: Some(is_admin),
1058 })
1059 .await
1060 .unwrap()
1061 .id
1062 }
1063
1064 fn bearer(uri: &str, token: &str) -> Request<Body> {
1065 Request::builder()
1066 .uri(uri)
1067 .header("authorization", format!("Bearer {token}"))
1068 .body(Body::empty())
1069 .unwrap()
1070 }
1071
1072 #[tokio::test]
1073 async fn jwt_extractor_accepts_token_at_current_version() {
1074 let state = test_state();
1075 let user_id = stored_user(&state.store, "alice", false).await;
1076 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
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::OK);
1083 }
1084
1085 #[tokio::test]
1086 async fn jwt_extractor_rejects_token_after_session_revoked() {
1087 let state = test_state();
1088 let user_id = stored_user(&state.store, "alice", false).await;
1089 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1090 state.store.revoke_user_sessions(user_id).await.unwrap();
1091
1092 let app = Router::new()
1093 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1094 .with_state(state);
1095
1096 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1097 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1098 let json = response_json(resp).await;
1099 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1100 }
1101
1102 #[tokio::test]
1103 async fn jwt_extractor_accepts_token_issued_at_new_version() {
1104 let state = test_state();
1105 let user_id = stored_user(&state.store, "alice", false).await;
1106 let v = state.store.revoke_user_sessions(user_id).await.unwrap();
1107 let jwt = state.jwt_config.clone();
1108 let token = AccessToken::for_user_with_version(user_id, "alice", false, v, &jwt).unwrap();
1109
1110 let app = Router::new()
1111 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1112 .with_state(state);
1113
1114 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1115 assert_eq!(resp.status(), StatusCode::OK);
1116 }
1117
1118 #[tokio::test]
1119 async fn dual_auth_rejects_jwt_after_session_revoked() {
1120 let state = test_state();
1121 let user_id = stored_user(&state.store, "alice", false).await;
1122 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1123 state.store.revoke_user_sessions(user_id).await.unwrap();
1124
1125 let app = Router::new()
1126 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
1127 .with_state(state);
1128
1129 let resp = app.oneshot(bearer("/auth", &token.0)).await.unwrap();
1130 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1131 let json = response_json(resp).await;
1132 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1133 }
1134
1135 #[tokio::test]
1136 async fn jwt_extractor_rejects_token_of_deleted_user() {
1137 let state = test_state();
1138 let user_id = stored_user(&state.store, "alice", false).await;
1139 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1140 state.store.delete_user(user_id).await.unwrap();
1141
1142 let app = Router::new()
1143 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1144 .with_state(state);
1145
1146 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1147 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1148 let json = response_json(resp).await;
1149 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1150 }
1151
1152 #[tokio::test]
1153 async fn jwt_extractor_rejects_token_of_unknown_user() {
1154 let state = test_state();
1155 let token =
1156 AccessToken::for_user(Uuid::now_v7(), "ghost", true, &state.jwt_config).unwrap();
1157
1158 let app = Router::new()
1159 .route("/me", get(|_user: AuthenticatedUser| async { "ok" }))
1160 .with_state(state);
1161
1162 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1163 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1164 let json = response_json(resp).await;
1165 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1166 }
1167
1168 #[tokio::test]
1169 async fn dual_auth_rejects_jwt_of_deleted_user() {
1170 let state = test_state();
1171 let user_id = stored_user(&state.store, "alice", false).await;
1172 let token = AccessToken::for_user(user_id, "alice", false, &state.jwt_config).unwrap();
1173 state.store.delete_user(user_id).await.unwrap();
1174
1175 let app = Router::new()
1176 .route("/auth", get(|_auth: Authenticated| async { "ok" }))
1177 .with_state(state);
1178
1179 let resp = app.oneshot(bearer("/auth", &token.0)).await.unwrap();
1180 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1181 let json = response_json(resp).await;
1182 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1183 }
1184
1185 #[tokio::test]
1186 async fn demoted_admin_token_is_rejected() {
1187 let state = test_state();
1188 let admin_id = stored_user(&state.store, "root", true).await;
1189 let token = AccessToken::for_user(admin_id, "root", true, &state.jwt_config).unwrap();
1190 state.store.update_user_role(admin_id, false).await.unwrap();
1191
1192 let app = Router::new()
1193 .route("/admin", get(|_auth: Authenticated| async { "ok" }))
1194 .with_state(state);
1195
1196 let resp = app.oneshot(bearer("/admin", &token.0)).await.unwrap();
1197 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
1198 let json = response_json(resp).await;
1199 assert_eq!(json["error"]["code"], "TOKEN_REVOKED");
1200 }
1201
1202 #[tokio::test]
1203 async fn jwt_is_admin_comes_from_store() {
1204 let state = test_state();
1205 let user_id = stored_user(&state.store, "member", false).await;
1206 let token = AccessToken::for_user(user_id, "forged", true, &state.jwt_config).unwrap();
1208
1209 let app = Router::new()
1210 .route(
1211 "/auth",
1212 get(|auth: Authenticated| async move {
1213 let username = match &auth.method {
1214 AuthMethod::Jwt { username, .. } => username.clone(),
1215 AuthMethod::ApiKey { .. } => String::new(),
1216 };
1217 Json(json!({ "is_admin": auth.is_admin(), "username": username }))
1218 }),
1219 )
1220 .route(
1221 "/me",
1222 get(|user: AuthenticatedUser| async move {
1223 Json(json!({ "is_admin": user.is_admin, "username": user.username }))
1224 }),
1225 )
1226 .with_state(state);
1227
1228 let resp = app
1229 .clone()
1230 .oneshot(bearer("/auth", &token.0))
1231 .await
1232 .unwrap();
1233 assert_eq!(resp.status(), StatusCode::OK);
1234 let json = response_json(resp).await;
1235 assert_eq!(json["is_admin"], false);
1236 assert_eq!(json["username"], "member");
1237
1238 let resp = app.oneshot(bearer("/me", &token.0)).await.unwrap();
1239 assert_eq!(resp.status(), StatusCode::OK);
1240 let json = response_json(resp).await;
1241 assert_eq!(json["is_admin"], false);
1242 assert_eq!(json["username"], "member");
1243 }
1244}