1use async_trait::async_trait;
2use camel_api::CamelError;
3use camel_api::security_policy::{Principal, TransportId};
4
5use crate::jwt::JwtValidator;
6
7pub struct AuthnRequest<'a> {
15 pub token: &'a str,
16 pub audiences: &'a [String],
17 pub accepted_issuers: &'a [String],
18 pub transport: TransportId,
19}
20
21#[async_trait]
27pub trait TokenAuthenticator: Send + Sync {
28 async fn authenticate_bearer(&self, token: &str) -> Result<Principal, CamelError>;
30
31 async fn authenticate(&self, req: AuthnRequest<'_>) -> Result<Principal, CamelError> {
37 self.authenticate_bearer(req.token).await
38 }
39}
40
41#[async_trait]
42impl<T: JwtValidator> TokenAuthenticator for T {
43 async fn authenticate_bearer(&self, token: &str) -> Result<Principal, CamelError> {
44 self.validate(token).await.map_err(CamelError::from)
45 }
46
47 async fn authenticate(&self, req: AuthnRequest<'_>) -> Result<Principal, CamelError> {
48 if req.audiences.is_empty() && req.accepted_issuers.is_empty() {
50 return self.authenticate_bearer(req.token).await;
51 }
52
53 let principal = self
56 .validate_signature(req.token)
57 .await
58 .map_err(CamelError::from)?;
59
60 if !req.accepted_issuers.is_empty()
61 && !req.accepted_issuers.iter().any(|i| i == &principal.issuer)
62 {
63 return Err(CamelError::Unauthenticated(format!(
64 "token issuer {:?} not in accepted set", principal.issuer
66 )));
67 }
68
69 if !req.audiences.is_empty()
70 && !principal.audience.iter().any(|a| req.audiences.contains(a))
71 {
72 return Err(CamelError::Unauthenticated(format!(
73 "token audience {:?} not in accepted set", principal.audience
75 )));
76 }
77
78 Ok(principal)
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::types::AuthError;
86 use serde_json::json;
87
88 struct MockValidator {
89 principal: Option<Principal>,
90 should_fail: bool,
91 }
92
93 #[async_trait]
94 impl JwtValidator for MockValidator {
95 async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
96 if self.should_fail {
97 return Err(AuthError::TokenInvalid("bad token".into()));
98 }
99 self.principal
100 .clone()
101 .ok_or_else(|| AuthError::TokenInvalid("no principal".into()))
102 }
103 }
104
105 fn test_principal() -> Principal {
106 Principal {
107 subject: "user1".into(),
108 issuer: "test-issuer".into(),
109 audience: vec!["api".into()],
110 scopes: vec!["read".into()],
111 roles: vec!["admin".into()],
112 claims: json!({"sub": "user1"}),
113 }
114 }
115
116 #[tokio::test]
117 async fn test_authenticate_bearer_success() {
118 let validator = MockValidator {
119 principal: Some(test_principal()),
120 should_fail: false,
121 };
122 let result = validator.authenticate_bearer("valid-token").await;
123 assert!(result.is_ok());
124 assert_eq!(result.unwrap().subject, "user1");
125 }
126
127 #[tokio::test]
128 async fn test_authenticate_bearer_invalid_token() {
129 let validator = MockValidator {
130 principal: None,
131 should_fail: true,
132 };
133 let err = validator.authenticate_bearer("bad").await.unwrap_err();
134 match err {
135 CamelError::Unauthenticated(msg) => assert!(msg.contains("bad token")),
136 _ => panic!("expected Unauthenticated, got: {err:?}"),
137 }
138 }
139
140 #[tokio::test]
141 async fn test_authenticate_bearer_provider_unavailable() {
142 struct UnavailableValidator;
143 #[async_trait]
144 impl JwtValidator for UnavailableValidator {
145 async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
146 Err(AuthError::ProviderUnavailable("connection refused".into()))
147 }
148 }
149 let err = UnavailableValidator
150 .authenticate_bearer("token")
151 .await
152 .unwrap_err();
153 match err {
154 CamelError::ProcessorError(msg) => assert!(msg.contains("auth provider unavailable")),
155 _ => panic!("expected ProcessorError, got: {err:?}"),
156 }
157 }
158
159 #[tokio::test]
160 async fn test_authenticate_bearer_token_expired() {
161 struct ExpiredValidator;
162 #[async_trait]
163 impl JwtValidator for ExpiredValidator {
164 async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
165 Err(AuthError::TokenExpired)
166 }
167 }
168 let err = ExpiredValidator
169 .authenticate_bearer("expired-token")
170 .await
171 .unwrap_err();
172 match err {
173 CamelError::Unauthenticated(msg) => assert!(msg.contains("token expired")),
174 _ => panic!("expected Unauthenticated, got: {err:?}"),
175 }
176 }
177
178 #[tokio::test]
179 async fn test_authenticate_bearer_unauthorized() {
180 struct UnauthorizedValidator;
181 #[async_trait]
182 impl JwtValidator for UnauthorizedValidator {
183 async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
184 Err(AuthError::Unauthorized("insufficient permissions".into()))
185 }
186 }
187 let err = UnauthorizedValidator
188 .authenticate_bearer("token")
189 .await
190 .unwrap_err();
191 match err {
192 CamelError::Unauthorized(msg) => assert!(msg.contains("insufficient permissions")),
193 _ => panic!("expected Unauthorized, got: {err:?}"),
194 }
195 }
196
197 #[tokio::test]
198 async fn test_authenticate_bearer_config_error() {
199 struct ConfigErrorValidator;
200 #[async_trait]
201 impl JwtValidator for ConfigErrorValidator {
202 async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
203 Err(AuthError::ConfigError("missing issuer".into()))
204 }
205 }
206 let err = ConfigErrorValidator
207 .authenticate_bearer("token")
208 .await
209 .unwrap_err();
210 match err {
211 CamelError::Config(msg) => assert!(msg.contains("missing issuer")),
212 _ => panic!("expected Config, got: {err:?}"),
213 }
214 }
215
216 use crate::claims::{ClaimPaths, JsonPointerClaimsMapper};
219 use crate::jwks::{Jwk, JwksProvider};
220 use crate::jwt::LocalJwtValidator;
221 use std::sync::Arc;
222
223 static TEST_RSA_PRIVATE_PEM: &[u8] = include_bytes!("../tests/fixtures/test_rsa_private.pem");
224 static TEST_RSA_PUBLIC_PEM: &[u8] = include_bytes!("../tests/fixtures/test_rsa_public.pem");
225
226 struct MockJwks {
227 kid: String,
228 public_pem: &'static [u8],
229 }
230
231 #[async_trait]
232 impl JwksProvider for MockJwks {
233 async fn get_signing_keys(&self) -> Result<Vec<Jwk>, AuthError> {
234 Ok(vec![Jwk {
235 kid: self.kid.clone(),
236 kty: "RSA".into(),
237 alg: Some("RS256".into()),
238 r#use: None,
239 n: String::from_utf8_lossy(self.public_pem).into_owned(),
240 e: "AQAB".into(),
241 }])
242 }
243
244 async fn refresh(&self) -> Result<(), AuthError> {
245 Ok(())
246 }
247 }
248
249 fn jwt_validator(audience: Vec<&str>, issuer: &str) -> LocalJwtValidator {
250 let mapper = Arc::new(JsonPointerClaimsMapper::new(ClaimPaths {
251 subject: "/sub".into(),
252 roles: vec!["/groups".into()],
253 scopes: Some("/scope".into()),
254 }));
255 LocalJwtValidator::new(
256 audience.iter().map(|s| s.to_string()).collect(),
257 issuer.to_string(),
258 Arc::new(MockJwks {
259 kid: "test-key".into(),
260 public_pem: TEST_RSA_PUBLIC_PEM,
261 }),
262 mapper,
263 )
264 }
265
266 fn make_token(kid: &str, claims: &serde_json::Value) -> String {
267 let mut header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
268 header.kid = Some(kid.to_string());
269 let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(TEST_RSA_PRIVATE_PEM).unwrap();
270 jsonwebtoken::encode(&header, claims, &encoding_key).unwrap()
271 }
272
273 fn valid_claims(iss: &str, aud: &str) -> serde_json::Value {
274 let now = chrono::Utc::now().timestamp() as u64;
275 json!({
276 "sub": "user-1",
277 "iss": iss,
278 "aud": aud,
279 "exp": now + 3600,
280 "iat": now,
281 })
282 }
283
284 fn req<'a>(token: &'a str, audiences: &'a [String], issuers: &'a [String]) -> AuthnRequest<'a> {
285 AuthnRequest {
286 token,
287 audiences,
288 accepted_issuers: issuers,
289 transport: TransportId::Http,
290 }
291 }
292
293 #[tokio::test]
294 async fn issuer_not_accepted_rejects() {
295 let v = jwt_validator(vec!["api"], "https://a");
296 let token = make_token("test-key", &valid_claims("https://b", "api"));
297 let err = v
298 .authenticate(req(
299 &token,
300 &["api".to_string()],
301 &["https://a".to_string()],
302 ))
303 .await
304 .unwrap_err();
305 assert!(matches!(err, CamelError::Unauthenticated(_)));
306 }
307
308 #[tokio::test]
309 async fn audience_mismatch_rejects() {
310 let v = jwt_validator(vec!["api"], "https://a");
311 let token = make_token("test-key", &valid_claims("https://a", "api-b"));
312 let err = v
313 .authenticate(req(
314 &token,
315 &["api-a".to_string(), "api-c".to_string()],
316 &["https://a".to_string()],
317 ))
318 .await
319 .unwrap_err();
320 assert!(matches!(err, CamelError::Unauthenticated(_)));
321 }
322
323 #[tokio::test]
324 async fn request_audience_overrides_constructor_default() {
325 let v = jwt_validator(vec!["api"], "https://a");
327 let token = make_token("test-key", &valid_claims("https://a", "api-2"));
328
329 let principal = v
331 .authenticate(req(
332 &token,
333 &["api-2".to_string()],
334 &["https://a".to_string()],
335 ))
336 .await
337 .unwrap();
338 assert_eq!(principal.subject, "user-1");
339
340 let err = v.authenticate(req(&token, &[], &[])).await.unwrap_err();
343 assert!(matches!(err, CamelError::Unauthenticated(_)));
344 }
345
346 #[tokio::test]
347 async fn default_delegation_backcompat() {
348 struct OnlyBearer;
349 #[async_trait]
350 impl TokenAuthenticator for OnlyBearer {
351 async fn authenticate_bearer(&self, _token: &str) -> Result<Principal, CamelError> {
352 Ok(test_principal())
353 }
354 }
355 let principal = OnlyBearer
356 .authenticate(req("tok", &["api".to_string()], &["https://a".to_string()]))
357 .await
358 .unwrap();
359 assert_eq!(principal.subject, "user1");
360 }
361
362 #[tokio::test]
363 async fn empty_issuers_accepts_any() {
364 let v = jwt_validator(vec!["api"], "https://a");
365 let token = make_token("test-key", &valid_claims("https://anything", "api"));
366 let principal = v
367 .authenticate(req(&token, &["api".to_string()], &[]))
368 .await
369 .unwrap();
370 assert_eq!(principal.subject, "user-1");
371 }
372
373 #[tokio::test]
374 async fn empty_constructor_audience_fails_closed() {
375 let v = jwt_validator(vec![], "https://a");
379 let token = make_token("test-key", &valid_claims("https://a", "api"));
380 let err = v.authenticate(req(&token, &[], &[])).await.unwrap_err();
381 assert!(matches!(err, CamelError::Unauthenticated(_)));
382 }
383
384 #[tokio::test]
385 async fn empty_constructor_issuer_fails_closed() {
386 let v = jwt_validator(vec!["api"], "");
389 let token = make_token("test-key", &valid_claims("https://a", "api"));
390 let err = v.authenticate(req(&token, &[], &[])).await.unwrap_err();
391 assert!(matches!(err, CamelError::Unauthenticated(_)));
392 }
393
394 #[tokio::test]
395 async fn empty_constructor_audience_replaced_by_request_audiences() {
396 let v = jwt_validator(vec![], "https://a");
400 let token = make_token("test-key", &valid_claims("https://a", "api"));
401 let principal = v
402 .authenticate(req(
403 &token,
404 &["api".to_string()],
405 &["https://a".to_string()],
406 ))
407 .await
408 .unwrap();
409 assert_eq!(principal.subject, "user-1");
410 }
411}