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