Skip to main content

camel_auth/
token_authenticator.rs

1use async_trait::async_trait;
2use camel_api::CamelError;
3use camel_api::security_policy::Principal;
4
5use crate::jwt::JwtValidator;
6
7/// Separates authentication (token → Principal) from authorization (SecurityPolicy check).
8///
9/// Provides a blanket implementation for any [`JwtValidator`], converting
10/// provider-specific [`AuthError`](crate::types::AuthError) variants into
11/// domain-level [`CamelError`] variants.
12#[async_trait]
13pub trait TokenAuthenticator: Send + Sync {
14    /// Authenticate a Bearer token and return the associated [`Principal`].
15    async fn authenticate_bearer(&self, token: &str) -> Result<Principal, CamelError>;
16}
17
18#[async_trait]
19impl<T: JwtValidator> TokenAuthenticator for T {
20    async fn authenticate_bearer(&self, token: &str) -> Result<Principal, CamelError> {
21        self.validate(token).await.map_err(CamelError::from)
22    }
23}
24
25#[cfg(test)]
26mod tests {
27    use super::*;
28    use crate::types::AuthError;
29    use serde_json::json;
30
31    struct MockValidator {
32        principal: Option<Principal>,
33        should_fail: bool,
34    }
35
36    #[async_trait]
37    impl JwtValidator for MockValidator {
38        async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
39            if self.should_fail {
40                return Err(AuthError::TokenInvalid("bad token".into()));
41            }
42            self.principal
43                .clone()
44                .ok_or_else(|| AuthError::TokenInvalid("no principal".into()))
45        }
46    }
47
48    fn test_principal() -> Principal {
49        Principal {
50            subject: "user1".into(),
51            issuer: "test-issuer".into(),
52            audience: vec!["api".into()],
53            scopes: vec!["read".into()],
54            roles: vec!["admin".into()],
55            claims: json!({"sub": "user1"}),
56        }
57    }
58
59    #[tokio::test]
60    async fn test_authenticate_bearer_success() {
61        let validator = MockValidator {
62            principal: Some(test_principal()),
63            should_fail: false,
64        };
65        let result = validator.authenticate_bearer("valid-token").await;
66        assert!(result.is_ok());
67        assert_eq!(result.unwrap().subject, "user1");
68    }
69
70    #[tokio::test]
71    async fn test_authenticate_bearer_invalid_token() {
72        let validator = MockValidator {
73            principal: None,
74            should_fail: true,
75        };
76        let err = validator.authenticate_bearer("bad").await.unwrap_err();
77        match err {
78            CamelError::Unauthenticated(msg) => assert!(msg.contains("bad token")),
79            _ => panic!("expected Unauthenticated, got: {err:?}"),
80        }
81    }
82
83    #[tokio::test]
84    async fn test_authenticate_bearer_provider_unavailable() {
85        struct UnavailableValidator;
86        #[async_trait]
87        impl JwtValidator for UnavailableValidator {
88            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
89                Err(AuthError::ProviderUnavailable("connection refused".into()))
90            }
91        }
92        let err = UnavailableValidator
93            .authenticate_bearer("token")
94            .await
95            .unwrap_err();
96        match err {
97            CamelError::ProcessorError(msg) => assert!(msg.contains("auth provider unavailable")),
98            _ => panic!("expected ProcessorError, got: {err:?}"),
99        }
100    }
101
102    #[tokio::test]
103    async fn test_authenticate_bearer_token_expired() {
104        struct ExpiredValidator;
105        #[async_trait]
106        impl JwtValidator for ExpiredValidator {
107            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
108                Err(AuthError::TokenExpired)
109            }
110        }
111        let err = ExpiredValidator
112            .authenticate_bearer("expired-token")
113            .await
114            .unwrap_err();
115        match err {
116            CamelError::Unauthenticated(msg) => assert!(msg.contains("token expired")),
117            _ => panic!("expected Unauthenticated, got: {err:?}"),
118        }
119    }
120
121    #[tokio::test]
122    async fn test_authenticate_bearer_unauthorized() {
123        struct UnauthorizedValidator;
124        #[async_trait]
125        impl JwtValidator for UnauthorizedValidator {
126            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
127                Err(AuthError::Unauthorized("insufficient permissions".into()))
128            }
129        }
130        let err = UnauthorizedValidator
131            .authenticate_bearer("token")
132            .await
133            .unwrap_err();
134        match err {
135            CamelError::Unauthorized(msg) => assert!(msg.contains("insufficient permissions")),
136            _ => panic!("expected Unauthorized, got: {err:?}"),
137        }
138    }
139
140    #[tokio::test]
141    async fn test_authenticate_bearer_config_error() {
142        struct ConfigErrorValidator;
143        #[async_trait]
144        impl JwtValidator for ConfigErrorValidator {
145            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
146                Err(AuthError::ConfigError("missing issuer".into()))
147            }
148        }
149        let err = ConfigErrorValidator
150            .authenticate_bearer("token")
151            .await
152            .unwrap_err();
153        match err {
154            CamelError::Config(msg) => assert!(msg.contains("missing issuer")),
155            _ => panic!("expected Config, got: {err:?}"),
156        }
157    }
158}