1use std::any::Any;
2use std::collections::{HashMap, HashSet};
3use std::net::SocketAddr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use tokio_tungstenite::tungstenite::http::Request;
9
10pub use arete_auth::AuthContext;
12pub use arete_auth::AuthErrorCode;
14pub use arete_auth::RetryPolicy;
16pub use arete_auth::{
18 auth_failure_event, auth_success_event, rate_limit_event, AuditEvent, AuditSeverity,
19 ChannelAuditLogger, NoOpAuditLogger, SecurityAuditEvent, SecurityAuditLogger,
20};
21pub use arete_auth::{AuthMetrics, AuthMetricsCollector, AuthMetricsSnapshot};
23pub use arete_auth::{MultiKeyVerifier, MultiKeyVerifierBuilder, RotationKey};
25
26#[derive(Debug, Clone)]
27pub struct ConnectionAuthRequest {
28 pub remote_addr: SocketAddr,
29 pub path: String,
30 pub query: Option<String>,
31 pub headers: HashMap<String, String>,
32 pub origin: Option<String>,
34}
35
36impl ConnectionAuthRequest {
37 pub fn from_http_request<B>(remote_addr: SocketAddr, request: &Request<B>) -> Self {
38 let mut headers = HashMap::new();
39 for (name, value) in request.headers() {
40 if let Ok(value_str) = value.to_str() {
41 headers.insert(name.as_str().to_ascii_lowercase(), value_str.to_string());
42 }
43 }
44
45 let origin = headers.get("origin").cloned();
46
47 Self {
48 remote_addr,
49 path: request.uri().path().to_string(),
50 query: request.uri().query().map(|q| q.to_string()),
51 headers,
52 origin,
53 }
54 }
55
56 pub fn header(&self, name: &str) -> Option<&str> {
57 self.headers
58 .get(&name.to_ascii_lowercase())
59 .map(String::as_str)
60 }
61
62 pub fn bearer_token(&self) -> Option<&str> {
63 let value = self.header("authorization")?;
64 let (scheme, token) = value.split_once(' ')?;
65 if scheme.eq_ignore_ascii_case("bearer") {
66 Some(token)
67 } else {
68 None
69 }
70 }
71
72 pub fn query_param(&self, key: &str) -> Option<&str> {
73 let query = self.query.as_deref()?;
74 query
75 .split('&')
76 .filter_map(|pair| pair.split_once('='))
77 .find_map(|(k, v)| if k == key { Some(v) } else { None })
78 }
79}
80
81#[derive(Debug, Clone, Default)]
83pub struct AuthErrorDetails {
84 pub field: Option<String>,
86 pub context: Option<String>,
88 pub suggested_action: Option<String>,
90 pub docs_url: Option<String>,
92}
93
94#[derive(Debug, Clone)]
96pub struct AuthDeny {
97 pub reason: String,
98 pub code: AuthErrorCode,
99 pub details: AuthErrorDetails,
101 pub retry_policy: RetryPolicy,
103 pub http_status: u16,
105 pub reset_at: Option<std::time::SystemTime>,
107}
108
109impl AuthDeny {
110 pub fn new(code: AuthErrorCode, reason: impl Into<String>) -> Self {
112 Self {
113 reason: reason.into(),
114 code,
115 details: AuthErrorDetails::default(),
116 retry_policy: code.default_retry_policy(),
117 http_status: code.http_status(),
118 reset_at: None,
119 }
120 }
121
122 pub fn token_missing() -> Self {
124 Self::new(
125 AuthErrorCode::TokenMissing,
126 "Missing session token (expected Authorization: Bearer <token> or query token)",
127 )
128 .with_suggested_action(
129 "Provide a valid session token in the Authorization header or as a query parameter",
130 )
131 }
132
133 pub fn from_verify_error(err: arete_auth::VerifyError) -> Self {
135 let code = AuthErrorCode::from(&err);
136 Self::new(code, format!("Token verification failed: {}", err))
137 }
138
139 pub fn with_details(mut self, details: AuthErrorDetails) -> Self {
141 self.details = details;
142 self
143 }
144
145 pub fn with_field(mut self, field: impl Into<String>) -> Self {
147 self.details.field = Some(field.into());
148 self
149 }
150
151 pub fn with_context(mut self, context: impl Into<String>) -> Self {
153 self.details.context = Some(context.into());
154 self
155 }
156
157 pub fn with_suggested_action(mut self, action: impl Into<String>) -> Self {
159 self.details.suggested_action = Some(action.into());
160 self
161 }
162
163 pub fn with_docs_url(mut self, url: impl Into<String>) -> Self {
165 self.details.docs_url = Some(url.into());
166 self
167 }
168
169 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
171 self.retry_policy = policy;
172 self
173 }
174
175 pub fn with_reset_at(mut self, reset_at: std::time::SystemTime) -> Self {
177 self.reset_at = Some(reset_at);
178 self
179 }
180
181 pub fn rate_limited(retry_after: Duration, limit_type: &str) -> Self {
183 let reset_at = std::time::SystemTime::now() + retry_after;
184 Self::new(
185 AuthErrorCode::RateLimitExceeded,
186 format!(
187 "Rate limit exceeded for {}. Please retry after {:?}.",
188 limit_type, retry_after
189 ),
190 )
191 .with_retry_policy(RetryPolicy::RetryAfter(retry_after))
192 .with_reset_at(reset_at)
193 .with_suggested_action(format!(
194 "Wait {:?} before retrying the request",
195 retry_after
196 ))
197 }
198
199 pub fn connection_limit_exceeded(limit_type: &str, current: usize, max: usize) -> Self {
201 Self::new(
202 AuthErrorCode::ConnectionLimitExceeded,
203 format!(
204 "Connection limit exceeded: {} has {} of {} allowed connections",
205 limit_type, current, max
206 ),
207 )
208 .with_suggested_action(
209 "Disconnect existing connections or wait for other connections to close",
210 )
211 }
212
213 pub fn to_error_response(&self) -> ErrorResponse {
215 ErrorResponse {
216 error: self.code.as_str().to_string(),
217 message: self.reason.clone(),
218 code: self.code.to_string(),
219 retryable: matches!(
220 self.retry_policy,
221 RetryPolicy::RetryImmediately
222 | RetryPolicy::RetryAfter(_)
223 | RetryPolicy::RetryWithBackoff { .. }
224 | RetryPolicy::RetryWithFreshToken
225 ),
226 retry_after: match self.retry_policy {
227 RetryPolicy::RetryAfter(d) => Some(d.as_secs()),
228 _ => None,
229 },
230 suggested_action: self.details.suggested_action.clone(),
231 docs_url: self.details.docs_url.clone(),
232 }
233 }
234}
235
236#[derive(Debug, Clone, serde::Serialize)]
238pub struct ErrorResponse {
239 pub error: String,
240 pub message: String,
241 pub code: String,
242 pub retryable: bool,
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub retry_after: Option<u64>,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub suggested_action: Option<String>,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub docs_url: Option<String>,
249}
250
251#[derive(Debug, Clone)]
253#[allow(clippy::large_enum_variant)] pub enum AuthDecision {
255 Allow(AuthContext),
257 Deny(AuthDeny),
259}
260
261impl AuthDecision {
262 pub fn is_allowed(&self) -> bool {
264 matches!(self, AuthDecision::Allow(_))
265 }
266
267 pub fn auth_context(&self) -> Option<&AuthContext> {
269 match self {
270 AuthDecision::Allow(ctx) => Some(ctx),
271 AuthDecision::Deny(_) => None,
272 }
273 }
274}
275
276#[async_trait]
277pub trait WebSocketAuthPlugin: Send + Sync + Any {
278 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision;
279
280 fn as_any(&self) -> &dyn Any;
281
282 fn audit_logger(&self) -> Option<&dyn SecurityAuditLogger> {
284 None
285 }
286
287 async fn log_audit(&self, event: SecurityAuditEvent) {
289 if let Some(logger) = self.audit_logger() {
290 logger.log(event).await;
291 }
292 }
293
294 fn auth_metrics(&self) -> Option<&AuthMetrics> {
296 None
297 }
298}
299
300pub struct AllowAllAuthPlugin;
305
306#[async_trait]
307impl WebSocketAuthPlugin for AllowAllAuthPlugin {
308 async fn authorize(&self, _request: &ConnectionAuthRequest) -> AuthDecision {
309 let context = AuthContext {
311 subject: "anonymous".to_string(),
312 issuer: "allow-all".to_string(),
313 audience: "allow-all".to_string(),
314 key_class: arete_auth::KeyClass::Secret,
315 metering_key: "dev".to_string(),
316 deployment_id: None,
317 target_kind: None,
318 target_id: None,
319 program_id: None,
320 program_release_hash: None,
321 expires_at: u64::MAX, scope: "read write".to_string(),
323 limits: Default::default(),
324 plan: None,
325 origin: None,
326 client_ip: None,
327 jti: uuid::Uuid::new_v4().to_string(),
328 actor_key: None,
329 account_key: None,
330 consumer_key: None,
331 policy_version: None,
332 account_limits: Default::default(),
333 row_scopes: None,
334 };
335 AuthDecision::Allow(context)
336 }
337
338 fn as_any(&self) -> &dyn Any {
339 self
340 }
341}
342
343#[derive(Debug, Clone)]
344pub struct StaticTokenAuthPlugin {
345 tokens: HashSet<String>,
346 query_param_name: String,
347}
348
349impl StaticTokenAuthPlugin {
350 pub fn new(tokens: impl IntoIterator<Item = String>) -> Self {
351 Self {
352 tokens: tokens.into_iter().collect(),
353 query_param_name: "token".to_string(),
354 }
355 }
356
357 pub fn with_query_param_name(mut self, query_param_name: impl Into<String>) -> Self {
358 self.query_param_name = query_param_name.into();
359 self
360 }
361
362 fn extract_token<'a>(&self, request: &'a ConnectionAuthRequest) -> Option<&'a str> {
363 request
364 .bearer_token()
365 .or_else(|| request.query_param(&self.query_param_name))
366 }
367}
368
369#[async_trait]
370impl WebSocketAuthPlugin for StaticTokenAuthPlugin {
371 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
372 let token = match self.extract_token(request) {
373 Some(token) => token,
374 None => {
375 return AuthDecision::Deny(AuthDeny::token_missing());
376 }
377 };
378
379 if self.tokens.contains(token) {
380 let context = AuthContext {
382 subject: format!("static:{}", &token[..token.len().min(8)]),
383 issuer: "static-token".to_string(),
384 audience: "static-token".to_string(),
385 key_class: arete_auth::KeyClass::Secret,
386 metering_key: token.to_string(),
387 deployment_id: None,
388 target_kind: None,
389 target_id: None,
390 program_id: None,
391 program_release_hash: None,
392 expires_at: u64::MAX, scope: "read".to_string(),
394 limits: Default::default(),
395 plan: None,
396 origin: request.origin.clone(),
397 client_ip: None,
398 jti: uuid::Uuid::new_v4().to_string(),
399 actor_key: None,
400 account_key: None,
401 consumer_key: None,
402 policy_version: None,
403 account_limits: Default::default(),
404 row_scopes: None,
405 };
406 AuthDecision::Allow(context)
407 } else {
408 AuthDecision::Deny(AuthDeny::new(
409 AuthErrorCode::InvalidStaticToken,
410 "Invalid auth token",
411 ))
412 }
413 }
414
415 fn as_any(&self) -> &dyn Any {
416 self
417 }
418}
419
420enum SignedSessionVerifier {
427 Static(arete_auth::TokenVerifier),
428 CachedJwks(arete_auth::AsyncVerifier),
429 MultiKey(arete_auth::MultiKeyVerifier),
430}
431
432pub struct SignedSessionAuthPlugin {
433 verifier: SignedSessionVerifier,
434 query_param_name: String,
435 require_origin: bool,
436 audit_logger: Option<Arc<dyn SecurityAuditLogger>>,
437 metrics: Option<Arc<AuthMetrics>>,
438}
439
440impl SignedSessionAuthPlugin {
441 pub fn new(verifier: arete_auth::TokenVerifier) -> Self {
443 Self {
444 verifier: SignedSessionVerifier::Static(verifier),
445 query_param_name: "hs_token".to_string(),
446 require_origin: false,
447 audit_logger: None,
448 metrics: None,
449 }
450 }
451
452 pub fn new_with_async_verifier(verifier: arete_auth::AsyncVerifier) -> Self {
454 Self {
455 verifier: SignedSessionVerifier::CachedJwks(verifier),
456 query_param_name: "hs_token".to_string(),
457 require_origin: false,
458 audit_logger: None,
459 metrics: None,
460 }
461 }
462
463 pub fn new_with_multi_key_verifier(verifier: arete_auth::MultiKeyVerifier) -> Self {
465 Self {
466 verifier: SignedSessionVerifier::MultiKey(verifier),
467 query_param_name: "hs_token".to_string(),
468 require_origin: false,
469 audit_logger: None,
470 metrics: None,
471 }
472 }
473
474 pub fn with_query_param_name(mut self, name: impl Into<String>) -> Self {
476 self.query_param_name = name.into();
477 self
478 }
479
480 pub fn with_origin_validation(mut self) -> Self {
482 self.require_origin = true;
483 self
484 }
485
486 pub fn with_audit_logger(mut self, logger: Arc<dyn SecurityAuditLogger>) -> Self {
488 self.audit_logger = Some(logger);
489 self
490 }
491
492 pub fn with_metrics(mut self, metrics: Arc<AuthMetrics>) -> Self {
494 self.metrics = Some(metrics);
495 self
496 }
497
498 pub fn metrics_snapshot(&self) -> Option<AuthMetricsSnapshot> {
500 self.metrics.as_ref().map(|m| m.snapshot())
501 }
502
503 fn extract_token<'a>(&self, request: &'a ConnectionAuthRequest) -> Option<&'a str> {
504 request
505 .bearer_token()
506 .or_else(|| request.query_param(&self.query_param_name))
507 }
508
509 #[allow(clippy::result_large_err)]
515 pub async fn verify_refresh_token(&self, token: &str) -> Result<AuthContext, AuthDeny> {
516 let result = match &self.verifier {
517 SignedSessionVerifier::Static(verifier) => verifier.verify(token, None, None),
518 SignedSessionVerifier::CachedJwks(verifier) => {
519 verifier.verify_with_cache(token, None, None).await
520 }
521 SignedSessionVerifier::MultiKey(verifier) => verifier.verify(token, None, None).await,
522 };
523
524 match result {
525 Ok(context) => Ok(context),
526 Err(e) => Err(AuthDeny::from_verify_error(e)),
527 }
528 }
529}
530
531#[async_trait]
532impl WebSocketAuthPlugin for SignedSessionAuthPlugin {
533 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
534 let token = match self.extract_token(request) {
535 Some(token) => token,
536 None => {
537 return AuthDecision::Deny(AuthDeny::token_missing());
538 }
539 };
540
541 let expected_origin = request.origin.as_deref();
542
543 let expected_client_ip = None; let result = match &self.verifier {
546 SignedSessionVerifier::Static(verifier) => {
547 verifier.verify(token, expected_origin, expected_client_ip)
548 }
549 SignedSessionVerifier::CachedJwks(verifier) => {
550 verifier
551 .verify_with_cache(token, expected_origin, expected_client_ip)
552 .await
553 }
554 SignedSessionVerifier::MultiKey(verifier) => {
555 verifier
556 .verify(token, expected_origin, expected_client_ip)
557 .await
558 }
559 };
560
561 match result {
562 Ok(context) => {
563 let event = auth_success_event(&context.subject)
565 .with_client_ip(request.remote_addr)
566 .with_path(&request.path);
567 if let Some(origin) = &request.origin {
568 let event = event.with_origin(origin.clone());
569 self.log_audit(event).await;
570 } else {
571 self.log_audit(event).await;
572 }
573 AuthDecision::Allow(context)
574 }
575 Err(e) => {
576 let deny = AuthDeny::from_verify_error(e);
577 let event = auth_failure_event(&deny.code, &deny.reason)
579 .with_client_ip(request.remote_addr)
580 .with_path(&request.path);
581 let event = if let Some(origin) = &request.origin {
582 event.with_origin(origin.clone())
583 } else {
584 event
585 };
586 self.log_audit(event).await;
587 AuthDecision::Deny(deny)
588 }
589 }
590 }
591
592 fn as_any(&self) -> &dyn Any {
593 self
594 }
595
596 fn audit_logger(&self) -> Option<&dyn SecurityAuditLogger> {
597 self.audit_logger.as_ref().map(|l| l.as_ref())
598 }
599
600 fn auth_metrics(&self) -> Option<&AuthMetrics> {
601 self.metrics.as_ref().map(|m| m.as_ref())
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 #[test]
610 fn extracts_bearer_and_query_tokens() {
611 let request = Request::builder()
612 .uri("/ws?token=query-token")
613 .header("Authorization", "Bearer header-token")
614 .body(())
615 .expect("request should build");
616
617 let auth_request = ConnectionAuthRequest::from_http_request(
618 "127.0.0.1:8877".parse().expect("socket addr should parse"),
619 &request,
620 );
621
622 assert_eq!(auth_request.bearer_token(), Some("header-token"));
623 assert_eq!(auth_request.query_param("token"), Some("query-token"));
624 }
625
626 #[tokio::test]
627 async fn static_token_plugin_allows_matching_token() {
628 let plugin = StaticTokenAuthPlugin::new(["secret".to_string()]);
629 let request = Request::builder()
630 .uri("/ws?token=secret")
631 .body(())
632 .expect("request should build");
633 let auth_request = ConnectionAuthRequest::from_http_request(
634 "127.0.0.1:8877".parse().expect("socket addr should parse"),
635 &request,
636 );
637
638 let decision = plugin.authorize(&auth_request).await;
639 assert!(decision.is_allowed());
640 assert!(decision.auth_context().is_some());
641 }
642
643 #[tokio::test]
644 async fn static_token_plugin_denies_missing_token() {
645 let plugin = StaticTokenAuthPlugin::new(["secret".to_string()]);
646 let request = Request::builder()
647 .uri("/ws")
648 .body(())
649 .expect("request should build");
650 let auth_request = ConnectionAuthRequest::from_http_request(
651 "127.0.0.1:8877".parse().expect("socket addr should parse"),
652 &request,
653 );
654
655 let decision = plugin.authorize(&auth_request).await;
656 assert!(!decision.is_allowed());
657 }
658
659 #[tokio::test]
660 async fn allow_all_plugin_allows_with_context() {
661 let plugin = AllowAllAuthPlugin;
662 let request = Request::builder()
663 .uri("/ws")
664 .body(())
665 .expect("request should build");
666 let auth_request = ConnectionAuthRequest::from_http_request(
667 "127.0.0.1:8877".parse().expect("socket addr should parse"),
668 &request,
669 );
670
671 let decision = plugin.authorize(&auth_request).await;
672 assert!(decision.is_allowed());
673 let ctx = decision.auth_context().unwrap();
674 assert_eq!(ctx.subject, "anonymous");
675 }
676
677 #[tokio::test]
680 async fn signed_session_plugin_denies_missing_token() {
681 let signing_key = arete_auth::SigningKey::generate();
682 let verifying_key = signing_key.verifying_key();
683 let verifier =
684 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
685 let plugin = SignedSessionAuthPlugin::new(verifier);
686
687 let request = Request::builder()
688 .uri("/ws")
689 .body(())
690 .expect("request should build");
691 let auth_request = ConnectionAuthRequest::from_http_request(
692 "127.0.0.1:8877".parse().expect("socket addr should parse"),
693 &request,
694 );
695
696 let decision = plugin.authorize(&auth_request).await;
697 assert!(!decision.is_allowed());
698
699 if let AuthDecision::Deny(deny) = decision {
700 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
701 } else {
702 panic!("Expected Deny decision");
703 }
704 }
705
706 #[tokio::test]
707 async fn signed_session_plugin_denies_expired_token() {
708 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
709 use std::time::{SystemTime, UNIX_EPOCH};
710
711 let signing_key = arete_auth::SigningKey::generate();
712 let verifying_key = signing_key.verifying_key();
713 let signer = TokenSigner::new(signing_key, "test-issuer");
714 let verifier =
715 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
716 let plugin = SignedSessionAuthPlugin::new(verifier);
717
718 let now = SystemTime::now()
720 .duration_since(UNIX_EPOCH)
721 .unwrap()
722 .as_secs();
723 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
724 .with_scope("read")
725 .with_key_class(KeyClass::Secret)
726 .build();
727
728 let mut expired_claims = claims;
730 expired_claims.exp = now - 3600; expired_claims.iat = now - 7200; expired_claims.nbf = now - 7200;
733
734 let token = signer.sign(expired_claims).unwrap();
735
736 let request = Request::builder()
737 .uri(format!("/ws?hs_token={}", token))
738 .body(())
739 .expect("request should build");
740 let auth_request = ConnectionAuthRequest::from_http_request(
741 "127.0.0.1:8877".parse().expect("socket addr should parse"),
742 &request,
743 );
744
745 let decision = plugin.authorize(&auth_request).await;
746 assert!(!decision.is_allowed());
747
748 if let AuthDecision::Deny(deny) = decision {
749 assert_eq!(deny.code, AuthErrorCode::TokenExpired);
750 } else {
751 panic!("Expected Deny decision for expired token");
752 }
753 }
754
755 #[tokio::test]
756 async fn signed_session_plugin_denies_invalid_signature() {
757 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
758
759 let signing_key = arete_auth::SigningKey::generate();
761 let wrong_key = arete_auth::SigningKey::generate();
762
763 let signer = TokenSigner::new(signing_key, "test-issuer");
765 let wrong_verifying_key = wrong_key.verifying_key();
766 let verifier =
767 arete_auth::TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
768 let plugin = SignedSessionAuthPlugin::new(verifier);
769
770 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
771 .with_scope("read")
772 .with_key_class(KeyClass::Secret)
773 .build();
774
775 let token = signer.sign(claims).unwrap();
776
777 let request = Request::builder()
778 .uri(format!("/ws?hs_token={}", token))
779 .body(())
780 .expect("request should build");
781 let auth_request = ConnectionAuthRequest::from_http_request(
782 "127.0.0.1:8877".parse().expect("socket addr should parse"),
783 &request,
784 );
785
786 let decision = plugin.authorize(&auth_request).await;
787 assert!(!decision.is_allowed());
788
789 if let AuthDecision::Deny(deny) = decision {
790 assert_eq!(deny.code, AuthErrorCode::TokenInvalidSignature);
791 } else {
792 panic!("Expected Deny decision for invalid signature");
793 }
794 }
795
796 #[tokio::test]
797 async fn signed_session_plugin_denies_wrong_audience() {
798 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
799
800 let signing_key = arete_auth::SigningKey::generate();
801 let verifying_key = signing_key.verifying_key();
802 let signer = TokenSigner::new(signing_key, "test-issuer");
803
804 let verifier =
806 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
807 let plugin = SignedSessionAuthPlugin::new(verifier);
808
809 let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
810 .with_scope("read")
811 .with_key_class(KeyClass::Secret)
812 .build();
813
814 let token = signer.sign(claims).unwrap();
815
816 let request = Request::builder()
817 .uri(format!("/ws?hs_token={}", token))
818 .body(())
819 .expect("request should build");
820 let auth_request = ConnectionAuthRequest::from_http_request(
821 "127.0.0.1:8877".parse().expect("socket addr should parse"),
822 &request,
823 );
824
825 let decision = plugin.authorize(&auth_request).await;
826 assert!(!decision.is_allowed());
827
828 if let AuthDecision::Deny(deny) = decision {
829 assert_eq!(deny.code, AuthErrorCode::TokenInvalidAudience);
830 } else {
831 panic!("Expected Deny decision for wrong audience");
832 }
833 }
834
835 #[tokio::test]
836 async fn signed_session_plugin_denies_origin_mismatch() {
837 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
838
839 let signing_key = arete_auth::SigningKey::generate();
840 let verifying_key = signing_key.verifying_key();
841 let signer = TokenSigner::new(signing_key, "test-issuer");
842
843 let verifier =
845 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
846 .with_origin_validation();
847 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
848
849 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
851 .with_scope("read")
852 .with_key_class(KeyClass::Secret)
853 .with_origin("https://allowed.example.com")
854 .build();
855
856 let token = signer.sign(claims).unwrap();
857
858 let request = Request::builder()
860 .uri(format!("/ws?hs_token={}", token))
861 .header("Origin", "https://evil.example.com")
862 .body(())
863 .expect("request should build");
864 let auth_request = ConnectionAuthRequest::from_http_request(
865 "127.0.0.1:8877".parse().expect("socket addr should parse"),
866 &request,
867 );
868
869 let decision = plugin.authorize(&auth_request).await;
870 assert!(!decision.is_allowed());
871
872 if let AuthDecision::Deny(deny) = decision {
873 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
874 } else {
875 panic!("Expected Deny decision for origin mismatch");
876 }
877 }
878
879 #[tokio::test]
880 async fn signed_session_plugin_allows_valid_token() {
881 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
882
883 let signing_key = arete_auth::SigningKey::generate();
884 let verifying_key = signing_key.verifying_key();
885 let signer = TokenSigner::new(signing_key, "test-issuer");
886 let verifier =
887 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
888 let plugin = SignedSessionAuthPlugin::new(verifier);
889
890 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
891 .with_scope("read")
892 .with_key_class(KeyClass::Secret)
893 .with_metering_key("meter-123")
894 .build();
895
896 let token = signer.sign(claims).unwrap();
897
898 let request = Request::builder()
899 .uri(format!("/ws?hs_token={}", token))
900 .body(())
901 .expect("request should build");
902 let auth_request = ConnectionAuthRequest::from_http_request(
903 "127.0.0.1:8877".parse().expect("socket addr should parse"),
904 &request,
905 );
906
907 let decision = plugin.authorize(&auth_request).await;
908 assert!(decision.is_allowed());
909
910 if let AuthDecision::Allow(ctx) = decision {
911 assert_eq!(ctx.subject, "test-subject");
912 assert_eq!(ctx.metering_key, "meter-123");
913 assert_eq!(ctx.key_class, KeyClass::Secret);
914 } else {
915 panic!("Expected Allow decision");
916 }
917 }
918
919 #[tokio::test]
920 async fn signed_session_plugin_allows_with_matching_origin() {
921 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
922
923 let signing_key = arete_auth::SigningKey::generate();
924 let verifying_key = signing_key.verifying_key();
925 let signer = TokenSigner::new(signing_key, "test-issuer");
926
927 let verifier =
928 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
929 .with_origin_validation();
930 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
931
932 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
933 .with_scope("read")
934 .with_key_class(KeyClass::Secret)
935 .with_origin("https://trusted.example.com")
936 .build();
937
938 let token = signer.sign(claims).unwrap();
939
940 let request = Request::builder()
941 .uri(format!("/ws?hs_token={}", token))
942 .header("Origin", "https://trusted.example.com")
943 .body(())
944 .expect("request should build");
945 let auth_request = ConnectionAuthRequest::from_http_request(
946 "127.0.0.1:8877".parse().expect("socket addr should parse"),
947 &request,
948 );
949
950 let decision = plugin.authorize(&auth_request).await;
951 assert!(decision.is_allowed());
952
953 if let AuthDecision::Allow(ctx) = decision {
954 assert_eq!(ctx.origin, Some("https://trusted.example.com".to_string()));
955 } else {
956 panic!("Expected Allow decision");
957 }
958 }
959
960 #[tokio::test]
961 async fn signed_session_plugin_allows_token_with_origin_when_no_origin_provided_and_not_required(
962 ) {
963 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
968
969 let signing_key = arete_auth::SigningKey::generate();
970 let verifying_key = signing_key.verifying_key();
971 let signer = TokenSigner::new(signing_key, "test-issuer");
972
973 let verifier =
975 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
976 let plugin = SignedSessionAuthPlugin::new(verifier);
977
978 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
979 .with_scope("read")
980 .with_key_class(KeyClass::Publishable)
981 .with_origin("https://example.com") .build();
983
984 let token = signer.sign(claims).unwrap();
985
986 let request = Request::builder()
988 .uri(format!("/ws?hs_token={}", token))
989 .body(())
990 .expect("request should build");
991 let auth_request = ConnectionAuthRequest::from_http_request(
992 "127.0.0.1:8877".parse().expect("socket addr should parse"),
993 &request,
994 );
995
996 let decision = plugin.authorize(&auth_request).await;
998 assert!(
999 decision.is_allowed(),
1000 "Expected Allow decision for non-browser client without Origin"
1001 );
1002
1003 if let AuthDecision::Allow(ctx) = decision {
1004 assert_eq!(ctx.origin, Some("https://example.com".to_string()));
1005 } else {
1006 panic!("Expected Allow decision");
1007 }
1008 }
1009
1010 #[tokio::test]
1011 async fn signed_session_plugin_validates_origin_when_provided_even_when_not_required() {
1012 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1015
1016 let signing_key = arete_auth::SigningKey::generate();
1017 let verifying_key = signing_key.verifying_key();
1018 let signer = TokenSigner::new(signing_key, "test-issuer");
1019
1020 let verifier =
1022 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1023 let plugin = SignedSessionAuthPlugin::new(verifier);
1024
1025 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1026 .with_scope("read")
1027 .with_key_class(KeyClass::Publishable)
1028 .with_origin("https://allowed.example.com")
1029 .build();
1030
1031 let token = signer.sign(claims).unwrap();
1032
1033 let request = Request::builder()
1035 .uri(format!("/ws?hs_token={}", token))
1036 .header("Origin", "https://allowed.example.com")
1037 .body(())
1038 .expect("request should build");
1039 let auth_request = ConnectionAuthRequest::from_http_request(
1040 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1041 &request,
1042 );
1043
1044 let decision = plugin.authorize(&auth_request).await;
1045 assert!(decision.is_allowed());
1046
1047 let request = Request::builder()
1049 .uri(format!("/ws?hs_token={}", token))
1050 .header("Origin", "https://evil.example.com")
1051 .body(())
1052 .expect("request should build");
1053 let auth_request = ConnectionAuthRequest::from_http_request(
1054 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1055 &request,
1056 );
1057
1058 let decision = plugin.authorize(&auth_request).await;
1059 assert!(!decision.is_allowed());
1060
1061 if let AuthDecision::Deny(deny) = decision {
1062 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
1063 } else {
1064 panic!("Expected Deny decision for origin mismatch");
1065 }
1066 }
1067
1068 #[test]
1070 fn auth_error_code_should_retry_logic() {
1071 assert!(AuthErrorCode::RateLimitExceeded.should_retry());
1072 assert!(AuthErrorCode::InternalError.should_retry());
1073 assert!(!AuthErrorCode::TokenExpired.should_retry());
1074 assert!(!AuthErrorCode::TokenInvalidSignature.should_retry());
1075 assert!(!AuthErrorCode::TokenMissing.should_retry());
1076 }
1077
1078 #[test]
1079 fn auth_error_code_should_refresh_token_logic() {
1080 assert!(AuthErrorCode::TokenExpired.should_refresh_token());
1081 assert!(AuthErrorCode::TokenInvalidSignature.should_refresh_token());
1082 assert!(AuthErrorCode::TokenInvalidFormat.should_refresh_token());
1083 assert!(AuthErrorCode::TokenInvalidIssuer.should_refresh_token());
1084 assert!(AuthErrorCode::TokenInvalidAudience.should_refresh_token());
1085 assert!(AuthErrorCode::TokenKeyNotFound.should_refresh_token());
1086 assert!(!AuthErrorCode::TokenMissing.should_refresh_token());
1087 assert!(!AuthErrorCode::RateLimitExceeded.should_refresh_token());
1088 assert!(!AuthErrorCode::ConnectionLimitExceeded.should_refresh_token());
1089 }
1090
1091 #[test]
1092 fn auth_error_code_string_representation() {
1093 assert_eq!(AuthErrorCode::TokenMissing.as_str(), "token-missing");
1094 assert_eq!(AuthErrorCode::TokenExpired.as_str(), "token-expired");
1095 assert_eq!(
1096 AuthErrorCode::TokenInvalidSignature.as_str(),
1097 "token-invalid-signature"
1098 );
1099 assert_eq!(
1100 AuthErrorCode::RateLimitExceeded.as_str(),
1101 "rate-limit-exceeded"
1102 );
1103 assert_eq!(
1104 AuthErrorCode::ConnectionLimitExceeded.as_str(),
1105 "connection-limit-exceeded"
1106 );
1107 }
1108
1109 #[test]
1111 fn auth_deny_token_missing_factory() {
1112 let deny = AuthDeny::token_missing();
1113 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
1114 assert!(deny.reason.contains("Missing session token"));
1115 }
1116
1117 #[test]
1118 fn auth_deny_from_verify_error_mapping() {
1119 use arete_auth::VerifyError;
1120
1121 let test_cases = vec![
1122 (VerifyError::Expired, AuthErrorCode::TokenExpired),
1123 (
1124 VerifyError::InvalidSignature,
1125 AuthErrorCode::TokenInvalidSignature,
1126 ),
1127 (
1128 VerifyError::InvalidIssuer,
1129 AuthErrorCode::TokenInvalidIssuer,
1130 ),
1131 (
1132 VerifyError::InvalidAudience,
1133 AuthErrorCode::TokenInvalidAudience,
1134 ),
1135 (
1136 VerifyError::KeyNotFound("kid123".to_string()),
1137 AuthErrorCode::TokenKeyNotFound,
1138 ),
1139 (
1140 VerifyError::OriginMismatch {
1141 expected: "a".to_string(),
1142 actual: "b".to_string(),
1143 },
1144 AuthErrorCode::OriginMismatch,
1145 ),
1146 ];
1147
1148 for (err, expected_code) in test_cases {
1149 let deny = AuthDeny::from_verify_error(err);
1150 assert_eq!(deny.code, expected_code);
1151 }
1152 }
1153
1154 #[tokio::test]
1156 async fn signed_session_plugin_handles_multiple_failure_reasons() {
1157 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1158
1159 let signing_key = arete_auth::SigningKey::generate();
1160 let verifying_key = signing_key.verifying_key();
1161 let signer = TokenSigner::new(signing_key, "test-issuer");
1162 let verifier =
1163 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1164 .with_origin_validation();
1165 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
1166
1167 let request = Request::builder()
1169 .uri("/ws")
1170 .body(())
1171 .expect("request should build");
1172 let auth_request = ConnectionAuthRequest::from_http_request(
1173 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1174 &request,
1175 );
1176 let decision = plugin.authorize(&auth_request).await;
1177 assert!(!decision.is_allowed());
1178 match decision {
1179 AuthDecision::Deny(deny) => assert_eq!(deny.code, AuthErrorCode::TokenMissing),
1180 _ => panic!("Expected Deny decision"),
1181 }
1182
1183 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1185 .with_scope("read")
1186 .with_key_class(KeyClass::Secret)
1187 .with_origin("https://allowed.example.com")
1188 .build();
1189 let token = signer.sign(claims).unwrap();
1190
1191 let request = Request::builder()
1192 .uri(format!("/ws?hs_token={}", token))
1193 .header("Origin", "https://evil.example.com")
1194 .body(())
1195 .expect("request should build");
1196 let auth_request = ConnectionAuthRequest::from_http_request(
1197 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1198 &request,
1199 );
1200 let decision = plugin.authorize(&auth_request).await;
1201 assert!(!decision.is_allowed());
1202 match decision {
1203 AuthDecision::Deny(deny) => assert_eq!(deny.code, AuthErrorCode::OriginMismatch),
1204 _ => panic!("Expected Deny decision for origin mismatch"),
1205 }
1206
1207 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1209 .with_scope("read")
1210 .with_key_class(KeyClass::Secret)
1211 .with_origin("https://allowed.example.com")
1212 .build();
1213 let token = signer.sign(claims).unwrap();
1214
1215 let request = Request::builder()
1216 .uri(format!("/ws?hs_token={}", token))
1217 .header("Origin", "https://allowed.example.com")
1218 .body(())
1219 .expect("request should build");
1220 let auth_request = ConnectionAuthRequest::from_http_request(
1221 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1222 &request,
1223 );
1224 let decision = plugin.authorize(&auth_request).await;
1225 assert!(decision.is_allowed());
1226 }
1227
1228 #[tokio::test]
1230 async fn auth_deney_with_rate_limit_code() {
1231 let deny = AuthDeny::new(
1232 AuthErrorCode::RateLimitExceeded,
1233 "Too many requests from this IP",
1234 );
1235 assert_eq!(deny.code, AuthErrorCode::RateLimitExceeded);
1236 assert!(deny.code.should_retry());
1237 assert!(!deny.code.should_refresh_token());
1238 }
1239
1240 #[tokio::test]
1242 async fn auth_deny_with_connection_limit_code() {
1243 let deny = AuthDeny::new(
1244 AuthErrorCode::ConnectionLimitExceeded,
1245 "Maximum connections exceeded for subject user-123",
1246 );
1247 assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
1248 assert!(!deny.code.should_retry());
1249 assert!(!deny.code.should_refresh_token());
1250 }
1251
1252 #[test]
1254 fn token_extraction_priority() {
1255 let request = Request::builder()
1257 .uri("/ws?hs_token=query-value")
1258 .header("Authorization", "Bearer header-value")
1259 .body(())
1260 .expect("request should build");
1261 let auth_request = ConnectionAuthRequest::from_http_request(
1262 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1263 &request,
1264 );
1265
1266 assert_eq!(auth_request.bearer_token(), Some("header-value"));
1268 assert_eq!(auth_request.query_param("hs_token"), Some("query-value"));
1270 }
1271
1272 #[test]
1274 fn malformed_authorization_header() {
1275 let test_cases = vec![
1276 ("Basic dXNlcjpwYXNz", None), ("Bearer", None), ("", None), ("Bearer token extra", Some("token extra")), ];
1281
1282 for (header_value, expected) in test_cases {
1283 let request = Request::builder()
1284 .uri("/ws")
1285 .header("Authorization", header_value)
1286 .body(())
1287 .expect("request should build");
1288 let auth_request = ConnectionAuthRequest::from_http_request(
1289 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1290 &request,
1291 );
1292 assert_eq!(
1293 auth_request.bearer_token(),
1294 expected,
1295 "Failed for header: {}",
1296 header_value
1297 );
1298 }
1299 }
1300
1301 #[test]
1307 fn auth_deny_error_response_structure() {
1308 let deny = AuthDeny::new(AuthErrorCode::TokenExpired, "Token has expired")
1309 .with_field("exp")
1310 .with_context("Token expired 5 minutes ago")
1311 .with_suggested_action("Refresh your authentication token")
1312 .with_docs_url("https://docs.arete.run/auth/errors#token-expired");
1313
1314 let response = deny.to_error_response();
1315
1316 assert_eq!(response.code, "token-expired");
1317 assert_eq!(response.message, "Token has expired");
1318 assert_eq!(response.error, "token-expired");
1319 assert!(response.retryable);
1320 assert_eq!(
1321 response.suggested_action,
1322 Some("Refresh your authentication token".to_string())
1323 );
1324 assert_eq!(
1325 response.docs_url,
1326 Some("https://docs.arete.run/auth/errors#token-expired".to_string())
1327 );
1328 }
1329
1330 #[test]
1331 fn auth_deny_rate_limited_response() {
1332 use std::time::Duration;
1333
1334 let deny = AuthDeny::rate_limited(Duration::from_secs(30), "websocket connections");
1335 let response = deny.to_error_response();
1336
1337 assert_eq!(response.code, "rate-limit-exceeded");
1338 assert!(response.message.contains("30s"));
1339 assert!(response.retryable);
1340 assert_eq!(response.retry_after, Some(30));
1341 }
1342
1343 #[test]
1344 fn auth_deny_connection_limit_response() {
1345 let deny = AuthDeny::connection_limit_exceeded("user-123", 5, 5);
1346 let response = deny.to_error_response();
1347
1348 assert_eq!(response.code, "connection-limit-exceeded");
1349 assert!(response.message.contains("user-123"));
1350 assert!(response.message.contains("5 of 5"));
1351 assert!(response.retryable); }
1353
1354 #[test]
1355 fn retry_policy_immediate() {
1356 let deny = AuthDeny::new(AuthErrorCode::InternalError, "Transient error")
1357 .with_retry_policy(RetryPolicy::RetryImmediately);
1358
1359 assert_eq!(deny.retry_policy, RetryPolicy::RetryImmediately);
1360 }
1361
1362 #[test]
1363 fn retry_policy_with_backoff() {
1364 use std::time::Duration;
1365
1366 let deny = AuthDeny::new(AuthErrorCode::RateLimitExceeded, "Too many requests")
1367 .with_retry_policy(RetryPolicy::RetryWithBackoff {
1368 initial: Duration::from_secs(1),
1369 max: Duration::from_secs(60),
1370 });
1371
1372 match deny.retry_policy {
1373 RetryPolicy::RetryWithBackoff { initial, max } => {
1374 assert_eq!(initial, Duration::from_secs(1));
1375 assert_eq!(max, Duration::from_secs(60));
1376 }
1377 _ => panic!("Expected RetryWithBackoff"),
1378 }
1379 }
1380
1381 #[test]
1382 fn auth_error_code_http_status_mapping() {
1383 assert_eq!(AuthErrorCode::TokenMissing.http_status(), 401);
1384 assert_eq!(AuthErrorCode::TokenExpired.http_status(), 401);
1385 assert_eq!(AuthErrorCode::TokenInvalidSignature.http_status(), 401);
1386 assert_eq!(AuthErrorCode::OriginMismatch.http_status(), 403);
1387 assert_eq!(AuthErrorCode::RateLimitExceeded.http_status(), 429);
1388 assert_eq!(AuthErrorCode::ConnectionLimitExceeded.http_status(), 429);
1389 assert_eq!(AuthErrorCode::InternalError.http_status(), 500);
1390 }
1391
1392 #[test]
1393 fn auth_error_code_default_retry_policies() {
1394 assert!(matches!(
1396 AuthErrorCode::TokenExpired.default_retry_policy(),
1397 RetryPolicy::RetryWithFreshToken
1398 ));
1399 assert!(matches!(
1400 AuthErrorCode::TokenInvalidSignature.default_retry_policy(),
1401 RetryPolicy::RetryWithFreshToken
1402 ));
1403
1404 assert!(matches!(
1406 AuthErrorCode::RateLimitExceeded.default_retry_policy(),
1407 RetryPolicy::RetryWithBackoff { .. }
1408 ));
1409 assert!(matches!(
1410 AuthErrorCode::InternalError.default_retry_policy(),
1411 RetryPolicy::RetryWithBackoff { .. }
1412 ));
1413
1414 assert!(matches!(
1416 AuthErrorCode::TokenMissing.default_retry_policy(),
1417 RetryPolicy::NoRetry
1418 ));
1419 assert!(matches!(
1420 AuthErrorCode::OriginMismatch.default_retry_policy(),
1421 RetryPolicy::NoRetry
1422 ));
1423 }
1424
1425 #[tokio::test]
1428 async fn handshake_rejects_missing_token_with_proper_error() {
1429 let request = Request::builder()
1431 .uri("/ws")
1432 .body(())
1433 .expect("request should build");
1434
1435 let auth_request = ConnectionAuthRequest::from_http_request(
1436 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1437 &request,
1438 );
1439
1440 let static_plugin = StaticTokenAuthPlugin::new(["valid-token".to_string()]);
1443 let decision = static_plugin.authorize(&auth_request).await;
1444
1445 assert!(!decision.is_allowed());
1446
1447 if let AuthDecision::Deny(deny) = decision {
1448 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
1449 assert_eq!(deny.http_status, 401);
1450 assert!(deny.reason.contains("Missing"));
1451 } else {
1452 panic!("Expected Deny decision");
1453 }
1454 }
1455
1456 #[tokio::test]
1457 async fn handshake_rejects_expired_token_with_retry_hint() {
1458 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1459 use std::time::{SystemTime, UNIX_EPOCH};
1460
1461 let signing_key = arete_auth::SigningKey::generate();
1462 let verifying_key = signing_key.verifying_key();
1463 let signer = TokenSigner::new(signing_key, "test-issuer");
1464
1465 let now = SystemTime::now()
1467 .duration_since(UNIX_EPOCH)
1468 .unwrap()
1469 .as_secs();
1470 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1471 .with_scope("read")
1472 .with_key_class(KeyClass::Secret)
1473 .build();
1474
1475 let mut expired_claims = claims;
1476 expired_claims.exp = now - 3600;
1477 expired_claims.iat = now - 7200;
1478 expired_claims.nbf = now - 7200;
1479
1480 let token = signer.sign(expired_claims).unwrap();
1481
1482 let verifier =
1484 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1485 let plugin = SignedSessionAuthPlugin::new(verifier);
1486
1487 let request = Request::builder()
1488 .uri(format!("/ws?hs_token={}", token))
1489 .body(())
1490 .expect("request should build");
1491
1492 let auth_request = ConnectionAuthRequest::from_http_request(
1493 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1494 &request,
1495 );
1496
1497 let decision = plugin.authorize(&auth_request).await;
1498
1499 assert!(!decision.is_allowed());
1500
1501 if let AuthDecision::Deny(deny) = decision {
1502 assert_eq!(deny.code, AuthErrorCode::TokenExpired);
1503 assert_eq!(deny.http_status, 401);
1504 assert!(matches!(
1506 deny.retry_policy,
1507 RetryPolicy::RetryWithFreshToken
1508 ));
1509 } else {
1510 panic!("Expected Deny decision");
1511 }
1512 }
1513
1514 #[tokio::test]
1515 async fn handshake_rejects_invalid_signature_with_retry_hint() {
1516 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1517
1518 let signing_key = arete_auth::SigningKey::generate();
1520 let wrong_key = arete_auth::SigningKey::generate();
1521
1522 let signer = TokenSigner::new(signing_key, "test-issuer");
1524 let wrong_verifying_key = wrong_key.verifying_key();
1525 let verifier =
1526 arete_auth::TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
1527 let plugin = SignedSessionAuthPlugin::new(verifier);
1528
1529 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1530 .with_scope("read")
1531 .with_key_class(KeyClass::Secret)
1532 .build();
1533
1534 let token = signer.sign(claims).unwrap();
1535
1536 let request = Request::builder()
1537 .uri(format!("/ws?hs_token={}", token))
1538 .body(())
1539 .expect("request should build");
1540
1541 let auth_request = ConnectionAuthRequest::from_http_request(
1542 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1543 &request,
1544 );
1545
1546 let decision = plugin.authorize(&auth_request).await;
1547
1548 assert!(!decision.is_allowed());
1549
1550 if let AuthDecision::Deny(deny) = decision {
1551 assert_eq!(deny.code, AuthErrorCode::TokenInvalidSignature);
1552 assert_eq!(deny.http_status, 401);
1553 assert!(matches!(
1555 deny.retry_policy,
1556 RetryPolicy::RetryWithFreshToken
1557 ));
1558 } else {
1559 panic!("Expected Deny decision");
1560 }
1561 }
1562
1563 #[tokio::test]
1564 async fn handshake_rejects_origin_mismatch_without_retry() {
1565 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1566
1567 let signing_key = arete_auth::SigningKey::generate();
1568 let verifying_key = signing_key.verifying_key();
1569 let signer = TokenSigner::new(signing_key, "test-issuer");
1570
1571 let verifier =
1572 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1573 .with_origin_validation();
1574 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
1575
1576 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1578 .with_scope("read")
1579 .with_key_class(KeyClass::Secret)
1580 .with_origin("https://allowed.example.com")
1581 .build();
1582
1583 let token = signer.sign(claims).unwrap();
1584
1585 let request = Request::builder()
1587 .uri(format!("/ws?hs_token={}", token))
1588 .header("Origin", "https://evil.example.com")
1589 .body(())
1590 .expect("request should build");
1591
1592 let auth_request = ConnectionAuthRequest::from_http_request(
1593 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1594 &request,
1595 );
1596
1597 let decision = plugin.authorize(&auth_request).await;
1598
1599 assert!(!decision.is_allowed());
1600
1601 if let AuthDecision::Deny(deny) = decision {
1602 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
1603 assert_eq!(deny.http_status, 403);
1604 assert!(matches!(deny.retry_policy, RetryPolicy::NoRetry));
1606 } else {
1607 panic!("Expected Deny decision");
1608 }
1609 }
1610
1611 #[test]
1613 fn auth_deny_to_http_response() {
1614 let deny = AuthDeny::new(AuthErrorCode::RateLimitExceeded, "Too many requests")
1615 .with_suggested_action("Wait before retrying")
1616 .with_retry_policy(RetryPolicy::RetryAfter(Duration::from_secs(30)));
1617
1618 let response = deny.to_error_response();
1619
1620 let json = serde_json::to_string(&response).expect("Should serialize");
1622 assert!(json.contains("rate-limit-exceeded"));
1623 assert!(json.contains("Too many requests"));
1624 assert!(json.contains("Wait before retrying"));
1625 assert!(json.contains("\"retryable\":true"));
1626 assert!(json.contains("\"retry_after\":30"));
1627 }
1628
1629 #[tokio::test]
1631 async fn comprehensive_auth_error_scenarios() {
1632 let signing_key = arete_auth::SigningKey::generate();
1633 let verifying_key = signing_key.verifying_key();
1634 let verifier =
1635 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1636 let plugin = SignedSessionAuthPlugin::new(verifier);
1637
1638 let test_cases = vec![
1639 ("missing_token", None, AuthErrorCode::TokenMissing),
1640 (
1641 "invalid_format",
1642 Some("not-a-valid-token"),
1643 AuthErrorCode::TokenInvalidFormat,
1644 ),
1645 ];
1646
1647 for (name, token, expected_code) in test_cases {
1648 let uri = token.map_or_else(|| "/ws".to_string(), |t| format!("/ws?hs_token={}", t));
1649
1650 let request = Request::builder()
1651 .uri(&uri)
1652 .body(())
1653 .expect("request should build");
1654
1655 let auth_request = ConnectionAuthRequest::from_http_request(
1656 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1657 &request,
1658 );
1659
1660 let decision = plugin.authorize(&auth_request).await;
1661
1662 assert!(!decision.is_allowed(), "{}: should deny", name);
1663
1664 if let AuthDecision::Deny(deny) = decision {
1665 assert_eq!(deny.code, expected_code, "{}: wrong error code", name);
1666 } else {
1667 panic!("{}: Expected Deny decision", name);
1668 }
1669 }
1670 }
1671}