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