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 for_publishable_session(self) -> Self {
219 if self.code != AuthErrorCode::ConnectionLimitExceeded {
220 return self;
221 }
222 let detail = self.reason;
223 Self::new(
224 AuthErrorCode::ConnectionLimitExceeded,
225 "Connection limit reached",
226 )
227 .with_context(detail)
228 .with_suggested_action("Close other connections or retry later")
229 }
230
231 pub fn to_error_response(&self) -> ErrorResponse {
233 ErrorResponse {
234 error: self.code.as_str().to_string(),
235 message: self.reason.clone(),
236 code: self.code.to_string(),
237 retryable: matches!(
238 self.retry_policy,
239 RetryPolicy::RetryImmediately
240 | RetryPolicy::RetryAfter(_)
241 | RetryPolicy::RetryWithBackoff { .. }
242 | RetryPolicy::RetryWithFreshToken
243 ),
244 retry_after: match self.retry_policy {
245 RetryPolicy::RetryAfter(d) => Some(d.as_secs()),
246 _ => None,
247 },
248 suggested_action: self.details.suggested_action.clone(),
249 docs_url: self.details.docs_url.clone(),
250 }
251 }
252}
253
254#[derive(Debug, Clone, serde::Serialize)]
256pub struct ErrorResponse {
257 pub error: String,
258 pub message: String,
259 pub code: String,
260 pub retryable: bool,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub retry_after: Option<u64>,
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub suggested_action: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
266 pub docs_url: Option<String>,
267}
268
269#[derive(Debug, Clone)]
271#[allow(clippy::large_enum_variant)] pub enum AuthDecision {
273 Allow(AuthContext),
275 Deny(AuthDeny),
277}
278
279impl AuthDecision {
280 pub fn is_allowed(&self) -> bool {
282 matches!(self, AuthDecision::Allow(_))
283 }
284
285 pub fn auth_context(&self) -> Option<&AuthContext> {
287 match self {
288 AuthDecision::Allow(ctx) => Some(ctx),
289 AuthDecision::Deny(_) => None,
290 }
291 }
292}
293
294#[async_trait]
295pub trait WebSocketAuthPlugin: Send + Sync + Any {
296 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision;
297
298 fn as_any(&self) -> &dyn Any;
299
300 fn audit_logger(&self) -> Option<&dyn SecurityAuditLogger> {
302 None
303 }
304
305 async fn log_audit(&self, event: SecurityAuditEvent) {
307 if let Some(logger) = self.audit_logger() {
308 logger.log(event).await;
309 }
310 }
311
312 fn auth_metrics(&self) -> Option<&AuthMetrics> {
314 None
315 }
316}
317
318pub struct AllowAllAuthPlugin;
323
324#[async_trait]
325impl WebSocketAuthPlugin for AllowAllAuthPlugin {
326 async fn authorize(&self, _request: &ConnectionAuthRequest) -> AuthDecision {
327 let context = AuthContext {
329 subject: "anonymous".to_string(),
330 issuer: "allow-all".to_string(),
331 audience: "allow-all".to_string(),
332 key_class: arete_auth::KeyClass::Secret,
333 metering_key: "dev".to_string(),
334 deployment_id: None,
335 target_kind: None,
336 target_id: None,
337 program_id: None,
338 program_release_hash: None,
339 expires_at: u64::MAX, scope: "read write".to_string(),
341 limits: Default::default(),
342 plan: None,
343 origin: None,
344 client_ip: None,
345 jti: uuid::Uuid::new_v4().to_string(),
346 actor_key: None,
347 account_key: None,
348 consumer_key: None,
349 policy_version: None,
350 account_limits: Default::default(),
351 row_scopes: None,
352 };
353 AuthDecision::Allow(context)
354 }
355
356 fn as_any(&self) -> &dyn Any {
357 self
358 }
359}
360
361#[derive(Debug, Clone)]
362pub struct StaticTokenAuthPlugin {
363 tokens: HashSet<String>,
364 query_param_name: String,
365}
366
367impl StaticTokenAuthPlugin {
368 pub fn new(tokens: impl IntoIterator<Item = String>) -> Self {
369 Self {
370 tokens: tokens.into_iter().collect(),
371 query_param_name: "token".to_string(),
372 }
373 }
374
375 pub fn with_query_param_name(mut self, query_param_name: impl Into<String>) -> Self {
376 self.query_param_name = query_param_name.into();
377 self
378 }
379
380 fn extract_token<'a>(&self, request: &'a ConnectionAuthRequest) -> Option<&'a str> {
381 request
382 .bearer_token()
383 .or_else(|| request.query_param(&self.query_param_name))
384 }
385}
386
387#[async_trait]
388impl WebSocketAuthPlugin for StaticTokenAuthPlugin {
389 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
390 let token = match self.extract_token(request) {
391 Some(token) => token,
392 None => {
393 return AuthDecision::Deny(AuthDeny::token_missing());
394 }
395 };
396
397 if self.tokens.contains(token) {
398 let context = AuthContext {
400 subject: format!("static:{}", &token[..token.len().min(8)]),
401 issuer: "static-token".to_string(),
402 audience: "static-token".to_string(),
403 key_class: arete_auth::KeyClass::Secret,
404 metering_key: token.to_string(),
405 deployment_id: None,
406 target_kind: None,
407 target_id: None,
408 program_id: None,
409 program_release_hash: None,
410 expires_at: u64::MAX, scope: "read".to_string(),
412 limits: Default::default(),
413 plan: None,
414 origin: request.origin.clone(),
415 client_ip: None,
416 jti: uuid::Uuid::new_v4().to_string(),
417 actor_key: None,
418 account_key: None,
419 consumer_key: None,
420 policy_version: None,
421 account_limits: Default::default(),
422 row_scopes: None,
423 };
424 AuthDecision::Allow(context)
425 } else {
426 AuthDecision::Deny(AuthDeny::new(
427 AuthErrorCode::InvalidStaticToken,
428 "Invalid auth token",
429 ))
430 }
431 }
432
433 fn as_any(&self) -> &dyn Any {
434 self
435 }
436}
437
438enum SignedSessionVerifier {
445 Static(arete_auth::TokenVerifier),
446 CachedJwks(arete_auth::AsyncVerifier),
447 MultiKey(arete_auth::MultiKeyVerifier),
448}
449
450pub struct SignedSessionAuthPlugin {
451 verifier: SignedSessionVerifier,
452 query_param_name: String,
453 require_origin: bool,
454 audit_logger: Option<Arc<dyn SecurityAuditLogger>>,
455 metrics: Option<Arc<AuthMetrics>>,
456}
457
458impl SignedSessionAuthPlugin {
459 pub fn new(verifier: arete_auth::TokenVerifier) -> Self {
461 Self {
462 verifier: SignedSessionVerifier::Static(verifier),
463 query_param_name: "hs_token".to_string(),
464 require_origin: false,
465 audit_logger: None,
466 metrics: None,
467 }
468 }
469
470 pub fn new_with_async_verifier(verifier: arete_auth::AsyncVerifier) -> Self {
472 Self {
473 verifier: SignedSessionVerifier::CachedJwks(verifier),
474 query_param_name: "hs_token".to_string(),
475 require_origin: false,
476 audit_logger: None,
477 metrics: None,
478 }
479 }
480
481 pub fn new_with_multi_key_verifier(verifier: arete_auth::MultiKeyVerifier) -> Self {
483 Self {
484 verifier: SignedSessionVerifier::MultiKey(verifier),
485 query_param_name: "hs_token".to_string(),
486 require_origin: false,
487 audit_logger: None,
488 metrics: None,
489 }
490 }
491
492 pub fn with_query_param_name(mut self, name: impl Into<String>) -> Self {
494 self.query_param_name = name.into();
495 self
496 }
497
498 pub fn with_origin_validation(mut self) -> Self {
500 self.require_origin = true;
501 self
502 }
503
504 pub fn with_audit_logger(mut self, logger: Arc<dyn SecurityAuditLogger>) -> Self {
506 self.audit_logger = Some(logger);
507 self
508 }
509
510 pub fn with_metrics(mut self, metrics: Arc<AuthMetrics>) -> Self {
512 self.metrics = Some(metrics);
513 self
514 }
515
516 pub fn metrics_snapshot(&self) -> Option<AuthMetricsSnapshot> {
518 self.metrics.as_ref().map(|m| m.snapshot())
519 }
520
521 fn extract_token<'a>(&self, request: &'a ConnectionAuthRequest) -> Option<&'a str> {
522 request
523 .bearer_token()
524 .or_else(|| request.query_param(&self.query_param_name))
525 }
526
527 #[allow(clippy::result_large_err)]
533 pub async fn verify_refresh_token(&self, token: &str) -> Result<AuthContext, AuthDeny> {
534 let result = match &self.verifier {
535 SignedSessionVerifier::Static(verifier) => verifier.verify(token, None, None),
536 SignedSessionVerifier::CachedJwks(verifier) => {
537 verifier.verify_with_cache(token, None, None).await
538 }
539 SignedSessionVerifier::MultiKey(verifier) => verifier.verify(token, None, None).await,
540 };
541
542 match result {
543 Ok(context) => Ok(context),
544 Err(e) => Err(AuthDeny::from_verify_error(e)),
545 }
546 }
547}
548
549#[async_trait]
550impl WebSocketAuthPlugin for SignedSessionAuthPlugin {
551 async fn authorize(&self, request: &ConnectionAuthRequest) -> AuthDecision {
552 let token = match self.extract_token(request) {
553 Some(token) => token,
554 None => {
555 return AuthDecision::Deny(AuthDeny::token_missing());
556 }
557 };
558
559 let expected_origin = request.origin.as_deref();
560
561 let expected_client_ip = None; let result = match &self.verifier {
564 SignedSessionVerifier::Static(verifier) => {
565 verifier.verify(token, expected_origin, expected_client_ip)
566 }
567 SignedSessionVerifier::CachedJwks(verifier) => {
568 verifier
569 .verify_with_cache(token, expected_origin, expected_client_ip)
570 .await
571 }
572 SignedSessionVerifier::MultiKey(verifier) => {
573 verifier
574 .verify(token, expected_origin, expected_client_ip)
575 .await
576 }
577 };
578
579 match result {
580 Ok(context) => {
581 let event = auth_success_event(&context.subject)
583 .with_client_ip(request.remote_addr)
584 .with_path(&request.path);
585 if let Some(origin) = &request.origin {
586 let event = event.with_origin(origin.clone());
587 self.log_audit(event).await;
588 } else {
589 self.log_audit(event).await;
590 }
591 AuthDecision::Allow(context)
592 }
593 Err(e) => {
594 let deny = AuthDeny::from_verify_error(e);
595 let event = auth_failure_event(&deny.code, &deny.reason)
597 .with_client_ip(request.remote_addr)
598 .with_path(&request.path);
599 let event = if let Some(origin) = &request.origin {
600 event.with_origin(origin.clone())
601 } else {
602 event
603 };
604 self.log_audit(event).await;
605 AuthDecision::Deny(deny)
606 }
607 }
608 }
609
610 fn as_any(&self) -> &dyn Any {
611 self
612 }
613
614 fn audit_logger(&self) -> Option<&dyn SecurityAuditLogger> {
615 self.audit_logger.as_ref().map(|l| l.as_ref())
616 }
617
618 fn auth_metrics(&self) -> Option<&AuthMetrics> {
619 self.metrics.as_ref().map(|m| m.as_ref())
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
628 fn extracts_bearer_and_query_tokens() {
629 let request = Request::builder()
630 .uri("/ws?token=query-token")
631 .header("Authorization", "Bearer header-token")
632 .body(())
633 .expect("request should build");
634
635 let auth_request = ConnectionAuthRequest::from_http_request(
636 "127.0.0.1:8877".parse().expect("socket addr should parse"),
637 &request,
638 );
639
640 assert_eq!(auth_request.bearer_token(), Some("header-token"));
641 assert_eq!(auth_request.query_param("token"), Some("query-token"));
642 }
643
644 #[tokio::test]
645 async fn static_token_plugin_allows_matching_token() {
646 let plugin = StaticTokenAuthPlugin::new(["secret".to_string()]);
647 let request = Request::builder()
648 .uri("/ws?token=secret")
649 .body(())
650 .expect("request should build");
651 let auth_request = ConnectionAuthRequest::from_http_request(
652 "127.0.0.1:8877".parse().expect("socket addr should parse"),
653 &request,
654 );
655
656 let decision = plugin.authorize(&auth_request).await;
657 assert!(decision.is_allowed());
658 assert!(decision.auth_context().is_some());
659 }
660
661 #[tokio::test]
662 async fn static_token_plugin_denies_missing_token() {
663 let plugin = StaticTokenAuthPlugin::new(["secret".to_string()]);
664 let request = Request::builder()
665 .uri("/ws")
666 .body(())
667 .expect("request should build");
668 let auth_request = ConnectionAuthRequest::from_http_request(
669 "127.0.0.1:8877".parse().expect("socket addr should parse"),
670 &request,
671 );
672
673 let decision = plugin.authorize(&auth_request).await;
674 assert!(!decision.is_allowed());
675 }
676
677 #[tokio::test]
678 async fn allow_all_plugin_allows_with_context() {
679 let plugin = AllowAllAuthPlugin;
680 let request = Request::builder()
681 .uri("/ws")
682 .body(())
683 .expect("request should build");
684 let auth_request = ConnectionAuthRequest::from_http_request(
685 "127.0.0.1:8877".parse().expect("socket addr should parse"),
686 &request,
687 );
688
689 let decision = plugin.authorize(&auth_request).await;
690 assert!(decision.is_allowed());
691 let ctx = decision.auth_context().unwrap();
692 assert_eq!(ctx.subject, "anonymous");
693 }
694
695 #[tokio::test]
698 async fn signed_session_plugin_denies_missing_token() {
699 let signing_key = arete_auth::SigningKey::generate();
700 let verifying_key = signing_key.verifying_key();
701 let verifier =
702 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
703 let plugin = SignedSessionAuthPlugin::new(verifier);
704
705 let request = Request::builder()
706 .uri("/ws")
707 .body(())
708 .expect("request should build");
709 let auth_request = ConnectionAuthRequest::from_http_request(
710 "127.0.0.1:8877".parse().expect("socket addr should parse"),
711 &request,
712 );
713
714 let decision = plugin.authorize(&auth_request).await;
715 assert!(!decision.is_allowed());
716
717 if let AuthDecision::Deny(deny) = decision {
718 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
719 } else {
720 panic!("Expected Deny decision");
721 }
722 }
723
724 #[tokio::test]
725 async fn signed_session_plugin_denies_expired_token() {
726 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
727 use std::time::{SystemTime, UNIX_EPOCH};
728
729 let signing_key = arete_auth::SigningKey::generate();
730 let verifying_key = signing_key.verifying_key();
731 let signer = TokenSigner::new(signing_key, "test-issuer");
732 let verifier =
733 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
734 let plugin = SignedSessionAuthPlugin::new(verifier);
735
736 let now = SystemTime::now()
738 .duration_since(UNIX_EPOCH)
739 .unwrap()
740 .as_secs();
741 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
742 .with_scope("read")
743 .with_key_class(KeyClass::Secret)
744 .build();
745
746 let mut expired_claims = claims;
748 expired_claims.exp = now - 3600; expired_claims.iat = now - 7200; expired_claims.nbf = now - 7200;
751
752 let token = signer.sign(expired_claims).unwrap();
753
754 let request = Request::builder()
755 .uri(format!("/ws?hs_token={}", token))
756 .body(())
757 .expect("request should build");
758 let auth_request = ConnectionAuthRequest::from_http_request(
759 "127.0.0.1:8877".parse().expect("socket addr should parse"),
760 &request,
761 );
762
763 let decision = plugin.authorize(&auth_request).await;
764 assert!(!decision.is_allowed());
765
766 if let AuthDecision::Deny(deny) = decision {
767 assert_eq!(deny.code, AuthErrorCode::TokenExpired);
768 } else {
769 panic!("Expected Deny decision for expired token");
770 }
771 }
772
773 #[tokio::test]
774 async fn signed_session_plugin_denies_invalid_signature() {
775 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
776
777 let signing_key = arete_auth::SigningKey::generate();
779 let wrong_key = arete_auth::SigningKey::generate();
780
781 let signer = TokenSigner::new(signing_key, "test-issuer");
783 let wrong_verifying_key = wrong_key.verifying_key();
784 let verifier =
785 arete_auth::TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
786 let plugin = SignedSessionAuthPlugin::new(verifier);
787
788 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
789 .with_scope("read")
790 .with_key_class(KeyClass::Secret)
791 .build();
792
793 let token = signer.sign(claims).unwrap();
794
795 let request = Request::builder()
796 .uri(format!("/ws?hs_token={}", token))
797 .body(())
798 .expect("request should build");
799 let auth_request = ConnectionAuthRequest::from_http_request(
800 "127.0.0.1:8877".parse().expect("socket addr should parse"),
801 &request,
802 );
803
804 let decision = plugin.authorize(&auth_request).await;
805 assert!(!decision.is_allowed());
806
807 if let AuthDecision::Deny(deny) = decision {
808 assert_eq!(deny.code, AuthErrorCode::TokenInvalidSignature);
809 } else {
810 panic!("Expected Deny decision for invalid signature");
811 }
812 }
813
814 #[tokio::test]
815 async fn signed_session_plugin_denies_wrong_audience() {
816 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
817
818 let signing_key = arete_auth::SigningKey::generate();
819 let verifying_key = signing_key.verifying_key();
820 let signer = TokenSigner::new(signing_key, "test-issuer");
821
822 let verifier =
824 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
825 let plugin = SignedSessionAuthPlugin::new(verifier);
826
827 let claims = SessionClaims::builder("test-issuer", "test-subject", "wrong-audience")
828 .with_scope("read")
829 .with_key_class(KeyClass::Secret)
830 .build();
831
832 let token = signer.sign(claims).unwrap();
833
834 let request = Request::builder()
835 .uri(format!("/ws?hs_token={}", token))
836 .body(())
837 .expect("request should build");
838 let auth_request = ConnectionAuthRequest::from_http_request(
839 "127.0.0.1:8877".parse().expect("socket addr should parse"),
840 &request,
841 );
842
843 let decision = plugin.authorize(&auth_request).await;
844 assert!(!decision.is_allowed());
845
846 if let AuthDecision::Deny(deny) = decision {
847 assert_eq!(deny.code, AuthErrorCode::TokenInvalidAudience);
848 } else {
849 panic!("Expected Deny decision for wrong audience");
850 }
851 }
852
853 #[tokio::test]
854 async fn signed_session_plugin_denies_origin_mismatch() {
855 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
856
857 let signing_key = arete_auth::SigningKey::generate();
858 let verifying_key = signing_key.verifying_key();
859 let signer = TokenSigner::new(signing_key, "test-issuer");
860
861 let verifier =
863 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
864 .with_origin_validation();
865 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
866
867 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
869 .with_scope("read")
870 .with_key_class(KeyClass::Secret)
871 .with_origin("https://allowed.example.com")
872 .build();
873
874 let token = signer.sign(claims).unwrap();
875
876 let request = Request::builder()
878 .uri(format!("/ws?hs_token={}", token))
879 .header("Origin", "https://evil.example.com")
880 .body(())
881 .expect("request should build");
882 let auth_request = ConnectionAuthRequest::from_http_request(
883 "127.0.0.1:8877".parse().expect("socket addr should parse"),
884 &request,
885 );
886
887 let decision = plugin.authorize(&auth_request).await;
888 assert!(!decision.is_allowed());
889
890 if let AuthDecision::Deny(deny) = decision {
891 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
892 } else {
893 panic!("Expected Deny decision for origin mismatch");
894 }
895 }
896
897 #[tokio::test]
898 async fn signed_session_plugin_allows_valid_token() {
899 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
900
901 let signing_key = arete_auth::SigningKey::generate();
902 let verifying_key = signing_key.verifying_key();
903 let signer = TokenSigner::new(signing_key, "test-issuer");
904 let verifier =
905 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
906 let plugin = SignedSessionAuthPlugin::new(verifier);
907
908 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
909 .with_scope("read")
910 .with_key_class(KeyClass::Secret)
911 .with_metering_key("meter-123")
912 .build();
913
914 let token = signer.sign(claims).unwrap();
915
916 let request = Request::builder()
917 .uri(format!("/ws?hs_token={}", token))
918 .body(())
919 .expect("request should build");
920 let auth_request = ConnectionAuthRequest::from_http_request(
921 "127.0.0.1:8877".parse().expect("socket addr should parse"),
922 &request,
923 );
924
925 let decision = plugin.authorize(&auth_request).await;
926 assert!(decision.is_allowed());
927
928 if let AuthDecision::Allow(ctx) = decision {
929 assert_eq!(ctx.subject, "test-subject");
930 assert_eq!(ctx.metering_key, "meter-123");
931 assert_eq!(ctx.key_class, KeyClass::Secret);
932 } else {
933 panic!("Expected Allow decision");
934 }
935 }
936
937 #[tokio::test]
938 async fn signed_session_plugin_allows_with_matching_origin() {
939 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
940
941 let signing_key = arete_auth::SigningKey::generate();
942 let verifying_key = signing_key.verifying_key();
943 let signer = TokenSigner::new(signing_key, "test-issuer");
944
945 let verifier =
946 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
947 .with_origin_validation();
948 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
949
950 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
951 .with_scope("read")
952 .with_key_class(KeyClass::Secret)
953 .with_origin("https://trusted.example.com")
954 .build();
955
956 let token = signer.sign(claims).unwrap();
957
958 let request = Request::builder()
959 .uri(format!("/ws?hs_token={}", token))
960 .header("Origin", "https://trusted.example.com")
961 .body(())
962 .expect("request should build");
963 let auth_request = ConnectionAuthRequest::from_http_request(
964 "127.0.0.1:8877".parse().expect("socket addr should parse"),
965 &request,
966 );
967
968 let decision = plugin.authorize(&auth_request).await;
969 assert!(decision.is_allowed());
970
971 if let AuthDecision::Allow(ctx) = decision {
972 assert_eq!(ctx.origin, Some("https://trusted.example.com".to_string()));
973 } else {
974 panic!("Expected Allow decision");
975 }
976 }
977
978 #[tokio::test]
979 async fn signed_session_plugin_allows_token_with_origin_when_no_origin_provided_and_not_required(
980 ) {
981 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
986
987 let signing_key = arete_auth::SigningKey::generate();
988 let verifying_key = signing_key.verifying_key();
989 let signer = TokenSigner::new(signing_key, "test-issuer");
990
991 let verifier =
993 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
994 let plugin = SignedSessionAuthPlugin::new(verifier);
995
996 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
997 .with_scope("read")
998 .with_key_class(KeyClass::Publishable)
999 .with_origin("https://example.com") .build();
1001
1002 let token = signer.sign(claims).unwrap();
1003
1004 let request = Request::builder()
1006 .uri(format!("/ws?hs_token={}", token))
1007 .body(())
1008 .expect("request should build");
1009 let auth_request = ConnectionAuthRequest::from_http_request(
1010 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1011 &request,
1012 );
1013
1014 let decision = plugin.authorize(&auth_request).await;
1016 assert!(
1017 decision.is_allowed(),
1018 "Expected Allow decision for non-browser client without Origin"
1019 );
1020
1021 if let AuthDecision::Allow(ctx) = decision {
1022 assert_eq!(ctx.origin, Some("https://example.com".to_string()));
1023 } else {
1024 panic!("Expected Allow decision");
1025 }
1026 }
1027
1028 #[tokio::test]
1029 async fn signed_session_plugin_validates_origin_when_provided_even_when_not_required() {
1030 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1033
1034 let signing_key = arete_auth::SigningKey::generate();
1035 let verifying_key = signing_key.verifying_key();
1036 let signer = TokenSigner::new(signing_key, "test-issuer");
1037
1038 let verifier =
1040 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1041 let plugin = SignedSessionAuthPlugin::new(verifier);
1042
1043 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1044 .with_scope("read")
1045 .with_key_class(KeyClass::Publishable)
1046 .with_origin("https://allowed.example.com")
1047 .build();
1048
1049 let token = signer.sign(claims).unwrap();
1050
1051 let request = Request::builder()
1053 .uri(format!("/ws?hs_token={}", token))
1054 .header("Origin", "https://allowed.example.com")
1055 .body(())
1056 .expect("request should build");
1057 let auth_request = ConnectionAuthRequest::from_http_request(
1058 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1059 &request,
1060 );
1061
1062 let decision = plugin.authorize(&auth_request).await;
1063 assert!(decision.is_allowed());
1064
1065 let request = Request::builder()
1067 .uri(format!("/ws?hs_token={}", token))
1068 .header("Origin", "https://evil.example.com")
1069 .body(())
1070 .expect("request should build");
1071 let auth_request = ConnectionAuthRequest::from_http_request(
1072 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1073 &request,
1074 );
1075
1076 let decision = plugin.authorize(&auth_request).await;
1077 assert!(!decision.is_allowed());
1078
1079 if let AuthDecision::Deny(deny) = decision {
1080 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
1081 } else {
1082 panic!("Expected Deny decision for origin mismatch");
1083 }
1084 }
1085
1086 #[test]
1088 fn auth_error_code_should_retry_logic() {
1089 assert!(AuthErrorCode::RateLimitExceeded.should_retry());
1090 assert!(AuthErrorCode::InternalError.should_retry());
1091 assert!(!AuthErrorCode::TokenExpired.should_retry());
1092 assert!(!AuthErrorCode::TokenInvalidSignature.should_retry());
1093 assert!(!AuthErrorCode::TokenMissing.should_retry());
1094 }
1095
1096 #[test]
1097 fn auth_error_code_should_refresh_token_logic() {
1098 assert!(AuthErrorCode::TokenExpired.should_refresh_token());
1099 assert!(AuthErrorCode::TokenInvalidSignature.should_refresh_token());
1100 assert!(AuthErrorCode::TokenInvalidFormat.should_refresh_token());
1101 assert!(AuthErrorCode::TokenInvalidIssuer.should_refresh_token());
1102 assert!(AuthErrorCode::TokenInvalidAudience.should_refresh_token());
1103 assert!(AuthErrorCode::TokenKeyNotFound.should_refresh_token());
1104 assert!(!AuthErrorCode::TokenMissing.should_refresh_token());
1105 assert!(!AuthErrorCode::RateLimitExceeded.should_refresh_token());
1106 assert!(!AuthErrorCode::ConnectionLimitExceeded.should_refresh_token());
1107 }
1108
1109 #[test]
1110 fn auth_error_code_string_representation() {
1111 assert_eq!(AuthErrorCode::TokenMissing.as_str(), "token-missing");
1112 assert_eq!(AuthErrorCode::TokenExpired.as_str(), "token-expired");
1113 assert_eq!(
1114 AuthErrorCode::TokenInvalidSignature.as_str(),
1115 "token-invalid-signature"
1116 );
1117 assert_eq!(
1118 AuthErrorCode::RateLimitExceeded.as_str(),
1119 "rate-limit-exceeded"
1120 );
1121 assert_eq!(
1122 AuthErrorCode::ConnectionLimitExceeded.as_str(),
1123 "connection-limit-exceeded"
1124 );
1125 }
1126
1127 #[test]
1129 fn auth_deny_token_missing_factory() {
1130 let deny = AuthDeny::token_missing();
1131 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
1132 assert!(deny.reason.contains("Missing session token"));
1133 }
1134
1135 #[test]
1136 fn auth_deny_from_verify_error_mapping() {
1137 use arete_auth::VerifyError;
1138
1139 let test_cases = vec![
1140 (VerifyError::Expired, AuthErrorCode::TokenExpired),
1141 (
1142 VerifyError::InvalidSignature,
1143 AuthErrorCode::TokenInvalidSignature,
1144 ),
1145 (
1146 VerifyError::InvalidIssuer,
1147 AuthErrorCode::TokenInvalidIssuer,
1148 ),
1149 (
1150 VerifyError::InvalidAudience,
1151 AuthErrorCode::TokenInvalidAudience,
1152 ),
1153 (
1154 VerifyError::KeyNotFound("kid123".to_string()),
1155 AuthErrorCode::TokenKeyNotFound,
1156 ),
1157 (
1158 VerifyError::OriginMismatch {
1159 expected: "a".to_string(),
1160 actual: "b".to_string(),
1161 },
1162 AuthErrorCode::OriginMismatch,
1163 ),
1164 ];
1165
1166 for (err, expected_code) in test_cases {
1167 let deny = AuthDeny::from_verify_error(err);
1168 assert_eq!(deny.code, expected_code);
1169 }
1170 }
1171
1172 #[tokio::test]
1174 async fn signed_session_plugin_handles_multiple_failure_reasons() {
1175 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1176
1177 let signing_key = arete_auth::SigningKey::generate();
1178 let verifying_key = signing_key.verifying_key();
1179 let signer = TokenSigner::new(signing_key, "test-issuer");
1180 let verifier =
1181 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1182 .with_origin_validation();
1183 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
1184
1185 let request = Request::builder()
1187 .uri("/ws")
1188 .body(())
1189 .expect("request should build");
1190 let auth_request = ConnectionAuthRequest::from_http_request(
1191 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1192 &request,
1193 );
1194 let decision = plugin.authorize(&auth_request).await;
1195 assert!(!decision.is_allowed());
1196 match decision {
1197 AuthDecision::Deny(deny) => assert_eq!(deny.code, AuthErrorCode::TokenMissing),
1198 _ => panic!("Expected Deny decision"),
1199 }
1200
1201 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1203 .with_scope("read")
1204 .with_key_class(KeyClass::Secret)
1205 .with_origin("https://allowed.example.com")
1206 .build();
1207 let token = signer.sign(claims).unwrap();
1208
1209 let request = Request::builder()
1210 .uri(format!("/ws?hs_token={}", token))
1211 .header("Origin", "https://evil.example.com")
1212 .body(())
1213 .expect("request should build");
1214 let auth_request = ConnectionAuthRequest::from_http_request(
1215 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1216 &request,
1217 );
1218 let decision = plugin.authorize(&auth_request).await;
1219 assert!(!decision.is_allowed());
1220 match decision {
1221 AuthDecision::Deny(deny) => assert_eq!(deny.code, AuthErrorCode::OriginMismatch),
1222 _ => panic!("Expected Deny decision for origin mismatch"),
1223 }
1224
1225 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1227 .with_scope("read")
1228 .with_key_class(KeyClass::Secret)
1229 .with_origin("https://allowed.example.com")
1230 .build();
1231 let token = signer.sign(claims).unwrap();
1232
1233 let request = Request::builder()
1234 .uri(format!("/ws?hs_token={}", token))
1235 .header("Origin", "https://allowed.example.com")
1236 .body(())
1237 .expect("request should build");
1238 let auth_request = ConnectionAuthRequest::from_http_request(
1239 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1240 &request,
1241 );
1242 let decision = plugin.authorize(&auth_request).await;
1243 assert!(decision.is_allowed());
1244 }
1245
1246 #[tokio::test]
1248 async fn auth_deney_with_rate_limit_code() {
1249 let deny = AuthDeny::new(
1250 AuthErrorCode::RateLimitExceeded,
1251 "Too many requests from this IP",
1252 );
1253 assert_eq!(deny.code, AuthErrorCode::RateLimitExceeded);
1254 assert!(deny.code.should_retry());
1255 assert!(!deny.code.should_refresh_token());
1256 }
1257
1258 #[tokio::test]
1260 async fn auth_deny_with_connection_limit_code() {
1261 let deny = AuthDeny::new(
1262 AuthErrorCode::ConnectionLimitExceeded,
1263 "Maximum connections exceeded for subject user-123",
1264 );
1265 assert_eq!(deny.code, AuthErrorCode::ConnectionLimitExceeded);
1266 assert!(!deny.code.should_retry());
1267 assert!(!deny.code.should_refresh_token());
1268 }
1269
1270 #[test]
1272 fn token_extraction_priority() {
1273 let request = Request::builder()
1275 .uri("/ws?hs_token=query-value")
1276 .header("Authorization", "Bearer header-value")
1277 .body(())
1278 .expect("request should build");
1279 let auth_request = ConnectionAuthRequest::from_http_request(
1280 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1281 &request,
1282 );
1283
1284 assert_eq!(auth_request.bearer_token(), Some("header-value"));
1286 assert_eq!(auth_request.query_param("hs_token"), Some("query-value"));
1288 }
1289
1290 #[test]
1292 fn malformed_authorization_header() {
1293 let test_cases = vec![
1294 ("Basic dXNlcjpwYXNz", None), ("Bearer", None), ("", None), ("Bearer token extra", Some("token extra")), ];
1299
1300 for (header_value, expected) in test_cases {
1301 let request = Request::builder()
1302 .uri("/ws")
1303 .header("Authorization", header_value)
1304 .body(())
1305 .expect("request should build");
1306 let auth_request = ConnectionAuthRequest::from_http_request(
1307 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1308 &request,
1309 );
1310 assert_eq!(
1311 auth_request.bearer_token(),
1312 expected,
1313 "Failed for header: {}",
1314 header_value
1315 );
1316 }
1317 }
1318
1319 #[test]
1325 fn auth_deny_error_response_structure() {
1326 let deny = AuthDeny::new(AuthErrorCode::TokenExpired, "Token has expired")
1327 .with_field("exp")
1328 .with_context("Token expired 5 minutes ago")
1329 .with_suggested_action("Refresh your authentication token")
1330 .with_docs_url("https://docs.arete.run/auth/errors#token-expired");
1331
1332 let response = deny.to_error_response();
1333
1334 assert_eq!(response.code, "token-expired");
1335 assert_eq!(response.message, "Token has expired");
1336 assert_eq!(response.error, "token-expired");
1337 assert!(response.retryable);
1338 assert_eq!(
1339 response.suggested_action,
1340 Some("Refresh your authentication token".to_string())
1341 );
1342 assert_eq!(
1343 response.docs_url,
1344 Some("https://docs.arete.run/auth/errors#token-expired".to_string())
1345 );
1346 }
1347
1348 #[test]
1349 fn auth_deny_rate_limited_response() {
1350 use std::time::Duration;
1351
1352 let deny = AuthDeny::rate_limited(Duration::from_secs(30), "websocket connections");
1353 let response = deny.to_error_response();
1354
1355 assert_eq!(response.code, "rate-limit-exceeded");
1356 assert!(response.message.contains("30s"));
1357 assert!(response.retryable);
1358 assert_eq!(response.retry_after, Some(30));
1359 }
1360
1361 #[test]
1362 fn auth_deny_connection_limit_response() {
1363 let deny = AuthDeny::connection_limit_exceeded("user-123", 5, 5);
1364 let response = deny.to_error_response();
1365
1366 assert_eq!(response.code, "connection-limit-exceeded");
1367 assert!(response.message.contains("user-123"));
1368 assert!(response.message.contains("5 of 5"));
1369 assert!(response.retryable); }
1371
1372 #[test]
1373 fn retry_policy_immediate() {
1374 let deny = AuthDeny::new(AuthErrorCode::InternalError, "Transient error")
1375 .with_retry_policy(RetryPolicy::RetryImmediately);
1376
1377 assert_eq!(deny.retry_policy, RetryPolicy::RetryImmediately);
1378 }
1379
1380 #[test]
1381 fn retry_policy_with_backoff() {
1382 use std::time::Duration;
1383
1384 let deny = AuthDeny::new(AuthErrorCode::RateLimitExceeded, "Too many requests")
1385 .with_retry_policy(RetryPolicy::RetryWithBackoff {
1386 initial: Duration::from_secs(1),
1387 max: Duration::from_secs(60),
1388 });
1389
1390 match deny.retry_policy {
1391 RetryPolicy::RetryWithBackoff { initial, max } => {
1392 assert_eq!(initial, Duration::from_secs(1));
1393 assert_eq!(max, Duration::from_secs(60));
1394 }
1395 _ => panic!("Expected RetryWithBackoff"),
1396 }
1397 }
1398
1399 #[test]
1400 fn auth_error_code_http_status_mapping() {
1401 assert_eq!(AuthErrorCode::TokenMissing.http_status(), 401);
1402 assert_eq!(AuthErrorCode::TokenExpired.http_status(), 401);
1403 assert_eq!(AuthErrorCode::TokenInvalidSignature.http_status(), 401);
1404 assert_eq!(AuthErrorCode::OriginMismatch.http_status(), 403);
1405 assert_eq!(AuthErrorCode::RateLimitExceeded.http_status(), 429);
1406 assert_eq!(AuthErrorCode::ConnectionLimitExceeded.http_status(), 429);
1407 assert_eq!(AuthErrorCode::InternalError.http_status(), 500);
1408 }
1409
1410 #[test]
1411 fn auth_error_code_default_retry_policies() {
1412 assert!(matches!(
1414 AuthErrorCode::TokenExpired.default_retry_policy(),
1415 RetryPolicy::RetryWithFreshToken
1416 ));
1417 assert!(matches!(
1418 AuthErrorCode::TokenInvalidSignature.default_retry_policy(),
1419 RetryPolicy::RetryWithFreshToken
1420 ));
1421
1422 assert!(matches!(
1424 AuthErrorCode::RateLimitExceeded.default_retry_policy(),
1425 RetryPolicy::RetryWithBackoff { .. }
1426 ));
1427 assert!(matches!(
1428 AuthErrorCode::InternalError.default_retry_policy(),
1429 RetryPolicy::RetryWithBackoff { .. }
1430 ));
1431
1432 assert!(matches!(
1434 AuthErrorCode::TokenMissing.default_retry_policy(),
1435 RetryPolicy::NoRetry
1436 ));
1437 assert!(matches!(
1438 AuthErrorCode::OriginMismatch.default_retry_policy(),
1439 RetryPolicy::NoRetry
1440 ));
1441 }
1442
1443 #[tokio::test]
1446 async fn handshake_rejects_missing_token_with_proper_error() {
1447 let request = Request::builder()
1449 .uri("/ws")
1450 .body(())
1451 .expect("request should build");
1452
1453 let auth_request = ConnectionAuthRequest::from_http_request(
1454 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1455 &request,
1456 );
1457
1458 let static_plugin = StaticTokenAuthPlugin::new(["valid-token".to_string()]);
1461 let decision = static_plugin.authorize(&auth_request).await;
1462
1463 assert!(!decision.is_allowed());
1464
1465 if let AuthDecision::Deny(deny) = decision {
1466 assert_eq!(deny.code, AuthErrorCode::TokenMissing);
1467 assert_eq!(deny.http_status, 401);
1468 assert!(deny.reason.contains("Missing"));
1469 } else {
1470 panic!("Expected Deny decision");
1471 }
1472 }
1473
1474 #[tokio::test]
1475 async fn handshake_rejects_expired_token_with_retry_hint() {
1476 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1477 use std::time::{SystemTime, UNIX_EPOCH};
1478
1479 let signing_key = arete_auth::SigningKey::generate();
1480 let verifying_key = signing_key.verifying_key();
1481 let signer = TokenSigner::new(signing_key, "test-issuer");
1482
1483 let now = SystemTime::now()
1485 .duration_since(UNIX_EPOCH)
1486 .unwrap()
1487 .as_secs();
1488 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1489 .with_scope("read")
1490 .with_key_class(KeyClass::Secret)
1491 .build();
1492
1493 let mut expired_claims = claims;
1494 expired_claims.exp = now - 3600;
1495 expired_claims.iat = now - 7200;
1496 expired_claims.nbf = now - 7200;
1497
1498 let token = signer.sign(expired_claims).unwrap();
1499
1500 let verifier =
1502 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1503 let plugin = SignedSessionAuthPlugin::new(verifier);
1504
1505 let request = Request::builder()
1506 .uri(format!("/ws?hs_token={}", token))
1507 .body(())
1508 .expect("request should build");
1509
1510 let auth_request = ConnectionAuthRequest::from_http_request(
1511 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1512 &request,
1513 );
1514
1515 let decision = plugin.authorize(&auth_request).await;
1516
1517 assert!(!decision.is_allowed());
1518
1519 if let AuthDecision::Deny(deny) = decision {
1520 assert_eq!(deny.code, AuthErrorCode::TokenExpired);
1521 assert_eq!(deny.http_status, 401);
1522 assert!(matches!(
1524 deny.retry_policy,
1525 RetryPolicy::RetryWithFreshToken
1526 ));
1527 } else {
1528 panic!("Expected Deny decision");
1529 }
1530 }
1531
1532 #[tokio::test]
1533 async fn handshake_rejects_invalid_signature_with_retry_hint() {
1534 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1535
1536 let signing_key = arete_auth::SigningKey::generate();
1538 let wrong_key = arete_auth::SigningKey::generate();
1539
1540 let signer = TokenSigner::new(signing_key, "test-issuer");
1542 let wrong_verifying_key = wrong_key.verifying_key();
1543 let verifier =
1544 arete_auth::TokenVerifier::new(wrong_verifying_key, "test-issuer", "test-audience");
1545 let plugin = SignedSessionAuthPlugin::new(verifier);
1546
1547 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1548 .with_scope("read")
1549 .with_key_class(KeyClass::Secret)
1550 .build();
1551
1552 let token = signer.sign(claims).unwrap();
1553
1554 let request = Request::builder()
1555 .uri(format!("/ws?hs_token={}", token))
1556 .body(())
1557 .expect("request should build");
1558
1559 let auth_request = ConnectionAuthRequest::from_http_request(
1560 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1561 &request,
1562 );
1563
1564 let decision = plugin.authorize(&auth_request).await;
1565
1566 assert!(!decision.is_allowed());
1567
1568 if let AuthDecision::Deny(deny) = decision {
1569 assert_eq!(deny.code, AuthErrorCode::TokenInvalidSignature);
1570 assert_eq!(deny.http_status, 401);
1571 assert!(matches!(
1573 deny.retry_policy,
1574 RetryPolicy::RetryWithFreshToken
1575 ));
1576 } else {
1577 panic!("Expected Deny decision");
1578 }
1579 }
1580
1581 #[tokio::test]
1582 async fn handshake_rejects_origin_mismatch_without_retry() {
1583 use arete_auth::{KeyClass, SessionClaims, TokenSigner};
1584
1585 let signing_key = arete_auth::SigningKey::generate();
1586 let verifying_key = signing_key.verifying_key();
1587 let signer = TokenSigner::new(signing_key, "test-issuer");
1588
1589 let verifier =
1590 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience")
1591 .with_origin_validation();
1592 let plugin = SignedSessionAuthPlugin::new(verifier).with_origin_validation();
1593
1594 let claims = SessionClaims::builder("test-issuer", "test-subject", "test-audience")
1596 .with_scope("read")
1597 .with_key_class(KeyClass::Secret)
1598 .with_origin("https://allowed.example.com")
1599 .build();
1600
1601 let token = signer.sign(claims).unwrap();
1602
1603 let request = Request::builder()
1605 .uri(format!("/ws?hs_token={}", token))
1606 .header("Origin", "https://evil.example.com")
1607 .body(())
1608 .expect("request should build");
1609
1610 let auth_request = ConnectionAuthRequest::from_http_request(
1611 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1612 &request,
1613 );
1614
1615 let decision = plugin.authorize(&auth_request).await;
1616
1617 assert!(!decision.is_allowed());
1618
1619 if let AuthDecision::Deny(deny) = decision {
1620 assert_eq!(deny.code, AuthErrorCode::OriginMismatch);
1621 assert_eq!(deny.http_status, 403);
1622 assert!(matches!(deny.retry_policy, RetryPolicy::NoRetry));
1624 } else {
1625 panic!("Expected Deny decision");
1626 }
1627 }
1628
1629 #[test]
1631 fn auth_deny_to_http_response() {
1632 let deny = AuthDeny::new(AuthErrorCode::RateLimitExceeded, "Too many requests")
1633 .with_suggested_action("Wait before retrying")
1634 .with_retry_policy(RetryPolicy::RetryAfter(Duration::from_secs(30)));
1635
1636 let response = deny.to_error_response();
1637
1638 let json = serde_json::to_string(&response).expect("Should serialize");
1640 assert!(json.contains("rate-limit-exceeded"));
1641 assert!(json.contains("Too many requests"));
1642 assert!(json.contains("Wait before retrying"));
1643 assert!(json.contains("\"retryable\":true"));
1644 assert!(json.contains("\"retry_after\":30"));
1645 }
1646
1647 #[tokio::test]
1649 async fn comprehensive_auth_error_scenarios() {
1650 let signing_key = arete_auth::SigningKey::generate();
1651 let verifying_key = signing_key.verifying_key();
1652 let verifier =
1653 arete_auth::TokenVerifier::new(verifying_key, "test-issuer", "test-audience");
1654 let plugin = SignedSessionAuthPlugin::new(verifier);
1655
1656 let test_cases = vec![
1657 ("missing_token", None, AuthErrorCode::TokenMissing),
1658 (
1659 "invalid_format",
1660 Some("not-a-valid-token"),
1661 AuthErrorCode::TokenInvalidFormat,
1662 ),
1663 ];
1664
1665 for (name, token, expected_code) in test_cases {
1666 let uri = token.map_or_else(|| "/ws".to_string(), |t| format!("/ws?hs_token={}", t));
1667
1668 let request = Request::builder()
1669 .uri(&uri)
1670 .body(())
1671 .expect("request should build");
1672
1673 let auth_request = ConnectionAuthRequest::from_http_request(
1674 "127.0.0.1:8877".parse().expect("socket addr should parse"),
1675 &request,
1676 );
1677
1678 let decision = plugin.authorize(&auth_request).await;
1679
1680 assert!(!decision.is_allowed(), "{}: should deny", name);
1681
1682 if let AuthDecision::Deny(deny) = decision {
1683 assert_eq!(deny.code, expected_code, "{}: wrong error code", name);
1684 } else {
1685 panic!("{}: Expected Deny decision", name);
1686 }
1687 }
1688 }
1689}