Skip to main content

rvoip_sip/auth/
mod.rs

1//! SIP access authentication for UAC and UAS applications.
2//!
3//! This module is the cargo-doc entry point for SIP header authentication in
4//! `rvoip-sip`. It re-exports the lower-level Digest and Bearer primitives from
5//! `rvoip-auth-core`, then adds SIP-aware UAC retry configuration and UAS
6//! challenge/validation helpers.
7//!
8//! # Header Flow
9//!
10//! SIP authentication is negotiated in SIP headers:
11//!
12//! | Challenge | Retry header | Common meaning |
13//! | --- | --- | --- |
14//! | `401 WWW-Authenticate` | `Authorization` | Origin-server authentication |
15//! | `407 Proxy-Authenticate` | `Proxy-Authorization` | Proxy authentication |
16//!
17//! SDP is not an auth negotiation channel. It is only part of authentication
18//! when Digest `qop=auth-int` hashes the SIP message body.
19//!
20//! # Which API Do I Use?
21//!
22//! | Role | Use | Notes |
23//! | --- | --- | --- |
24//! | PBX Digest account | [`SipAccount`](crate::SipAccount) and [`EndpointBuilder::sip_account`](crate::EndpointBuilder::sip_account) | Configures REGISTER plus challenged outbound requests from one account model. |
25//! | UAC with one scheme | [`SipClientAuth::digest`], [`SipClientAuth::bearer_token`], [`SipClientAuth::basic`], or [`SipClientAuth::aka`] | Attach through [`Config::auth`](crate::Config::auth), peer builders, or per-request `.with_auth(...)`. |
26//! | UAC with negotiation | [`SipClientAuth::any`] | Chooses the strongest configured compatible challenge. |
27//! | UAS validation | [`SipAuthService`] | Validates inbound requests and returns [`AuthIdentity`]. |
28//! | Digest-only UAS compatibility | [`SipDigestAuthService`] | Small wrapper for Digest challenge/validation only. |
29//! | Enterprise UAS hooks | [`SipAuthService::with_audit_sink`], [`SipAuthService::with_rate_limiter`], [`SipAuthService::with_digest_replay_store`] | Redacted audit, rate-limit/lockout, and shared Digest replay state. |
30//! | Raw auth-core primitives | [`DigestAuthenticator`], [`DigestAuth`], [`BearerValidator`], [`JwtValidator`], [`JwksJwtValidator`], [`OAuth2IntrospectionValidator`], [`AAuthValidator`] | Crypto/token primitives; retry orchestration stays in `rvoip-sip`. |
31//!
32//! # Scheme and Algorithm Support
33//!
34//! | Scheme | UAC | UAS | Algorithms / providers | Security notes |
35//! | --- | --- | --- | --- | --- |
36//! | Digest | `SipClientAuth::digest`, `SipAccount`, `Credentials` shorthand | `SipAuthService::digest`, `SipDigestAuthService` | MD5, MD5-sess, SHA-256, SHA-256-sess, SHA-512-256, SHA-512-256-sess; `qop=auth` and `qop=auth-int` | Omitted algorithm defaults to MD5 for legacy PBX compatibility; unknown algorithms fail. |
37//! | Bearer | `SipClientAuth::bearer_token` | `SipAuthService::with_bearer_validator` | Validator-dependent JWT/JWKS/OAuth2 introspection/AAuth/opaque tokens through `rvoip-auth-core` | UAS exposes subject/scopes through `AuthIdentity`. |
38//! | Basic | `SipClientAuth::basic` | `SipAuthService::with_basic_realm` / `with_basic_user` | None | Legacy compatibility only. Cleartext SIP is rejected unless explicitly allowed. Prefer TLS or stronger schemes. |
39//! | AKA | `SipClientAuth::aka` with [`AkaClientProvider`] | `SipAuthService::with_aka_provider` with [`AkaVectorProvider`] | Provider-backed `AKAv1-MD5` / `AKAv2-MD5` | The crate supplies the API shape, not SIM/USIM infrastructure or carrier IMS certification. |
40//!
41//! When several challenges are offered and the UAC is configured with
42//! [`SipClientAuth::any`], selection prefers AKA, then Bearer, then Digest,
43//! then Basic among the configured compatible options. Basic is still subject
44//! to the cleartext policy.
45//!
46//! # Enterprise UAS Hooks
47//!
48//! `SipAuthService` can call [`AuthAuditSink`], [`AuthRateLimiter`], and
49//! [`DigestReplayStore`] providers from `rvoip-auth-core`. Rate-limit provider
50//! errors fail closed. Audit sink errors fail open by default and fail closed
51//! when [`AuditFailurePolicy::FailClosed`] is configured. Audit events are
52//! redacted and must not contain raw credentials.
53//!
54//! The sync [`SipAuthService::challenges`] helper is for local/simple use. When
55//! a shared Digest replay store is configured, use
56//! [`SipAuthService::challenges_async`] or the async inbound authentication
57//! helpers so issued nonces are recorded in shared storage. Digest-only users
58//! can use
59//! [`SipDigestAuthService::authenticate_authorization_with_replay_store`].
60//!
61//! # Examples
62//!
63//! PBX Digest account with Endpoint:
64//!
65//! ```rust,no_run
66//! use rvoip_sip::{Endpoint, Result, SipAccount};
67//!
68//! # async fn example() -> Result<()> {
69//! let account = SipAccount::new("sips:pbx.example.com:5061", "1001", "secret")
70//!     .auth_username("auth1001");
71//!
72//! let mut endpoint = Endpoint::builder()
73//!     .sip_account(account)
74//!     .build()
75//!     .await?;
76//!
77//! endpoint.register().await?;
78//! endpoint.shutdown().await?;
79//! # Ok(())
80//! # }
81//! ```
82//!
83//! Bearer UAC retry on an out-of-dialog request:
84//!
85//! ```rust,no_run
86//! use rvoip_sip::{Config, Result, UnifiedCoordinator};
87//!
88//! # async fn example() -> Result<()> {
89//! let coordinator = UnifiedCoordinator::new(Config::local("alice", 5060)).await?;
90//! coordinator
91//!     .message("sip:bob@example.com")
92//!     .with_bearer_token("access-token")
93//!     .with_body("hello")
94//!     .send()
95//!     .await?;
96//! coordinator.shutdown();
97//! # Ok(())
98//! # }
99//! ```
100//!
101//! Bearer UAS validation:
102//!
103//! ```rust
104//! use std::sync::Arc;
105//! use rvoip_sip::{BearerValidator, SipAuthService};
106//!
107//! fn service(validator: Arc<dyn BearerValidator>) -> SipAuthService {
108//!     SipAuthService::new()
109//!         .with_bearer_validator("api", validator)
110//!         .with_bearer_scope("calls:write")
111//! }
112//! ```
113//!
114//! Basic over TLS, and explicit cleartext opt-in for legacy peers:
115//!
116//! ```rust
117//! use rvoip_sip::SipClientAuth;
118//!
119//! let tls_basic = SipClientAuth::basic("alice", "secret");
120//! let legacy_cleartext_basic =
121//!     SipClientAuth::basic("legacy", "secret").allow_basic_over_cleartext(true);
122//! # let _ = (tls_basic, legacy_cleartext_basic);
123//! ```
124//!
125//! Composite UAC negotiation:
126//!
127//! ```rust
128//! use rvoip_sip::SipClientAuth;
129//!
130//! let auth = SipClientAuth::any([
131//!     SipClientAuth::bearer_token("access-token"),
132//!     SipClientAuth::digest("1001", "secret"),
133//! ]);
134//! # let _ = auth;
135//! ```
136//!
137//! Provider-backed AKA shape:
138//!
139//! ```rust
140//! use std::sync::Arc;
141//! use rvoip_sip::{AkaClientConfig, AkaClientProvider, SipClientAuth};
142//!
143//! fn aka_auth(provider: Arc<dyn AkaClientProvider>) -> SipClientAuth {
144//!     SipClientAuth::aka(AkaClientConfig::new(provider))
145//! }
146//! ```
147//!
148//! Users-core-backed UAS service shape:
149//!
150//! ```rust,ignore
151//! use std::sync::Arc;
152//! use rvoip_sip::{DigestAlgorithm, SipAuthService};
153//! use users_core::{AuthenticationService, UsersCoreAuthProvider};
154//!
155//! fn users_core_auth(auth_service: Arc<AuthenticationService>) -> SipAuthService {
156//!     let provider = UsersCoreAuthProvider::shared(auth_service);
157//!     SipAuthService::new()
158//!         .with_bearer_validator("users-core", provider.clone())
159//!         .with_basic_verifier("users-core", provider.clone())
160//!         .with_digest_provider("pbx.example.com", provider)
161//!         .with_digest_provider_algorithm(DigestAlgorithm::SHA256)
162//! }
163//! ```
164//!
165//! External opaque-token introspection shape:
166//!
167//! ```rust,ignore
168//! use rvoip_sip::{OAuth2IntrospectionValidator, SipAuthService};
169//! use url::Url;
170//!
171//! fn introspection_auth(endpoint: Url) -> SipAuthService {
172//!     let validator = OAuth2IntrospectionValidator::new(endpoint)
173//!         .with_issuer(["https://idp.example.com"])
174//!         .with_audience(["rvoip-sip"])
175//!         .into_arc();
176//!     SipAuthService::new().with_bearer_validator("idp", validator)
177//! }
178//! ```
179//!
180//! External JWT/JWKS shape:
181//!
182//! ```rust,ignore
183//! use rvoip_sip::{JwksJwtValidator, SipAuthService};
184//! use url::Url;
185//!
186//! fn jwks_auth(jwks_url: Url) -> SipAuthService {
187//!     let validator = JwksJwtValidator::new(jwks_url)
188//!         .with_issuer(["https://idp.example.com"])
189//!         .with_audience(["rvoip-sip"])
190//!         .into_arc();
191//!     SipAuthService::new().with_bearer_validator("idp", validator)
192//! }
193//! ```
194//!
195//! Custom Digest provider shape:
196//!
197//! ```rust,ignore
198//! use rvoip_sip::{DigestAlgorithm, DigestSecret, DigestSecretProvider};
199//!
200//! struct MyDigestProvider;
201//!
202//! #[async_trait::async_trait]
203//! impl DigestSecretProvider for MyDigestProvider {
204//!     async fn lookup_digest_secret(
205//!         &self,
206//!         username: &str,
207//!         realm: &str,
208//!         algorithm: DigestAlgorithm,
209//!     ) -> Result<Option<DigestSecret>, rvoip_sip::CredentialAuthError> {
210//!         let ha1 = load_ha1_from_my_database(username, realm, algorithm).await?;
211//!         Ok(ha1.map(DigestSecret::Ha1))
212//!     }
213//! }
214//! ```
215
216use std::collections::{BTreeMap, HashMap};
217use std::str::FromStr;
218use std::sync::{Arc, RwLock};
219use std::time::{Duration, Instant, SystemTime};
220
221use async_trait::async_trait;
222use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
223use base64::Engine as _;
224use rvoip_core_traits::identity::IdentityAssurance;
225
226use crate::errors::{Result, SessionError};
227use crate::types::Credentials;
228
229// Re-export digest authentication from auth-core.
230pub use rvoip_auth_core::{
231    AAuthValidator, ApiKeyVerifier, AuthAuditEvent, AuthAuditOutcome, AuthAuditScheme,
232    AuthAuditSink, AuthFailureReason, AuthRateLimitKey, AuthRateLimitKind, AuthRateLimitVerdict,
233    AuthRateLimiter, BearerAuthError, BearerValidator, CredentialAuthError, DigestAlgorithm,
234    DigestAuthenticator, DigestChallenge, DigestChallengeDetails, DigestClient as DigestAuth,
235    DigestComputed, DigestNonceStatus, DigestReplayStore, DigestResponse, DigestSecret,
236    DigestSecretProvider, JwksJwtValidator, JwtValidator, OAuth2IntrospectionValidator,
237    PasswordVerifier, TokenRevocationChecker, TokenRevocationContext, TokenRevocationStatus,
238};
239
240/// SIP authentication scheme shared by UAC negotiation, UAS challenges, and
241/// authenticated identity results.
242///
243/// SIP access authentication is carried in SIP headers:
244/// `WWW-Authenticate`, `Proxy-Authenticate`, `Authorization`, and
245/// `Proxy-Authorization`. SDP is only relevant to Digest when
246/// `qop=auth-int` hashes the request body.
247#[non_exhaustive]
248#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum SipAuthScheme {
250    /// SIP Digest authentication.
251    Digest,
252    /// Bearer-token authentication.
253    Bearer,
254    /// Basic username/password authentication.
255    Basic,
256    /// IMS AKA authentication (`AKAv1-MD5` / `AKAv2-MD5`).
257    Aka,
258    /// Unknown or future auth scheme.
259    Other(String),
260}
261
262/// Whether an authenticated identity came from origin or proxy auth.
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub enum SipAuthSource {
265    /// `Authorization` in response to `WWW-Authenticate`.
266    Origin,
267    /// `Proxy-Authorization` in response to `Proxy-Authenticate`.
268    Proxy,
269}
270
271/// Authenticated identity returned by the UAS-side [`SipAuthService`].
272///
273/// Digest and Basic usually populate [`username`](Self::username). Bearer
274/// validators commonly populate [`subject`](Self::subject) and
275/// [`scopes`](Self::scopes). AKA providers decide which identity fields they
276/// can assert from the vector infrastructure they integrate with.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct AuthIdentity {
279    /// Scheme that authenticated the peer.
280    pub scheme: SipAuthScheme,
281    /// SIP username, when the scheme authenticates a named SIP user.
282    pub username: Option<String>,
283    /// Token subject or external identity, when available.
284    pub subject: Option<String>,
285    /// Authentication realm, when the scheme carries one.
286    pub realm: Option<String>,
287    /// Token scopes mapped from Bearer/AAuth/JWT validators.
288    pub scopes: Vec<String>,
289    /// Whether the accepted credential came from origin or proxy auth.
290    pub source: SipAuthSource,
291}
292
293/// Result of evaluating inbound UAS authentication across all enabled
294/// schemes in [`SipAuthService`].
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub enum SipAuthDecision {
297    /// The inbound request carried acceptable credentials.
298    Authorized(AuthIdentity),
299    /// The inbound request should be challenged or rejected.
300    Rejected {
301        /// Challenge header values in priority order.
302        challenges: Vec<SipAuthChallenge>,
303    },
304}
305
306/// UAS challenge value generated by [`SipAuthService`].
307///
308/// Send [`value`](Self::value) in either `WWW-Authenticate` or
309/// `Proxy-Authenticate`, depending on [`source`](Self::source).
310#[derive(Debug, Clone, PartialEq, Eq)]
311pub struct SipAuthChallenge {
312    /// Scheme to advertise.
313    pub scheme: SipAuthScheme,
314    /// Header value, for example `Digest realm="...", nonce="..."`.
315    pub value: String,
316    /// Whether this is a proxy challenge (`407`) rather than origin (`401`).
317    pub source: SipAuthSource,
318}
319
320/// Non-secret context supplied to UAS-side authentication.
321///
322/// Context values are used for rate-limit keys and redacted audit events. Do
323/// not put passwords, bearer tokens, API keys, HA1 values, full JWTs, or raw
324/// Authorization headers in [`metadata`](Self::metadata).
325#[derive(Debug, Clone, Default, PartialEq, Eq)]
326pub struct SipAuthContext {
327    /// Source peer, IP, connection id, or deployment-specific peer handle.
328    pub peer: Option<String>,
329    /// Additional non-secret metadata to attach to audit/rate-limit events.
330    pub metadata: BTreeMap<String, String>,
331}
332
333impl SipAuthContext {
334    /// Create an empty context.
335    pub fn new() -> Self {
336        Self::default()
337    }
338
339    /// Attach a peer identifier.
340    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
341        self.peer = Some(peer.into());
342        self
343    }
344
345    /// Attach non-secret metadata.
346    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
347        self.metadata.insert(key.into(), value.into());
348        self
349    }
350}
351
352/// How `SipAuthService` handles audit-sink failures.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub enum AuditFailurePolicy {
355    /// Ignore audit sink failures after credential handling completes.
356    FailOpen,
357    /// Return an auth error when the audit sink cannot record an event.
358    FailClosed,
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362enum AuthAttemptScheme {
363    Digest,
364    Bearer,
365    Basic,
366    Aka,
367    Unknown,
368    Missing,
369}
370
371impl AuthAttemptScheme {
372    fn audit_scheme(self) -> AuthAuditScheme {
373        match self {
374            Self::Digest => AuthAuditScheme::Digest,
375            Self::Bearer => AuthAuditScheme::Bearer,
376            Self::Basic => AuthAuditScheme::Basic,
377            Self::Aka => AuthAuditScheme::Aka,
378            Self::Unknown | Self::Missing => AuthAuditScheme::Other("sip".to_string()),
379        }
380    }
381
382    fn rate_limit_kind(self) -> AuthRateLimitKind {
383        match self {
384            Self::Digest => AuthRateLimitKind::Digest,
385            Self::Bearer => AuthRateLimitKind::BearerToken,
386            Self::Basic => AuthRateLimitKind::BasicPassword,
387            Self::Aka => AuthRateLimitKind::SipRequest,
388            Self::Unknown | Self::Missing => AuthRateLimitKind::SipRequest,
389        }
390    }
391}
392
393/// UAS-side authentication policy for [`SipAuthService`].
394///
395/// This policy is additive to provider configuration. Providers answer
396/// credentials; policy decides which schemes and transport/security posture are
397/// acceptable before provider validation is trusted.
398#[derive(Debug, Clone)]
399pub struct SipAuthPolicy {
400    enabled_schemes: Option<Vec<SipAuthScheme>>,
401    minimum_digest_algorithm: Option<DigestAlgorithm>,
402    allow_basic_over_cleartext: bool,
403    allow_bearer_over_cleartext: bool,
404    require_digest_replay_store: bool,
405    audit_failure_policy: AuditFailurePolicy,
406}
407
408impl SipAuthPolicy {
409    /// Create the default policy.
410    ///
411    /// Defaults permit any configured scheme, reject Basic and Bearer over
412    /// cleartext, allow MD5 Digest for PBX compatibility, do not require shared
413    /// replay storage, and fail open on audit sink errors.
414    pub fn new() -> Self {
415        Self::default()
416    }
417
418    /// Restrict UAS auth to the listed schemes.
419    pub fn allow_only(mut self, schemes: impl IntoIterator<Item = SipAuthScheme>) -> Self {
420        self.enabled_schemes = Some(schemes.into_iter().collect());
421        self
422    }
423
424    /// Require Digest responses to use at least this algorithm strength.
425    ///
426    /// Strength order is MD5, MD5-sess, SHA-256, SHA-256-sess, SHA-512-256,
427    /// SHA-512-256-sess.
428    pub fn with_minimum_digest_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
429        self.minimum_digest_algorithm = Some(algorithm);
430        self
431    }
432
433    /// Permit Basic credentials on cleartext SIP transports.
434    pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
435        self.allow_basic_over_cleartext = allow;
436        self
437    }
438
439    /// Permit Bearer tokens on cleartext SIP transports.
440    pub fn allow_bearer_over_cleartext(mut self, allow: bool) -> Self {
441        self.allow_bearer_over_cleartext = allow;
442        self
443    }
444
445    /// Require a configured [`DigestReplayStore`] before accepting Digest.
446    pub fn require_digest_replay_store(mut self, require: bool) -> Self {
447        self.require_digest_replay_store = require;
448        self
449    }
450
451    /// Select whether audit sink failures fail open or fail closed.
452    pub fn with_audit_failure_policy(mut self, policy: AuditFailurePolicy) -> Self {
453        self.audit_failure_policy = policy;
454        self
455    }
456
457    fn scheme_allowed(&self, scheme: SipAuthScheme) -> bool {
458        self.enabled_schemes
459            .as_ref()
460            .is_none_or(|schemes| schemes.iter().any(|candidate| *candidate == scheme))
461    }
462
463    fn digest_algorithm_allowed(&self, algorithm: DigestAlgorithm) -> bool {
464        self.minimum_digest_algorithm.is_none_or(|minimum| {
465            digest_algorithm_strength(algorithm) >= digest_algorithm_strength(minimum)
466        })
467    }
468}
469
470impl Default for SipAuthPolicy {
471    fn default() -> Self {
472        Self {
473            enabled_schemes: None,
474            minimum_digest_algorithm: None,
475            allow_basic_over_cleartext: false,
476            allow_bearer_over_cleartext: false,
477            require_digest_replay_store: false,
478            audit_failure_policy: AuditFailurePolicy::FailOpen,
479        }
480    }
481}
482
483/// Transport security context for auth policy decisions.
484///
485/// Prefer values derived from the actual receiving/sending transport. The
486/// `from_request_uri_hint` constructor exists only as a compatibility fallback
487/// until every event path carries transport metadata.
488#[derive(Debug, Clone, Default, PartialEq, Eq)]
489pub struct SipTransportSecurityContext {
490    /// Transport flavor, for example `UDP`, `TCP`, `TLS`, `WS`, or `WSS`.
491    pub transport: Option<String>,
492    /// Local socket/address, if known.
493    pub local_addr: Option<String>,
494    /// Remote socket/address, if known.
495    pub remote_addr: Option<String>,
496    /// Whether the transport is protected for sending credentials such as
497    /// Basic passwords or Bearer tokens.
498    pub secure: bool,
499}
500
501impl SipTransportSecurityContext {
502    /// Unknown or cleartext transport. This is intentionally fail-closed for
503    /// Basic/Bearer cleartext policy.
504    pub fn unknown() -> Self {
505        Self::default()
506    }
507
508    /// Compatibility constructor from the legacy boolean parameter.
509    pub fn from_is_tls(is_tls: bool) -> Self {
510        if is_tls {
511            Self::secure("TLS")
512        } else {
513            Self::unknown()
514        }
515    }
516
517    /// Build from a concrete transport flavor.
518    pub fn from_transport_name(transport: impl Into<String>) -> Self {
519        let transport = transport.into();
520        let secure = transport_name_is_secure(&transport);
521        Self {
522            transport: Some(transport),
523            local_addr: None,
524            remote_addr: None,
525            secure,
526        }
527    }
528
529    /// Build from cross-crate SIP transport metadata.
530    pub fn from_transport_context(
531        context: &rvoip_infra_common::events::cross_crate::SipTransportContext,
532    ) -> Self {
533        Self {
534            transport: Some(context.transport.clone()),
535            local_addr: Some(context.local_addr.clone()),
536            remote_addr: Some(context.remote_addr.clone()),
537            secure: context.secure,
538        }
539    }
540
541    /// Compatibility fallback from request URI scheme.
542    ///
543    /// This is weaker than transport-truth metadata because a `sips:` URI is
544    /// not itself proof that this hop arrived or will be sent over TLS/WSS.
545    pub fn from_request_uri_hint(request_uri: &str) -> Self {
546        if request_uri.to_ascii_lowercase().starts_with("sips:") {
547            Self::secure("SIPS-URI")
548        } else {
549            Self::unknown()
550        }
551    }
552
553    /// Compatibility fallback from SIP URI syntax, including `;transport=`.
554    ///
555    /// This handles URI-selected transports such as
556    /// `sip:bob@example.com;transport=tls` and `;transport=wss`. It is still a
557    /// syntactic hint, not proof of the actual selected outbound transport.
558    pub fn from_request_uri_transport_hint(request_uri: &str) -> Self {
559        let Ok(uri) = rvoip_sip_core::Uri::from_str(request_uri) else {
560            return Self::from_request_uri_hint(request_uri);
561        };
562        let transport = rvoip_sip_transport::resolver::select_transport_for_uri(&uri);
563        Self::from_transport_name(transport.to_string())
564    }
565
566    /// Secure transport context with a named transport.
567    pub fn secure(transport: impl Into<String>) -> Self {
568        Self {
569            transport: Some(transport.into()),
570            local_addr: None,
571            remote_addr: None,
572            secure: true,
573        }
574    }
575
576    /// Attach local and remote non-secret address metadata.
577    pub fn with_addrs(
578        mut self,
579        local_addr: impl Into<String>,
580        remote_addr: impl Into<String>,
581    ) -> Self {
582        self.local_addr = Some(local_addr.into());
583        self.remote_addr = Some(remote_addr.into());
584        self
585    }
586
587    /// Whether this context is secure enough for credential-bearing schemes.
588    pub fn is_secure(&self) -> bool {
589        self.secure
590    }
591}
592
593fn transport_name_is_secure(transport: &str) -> bool {
594    matches!(
595        transport.trim().to_ascii_uppercase().as_str(),
596        "TLS" | "WSS" | "SIPS" | "SIPS-URI"
597    )
598}
599
600/// UAC-side authentication configuration for challenged outbound requests.
601///
602/// Attach this to default configuration with [`Config::auth`](crate::Config::auth)
603/// or to individual request builders with `.with_auth(...)`. The Digest-only
604/// `Credentials` shorthand still works and converts into
605/// [`SipClientAuth::Digest`].
606#[non_exhaustive]
607#[derive(Debug, Clone)]
608pub enum SipClientAuth {
609    /// Digest username/password credentials.
610    Digest(Credentials),
611    /// Bearer token. Sent after a Bearer challenge unless the request builder
612    /// explicitly authors a preemptive Authorization header.
613    BearerToken(String),
614    /// Bearer token with explicit cleartext SIP opt-in.
615    ///
616    /// Prefer [`SipClientAuth::bearer_token`] on TLS/WSS transports. This
617    /// variant exists for controlled legacy test/deployment environments where
618    /// sending a bearer token over cleartext SIP has been explicitly accepted.
619    BearerTokenCleartextAllowed(String),
620    /// Basic credentials. Disabled over cleartext unless the instance opts in.
621    Basic {
622        /// Basic username.
623        username: String,
624        /// Basic password.
625        password: String,
626        /// Permit sending Basic on non-TLS SIP transports.
627        allow_cleartext: bool,
628    },
629    /// IMS AKA client provider configuration.
630    Aka(AkaClientConfig),
631    /// Multiple configured auth options. Selection prefers AKA, then Bearer,
632    /// then Digest, then Basic when the peer offers several compatible
633    /// challenges.
634    Composite(Vec<SipClientAuth>),
635}
636
637impl SipClientAuth {
638    /// Build Digest username/password credentials.
639    pub fn digest(username: impl Into<String>, password: impl Into<String>) -> Self {
640        Self::Digest(Credentials::new(username, password))
641    }
642
643    /// Build a Bearer token response option.
644    ///
645    /// The token is sent only when the peer offers a Bearer challenge, unless
646    /// a lower-level caller explicitly authors a preemptive header.
647    pub fn bearer_token(token: impl Into<String>) -> Self {
648        Self::BearerToken(token.into())
649    }
650
651    /// Permit Bearer tokens on non-TLS SIP transports.
652    ///
653    /// This is an explicit legacy interoperability opt-in. Leave it disabled
654    /// unless the application has a deployment-specific reason to accept
655    /// cleartext bearer tokens on the wire.
656    pub fn allow_bearer_over_cleartext(self, allow: bool) -> Self {
657        match self {
658            Self::BearerToken(token) if allow => Self::BearerTokenCleartextAllowed(token),
659            Self::BearerTokenCleartextAllowed(token) if !allow => Self::BearerToken(token),
660            Self::Composite(auths) => Self::Composite(
661                auths
662                    .into_iter()
663                    .map(|auth| auth.allow_bearer_over_cleartext(allow))
664                    .collect(),
665            ),
666            _ => self,
667        }
668    }
669
670    /// Build Basic username/password credentials.
671    ///
672    /// Cleartext SIP is rejected unless
673    /// [`Self::allow_basic_over_cleartext`] is called on the returned value.
674    /// Use Basic only for legacy compatibility; prefer TLS plus Digest,
675    /// Bearer, or AKA where available.
676    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
677        Self::Basic {
678            username: username.into(),
679            password: password.into(),
680            allow_cleartext: false,
681        }
682    }
683
684    /// Build IMS AKA client configuration from an application-supplied
685    /// provider.
686    pub fn aka(config: AkaClientConfig) -> Self {
687        Self::Aka(config)
688    }
689
690    /// Configure multiple UAC auth options and negotiate the strongest
691    /// compatible scheme offered by the peer.
692    ///
693    /// Selection prefers AKA, then Bearer, then Digest, then Basic among the
694    /// configured options. Basic still obeys the cleartext policy.
695    pub fn any(auth: impl IntoIterator<Item = SipClientAuth>) -> Self {
696        Self::Composite(auth.into_iter().collect())
697    }
698
699    /// Permit Basic credentials on non-TLS SIP transports.
700    ///
701    /// This is an explicit legacy interoperability opt-in. Leave it disabled
702    /// unless the application has a deployment-specific reason to accept
703    /// cleartext Basic credentials on the wire.
704    pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
705        if let Self::Basic {
706            allow_cleartext, ..
707        } = &mut self
708        {
709            *allow_cleartext = allow;
710        }
711        self
712    }
713
714    /// Build an Authorization / Proxy-Authorization header value for the
715    /// selected challenge.
716    pub fn authorization_for_challenge(
717        &self,
718        challenge_header: &str,
719        method: &str,
720        request_uri: &str,
721        nonce_count: u32,
722        body: Option<&[u8]>,
723        is_tls: bool,
724    ) -> Result<ClientAuthHeader> {
725        self.authorization_for_challenge_with_transport_context(
726            challenge_header,
727            method,
728            request_uri,
729            nonce_count,
730            body,
731            &SipTransportSecurityContext::from_is_tls(is_tls),
732        )
733    }
734
735    /// Build an Authorization / Proxy-Authorization header value for the
736    /// selected challenge using transport-truth security context.
737    pub fn authorization_for_challenge_with_transport_context(
738        &self,
739        challenge_header: &str,
740        method: &str,
741        request_uri: &str,
742        nonce_count: u32,
743        body: Option<&[u8]>,
744        transport: &SipTransportSecurityContext,
745    ) -> Result<ClientAuthHeader> {
746        match self {
747            SipClientAuth::Digest(credentials) => {
748                let challenge = extract_digest_challenge(challenge_header).ok_or_else(|| {
749                    SessionError::AuthError(
750                        "Digest credentials cannot answer a non-Digest challenge".to_string(),
751                    )
752                })?;
753                let challenge = rvoip_auth_core::DigestAuthenticator::parse_challenge(&challenge)?;
754                let computed = rvoip_auth_core::DigestClient::compute_response_with_state(
755                    &credentials.username,
756                    &credentials.password,
757                    &challenge,
758                    method,
759                    request_uri,
760                    nonce_count,
761                    body,
762                )?;
763                let value = rvoip_auth_core::DigestClient::format_authorization_with_state(
764                    &credentials.username,
765                    &challenge,
766                    request_uri,
767                    &computed,
768                );
769                Ok(ClientAuthHeader {
770                    value,
771                    scheme: SipAuthScheme::Digest,
772                    digest_challenge: Some(challenge),
773                    stale: parse_digest_stale(challenge_header),
774                })
775            }
776            SipClientAuth::BearerToken(token) => {
777                if !contains_auth_scheme(challenge_header, "Bearer") {
778                    return Err(SessionError::AuthError(
779                        "Bearer token cannot answer a non-Bearer challenge".to_string(),
780                    ));
781                }
782                if !transport.is_secure() {
783                    return Err(SessionError::AuthError(
784                        "Bearer authentication over cleartext SIP is disabled".to_string(),
785                    ));
786                }
787                if token.is_empty() {
788                    return Err(SessionError::AuthError(
789                        "Bearer token cannot be empty".to_string(),
790                    ));
791                }
792                Ok(ClientAuthHeader {
793                    value: format!("Bearer {token}"),
794                    scheme: SipAuthScheme::Bearer,
795                    digest_challenge: None,
796                    stale: false,
797                })
798            }
799            SipClientAuth::BearerTokenCleartextAllowed(token) => {
800                if !contains_auth_scheme(challenge_header, "Bearer") {
801                    return Err(SessionError::AuthError(
802                        "Bearer token cannot answer a non-Bearer challenge".to_string(),
803                    ));
804                }
805                if token.is_empty() {
806                    return Err(SessionError::AuthError(
807                        "Bearer token cannot be empty".to_string(),
808                    ));
809                }
810                Ok(ClientAuthHeader {
811                    value: format!("Bearer {token}"),
812                    scheme: SipAuthScheme::Bearer,
813                    digest_challenge: None,
814                    stale: false,
815                })
816            }
817            SipClientAuth::Basic {
818                username,
819                password,
820                allow_cleartext,
821            } => {
822                if !contains_auth_scheme(challenge_header, "Basic") {
823                    return Err(SessionError::AuthError(
824                        "Basic credentials cannot answer a non-Basic challenge".to_string(),
825                    ));
826                }
827                if !transport.is_secure() && !*allow_cleartext {
828                    return Err(SessionError::AuthError(
829                        "Basic authentication over cleartext SIP is disabled".to_string(),
830                    ));
831                }
832                let token = BASE64_STANDARD.encode(format!("{username}:{password}"));
833                Ok(ClientAuthHeader {
834                    value: format!("Basic {token}"),
835                    scheme: SipAuthScheme::Basic,
836                    digest_challenge: None,
837                    stale: false,
838                })
839            }
840            SipClientAuth::Aka(config) => {
841                if !contains_aka_challenge(challenge_header) {
842                    return Err(SessionError::AuthError(
843                        "AKA credentials cannot answer a non-AKA challenge".to_string(),
844                    ));
845                }
846                let response =
847                    config.respond(challenge_header, method, request_uri, nonce_count)?;
848                Ok(ClientAuthHeader {
849                    value: response,
850                    scheme: SipAuthScheme::Aka,
851                    digest_challenge: None,
852                    stale: false,
853                })
854            }
855            SipClientAuth::Composite(auths) => select_composite_client_auth(
856                auths,
857                challenge_header,
858                method,
859                request_uri,
860                nonce_count,
861                body,
862                transport,
863            ),
864        }
865    }
866}
867
868impl From<Credentials> for SipClientAuth {
869    fn from(value: Credentials) -> Self {
870        Self::Digest(value)
871    }
872}
873
874/// UAC auth header value selected by [`SipClientAuth`] for a challenge.
875#[derive(Debug, Clone, PartialEq, Eq)]
876pub struct ClientAuthHeader {
877    /// Authorization header body.
878    pub value: String,
879    /// Scheme selected for the challenge.
880    pub scheme: SipAuthScheme,
881    /// Parsed Digest challenge when the selected scheme is Digest.
882    pub digest_challenge: Option<DigestChallenge>,
883    /// Whether the selected Digest challenge carried `stale=true`.
884    pub stale: bool,
885}
886
887/// UAC-side IMS AKA client configuration.
888///
889/// Production IMS integrations should provide a [`AkaClientProvider`].
890/// The in-tree API keeps AKA provider-backed because RES/CK/IK material is
891/// issued by SIM/USIM or IMS infrastructure, not by static passwords.
892#[derive(Clone)]
893pub struct AkaClientConfig {
894    provider: Arc<dyn AkaClientProvider>,
895}
896
897impl AkaClientConfig {
898    /// Create an AKA config from a provider.
899    pub fn new(provider: Arc<dyn AkaClientProvider>) -> Self {
900        Self { provider }
901    }
902
903    fn respond(
904        &self,
905        challenge_header: &str,
906        method: &str,
907        request_uri: &str,
908        nonce_count: u32,
909    ) -> Result<String> {
910        self.provider
911            .authorization(challenge_header, method, request_uri, nonce_count)
912    }
913}
914
915impl std::fmt::Debug for AkaClientConfig {
916    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
917        f.debug_struct("AkaClientConfig")
918            .field("provider", &"<AkaClientProvider>")
919            .finish()
920    }
921}
922
923impl PartialEq for AkaClientConfig {
924    fn eq(&self, other: &Self) -> bool {
925        Arc::ptr_eq(&self.provider, &other.provider)
926    }
927}
928
929impl Eq for AkaClientConfig {}
930
931/// UAC-side AKA response provider.
932///
933/// Applications implement this trait to calculate the AKA `Authorization`
934/// header body for a selected `AKAv1-MD5` or `AKAv2-MD5` challenge. The
935/// library does not embed SIM/USIM, Milenage, or carrier IMS infrastructure.
936pub trait AkaClientProvider: Send + Sync {
937    /// Build an AKA Authorization header body for the selected challenge.
938    fn authorization(
939        &self,
940        challenge_header: &str,
941        method: &str,
942        request_uri: &str,
943        nonce_count: u32,
944    ) -> Result<String>;
945}
946
947/// UAS-side AKA challenge and validation provider.
948///
949/// Applications implement this trait to issue IMS AKA vectors and validate
950/// inbound AKA credentials. The provider owns vector storage, resync policy,
951/// and any SIM/USIM or HSS/AuC integration.
952#[async_trait]
953pub trait AkaVectorProvider: Send + Sync {
954    /// Validate an inbound AKA Authorization header and return the AKA user.
955    async fn validate(
956        &self,
957        authorization: &str,
958        method: &str,
959        request_uri: &str,
960        body: Option<&[u8]>,
961    ) -> Result<Option<AuthIdentity>>;
962
963    /// Build an AKA challenge value, usually a Digest-family challenge with
964    /// `algorithm=AKAv1-MD5` or `algorithm=AKAv2-MD5`.
965    fn challenge(&self, source: SipAuthSource) -> SipAuthChallenge;
966}
967
968/// General UAS-side SIP authentication facade.
969///
970/// Enable one or more schemes, then pass the service to
971/// `IncomingCall::authenticate_with`, `IncomingRequest::authenticate_with`, or
972/// `IncomingRegister::authenticate_with`. Missing or rejected credentials
973/// return challenges for the enabled schemes; accepted credentials return an
974/// [`AuthIdentity`].
975///
976/// Bearer validators are responsible for token trust policy: issuer, audience
977/// or resource indicators, expiry, accepted algorithms, `kid` behavior,
978/// revocation/introspection strategy, and application-required scopes. A SIP
979/// auth realm only labels the challenge; it is not a substitute for validator
980/// policy.
981#[derive(Clone)]
982pub struct SipAuthService {
983    policy: SipAuthPolicy,
984    digest: Option<SipDigestAuthService>,
985    digest_provider: Option<DigestProviderAuthStore>,
986    bearer: Option<Arc<dyn BearerValidator>>,
987    bearer_realm: Option<String>,
988    bearer_scope: Option<String>,
989    basic: Option<BasicAuthStore>,
990    aka: Option<Arc<dyn AkaVectorProvider>>,
991    allow_bearer_over_cleartext: bool,
992    allow_basic_over_cleartext: bool,
993    audit_sink: Option<Arc<dyn AuthAuditSink>>,
994    audit_failure_policy: AuditFailurePolicy,
995    rate_limiter: Option<Arc<dyn AuthRateLimiter>>,
996    digest_replay_store: Option<Arc<dyn DigestReplayStore>>,
997}
998
999impl SipAuthService {
1000    /// Create an empty UAS auth service. Add schemes with the `with_*`
1001    /// methods.
1002    pub fn new() -> Self {
1003        Self {
1004            policy: SipAuthPolicy::default(),
1005            digest: None,
1006            digest_provider: None,
1007            bearer: None,
1008            bearer_realm: None,
1009            bearer_scope: None,
1010            basic: None,
1011            aka: None,
1012            allow_bearer_over_cleartext: false,
1013            allow_basic_over_cleartext: false,
1014            audit_sink: None,
1015            audit_failure_policy: AuditFailurePolicy::FailOpen,
1016            rate_limiter: None,
1017            digest_replay_store: None,
1018        }
1019    }
1020
1021    /// Create a UAS service with a Digest realm.
1022    pub fn digest(realm: impl Into<String>) -> Self {
1023        Self::new().with_digest_service(SipDigestAuthService::new(realm))
1024    }
1025
1026    /// Apply UAS authentication policy.
1027    pub fn with_policy(mut self, policy: SipAuthPolicy) -> Self {
1028        self.allow_basic_over_cleartext = policy.allow_basic_over_cleartext;
1029        self.allow_bearer_over_cleartext = policy.allow_bearer_over_cleartext;
1030        self.audit_failure_policy = policy.audit_failure_policy;
1031        self.policy = policy;
1032        self
1033    }
1034
1035    /// Add a Digest service.
1036    pub fn with_digest_service(mut self, service: SipDigestAuthService) -> Self {
1037        self.digest = Some(service);
1038        self
1039    }
1040
1041    /// Add provider-backed Digest validation for UAS requests.
1042    ///
1043    /// The provider supplies SIP Digest secret material, usually HA1 values
1044    /// from a user service. Nonce generation, stale handling, and nonce-count
1045    /// replay checks remain in `rvoip-sip`.
1046    pub fn with_digest_provider(
1047        mut self,
1048        realm: impl Into<String>,
1049        provider: Arc<dyn DigestSecretProvider>,
1050    ) -> Self {
1051        let mut digest = DigestProviderAuthStore::new(realm, provider);
1052        if let Some(replay_store) = self.digest_replay_store.clone() {
1053            digest = digest.with_replay_store(replay_store);
1054        }
1055        self.digest_provider = Some(digest);
1056        self
1057    }
1058
1059    /// Select the Digest algorithm used for provider-backed challenges.
1060    ///
1061    /// Omitted Digest algorithms remain MD5 for legacy PBX compatibility, but
1062    /// first-party services should prefer SHA-256 or SHA-512-256 when peers
1063    /// support them.
1064    pub fn with_digest_provider_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
1065        if let Some(digest) = self.digest_provider.take() {
1066            self.digest_provider = Some(digest.with_algorithm(algorithm));
1067        }
1068        self
1069    }
1070
1071    /// Add shared Digest nonce/replay storage for provider-backed Digest.
1072    ///
1073    /// The existing sync [`Self::challenges`] helper remains in-memory only.
1074    /// Use [`Self::challenges_async`] or
1075    /// [`Self::authenticate_authorization_with_context`] when shared replay
1076    /// state must record newly issued nonces.
1077    pub fn with_digest_replay_store(mut self, replay_store: Arc<dyn DigestReplayStore>) -> Self {
1078        if let Some(digest) = self.digest_provider.take() {
1079            self.digest_provider = Some(digest.with_replay_store(replay_store.clone()));
1080        }
1081        self.digest_replay_store = Some(replay_store);
1082        self
1083    }
1084
1085    /// Add a redacted audit sink for authentication events.
1086    pub fn with_audit_sink(mut self, sink: Arc<dyn AuthAuditSink>) -> Self {
1087        self.audit_sink = Some(sink);
1088        self
1089    }
1090
1091    /// Select how audit sink failures are handled.
1092    ///
1093    /// Default is [`AuditFailurePolicy::FailOpen`].
1094    pub fn with_audit_failure_policy(mut self, policy: AuditFailurePolicy) -> Self {
1095        self.audit_failure_policy = policy;
1096        self
1097    }
1098
1099    /// Add rate-limit/lockout policy for inbound auth attempts.
1100    ///
1101    /// Rate-limiter provider errors fail closed. Authentication fails before
1102    /// credential validation if the limiter cannot answer.
1103    pub fn with_rate_limiter(mut self, rate_limiter: Arc<dyn AuthRateLimiter>) -> Self {
1104        self.rate_limiter = Some(rate_limiter);
1105        self
1106    }
1107
1108    /// Add a Bearer validator for UAS token validation.
1109    ///
1110    /// The validator comes from `rvoip-auth-core`; this facade maps successful
1111    /// validation into [`AuthIdentity`]. The validator must enforce issuer,
1112    /// audience/resource, expiry, allowed algorithms, `kid` handling,
1113    /// revocation/introspection requirements, and application scopes.
1114    pub fn with_bearer_validator(
1115        mut self,
1116        realm: impl Into<String>,
1117        validator: Arc<dyn BearerValidator>,
1118    ) -> Self {
1119        self.bearer = Some(validator);
1120        self.bearer_realm = Some(realm.into());
1121        self
1122    }
1123
1124    /// Set Bearer challenge scope.
1125    pub fn with_bearer_scope(mut self, scope: impl Into<String>) -> Self {
1126        self.bearer_scope = Some(scope.into());
1127        self
1128    }
1129
1130    /// Add Basic validation with the given realm.
1131    ///
1132    /// Basic is rejected on cleartext SIP unless
1133    /// [`Self::allow_basic_over_cleartext`] is enabled.
1134    pub fn with_basic_realm(mut self, realm: impl Into<String>) -> Self {
1135        self.basic = Some(BasicAuthStore {
1136            realm: realm.into(),
1137            users: Arc::new(RwLock::new(HashMap::new())),
1138            verifier: None,
1139        });
1140        self
1141    }
1142
1143    /// Add provider-backed Basic password validation.
1144    ///
1145    /// Basic remains rejected on cleartext SIP unless
1146    /// [`Self::allow_basic_over_cleartext`] is enabled.
1147    pub fn with_basic_verifier(
1148        mut self,
1149        realm: impl Into<String>,
1150        verifier: Arc<dyn PasswordVerifier>,
1151    ) -> Self {
1152        self.basic = Some(BasicAuthStore {
1153            realm: realm.into(),
1154            users: Arc::new(RwLock::new(HashMap::new())),
1155            verifier: Some(verifier),
1156        });
1157        self
1158    }
1159
1160    /// Add or replace a Basic user. If Basic has not been enabled yet, it is
1161    /// enabled with the default `sip` realm.
1162    pub fn add_basic_user(&mut self, username: impl Into<String>, password: impl Into<String>) {
1163        if self.basic.is_none() {
1164            self.basic = Some(BasicAuthStore {
1165                realm: "sip".to_string(),
1166                users: Arc::new(RwLock::new(HashMap::new())),
1167                verifier: None,
1168            });
1169        }
1170        if let Some(basic) = &self.basic {
1171            let mut users = basic
1172                .users
1173                .write()
1174                .unwrap_or_else(|poisoned| poisoned.into_inner());
1175            users.insert(username.into(), password.into());
1176        }
1177    }
1178
1179    /// Fluent form of [`Self::add_basic_user`].
1180    pub fn with_basic_user(
1181        mut self,
1182        username: impl Into<String>,
1183        password: impl Into<String>,
1184    ) -> Self {
1185        self.add_basic_user(username, password);
1186        self
1187    }
1188
1189    /// Permit Bearer validation on non-TLS SIP transports.
1190    ///
1191    /// This is an explicit legacy interoperability opt-in. Leave it disabled
1192    /// unless the application has a deployment-specific reason to accept
1193    /// cleartext bearer tokens on the wire.
1194    pub fn allow_bearer_over_cleartext(mut self, allow: bool) -> Self {
1195        self.allow_bearer_over_cleartext = allow;
1196        self
1197    }
1198
1199    /// Permit Basic validation on non-TLS SIP transports.
1200    pub fn allow_basic_over_cleartext(mut self, allow: bool) -> Self {
1201        self.allow_basic_over_cleartext = allow;
1202        self
1203    }
1204
1205    /// Add a provider-backed AKA validation provider.
1206    pub fn with_aka_provider(mut self, provider: Arc<dyn AkaVectorProvider>) -> Self {
1207        self.aka = Some(provider);
1208        self
1209    }
1210
1211    /// Add a Digest user. If Digest has not been enabled yet, it is enabled
1212    /// with the default `sip` realm.
1213    pub fn add_digest_user(&mut self, username: impl Into<String>, password: impl Into<String>) {
1214        if self.digest.is_none() {
1215            self.digest = Some(SipDigestAuthService::new("sip"));
1216        }
1217        if let Some(digest) = &self.digest {
1218            digest.add_user(username, password);
1219        }
1220    }
1221
1222    /// Fluent form of [`Self::add_digest_user`].
1223    pub fn with_digest_user(
1224        mut self,
1225        username: impl Into<String>,
1226        password: impl Into<String>,
1227    ) -> Self {
1228        self.add_digest_user(username, password);
1229        self
1230    }
1231
1232    /// Validate an optional inbound `Authorization` or `Proxy-Authorization`
1233    /// value.
1234    pub async fn authenticate_authorization(
1235        &self,
1236        authorization: Option<&str>,
1237        method: &str,
1238        request_uri: &str,
1239        body: Option<&[u8]>,
1240        source: SipAuthSource,
1241        is_tls: bool,
1242    ) -> Result<SipAuthDecision> {
1243        self.authenticate_authorization_with_transport_context(
1244            authorization,
1245            method,
1246            request_uri,
1247            body,
1248            source,
1249            &SipTransportSecurityContext::from_is_tls(is_tls),
1250        )
1251        .await
1252    }
1253
1254    /// Validate an optional inbound auth header using transport-truth security
1255    /// context for Basic/Bearer cleartext policy decisions.
1256    pub async fn authenticate_authorization_with_transport_context(
1257        &self,
1258        authorization: Option<&str>,
1259        method: &str,
1260        request_uri: &str,
1261        body: Option<&[u8]>,
1262        source: SipAuthSource,
1263        transport: &SipTransportSecurityContext,
1264    ) -> Result<SipAuthDecision> {
1265        self.authenticate_authorization_with_context_and_transport(
1266            authorization,
1267            method,
1268            request_uri,
1269            body,
1270            source,
1271            transport,
1272            &SipAuthContext::default(),
1273        )
1274        .await
1275    }
1276
1277    /// Validate an optional inbound auth header with non-secret context for
1278    /// audit and rate-limit providers.
1279    pub async fn authenticate_authorization_with_context(
1280        &self,
1281        authorization: Option<&str>,
1282        method: &str,
1283        request_uri: &str,
1284        body: Option<&[u8]>,
1285        source: SipAuthSource,
1286        is_tls: bool,
1287        context: &SipAuthContext,
1288    ) -> Result<SipAuthDecision> {
1289        self.authenticate_authorization_with_context_and_transport(
1290            authorization,
1291            method,
1292            request_uri,
1293            body,
1294            source,
1295            &SipTransportSecurityContext::from_is_tls(is_tls),
1296            context,
1297        )
1298        .await
1299    }
1300
1301    /// Validate an optional inbound auth header with audit/rate-limit context
1302    /// and transport-truth security context.
1303    pub async fn authenticate_authorization_with_context_and_transport(
1304        &self,
1305        authorization: Option<&str>,
1306        method: &str,
1307        request_uri: &str,
1308        body: Option<&[u8]>,
1309        source: SipAuthSource,
1310        transport: &SipTransportSecurityContext,
1311        context: &SipAuthContext,
1312    ) -> Result<SipAuthDecision> {
1313        let attempt = auth_attempt_scheme(authorization);
1314        let rate_key = self.rate_limit_key(attempt, authorization, method, context);
1315
1316        let verdict = match self.check_rate_limit(&rate_key).await {
1317            Ok(verdict) => verdict,
1318            Err(err) => {
1319                let outcome = AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1320                self.audit_attempt(attempt, outcome, authorization, source, method, context)
1321                    .await?;
1322                return Err(SessionError::AuthError(format!(
1323                    "auth rate limiter unavailable: {err}"
1324                )));
1325            }
1326        };
1327
1328        match verdict {
1329            AuthRateLimitVerdict::Allowed => {}
1330            AuthRateLimitVerdict::Denied { .. } => {
1331                let outcome = AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected);
1332                self.record_rate_result_or_audit_unavailable(
1333                    &rate_key,
1334                    &outcome,
1335                    attempt,
1336                    authorization,
1337                    source,
1338                    method,
1339                    context,
1340                )
1341                .await?;
1342                self.audit_attempt(attempt, outcome, authorization, source, method, context)
1343                    .await?;
1344                return self.rejected_async(source).await;
1345            }
1346        }
1347
1348        let auth_result = match authorization {
1349            None => Ok((
1350                self.rejected_async(source).await?,
1351                Some(AuthFailureReason::MissingCredential),
1352            )),
1353            Some(authorization) => {
1354                let trimmed = authorization.trim();
1355                match attempt {
1356                    AuthAttemptScheme::Digest => {
1357                        if !self.policy.scheme_allowed(SipAuthScheme::Digest) {
1358                            Ok((
1359                                self.rejected_async(source).await?,
1360                                Some(AuthFailureReason::PolicyRejected),
1361                            ))
1362                        } else if self.policy.require_digest_replay_store
1363                            && self.digest_replay_store.is_none()
1364                        {
1365                            Ok((
1366                                self.rejected_async(source).await?,
1367                                Some(AuthFailureReason::PolicyRejected),
1368                            ))
1369                        } else if let Ok(response) =
1370                            DigestAuthenticator::parse_authorization(trimmed)
1371                        {
1372                            if !self.policy.digest_algorithm_allowed(response.algorithm) {
1373                                Ok((
1374                                    self.rejected_async(source).await?,
1375                                    Some(AuthFailureReason::PolicyRejected),
1376                                ))
1377                            } else {
1378                                self.authenticate_digest_with_reason(
1379                                    trimmed,
1380                                    method,
1381                                    request_uri,
1382                                    body,
1383                                    source,
1384                                )
1385                                .await
1386                            }
1387                        } else {
1388                            self.authenticate_digest_with_reason(
1389                                trimmed,
1390                                method,
1391                                request_uri,
1392                                body,
1393                                source,
1394                            )
1395                            .await
1396                        }
1397                    }
1398                    AuthAttemptScheme::Bearer => {
1399                        if !self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1400                            Ok((
1401                                self.rejected_async(source).await?,
1402                                Some(AuthFailureReason::PolicyRejected),
1403                            ))
1404                        } else if !transport.is_secure() && !self.allow_bearer_over_cleartext {
1405                            Ok((
1406                                self.rejected_async(source).await?,
1407                                Some(AuthFailureReason::PolicyRejected),
1408                            ))
1409                        } else {
1410                            self.authenticate_bearer_with_reason(trimmed, source).await
1411                        }
1412                    }
1413                    AuthAttemptScheme::Basic => {
1414                        if !self.policy.scheme_allowed(SipAuthScheme::Basic) {
1415                            Ok((
1416                                self.rejected_async(source).await?,
1417                                Some(AuthFailureReason::PolicyRejected),
1418                            ))
1419                        } else if !transport.is_secure() && !self.allow_basic_over_cleartext {
1420                            Ok((
1421                                self.rejected_async(source).await?,
1422                                Some(AuthFailureReason::PolicyRejected),
1423                            ))
1424                        } else {
1425                            self.authenticate_basic_with_reason(trimmed, source, transport)
1426                                .await
1427                        }
1428                    }
1429                    AuthAttemptScheme::Aka => {
1430                        if !self.policy.scheme_allowed(SipAuthScheme::Aka) {
1431                            Ok((
1432                                self.rejected_async(source).await?,
1433                                Some(AuthFailureReason::PolicyRejected),
1434                            ))
1435                        } else {
1436                            self.authenticate_aka_with_reason(
1437                                trimmed,
1438                                method,
1439                                request_uri,
1440                                body,
1441                                source,
1442                            )
1443                            .await
1444                        }
1445                    }
1446                    AuthAttemptScheme::Unknown | AuthAttemptScheme::Missing => Ok((
1447                        self.rejected_async(source).await?,
1448                        Some(AuthFailureReason::UnsupportedScheme),
1449                    )),
1450                }
1451            }
1452        };
1453
1454        let (result, failure_reason) = match auth_result {
1455            Ok(result) => result,
1456            Err(err) => {
1457                let outcome = AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1458                self.record_rate_result_or_audit_unavailable(
1459                    &rate_key,
1460                    &outcome,
1461                    attempt,
1462                    authorization,
1463                    source,
1464                    method,
1465                    context,
1466                )
1467                .await?;
1468                self.audit_attempt(attempt, outcome, authorization, source, method, context)
1469                    .await?;
1470                return Err(err);
1471            }
1472        };
1473
1474        let outcome = auth_outcome_for_decision(&result, failure_reason);
1475        self.record_rate_result_or_audit_unavailable(
1476            &rate_key,
1477            &outcome,
1478            attempt,
1479            authorization,
1480            source,
1481            method,
1482            context,
1483        )
1484        .await?;
1485        self.audit_attempt(attempt, outcome, authorization, source, method, context)
1486            .await?;
1487        Ok(result)
1488    }
1489
1490    fn rate_limit_key(
1491        &self,
1492        attempt: AuthAttemptScheme,
1493        authorization: Option<&str>,
1494        method: &str,
1495        context: &SipAuthContext,
1496    ) -> AuthRateLimitKey {
1497        let (subject, realm) = subject_realm_from_authorization(authorization);
1498        let kind = if method.eq_ignore_ascii_case("REGISTER") {
1499            AuthRateLimitKind::SipRegister
1500        } else {
1501            attempt.rate_limit_kind()
1502        };
1503        let mut key = AuthRateLimitKey::new(kind);
1504        if let Some(subject) = subject {
1505            key = key.with_subject(subject);
1506        }
1507        if let Some(realm) = realm {
1508            key = key.with_realm(realm);
1509        }
1510        if let Some(peer) = context.peer.as_ref() {
1511            key = key.with_peer(peer.clone());
1512        }
1513        key
1514    }
1515
1516    async fn check_rate_limit(
1517        &self,
1518        key: &AuthRateLimitKey,
1519    ) -> std::result::Result<AuthRateLimitVerdict, CredentialAuthError> {
1520        let Some(rate_limiter) = &self.rate_limiter else {
1521            return Ok(AuthRateLimitVerdict::Allowed);
1522        };
1523        rate_limiter.check_auth_attempt(key).await
1524    }
1525
1526    async fn record_rate_result_or_audit_unavailable(
1527        &self,
1528        key: &AuthRateLimitKey,
1529        outcome: &AuthAuditOutcome,
1530        attempt: AuthAttemptScheme,
1531        authorization: Option<&str>,
1532        source: SipAuthSource,
1533        method: &str,
1534        context: &SipAuthContext,
1535    ) -> Result<()> {
1536        let Some(rate_limiter) = &self.rate_limiter else {
1537            return Ok(());
1538        };
1539        if let Err(err) = rate_limiter.record_auth_result(key, outcome).await {
1540            let provider_outcome =
1541                AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable);
1542            self.audit_attempt(
1543                attempt,
1544                provider_outcome,
1545                authorization,
1546                source,
1547                method,
1548                context,
1549            )
1550            .await?;
1551            return Err(SessionError::AuthError(format!(
1552                "auth rate limiter unavailable: {err}"
1553            )));
1554        }
1555        Ok(())
1556    }
1557
1558    async fn audit_attempt(
1559        &self,
1560        attempt: AuthAttemptScheme,
1561        outcome: AuthAuditOutcome,
1562        authorization: Option<&str>,
1563        source: SipAuthSource,
1564        method: &str,
1565        context: &SipAuthContext,
1566    ) -> Result<()> {
1567        let Some(sink) = &self.audit_sink else {
1568            return Ok(());
1569        };
1570        let (subject, realm) = subject_realm_from_authorization(authorization);
1571        let mut event = AuthAuditEvent::new(attempt.audit_scheme(), outcome);
1572        if let Some(subject) = subject {
1573            event = event.with_subject(subject);
1574        }
1575        if let Some(realm) = realm {
1576            event = event.with_realm(realm);
1577        }
1578        if let Some(peer) = context.peer.as_ref() {
1579            event = event.with_peer(peer.clone());
1580        }
1581        event = event
1582            .with_metadata("method", method.to_ascii_uppercase())
1583            .with_metadata(
1584                "source",
1585                match source {
1586                    SipAuthSource::Origin => "origin",
1587                    SipAuthSource::Proxy => "proxy",
1588                },
1589            );
1590        for (key, value) in &context.metadata {
1591            event = event.with_metadata(key.clone(), value.clone());
1592        }
1593
1594        match sink.record_auth_event(event).await {
1595            Ok(()) => Ok(()),
1596            Err(err) if self.audit_failure_policy == AuditFailurePolicy::FailOpen => {
1597                let _ = err;
1598                Ok(())
1599            }
1600            Err(err) => Err(SessionError::AuthError(format!(
1601                "auth audit sink unavailable: {err}"
1602            ))),
1603        }
1604    }
1605
1606    fn challenges_with_digest_value(
1607        &self,
1608        source: SipAuthSource,
1609        digest_value: String,
1610    ) -> Vec<SipAuthChallenge> {
1611        let mut challenges = Vec::new();
1612        if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1613            if let Some(aka) = &self.aka {
1614                challenges.push(aka.challenge(source));
1615            }
1616        }
1617        if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1618            if let Some(bearer) = &self.bearer_realm {
1619                challenges.push(SipAuthChallenge {
1620                    scheme: SipAuthScheme::Bearer,
1621                    value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1622                    source,
1623                });
1624            }
1625        }
1626        if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1627            challenges.push(SipAuthChallenge {
1628                scheme: SipAuthScheme::Digest,
1629                value: digest_value,
1630                source,
1631            });
1632        }
1633        if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1634            if let Some(basic) = &self.basic {
1635                challenges.push(SipAuthChallenge {
1636                    scheme: SipAuthScheme::Basic,
1637                    value: format!("Basic realm=\"{}\"", basic.realm),
1638                    source,
1639                });
1640            }
1641        }
1642        challenges
1643    }
1644
1645    /// Build challenge header values for the enabled schemes.
1646    ///
1647    /// Use [`SipAuthSource::Origin`] for `WWW-Authenticate` / `401` and
1648    /// [`SipAuthSource::Proxy`] for `Proxy-Authenticate` / `407`.
1649    pub fn challenges(&self, source: SipAuthSource) -> Vec<SipAuthChallenge> {
1650        let mut challenges = Vec::new();
1651        if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1652            if let Some(aka) = &self.aka {
1653                challenges.push(aka.challenge(source));
1654            }
1655        }
1656        if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1657            if let Some(bearer) = &self.bearer_realm {
1658                challenges.push(SipAuthChallenge {
1659                    scheme: SipAuthScheme::Bearer,
1660                    value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1661                    source,
1662                });
1663            }
1664        }
1665        if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1666            if let Some(digest) = &self.digest_provider {
1667                let challenge = digest.challenge();
1668                if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1669                    challenges.push(SipAuthChallenge {
1670                        scheme: SipAuthScheme::Digest,
1671                        value: digest.www_authenticate(&challenge),
1672                        source,
1673                    });
1674                }
1675            } else if let Some(digest) = &self.digest {
1676                let challenge = digest.challenge();
1677                if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1678                    challenges.push(SipAuthChallenge {
1679                        scheme: SipAuthScheme::Digest,
1680                        value: digest.www_authenticate(&challenge),
1681                        source,
1682                    });
1683                }
1684            }
1685        }
1686        if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1687            if let Some(basic) = &self.basic {
1688                challenges.push(SipAuthChallenge {
1689                    scheme: SipAuthScheme::Basic,
1690                    value: format!("Basic realm=\"{}\"", basic.realm),
1691                    source,
1692                });
1693            }
1694        }
1695        challenges
1696    }
1697
1698    /// Build challenge values and record issued Digest nonces in configured
1699    /// shared replay storage.
1700    pub async fn challenges_async(&self, source: SipAuthSource) -> Result<Vec<SipAuthChallenge>> {
1701        let mut challenges = Vec::new();
1702        if self.policy.scheme_allowed(SipAuthScheme::Aka) {
1703            if let Some(aka) = &self.aka {
1704                challenges.push(aka.challenge(source));
1705            }
1706        }
1707        if self.policy.scheme_allowed(SipAuthScheme::Bearer) {
1708            if let Some(bearer) = &self.bearer_realm {
1709                challenges.push(SipAuthChallenge {
1710                    scheme: SipAuthScheme::Bearer,
1711                    value: bearer_challenge_value(bearer, self.bearer_scope.as_deref(), None, None),
1712                    source,
1713                });
1714            }
1715        }
1716        if self.policy.scheme_allowed(SipAuthScheme::Digest) {
1717            if let Some(digest) = &self.digest_provider {
1718                let challenge = digest.challenge_async().await?;
1719                if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1720                    challenges.push(SipAuthChallenge {
1721                        scheme: SipAuthScheme::Digest,
1722                        value: digest.www_authenticate(&challenge),
1723                        source,
1724                    });
1725                }
1726            } else if let Some(digest) = &self.digest {
1727                let challenge = if let Some(replay_store) = &self.digest_replay_store {
1728                    digest
1729                        .challenge_with_replay_store(replay_store.clone())
1730                        .await?
1731                } else {
1732                    digest.challenge()
1733                };
1734                if self.policy.digest_algorithm_allowed(challenge.algorithm) {
1735                    challenges.push(SipAuthChallenge {
1736                        scheme: SipAuthScheme::Digest,
1737                        value: digest.www_authenticate(&challenge),
1738                        source,
1739                    });
1740                }
1741            }
1742        }
1743        if self.policy.scheme_allowed(SipAuthScheme::Basic) {
1744            if let Some(basic) = &self.basic {
1745                challenges.push(SipAuthChallenge {
1746                    scheme: SipAuthScheme::Basic,
1747                    value: format!("Basic realm=\"{}\"", basic.realm),
1748                    source,
1749                });
1750            }
1751        }
1752        Ok(challenges)
1753    }
1754
1755    async fn rejected_async(&self, source: SipAuthSource) -> Result<SipAuthDecision> {
1756        Ok(SipAuthDecision::Rejected {
1757            challenges: self.challenges_async(source).await?,
1758        })
1759    }
1760
1761    async fn authenticate_digest_with_reason(
1762        &self,
1763        authorization: &str,
1764        method: &str,
1765        request_uri: &str,
1766        body: Option<&[u8]>,
1767        source: SipAuthSource,
1768    ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1769        if let Some(digest) = &self.digest_provider {
1770            return match digest
1771                .validate_authorization_detailed(authorization, method, request_uri, body)
1772                .await?
1773            {
1774                (AuthDecision::Authorized { username, realm }, failure_reason) => Ok((
1775                    SipAuthDecision::Authorized(AuthIdentity {
1776                        scheme: SipAuthScheme::Digest,
1777                        username: Some(username),
1778                        subject: None,
1779                        realm: Some(realm),
1780                        scopes: Vec::new(),
1781                        source,
1782                    }),
1783                    failure_reason,
1784                )),
1785                (
1786                    AuthDecision::Rejected {
1787                        www_authenticate, ..
1788                    },
1789                    failure_reason,
1790                ) => Ok((
1791                    SipAuthDecision::Rejected {
1792                        challenges: self.challenges_with_digest_value(source, www_authenticate),
1793                    },
1794                    failure_reason,
1795                )),
1796            };
1797        }
1798        let Some(digest) = &self.digest else {
1799            return Ok((
1800                self.rejected_async(source).await?,
1801                Some(AuthFailureReason::UnsupportedScheme),
1802            ));
1803        };
1804        let digest_decision = if let Some(replay_store) = &self.digest_replay_store {
1805            digest
1806                .validate_authorization_with_replay_store(
1807                    authorization,
1808                    method,
1809                    request_uri,
1810                    body,
1811                    replay_store.clone(),
1812                )
1813                .await?
1814        } else {
1815            digest.validate_authorization(authorization, method, request_uri, body)?
1816        };
1817        match digest_decision {
1818            AuthDecision::Authorized { username, realm } => Ok((
1819                SipAuthDecision::Authorized(AuthIdentity {
1820                    scheme: SipAuthScheme::Digest,
1821                    username: Some(username),
1822                    subject: None,
1823                    realm: Some(realm),
1824                    scopes: Vec::new(),
1825                    source,
1826                }),
1827                None,
1828            )),
1829            AuthDecision::Rejected {
1830                www_authenticate, ..
1831            } => {
1832                let reason = if www_authenticate.contains("stale=true") {
1833                    AuthFailureReason::StaleNonce
1834                } else {
1835                    AuthFailureReason::InvalidCredential
1836                };
1837                Ok((
1838                    SipAuthDecision::Rejected {
1839                        challenges: self.challenges_with_digest_value(source, www_authenticate),
1840                    },
1841                    Some(reason),
1842                ))
1843            }
1844        }
1845    }
1846
1847    async fn authenticate_bearer_with_reason(
1848        &self,
1849        authorization: &str,
1850        source: SipAuthSource,
1851    ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1852        let Some(validator) = &self.bearer else {
1853            return Ok((
1854                self.rejected_async(source).await?,
1855                Some(AuthFailureReason::UnsupportedScheme),
1856            ));
1857        };
1858        let token = authorization
1859            .split_once(char::is_whitespace)
1860            .map(|(_, value)| value.trim())
1861            .unwrap_or_default();
1862        match validator.validate(token).await {
1863            Ok(assurance) => Ok((
1864                SipAuthDecision::Authorized(identity_from_bearer_assurance(
1865                    assurance,
1866                    self.bearer_realm.clone(),
1867                    source,
1868                )),
1869                None,
1870            )),
1871            Err(BearerAuthError::Empty) | Err(BearerAuthError::Invalid(_)) => Ok((
1872                self.rejected_async(source).await?,
1873                Some(AuthFailureReason::InvalidCredential),
1874            )),
1875            Err(BearerAuthError::Unavailable(err)) => Err(SessionError::AuthError(format!(
1876                "Bearer validator unavailable: {err}"
1877            ))),
1878        }
1879    }
1880
1881    async fn authenticate_basic_with_reason(
1882        &self,
1883        authorization: &str,
1884        source: SipAuthSource,
1885        transport: &SipTransportSecurityContext,
1886    ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1887        let Some(basic) = &self.basic else {
1888            return Ok((
1889                self.rejected_async(source).await?,
1890                Some(AuthFailureReason::UnsupportedScheme),
1891            ));
1892        };
1893        if !transport.is_secure() && !self.allow_basic_over_cleartext {
1894            return Ok((
1895                self.rejected_async(source).await?,
1896                Some(AuthFailureReason::PolicyRejected),
1897            ));
1898        }
1899        let token = authorization
1900            .split_once(char::is_whitespace)
1901            .map(|(_, value)| value.trim())
1902            .unwrap_or_default();
1903        let decoded = match BASE64_STANDARD.decode(token) {
1904            Ok(decoded) => decoded,
1905            Err(_) => {
1906                return Ok((
1907                    self.rejected_async(source).await?,
1908                    Some(AuthFailureReason::MalformedCredential),
1909                ))
1910            }
1911        };
1912        let decoded = match String::from_utf8(decoded) {
1913            Ok(decoded) => decoded,
1914            Err(_) => {
1915                return Ok((
1916                    self.rejected_async(source).await?,
1917                    Some(AuthFailureReason::MalformedCredential),
1918                ))
1919            }
1920        };
1921        let Some((username, password)) = decoded.split_once(':') else {
1922            return Ok((
1923                self.rejected_async(source).await?,
1924                Some(AuthFailureReason::MalformedCredential),
1925            ));
1926        };
1927        if let Some(verifier) = &basic.verifier {
1928            return match verifier.verify_password(username, password).await {
1929                Ok(assurance) => {
1930                    let mut identity = identity_from_bearer_assurance(
1931                        assurance,
1932                        Some(basic.realm.clone()),
1933                        source,
1934                    );
1935                    identity.scheme = SipAuthScheme::Basic;
1936                    identity.username = Some(username.to_string());
1937                    Ok((SipAuthDecision::Authorized(identity), None))
1938                }
1939                Err(CredentialAuthError::Invalid) => Ok((
1940                    self.rejected_async(source).await?,
1941                    Some(AuthFailureReason::InvalidCredential),
1942                )),
1943                Err(CredentialAuthError::PolicyRejected(_)) => Ok((
1944                    self.rejected_async(source).await?,
1945                    Some(AuthFailureReason::PolicyRejected),
1946                )),
1947                Err(err) => Err(SessionError::AuthError(err.to_string())),
1948            };
1949        }
1950        let valid = {
1951            let users = basic
1952                .users
1953                .read()
1954                .unwrap_or_else(|poisoned| poisoned.into_inner());
1955            users.get(username).is_some_and(|stored| stored == password)
1956        };
1957        if !valid {
1958            return Ok((
1959                self.rejected_async(source).await?,
1960                Some(AuthFailureReason::InvalidCredential),
1961            ));
1962        }
1963        Ok((
1964            SipAuthDecision::Authorized(AuthIdentity {
1965                scheme: SipAuthScheme::Basic,
1966                username: Some(username.to_string()),
1967                subject: None,
1968                realm: Some(basic.realm.clone()),
1969                scopes: Vec::new(),
1970                source,
1971            }),
1972            None,
1973        ))
1974    }
1975
1976    async fn authenticate_aka_with_reason(
1977        &self,
1978        authorization: &str,
1979        method: &str,
1980        request_uri: &str,
1981        body: Option<&[u8]>,
1982        source: SipAuthSource,
1983    ) -> Result<(SipAuthDecision, Option<AuthFailureReason>)> {
1984        let Some(aka) = &self.aka else {
1985            return Ok((
1986                self.rejected_async(source).await?,
1987                Some(AuthFailureReason::UnsupportedScheme),
1988            ));
1989        };
1990        match aka
1991            .validate(authorization, method, request_uri, body)
1992            .await?
1993        {
1994            Some(mut identity) => {
1995                identity.scheme = SipAuthScheme::Aka;
1996                identity.source = source;
1997                Ok((SipAuthDecision::Authorized(identity), None))
1998            }
1999            None => Ok((
2000                self.rejected_async(source).await?,
2001                Some(AuthFailureReason::InvalidCredential),
2002            )),
2003        }
2004    }
2005}
2006
2007impl Default for SipAuthService {
2008    fn default() -> Self {
2009        Self::new()
2010    }
2011}
2012
2013/// Trait implemented by UAS auth services that can validate inbound SIP
2014/// request credentials from typed incoming API surfaces.
2015///
2016/// Application code normally uses [`SipAuthService`] or
2017/// [`SipDigestAuthService`], both of which implement this trait.
2018#[async_trait]
2019pub trait SipIncomingAuthenticator {
2020    /// Decision type returned by the service.
2021    type Decision: Send;
2022
2023    /// Validate the selected inbound auth header.
2024    async fn authenticate_incoming(
2025        &self,
2026        authorization: Option<&str>,
2027        method: &str,
2028        request_uri: &str,
2029        body: Option<&[u8]>,
2030        source: SipAuthSource,
2031        is_tls: bool,
2032    ) -> Result<Self::Decision>;
2033
2034    /// Validate the selected inbound auth header with transport-truth security
2035    /// context.
2036    ///
2037    /// The default bridges to the legacy boolean TLS method. Auth services
2038    /// that enforce scheme policy should override this method so callers can
2039    /// pass concrete TLS/WSS metadata without losing detail.
2040    async fn authenticate_incoming_with_transport_context(
2041        &self,
2042        authorization: Option<&str>,
2043        method: &str,
2044        request_uri: &str,
2045        body: Option<&[u8]>,
2046        source: SipAuthSource,
2047        transport: &SipTransportSecurityContext,
2048    ) -> Result<Self::Decision> {
2049        self.authenticate_incoming(
2050            authorization,
2051            method,
2052            request_uri,
2053            body,
2054            source,
2055            transport.is_secure(),
2056        )
2057        .await
2058    }
2059}
2060
2061#[async_trait]
2062impl SipIncomingAuthenticator for SipDigestAuthService {
2063    type Decision = AuthDecision;
2064
2065    async fn authenticate_incoming(
2066        &self,
2067        authorization: Option<&str>,
2068        method: &str,
2069        request_uri: &str,
2070        body: Option<&[u8]>,
2071        _source: SipAuthSource,
2072        _is_tls: bool,
2073    ) -> Result<Self::Decision> {
2074        self.authenticate_authorization(authorization, method, request_uri, body)
2075    }
2076}
2077
2078#[async_trait]
2079impl SipIncomingAuthenticator for SipAuthService {
2080    type Decision = SipAuthDecision;
2081
2082    async fn authenticate_incoming(
2083        &self,
2084        authorization: Option<&str>,
2085        method: &str,
2086        request_uri: &str,
2087        body: Option<&[u8]>,
2088        source: SipAuthSource,
2089        is_tls: bool,
2090    ) -> Result<Self::Decision> {
2091        self.authenticate_authorization(authorization, method, request_uri, body, source, is_tls)
2092            .await
2093    }
2094
2095    async fn authenticate_incoming_with_transport_context(
2096        &self,
2097        authorization: Option<&str>,
2098        method: &str,
2099        request_uri: &str,
2100        body: Option<&[u8]>,
2101        source: SipAuthSource,
2102        transport: &SipTransportSecurityContext,
2103    ) -> Result<Self::Decision> {
2104        self.authenticate_authorization_with_transport_context(
2105            authorization,
2106            method,
2107            request_uri,
2108            body,
2109            source,
2110            transport,
2111        )
2112        .await
2113    }
2114}
2115
2116impl std::fmt::Debug for SipAuthService {
2117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2118        f.debug_struct("SipAuthService")
2119            .field("policy", &self.policy)
2120            .field("digest", &self.digest.is_some())
2121            .field("digest_provider", &self.digest_provider.is_some())
2122            .field("bearer", &self.bearer.is_some())
2123            .field("bearer_realm", &self.bearer_realm)
2124            .field("bearer_scope", &self.bearer_scope)
2125            .field("basic", &self.basic.as_ref().map(|b| &b.realm))
2126            .field("aka", &self.aka.is_some())
2127            .field(
2128                "allow_bearer_over_cleartext",
2129                &self.allow_bearer_over_cleartext,
2130            )
2131            .field(
2132                "allow_basic_over_cleartext",
2133                &self.allow_basic_over_cleartext,
2134            )
2135            .field("audit_sink", &self.audit_sink.is_some())
2136            .field("audit_failure_policy", &self.audit_failure_policy)
2137            .field("rate_limiter", &self.rate_limiter.is_some())
2138            .field("digest_replay_store", &self.digest_replay_store.is_some())
2139            .finish()
2140    }
2141}
2142
2143#[derive(Clone)]
2144struct DigestProviderAuthStore {
2145    authenticator: DigestAuthenticator,
2146    realm: String,
2147    provider: Arc<dyn DigestSecretProvider>,
2148    nonces: Arc<RwLock<HashMap<String, Instant>>>,
2149    nonce_counts: Arc<RwLock<HashMap<(String, String), u32>>>,
2150    nonce_ttl: Duration,
2151    replay_store: Option<Arc<dyn DigestReplayStore>>,
2152}
2153
2154impl DigestProviderAuthStore {
2155    fn new(realm: impl Into<String>, provider: Arc<dyn DigestSecretProvider>) -> Self {
2156        let realm = realm.into();
2157        Self {
2158            authenticator: DigestAuthenticator::new(realm.clone()),
2159            realm,
2160            provider,
2161            nonces: Arc::new(RwLock::new(HashMap::new())),
2162            nonce_counts: Arc::new(RwLock::new(HashMap::new())),
2163            nonce_ttl: Duration::from_secs(300),
2164            replay_store: None,
2165        }
2166    }
2167
2168    fn with_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
2169        self.authenticator = self.authenticator.with_algorithm(algorithm);
2170        self
2171    }
2172
2173    fn with_replay_store(mut self, replay_store: Arc<dyn DigestReplayStore>) -> Self {
2174        self.replay_store = Some(replay_store);
2175        self
2176    }
2177
2178    fn challenge(&self) -> DigestChallenge {
2179        let challenge = self.authenticator.generate_challenge();
2180        self.record_nonce_local(&challenge.nonce);
2181        challenge
2182    }
2183
2184    async fn challenge_async(&self) -> Result<DigestChallenge> {
2185        let challenge = self.authenticator.generate_challenge();
2186        if let Some(replay_store) = &self.replay_store {
2187            replay_store
2188                .record_nonce(&challenge.nonce, system_time_after(self.nonce_ttl))
2189                .await
2190                .map_err(|err| SessionError::AuthError(err.to_string()))?;
2191        } else {
2192            self.record_nonce_local(&challenge.nonce);
2193        }
2194        Ok(challenge)
2195    }
2196
2197    fn record_nonce_local(&self, nonce: &str) {
2198        let expires_at = Instant::now()
2199            .checked_add(self.nonce_ttl)
2200            .unwrap_or_else(Instant::now);
2201        let mut nonces = self
2202            .nonces
2203            .write()
2204            .unwrap_or_else(|poisoned| poisoned.into_inner());
2205        nonces.insert(nonce.to_string(), expires_at);
2206    }
2207
2208    fn www_authenticate(&self, challenge: &DigestChallenge) -> String {
2209        self.authenticator.format_www_authenticate(challenge)
2210    }
2211
2212    fn www_authenticate_with_stale(&self, challenge: &DigestChallenge, stale: bool) -> String {
2213        self.authenticator
2214            .format_www_authenticate_with_stale(challenge, stale)
2215    }
2216
2217    async fn validate_authorization_detailed(
2218        &self,
2219        authorization: &str,
2220        method: &str,
2221        request_uri: &str,
2222        body: Option<&[u8]>,
2223    ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2224        let response = match DigestAuthenticator::parse_authorization(authorization) {
2225            Ok(response) => response,
2226            Err(_) => {
2227                return self
2228                    .rejected_with_reason(AuthFailureReason::MalformedCredential)
2229                    .await
2230            }
2231        };
2232
2233        if response.uri != request_uri || response.realm != self.realm {
2234            return self
2235                .rejected_with_reason(AuthFailureReason::InvalidCredential)
2236                .await;
2237        }
2238
2239        let secret = match self
2240            .provider
2241            .lookup_digest_secret(&response.username, &response.realm, response.algorithm)
2242            .await
2243        {
2244            Ok(Some(secret)) => secret,
2245            Ok(None) | Err(CredentialAuthError::Invalid) => {
2246                return self
2247                    .rejected_with_reason(AuthFailureReason::InvalidCredential)
2248                    .await
2249            }
2250            Err(CredentialAuthError::PolicyRejected(_)) => {
2251                return self
2252                    .rejected_with_reason(AuthFailureReason::PolicyRejected)
2253                    .await
2254            }
2255            Err(err) => return Err(SessionError::AuthError(err.to_string())),
2256        };
2257
2258        let valid = match self
2259            .authenticator
2260            .validate_response_with_secret_and_body(&response, method, &secret, body)
2261        {
2262            Ok(valid) => valid,
2263            Err(_) => {
2264                return self
2265                    .rejected_with_reason(AuthFailureReason::UnsupportedScheme)
2266                    .await
2267            }
2268        };
2269        if !valid {
2270            return self
2271                .rejected_with_reason(AuthFailureReason::InvalidCredential)
2272                .await;
2273        }
2274
2275        match self.nonce_status_async(&response.nonce).await? {
2276            NonceStatus::Active => {}
2277            NonceStatus::Expired => {
2278                return self
2279                    .rejected_stale_with_reason(AuthFailureReason::StaleNonce)
2280                    .await
2281            }
2282            NonceStatus::Unknown => {
2283                return self
2284                    .rejected_with_reason(AuthFailureReason::InvalidCredential)
2285                    .await
2286            }
2287        }
2288
2289        if let Some(reason) = self.accept_nonce_count_async(&response).await? {
2290            return self.rejected_with_reason(reason).await;
2291        }
2292
2293        Ok((
2294            AuthDecision::Authorized {
2295                username: response.username,
2296                realm: response.realm,
2297            },
2298            None,
2299        ))
2300    }
2301
2302    fn nonce_status(&self, nonce: &str) -> NonceStatus {
2303        let now = Instant::now();
2304        let mut nonces = self
2305            .nonces
2306            .write()
2307            .unwrap_or_else(|poisoned| poisoned.into_inner());
2308        match nonces.get(nonce).copied() {
2309            Some(expires_at) if expires_at > now => NonceStatus::Active,
2310            Some(_) => {
2311                nonces.remove(nonce);
2312                NonceStatus::Expired
2313            }
2314            None => NonceStatus::Unknown,
2315        }
2316    }
2317
2318    fn accept_nonce_count(&self, response: &DigestResponse) -> bool {
2319        let Some(qop) = response.qop.as_deref() else {
2320            return true;
2321        };
2322        if qop != "auth" && qop != "auth-int" {
2323            return false;
2324        }
2325        let Some(nc) = response
2326            .nc
2327            .as_deref()
2328            .and_then(|value| u32::from_str_radix(value, 16).ok())
2329        else {
2330            return false;
2331        };
2332        let Some(cnonce) = response.cnonce.clone() else {
2333            return false;
2334        };
2335        if cnonce.is_empty() {
2336            return false;
2337        }
2338        let key = (response.username.clone(), response.nonce.clone());
2339        let mut nonce_counts = self
2340            .nonce_counts
2341            .write()
2342            .unwrap_or_else(|poisoned| poisoned.into_inner());
2343        if nonce_counts.get(&key).is_some_and(|last| nc <= *last) {
2344            return false;
2345        }
2346        nonce_counts.insert(key, nc);
2347        true
2348    }
2349
2350    async fn nonce_status_async(&self, nonce: &str) -> Result<NonceStatus> {
2351        let Some(replay_store) = &self.replay_store else {
2352            return Ok(self.nonce_status(nonce));
2353        };
2354        match replay_store
2355            .nonce_status(nonce, SystemTime::now())
2356            .await
2357            .map_err(|err| SessionError::AuthError(err.to_string()))?
2358        {
2359            DigestNonceStatus::Active => Ok(NonceStatus::Active),
2360            DigestNonceStatus::Expired => Ok(NonceStatus::Expired),
2361            DigestNonceStatus::Unknown => Ok(NonceStatus::Unknown),
2362        }
2363    }
2364
2365    async fn accept_nonce_count_async(
2366        &self,
2367        response: &DigestResponse,
2368    ) -> Result<Option<AuthFailureReason>> {
2369        let Some(qop) = response.qop.as_deref() else {
2370            return Ok(None);
2371        };
2372        if qop != "auth" && qop != "auth-int" {
2373            return Ok(Some(AuthFailureReason::UnsupportedScheme));
2374        }
2375        let Some(nc) = response
2376            .nc
2377            .as_deref()
2378            .and_then(|value| u32::from_str_radix(value, 16).ok())
2379        else {
2380            return Ok(Some(AuthFailureReason::MalformedCredential));
2381        };
2382        let Some(cnonce) = response.cnonce.as_ref() else {
2383            return Ok(Some(AuthFailureReason::MalformedCredential));
2384        };
2385        if cnonce.is_empty() {
2386            return Ok(Some(AuthFailureReason::MalformedCredential));
2387        }
2388        let accepted = if let Some(replay_store) = &self.replay_store {
2389            replay_store
2390                .accept_nonce_count(&response.username, &response.nonce, nc)
2391                .await
2392                .map_err(|err| SessionError::AuthError(err.to_string()))?
2393        } else {
2394            self.accept_nonce_count(response)
2395        };
2396        if accepted {
2397            Ok(None)
2398        } else {
2399            Ok(Some(AuthFailureReason::ReplayRejected))
2400        }
2401    }
2402
2403    async fn rejected_async(&self) -> Result<AuthDecision> {
2404        let challenge = self.challenge_async().await?;
2405        let www_authenticate = self.www_authenticate(&challenge);
2406        Ok(AuthDecision::Rejected {
2407            challenge,
2408            www_authenticate,
2409        })
2410    }
2411
2412    async fn rejected_stale_async(&self) -> Result<AuthDecision> {
2413        let challenge = self.challenge_async().await?;
2414        let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
2415        Ok(AuthDecision::Rejected {
2416            challenge,
2417            www_authenticate,
2418        })
2419    }
2420
2421    async fn rejected_with_reason(
2422        &self,
2423        reason: AuthFailureReason,
2424    ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2425        Ok((self.rejected_async().await?, Some(reason)))
2426    }
2427
2428    async fn rejected_stale_with_reason(
2429        &self,
2430        reason: AuthFailureReason,
2431    ) -> Result<(AuthDecision, Option<AuthFailureReason>)> {
2432        Ok((self.rejected_stale_async().await?, Some(reason)))
2433    }
2434}
2435
2436#[derive(Clone)]
2437struct BasicAuthStore {
2438    realm: String,
2439    users: Arc<RwLock<HashMap<String, String>>>,
2440    verifier: Option<Arc<dyn PasswordVerifier>>,
2441}
2442
2443fn identity_from_bearer_assurance(
2444    assurance: IdentityAssurance,
2445    realm: Option<String>,
2446    source: SipAuthSource,
2447) -> AuthIdentity {
2448    match assurance {
2449        IdentityAssurance::UserAuthorized {
2450            user_id, scopes, ..
2451        } => AuthIdentity {
2452            scheme: SipAuthScheme::Bearer,
2453            username: None,
2454            subject: Some(user_id.to_string()),
2455            realm,
2456            scopes,
2457            source,
2458        },
2459        IdentityAssurance::TaskScoped {
2460            identity,
2461            task_id,
2462            scopes,
2463            ..
2464        } => AuthIdentity {
2465            scheme: SipAuthScheme::Bearer,
2466            username: None,
2467            subject: Some(format!("{}:{}", identity, task_id)),
2468            realm,
2469            scopes,
2470            source,
2471        },
2472        other => AuthIdentity {
2473            scheme: SipAuthScheme::Bearer,
2474            username: None,
2475            subject: Some(format!("{other:?}")),
2476            realm,
2477            scopes: Vec::new(),
2478            source,
2479        },
2480    }
2481}
2482
2483fn auth_attempt_scheme(authorization: Option<&str>) -> AuthAttemptScheme {
2484    let Some(authorization) = authorization
2485        .map(str::trim)
2486        .filter(|value| !value.is_empty())
2487    else {
2488        return AuthAttemptScheme::Missing;
2489    };
2490    let lower = authorization.to_ascii_lowercase();
2491    if lower.starts_with("bearer ") {
2492        AuthAttemptScheme::Bearer
2493    } else if lower.starts_with("basic ") {
2494        AuthAttemptScheme::Basic
2495    } else if lower.starts_with("digest ") {
2496        if contains_aka_challenge(authorization) {
2497            AuthAttemptScheme::Aka
2498        } else {
2499            AuthAttemptScheme::Digest
2500        }
2501    } else {
2502        AuthAttemptScheme::Unknown
2503    }
2504}
2505
2506fn auth_outcome_for_decision(
2507    decision: &SipAuthDecision,
2508    failure_reason: Option<AuthFailureReason>,
2509) -> AuthAuditOutcome {
2510    match decision {
2511        SipAuthDecision::Authorized(_) => AuthAuditOutcome::Success,
2512        SipAuthDecision::Rejected { .. } => AuthAuditOutcome::Failure(
2513            failure_reason.unwrap_or(AuthFailureReason::InvalidCredential),
2514        ),
2515    }
2516}
2517
2518fn subject_realm_from_authorization(
2519    authorization: Option<&str>,
2520) -> (Option<String>, Option<String>) {
2521    let Some(authorization) = authorization
2522        .map(str::trim)
2523        .filter(|value| !value.is_empty())
2524    else {
2525        return (None, None);
2526    };
2527    let lower = authorization.to_ascii_lowercase();
2528    if lower.starts_with("digest ") {
2529        return DigestAuthenticator::parse_authorization(authorization)
2530            .map(|response| (Some(response.username), Some(response.realm)))
2531            .unwrap_or((None, None));
2532    }
2533    if lower.starts_with("basic ") {
2534        let token = authorization
2535            .split_once(char::is_whitespace)
2536            .map(|(_, value)| value.trim())
2537            .unwrap_or_default();
2538        return BASE64_STANDARD
2539            .decode(token)
2540            .ok()
2541            .and_then(|decoded| String::from_utf8(decoded).ok())
2542            .and_then(|decoded| {
2543                decoded
2544                    .split_once(':')
2545                    .map(|(username, _)| username.to_string())
2546            })
2547            .map(|username| (Some(username), None))
2548            .unwrap_or((None, None));
2549    }
2550    (None, None)
2551}
2552
2553fn system_time_after(duration: Duration) -> SystemTime {
2554    SystemTime::now()
2555        .checked_add(duration)
2556        .unwrap_or_else(SystemTime::now)
2557}
2558
2559async fn accept_nonce_count_with_replay_store(
2560    response: &DigestResponse,
2561    replay_store: &dyn DigestReplayStore,
2562) -> Result<bool> {
2563    let Some(qop) = response.qop.as_deref() else {
2564        return Ok(true);
2565    };
2566    if qop != "auth" && qop != "auth-int" {
2567        return Ok(false);
2568    }
2569    let Some(nc) = response
2570        .nc
2571        .as_deref()
2572        .and_then(|value| u32::from_str_radix(value, 16).ok())
2573    else {
2574        return Ok(false);
2575    };
2576    let Some(cnonce) = response.cnonce.as_ref() else {
2577        return Ok(false);
2578    };
2579    if cnonce.is_empty() {
2580        return Ok(false);
2581    }
2582    replay_store
2583        .accept_nonce_count(&response.username, &response.nonce, nc)
2584        .await
2585        .map_err(|err| SessionError::AuthError(err.to_string()))
2586}
2587
2588fn bearer_challenge_value(
2589    realm: &str,
2590    scope: Option<&str>,
2591    error: Option<&str>,
2592    error_description: Option<&str>,
2593) -> String {
2594    let mut value = format!("Bearer realm=\"{realm}\"");
2595    if let Some(scope) = scope {
2596        value.push_str(&format!(", scope=\"{scope}\""));
2597    }
2598    if let Some(error) = error {
2599        value.push_str(&format!(", error=\"{error}\""));
2600    }
2601    if let Some(error_description) = error_description {
2602        value.push_str(&format!(", error_description=\"{error_description}\""));
2603    }
2604    value
2605}
2606
2607fn contains_auth_scheme(value: &str, scheme: &str) -> bool {
2608    split_auth_challenges(value).into_iter().any(|challenge| {
2609        let trimmed = challenge.trim_start();
2610        let token = trimmed
2611            .split_once(char::is_whitespace)
2612            .map(|(token, _)| token)
2613            .unwrap_or(trimmed);
2614        token.eq_ignore_ascii_case(scheme)
2615    })
2616}
2617
2618fn contains_aka_challenge(value: &str) -> bool {
2619    let upper = value.to_ascii_uppercase();
2620    upper.contains("AKAV1-MD5") || upper.contains("AKAV2-MD5")
2621}
2622
2623fn extract_digest_challenge(value: &str) -> Option<String> {
2624    let mut best = None;
2625    for challenge in split_auth_challenges(value)
2626        .into_iter()
2627        .filter(|challenge| {
2628            challenge
2629                .trim_start()
2630                .to_ascii_lowercase()
2631                .starts_with("digest ")
2632        })
2633    {
2634        let Ok(parsed) = rvoip_auth_core::DigestAuthenticator::parse_challenge(&challenge) else {
2635            if best.is_none() {
2636                best = Some((0, challenge));
2637            }
2638            continue;
2639        };
2640        let strength = digest_algorithm_strength(parsed.algorithm);
2641        if best
2642            .as_ref()
2643            .map_or(true, |(best_strength, _)| strength > *best_strength)
2644        {
2645            best = Some((strength, challenge));
2646        }
2647    }
2648    best.map(|(_, challenge)| challenge)
2649}
2650
2651fn parse_digest_stale(value: &str) -> bool {
2652    let Some(challenge) = extract_digest_challenge(value) else {
2653        return false;
2654    };
2655    rvoip_auth_core::DigestAuthenticator::parse_challenge_details(&challenge)
2656        .map(|details| details.stale)
2657        .unwrap_or(false)
2658}
2659
2660fn digest_algorithm_strength(algorithm: DigestAlgorithm) -> u8 {
2661    match algorithm {
2662        DigestAlgorithm::SHA512256Sess => 60,
2663        DigestAlgorithm::SHA512256 => 50,
2664        DigestAlgorithm::SHA256Sess => 40,
2665        DigestAlgorithm::SHA256 => 30,
2666        DigestAlgorithm::MD5Sess => 20,
2667        DigestAlgorithm::MD5 => 10,
2668    }
2669}
2670
2671fn split_auth_challenges(value: &str) -> Vec<String> {
2672    let mut challenges = Vec::new();
2673    let mut start = 0;
2674    let mut in_quotes = false;
2675    let chars: Vec<(usize, char)> = value.char_indices().collect();
2676
2677    for (position, (idx, ch)) in chars.iter().copied().enumerate() {
2678        if ch == '"' {
2679            in_quotes = !in_quotes;
2680            continue;
2681        }
2682        if ch != ',' || in_quotes {
2683            continue;
2684        }
2685        let next_idx = idx + ch.len_utf8();
2686        let rest = &value[next_idx..];
2687        let trimmed = rest.trim_start();
2688        if looks_like_auth_challenge_start(trimmed) {
2689            let current = value[start..idx].trim();
2690            if !current.is_empty() {
2691                challenges.push(current.to_string());
2692            }
2693            let whitespace = rest.len() - trimmed.len();
2694            start = next_idx + whitespace;
2695        }
2696
2697        if position + 1 == chars.len() {
2698            break;
2699        }
2700    }
2701
2702    let current = value[start..].trim();
2703    if !current.is_empty() {
2704        challenges.push(current.to_string());
2705    }
2706    challenges
2707}
2708
2709fn looks_like_auth_challenge_start(value: &str) -> bool {
2710    let mut token_len = 0;
2711    for ch in value.chars() {
2712        if ch.is_ascii_alphanumeric() || ch == '-' {
2713            token_len += ch.len_utf8();
2714        } else {
2715            break;
2716        }
2717    }
2718    if token_len == 0 {
2719        return false;
2720    }
2721    let rest = &value[token_len..];
2722    rest.starts_with(char::is_whitespace)
2723}
2724
2725fn select_composite_client_auth(
2726    auths: &[SipClientAuth],
2727    challenge_header: &str,
2728    method: &str,
2729    request_uri: &str,
2730    nonce_count: u32,
2731    body: Option<&[u8]>,
2732    transport: &SipTransportSecurityContext,
2733) -> Result<ClientAuthHeader> {
2734    let priorities: &[fn(&SipClientAuth) -> bool] = &[
2735        |auth| matches!(auth, SipClientAuth::Aka(_)),
2736        |auth| {
2737            matches!(
2738                auth,
2739                SipClientAuth::BearerToken(_) | SipClientAuth::BearerTokenCleartextAllowed(_)
2740            )
2741        },
2742        |auth| matches!(auth, SipClientAuth::Digest(_)),
2743        |auth| matches!(auth, SipClientAuth::Basic { .. }),
2744    ];
2745
2746    for matches_priority in priorities {
2747        for auth in auths.iter().filter(|auth| matches_priority(auth)) {
2748            if let Ok(header) = auth.authorization_for_challenge_with_transport_context(
2749                challenge_header,
2750                method,
2751                request_uri,
2752                nonce_count,
2753                body,
2754                transport,
2755            ) {
2756                return Ok(header);
2757            }
2758        }
2759    }
2760
2761    Err(SessionError::AuthError(
2762        "no configured auth option can answer the challenge".to_string(),
2763    ))
2764}
2765
2766/// Result of evaluating inbound SIP Digest authentication with
2767/// [`SipDigestAuthService`].
2768#[derive(Debug, Clone, PartialEq, Eq)]
2769pub enum AuthDecision {
2770    /// The inbound request carried a valid digest response.
2771    Authorized {
2772        /// Authenticated digest username.
2773        username: String,
2774        /// Realm from the digest response.
2775        realm: String,
2776    },
2777    /// The inbound request should be challenged or rejected.
2778    Rejected {
2779        /// Fresh challenge to send to the peer.
2780        challenge: DigestChallenge,
2781        /// Formatted `WWW-Authenticate` header value.
2782        www_authenticate: String,
2783    },
2784}
2785
2786/// UAS-side SIP Digest authentication facade.
2787///
2788/// The service stores a simple in-memory username/password map, generates typed
2789/// challenges, formats `WWW-Authenticate`, and validates inbound
2790/// `Authorization` values through `rvoip-auth-core`.
2791///
2792/// Prefer [`SipAuthService`] for new applications that need Bearer, Basic, AKA,
2793/// or multi-challenge UAS behavior. This type remains the small Digest-only
2794/// compatibility wrapper.
2795#[derive(Clone)]
2796pub struct SipDigestAuthService {
2797    authenticator: DigestAuthenticator,
2798    realm: String,
2799    users: Arc<RwLock<HashMap<String, String>>>,
2800    nonces: Arc<RwLock<HashMap<String, Instant>>>,
2801    nonce_counts: Arc<RwLock<HashMap<(String, String), u32>>>,
2802    nonce_ttl: Duration,
2803}
2804
2805impl SipDigestAuthService {
2806    /// Create a digest service for the given realm.
2807    pub fn new(realm: impl Into<String>) -> Self {
2808        let realm = realm.into();
2809        Self {
2810            authenticator: DigestAuthenticator::new(realm.clone()),
2811            realm,
2812            users: Arc::new(RwLock::new(HashMap::new())),
2813            nonces: Arc::new(RwLock::new(HashMap::new())),
2814            nonce_counts: Arc::new(RwLock::new(HashMap::new())),
2815            nonce_ttl: Duration::from_secs(300),
2816        }
2817    }
2818
2819    /// Select the algorithm used for generated challenges.
2820    pub fn with_algorithm(mut self, algorithm: DigestAlgorithm) -> Self {
2821        self.authenticator = self.authenticator.with_algorithm(algorithm);
2822        self
2823    }
2824
2825    /// Set how long generated nonces remain valid.
2826    pub fn with_nonce_ttl(mut self, ttl: Duration) -> Self {
2827        self.nonce_ttl = ttl;
2828        self
2829    }
2830
2831    /// Add or replace a digest user.
2832    pub fn add_user(&self, username: impl Into<String>, password: impl Into<String>) {
2833        let mut users = self
2834            .users
2835            .write()
2836            .unwrap_or_else(|poisoned| poisoned.into_inner());
2837        users.insert(username.into(), password.into());
2838    }
2839
2840    /// Generate a fresh digest challenge.
2841    pub fn challenge(&self) -> DigestChallenge {
2842        let challenge = self.authenticator.generate_challenge();
2843        let expires_at = Instant::now() + self.nonce_ttl;
2844        let mut nonces = self
2845            .nonces
2846            .write()
2847            .unwrap_or_else(|poisoned| poisoned.into_inner());
2848        nonces.insert(challenge.nonce.clone(), expires_at);
2849        challenge
2850    }
2851
2852    /// Generate a challenge and record its nonce in a shared replay store.
2853    ///
2854    /// Use this in clustered deployments that keep using the Digest-only
2855    /// compatibility service. Single-process deployments can continue using
2856    /// [`Self::challenge`].
2857    pub async fn challenge_with_replay_store(
2858        &self,
2859        replay_store: Arc<dyn DigestReplayStore>,
2860    ) -> Result<DigestChallenge> {
2861        let challenge = self.authenticator.generate_challenge();
2862        replay_store
2863            .record_nonce(&challenge.nonce, system_time_after(self.nonce_ttl))
2864            .await
2865            .map_err(|err| SessionError::AuthError(err.to_string()))?;
2866        Ok(challenge)
2867    }
2868
2869    /// Format a challenge as a `WWW-Authenticate` header value.
2870    pub fn www_authenticate(&self, challenge: &DigestChallenge) -> String {
2871        self.authenticator.format_www_authenticate(challenge)
2872    }
2873
2874    /// Format a challenge as a `WWW-Authenticate` header value with
2875    /// `stale=true` when the peer can retry using a fresh nonce.
2876    pub fn www_authenticate_with_stale(&self, challenge: &DigestChallenge, stale: bool) -> String {
2877        self.authenticator
2878            .format_www_authenticate_with_stale(challenge, stale)
2879    }
2880
2881    /// Validate an optional inbound `Authorization` value.
2882    ///
2883    /// Missing, malformed, unknown-user, or invalid digest values return
2884    /// [`AuthDecision::Rejected`] with a fresh challenge.
2885    pub fn authenticate_authorization(
2886        &self,
2887        authorization: Option<&str>,
2888        method: &str,
2889        request_uri: &str,
2890        body: Option<&[u8]>,
2891    ) -> Result<AuthDecision> {
2892        let Some(authorization) = authorization else {
2893            return Ok(self.rejected());
2894        };
2895
2896        self.validate_authorization(authorization, method, request_uri, body)
2897    }
2898
2899    /// Validate an optional inbound `Authorization` value using shared
2900    /// nonce/replay storage.
2901    ///
2902    /// This is the async compatibility path for deployments that need
2903    /// cluster-safe Digest replay protection but are not using
2904    /// [`SipAuthService`].
2905    pub async fn authenticate_authorization_with_replay_store(
2906        &self,
2907        authorization: Option<&str>,
2908        method: &str,
2909        request_uri: &str,
2910        body: Option<&[u8]>,
2911        replay_store: Arc<dyn DigestReplayStore>,
2912    ) -> Result<AuthDecision> {
2913        let Some(authorization) = authorization else {
2914            return self.rejected_with_replay_store(replay_store).await;
2915        };
2916
2917        self.validate_authorization_with_replay_store(
2918            authorization,
2919            method,
2920            request_uri,
2921            body,
2922            replay_store,
2923        )
2924        .await
2925    }
2926
2927    /// Validate a present inbound `Authorization` value.
2928    ///
2929    /// Malformed, unknown-user, wrong-password, realm mismatch, nonce mismatch,
2930    /// or request-URI mismatch return [`AuthDecision::Rejected`] with a fresh
2931    /// challenge so callers can answer 401/407 without raw string assembly.
2932    pub fn validate_authorization(
2933        &self,
2934        authorization: &str,
2935        method: &str,
2936        request_uri: &str,
2937        body: Option<&[u8]>,
2938    ) -> Result<AuthDecision> {
2939        let response = match DigestAuthenticator::parse_authorization(authorization) {
2940            Ok(response) => response,
2941            Err(_) => return Ok(self.rejected()),
2942        };
2943
2944        if response.uri != request_uri {
2945            return Ok(self.rejected());
2946        }
2947        if response.realm != self.realm {
2948            return Ok(self.rejected());
2949        }
2950
2951        let password = {
2952            let users = self
2953                .users
2954                .read()
2955                .unwrap_or_else(|poisoned| poisoned.into_inner());
2956            match users.get(&response.username) {
2957                Some(password) => password.clone(),
2958                None => return Ok(self.rejected()),
2959            }
2960        };
2961
2962        let valid = match self
2963            .authenticator
2964            .validate_response_with_body(&response, method, &password, body)
2965        {
2966            Ok(valid) => valid,
2967            Err(_) => return Ok(self.rejected()),
2968        };
2969        if !valid {
2970            return Ok(self.rejected());
2971        }
2972
2973        match self.nonce_status(&response.nonce) {
2974            NonceStatus::Active => {}
2975            NonceStatus::Expired => return Ok(self.rejected_stale()),
2976            NonceStatus::Unknown => return Ok(self.rejected()),
2977        }
2978
2979        if !self.accept_nonce_count(&response) {
2980            return Ok(self.rejected());
2981        }
2982
2983        Ok(AuthDecision::Authorized {
2984            username: response.username,
2985            realm: response.realm,
2986        })
2987    }
2988
2989    /// Validate a present inbound `Authorization` value using shared
2990    /// nonce/replay storage.
2991    pub async fn validate_authorization_with_replay_store(
2992        &self,
2993        authorization: &str,
2994        method: &str,
2995        request_uri: &str,
2996        body: Option<&[u8]>,
2997        replay_store: Arc<dyn DigestReplayStore>,
2998    ) -> Result<AuthDecision> {
2999        let response = match DigestAuthenticator::parse_authorization(authorization) {
3000            Ok(response) => response,
3001            Err(_) => return self.rejected_with_replay_store(replay_store).await,
3002        };
3003
3004        if response.uri != request_uri || response.realm != self.realm {
3005            return self.rejected_with_replay_store(replay_store).await;
3006        }
3007
3008        let password = {
3009            let users = self
3010                .users
3011                .read()
3012                .unwrap_or_else(|poisoned| poisoned.into_inner());
3013            users.get(&response.username).cloned()
3014        };
3015        let Some(password) = password else {
3016            return self.rejected_with_replay_store(replay_store).await;
3017        };
3018
3019        let valid = match self
3020            .authenticator
3021            .validate_response_with_body(&response, method, &password, body)
3022        {
3023            Ok(valid) => valid,
3024            Err(_) => return self.rejected_with_replay_store(replay_store).await,
3025        };
3026        if !valid {
3027            return self.rejected_with_replay_store(replay_store).await;
3028        }
3029
3030        match replay_store
3031            .nonce_status(&response.nonce, SystemTime::now())
3032            .await
3033            .map_err(|err| SessionError::AuthError(err.to_string()))?
3034        {
3035            DigestNonceStatus::Active => {}
3036            DigestNonceStatus::Expired => {
3037                return self.rejected_stale_with_replay_store(replay_store).await
3038            }
3039            DigestNonceStatus::Unknown => {
3040                return self.rejected_with_replay_store(replay_store).await
3041            }
3042        }
3043
3044        if !accept_nonce_count_with_replay_store(&response, replay_store.as_ref()).await? {
3045            return self.rejected_with_replay_store(replay_store).await;
3046        }
3047
3048        Ok(AuthDecision::Authorized {
3049            username: response.username,
3050            realm: response.realm,
3051        })
3052    }
3053
3054    fn nonce_status(&self, nonce: &str) -> NonceStatus {
3055        let now = Instant::now();
3056        let mut nonces = self
3057            .nonces
3058            .write()
3059            .unwrap_or_else(|poisoned| poisoned.into_inner());
3060        match nonces.get(nonce).copied() {
3061            Some(expires_at) if expires_at > now => NonceStatus::Active,
3062            Some(_) => {
3063                nonces.remove(nonce);
3064                NonceStatus::Expired
3065            }
3066            None => NonceStatus::Unknown,
3067        }
3068    }
3069
3070    fn accept_nonce_count(&self, response: &DigestResponse) -> bool {
3071        let Some(qop) = response.qop.as_deref() else {
3072            return true;
3073        };
3074        if qop != "auth" && qop != "auth-int" {
3075            return false;
3076        }
3077        let Some(nc) = response
3078            .nc
3079            .as_deref()
3080            .and_then(|value| u32::from_str_radix(value, 16).ok())
3081        else {
3082            return false;
3083        };
3084        let Some(cnonce) = response.cnonce.clone() else {
3085            return false;
3086        };
3087        if cnonce.is_empty() {
3088            return false;
3089        }
3090        let key = (response.username.clone(), response.nonce.clone());
3091        let mut nonce_counts = self
3092            .nonce_counts
3093            .write()
3094            .unwrap_or_else(|poisoned| poisoned.into_inner());
3095        if nonce_counts.get(&key).is_some_and(|last| nc <= *last) {
3096            return false;
3097        }
3098        nonce_counts.insert(key, nc);
3099        true
3100    }
3101
3102    fn rejected(&self) -> AuthDecision {
3103        let challenge = self.challenge();
3104        let www_authenticate = self.www_authenticate(&challenge);
3105        AuthDecision::Rejected {
3106            challenge,
3107            www_authenticate,
3108        }
3109    }
3110
3111    fn rejected_stale(&self) -> AuthDecision {
3112        let challenge = self.challenge();
3113        let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
3114        AuthDecision::Rejected {
3115            challenge,
3116            www_authenticate,
3117        }
3118    }
3119
3120    async fn rejected_with_replay_store(
3121        &self,
3122        replay_store: Arc<dyn DigestReplayStore>,
3123    ) -> Result<AuthDecision> {
3124        let challenge = self.challenge_with_replay_store(replay_store).await?;
3125        let www_authenticate = self.www_authenticate(&challenge);
3126        Ok(AuthDecision::Rejected {
3127            challenge,
3128            www_authenticate,
3129        })
3130    }
3131
3132    async fn rejected_stale_with_replay_store(
3133        &self,
3134        replay_store: Arc<dyn DigestReplayStore>,
3135    ) -> Result<AuthDecision> {
3136        let challenge = self.challenge_with_replay_store(replay_store).await?;
3137        let www_authenticate = self.www_authenticate_with_stale(&challenge, true);
3138        Ok(AuthDecision::Rejected {
3139            challenge,
3140            www_authenticate,
3141        })
3142    }
3143}
3144
3145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3146enum NonceStatus {
3147    Active,
3148    Expired,
3149    Unknown,
3150}
3151
3152impl std::fmt::Debug for SipDigestAuthService {
3153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3154        let user_count = self
3155            .users
3156            .read()
3157            .map(|users| users.len())
3158            .unwrap_or_default();
3159        let nonce_count = self
3160            .nonces
3161            .read()
3162            .map(|nonces| nonces.len())
3163            .unwrap_or_default();
3164        f.debug_struct("SipDigestAuthService")
3165            .field("authenticator", &self.authenticator)
3166            .field("user_count", &user_count)
3167            .field("nonce_count", &nonce_count)
3168            .field("nonce_ttl", &self.nonce_ttl)
3169            .finish()
3170    }
3171}
3172
3173#[cfg(test)]
3174mod tests {
3175    use super::*;
3176    use proptest::prelude::*;
3177    use std::sync::Mutex;
3178
3179    struct StaticPasswordVerifier;
3180
3181    #[async_trait::async_trait]
3182    impl PasswordVerifier for StaticPasswordVerifier {
3183        async fn verify_password(
3184            &self,
3185            username: &str,
3186            password: &str,
3187        ) -> std::result::Result<IdentityAssurance, CredentialAuthError> {
3188            if username == "alice" && password == "secret" {
3189                Ok(test_assurance())
3190            } else {
3191                Err(CredentialAuthError::Invalid)
3192            }
3193        }
3194    }
3195
3196    struct StaticDigestProvider;
3197
3198    #[async_trait::async_trait]
3199    impl DigestSecretProvider for StaticDigestProvider {
3200        async fn lookup_digest_secret(
3201            &self,
3202            username: &str,
3203            _realm: &str,
3204            _algorithm: DigestAlgorithm,
3205        ) -> std::result::Result<Option<DigestSecret>, CredentialAuthError> {
3206            if username == "alice" {
3207                Ok(Some(DigestSecret::PlaintextPassword("secret".to_string())))
3208            } else {
3209                Ok(None)
3210            }
3211        }
3212    }
3213
3214    #[derive(Clone, Default)]
3215    struct RecordingAuditSink {
3216        events: Arc<Mutex<Vec<AuthAuditEvent>>>,
3217        fail: bool,
3218    }
3219
3220    impl RecordingAuditSink {
3221        fn into_arc(self) -> Arc<dyn AuthAuditSink> {
3222            Arc::new(self)
3223        }
3224
3225        fn events(&self) -> Vec<AuthAuditEvent> {
3226            self.events.lock().unwrap().clone()
3227        }
3228    }
3229
3230    #[async_trait::async_trait]
3231    impl AuthAuditSink for RecordingAuditSink {
3232        async fn record_auth_event(
3233            &self,
3234            event: AuthAuditEvent,
3235        ) -> std::result::Result<(), CredentialAuthError> {
3236            if self.fail {
3237                return Err(CredentialAuthError::Unavailable("audit down".to_string()));
3238            }
3239            self.events.lock().unwrap().push(event);
3240            Ok(())
3241        }
3242    }
3243
3244    #[derive(Clone)]
3245    struct TestRateLimiter {
3246        verdict: AuthRateLimitVerdict,
3247        checked: Arc<Mutex<Vec<AuthRateLimitKey>>>,
3248        results: Arc<Mutex<Vec<AuthAuditOutcome>>>,
3249        fail_check: bool,
3250        fail_record: bool,
3251    }
3252
3253    impl TestRateLimiter {
3254        fn allow() -> Self {
3255            Self {
3256                verdict: AuthRateLimitVerdict::Allowed,
3257                checked: Arc::new(Mutex::new(Vec::new())),
3258                results: Arc::new(Mutex::new(Vec::new())),
3259                fail_check: false,
3260                fail_record: false,
3261            }
3262        }
3263
3264        fn deny() -> Self {
3265            Self {
3266                verdict: AuthRateLimitVerdict::Denied {
3267                    retry_after: Some(Duration::from_secs(1)),
3268                },
3269                checked: Arc::new(Mutex::new(Vec::new())),
3270                results: Arc::new(Mutex::new(Vec::new())),
3271                fail_check: false,
3272                fail_record: false,
3273            }
3274        }
3275
3276        fn fail_check() -> Self {
3277            Self {
3278                fail_check: true,
3279                ..Self::allow()
3280            }
3281        }
3282
3283        fn into_arc(self) -> Arc<dyn AuthRateLimiter> {
3284            Arc::new(self)
3285        }
3286
3287        fn results(&self) -> Vec<AuthAuditOutcome> {
3288            self.results.lock().unwrap().clone()
3289        }
3290    }
3291
3292    #[async_trait::async_trait]
3293    impl AuthRateLimiter for TestRateLimiter {
3294        async fn check_auth_attempt(
3295            &self,
3296            key: &AuthRateLimitKey,
3297        ) -> std::result::Result<AuthRateLimitVerdict, CredentialAuthError> {
3298            if self.fail_check {
3299                return Err(CredentialAuthError::Unavailable(
3300                    "rate limiter down".to_string(),
3301                ));
3302            }
3303            self.checked.lock().unwrap().push(key.clone());
3304            Ok(self.verdict.clone())
3305        }
3306
3307        async fn record_auth_result(
3308            &self,
3309            _key: &AuthRateLimitKey,
3310            outcome: &AuthAuditOutcome,
3311        ) -> std::result::Result<(), CredentialAuthError> {
3312            if self.fail_record {
3313                return Err(CredentialAuthError::Unavailable(
3314                    "rate limiter record down".to_string(),
3315                ));
3316            }
3317            self.results.lock().unwrap().push(outcome.clone());
3318            Ok(())
3319        }
3320    }
3321
3322    struct UnavailableBearer;
3323
3324    #[async_trait::async_trait]
3325    impl BearerValidator for UnavailableBearer {
3326        async fn validate(
3327            &self,
3328            _token: &str,
3329        ) -> std::result::Result<IdentityAssurance, BearerAuthError> {
3330            Err(BearerAuthError::Unavailable("idp down".to_string()))
3331        }
3332    }
3333
3334    #[derive(Default)]
3335    struct MemoryDigestReplayStore {
3336        nonces: Mutex<HashMap<String, SystemTime>>,
3337        nonce_counts: Mutex<HashMap<(String, String), u32>>,
3338        force_expired: Mutex<bool>,
3339    }
3340
3341    impl MemoryDigestReplayStore {
3342        fn set_force_expired(&self, expired: bool) {
3343            *self.force_expired.lock().unwrap() = expired;
3344        }
3345    }
3346
3347    #[async_trait::async_trait]
3348    impl DigestReplayStore for MemoryDigestReplayStore {
3349        async fn record_nonce(
3350            &self,
3351            nonce: &str,
3352            expires_at: SystemTime,
3353        ) -> std::result::Result<(), CredentialAuthError> {
3354            self.nonces
3355                .lock()
3356                .unwrap()
3357                .insert(nonce.to_string(), expires_at);
3358            Ok(())
3359        }
3360
3361        async fn nonce_status(
3362            &self,
3363            nonce: &str,
3364            now: SystemTime,
3365        ) -> std::result::Result<DigestNonceStatus, CredentialAuthError> {
3366            let nonces = self.nonces.lock().unwrap();
3367            let Some(expires_at) = nonces.get(nonce).copied() else {
3368                return Ok(DigestNonceStatus::Unknown);
3369            };
3370            if *self.force_expired.lock().unwrap() || expires_at <= now {
3371                Ok(DigestNonceStatus::Expired)
3372            } else {
3373                Ok(DigestNonceStatus::Active)
3374            }
3375        }
3376
3377        async fn accept_nonce_count(
3378            &self,
3379            username: &str,
3380            nonce: &str,
3381            nonce_count: u32,
3382        ) -> std::result::Result<bool, CredentialAuthError> {
3383            let key = (username.to_string(), nonce.to_string());
3384            let mut counts = self.nonce_counts.lock().unwrap();
3385            if counts.get(&key).is_some_and(|last| nonce_count <= *last) {
3386                return Ok(false);
3387            }
3388            counts.insert(key, nonce_count);
3389            Ok(true)
3390        }
3391    }
3392
3393    fn test_assurance() -> IdentityAssurance {
3394        let identity = rvoip_core_traits::ids::IdentityId::from_string("user_alice");
3395        IdentityAssurance::UserAuthorized {
3396            identity: identity.clone(),
3397            user_id: identity,
3398            scopes: vec!["sip.register".to_string()],
3399        }
3400    }
3401
3402    fn auth_token_strategy() -> impl Strategy<Value = String> {
3403        proptest::string::string_regex("[A-Za-z0-9._~-]{1,16}").unwrap()
3404    }
3405
3406    fn quoted_auth_value_strategy() -> impl Strategy<Value = String> {
3407        prop::collection::vec(
3408            proptest::string::string_regex("[A-Za-z0-9._~-]{1,12}").unwrap(),
3409            1..4,
3410        )
3411        .prop_map(|parts| parts.join(","))
3412    }
3413
3414    fn authorization_for(
3415        username: &str,
3416        password: &str,
3417        challenge: &DigestChallenge,
3418        method: &str,
3419        uri: &str,
3420        body: Option<&[u8]>,
3421    ) -> String {
3422        authorization_for_nc(username, password, challenge, method, uri, body, 1)
3423    }
3424
3425    fn authorization_for_nc(
3426        username: &str,
3427        password: &str,
3428        challenge: &DigestChallenge,
3429        method: &str,
3430        uri: &str,
3431        body: Option<&[u8]>,
3432        nc: u32,
3433    ) -> String {
3434        let computed = DigestAuth::compute_response_with_state(
3435            username, password, challenge, method, uri, nc, body,
3436        )
3437        .expect("digest computation");
3438        DigestAuth::format_authorization_with_state(username, challenge, uri, &computed)
3439    }
3440
3441    #[test]
3442    fn sip_digest_auth_service_accepts_valid_authorization() {
3443        let service =
3444            SipDigestAuthService::new("example.test").with_algorithm(DigestAlgorithm::SHA512256);
3445        service.add_user("alice", "secret");
3446        let challenge = service.challenge();
3447        let authorization = authorization_for(
3448            "alice",
3449            "secret",
3450            &challenge,
3451            "OPTIONS",
3452            "sip:bob@example.test",
3453            None,
3454        );
3455
3456        let decision = service
3457            .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3458            .expect("validation succeeds");
3459
3460        assert_eq!(
3461            decision,
3462            AuthDecision::Authorized {
3463                username: "alice".to_string(),
3464                realm: "example.test".to_string(),
3465            }
3466        );
3467    }
3468
3469    #[test]
3470    fn sip_digest_auth_service_rejects_missing_or_invalid_authorization() {
3471        let service = SipDigestAuthService::new("example.test");
3472        service.add_user("alice", "secret");
3473        let challenge = service.challenge();
3474        let wrong_password = authorization_for(
3475            "alice",
3476            "wrong",
3477            &challenge,
3478            "MESSAGE",
3479            "sip:bob@example.test",
3480            None,
3481        );
3482
3483        assert!(matches!(
3484            service
3485                .authenticate_authorization(None, "MESSAGE", "sip:bob@example.test", None)
3486                .expect("missing auth decision"),
3487            AuthDecision::Rejected { .. }
3488        ));
3489        assert!(matches!(
3490            service
3491                .validate_authorization(&wrong_password, "MESSAGE", "sip:bob@example.test", None)
3492                .expect("invalid auth decision"),
3493            AuthDecision::Rejected { .. }
3494        ));
3495    }
3496
3497    #[test]
3498    fn sip_digest_auth_service_rejects_realm_mismatch() {
3499        let service = SipDigestAuthService::new("example.test");
3500        service.add_user("alice", "secret");
3501        let mut challenge = service.challenge();
3502        challenge.realm = "wrong.realm".to_string();
3503        let authorization = authorization_for(
3504            "alice",
3505            "secret",
3506            &challenge,
3507            "OPTIONS",
3508            "sip:bob@example.test",
3509            None,
3510        );
3511
3512        assert!(matches!(
3513            service
3514                .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3515                .expect("realm mismatch decision"),
3516            AuthDecision::Rejected { .. }
3517        ));
3518    }
3519
3520    #[test]
3521    fn sip_digest_auth_service_marks_expired_issued_nonce_stale() {
3522        let service =
3523            SipDigestAuthService::new("example.test").with_nonce_ttl(Duration::from_millis(1));
3524        service.add_user("alice", "secret");
3525        let challenge = service.challenge();
3526        let authorization = authorization_for(
3527            "alice",
3528            "secret",
3529            &challenge,
3530            "OPTIONS",
3531            "sip:bob@example.test",
3532            None,
3533        );
3534
3535        std::thread::sleep(Duration::from_millis(5));
3536
3537        let decision = service
3538            .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3539            .expect("expired nonce decision");
3540
3541        match decision {
3542            AuthDecision::Rejected {
3543                www_authenticate, ..
3544            } => assert!(
3545                www_authenticate.contains("stale=true"),
3546                "expired nonce should produce stale challenge: {www_authenticate}"
3547            ),
3548            other => panic!("expected stale rejection, got {other:?}"),
3549        }
3550    }
3551
3552    #[test]
3553    fn sip_digest_auth_service_rejects_unknown_nonce_and_replay() {
3554        let service = SipDigestAuthService::new("example.test");
3555        service.add_user("alice", "secret");
3556
3557        let unknown_challenge = DigestChallenge {
3558            realm: "example.test".to_string(),
3559            nonce: "not-issued".to_string(),
3560            algorithm: DigestAlgorithm::MD5,
3561            qop: Some(vec!["auth".to_string()]),
3562            opaque: None,
3563        };
3564        let unknown_nonce_auth = authorization_for(
3565            "alice",
3566            "secret",
3567            &unknown_challenge,
3568            "OPTIONS",
3569            "sip:bob@example.test",
3570            None,
3571        );
3572        assert!(matches!(
3573            service
3574                .validate_authorization(
3575                    &unknown_nonce_auth,
3576                    "OPTIONS",
3577                    "sip:bob@example.test",
3578                    None
3579                )
3580                .expect("unknown nonce decision"),
3581            AuthDecision::Rejected { .. }
3582        ));
3583
3584        let challenge = service.challenge();
3585        let authorization = authorization_for(
3586            "alice",
3587            "secret",
3588            &challenge,
3589            "OPTIONS",
3590            "sip:bob@example.test",
3591            None,
3592        );
3593        assert!(matches!(
3594            service
3595                .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3596                .expect("first nonce-count decision"),
3597            AuthDecision::Authorized { .. }
3598        ));
3599        assert!(matches!(
3600            service
3601                .validate_authorization(&authorization, "OPTIONS", "sip:bob@example.test", None)
3602                .expect("replayed nonce-count decision"),
3603            AuthDecision::Rejected { .. }
3604        ));
3605
3606        let next_nonce_same_count = authorization_for_nc(
3607            "alice",
3608            "secret",
3609            &challenge,
3610            "OPTIONS",
3611            "sip:bob@example.test",
3612            None,
3613            1,
3614        );
3615        assert!(matches!(
3616            service
3617                .validate_authorization(
3618                    &next_nonce_same_count,
3619                    "OPTIONS",
3620                    "sip:bob@example.test",
3621                    None
3622                )
3623                .expect("same nonce-count with new cnonce decision"),
3624            AuthDecision::Rejected { .. }
3625        ));
3626
3627        let next_nonce_count = authorization_for_nc(
3628            "alice",
3629            "secret",
3630            &challenge,
3631            "OPTIONS",
3632            "sip:bob@example.test",
3633            None,
3634            2,
3635        );
3636        assert!(matches!(
3637            service
3638                .validate_authorization(&next_nonce_count, "OPTIONS", "sip:bob@example.test", None)
3639                .expect("higher nonce-count decision"),
3640            AuthDecision::Authorized { .. }
3641        ));
3642    }
3643
3644    #[test]
3645    fn sip_digest_auth_service_validates_auth_int_body() {
3646        let service = SipDigestAuthService::new("example.test");
3647        service.add_user("alice", "secret");
3648        let mut challenge = service.challenge();
3649        challenge.qop = Some(vec!["auth-int".to_string()]);
3650        let body = b"hello";
3651        let authorization = authorization_for(
3652            "alice",
3653            "secret",
3654            &challenge,
3655            "MESSAGE",
3656            "sip:bob@example.test",
3657            Some(body),
3658        );
3659
3660        assert!(matches!(
3661            service
3662                .validate_authorization(
3663                    &authorization,
3664                    "MESSAGE",
3665                    "sip:bob@example.test",
3666                    Some(body)
3667                )
3668                .expect("auth-int decision"),
3669            AuthDecision::Authorized { .. }
3670        ));
3671    }
3672
3673    #[tokio::test]
3674    async fn sip_auth_service_accepts_basic_when_cleartext_explicitly_allowed() {
3675        let mut service = SipAuthService::new()
3676            .with_basic_realm("legacy")
3677            .allow_basic_over_cleartext(true);
3678        service.add_basic_user("alice", "secret");
3679        let token = BASE64_STANDARD.encode("alice:secret");
3680
3681        let decision = service
3682            .authenticate_authorization(
3683                Some(&format!("Basic {token}")),
3684                "OPTIONS",
3685                "sip:bob@example.test",
3686                None,
3687                SipAuthSource::Origin,
3688                false,
3689            )
3690            .await
3691            .expect("basic validation");
3692
3693        assert_eq!(
3694            decision,
3695            SipAuthDecision::Authorized(AuthIdentity {
3696                scheme: SipAuthScheme::Basic,
3697                username: Some("alice".to_string()),
3698                subject: None,
3699                realm: Some("legacy".to_string()),
3700                scopes: Vec::new(),
3701                source: SipAuthSource::Origin,
3702            })
3703        );
3704    }
3705
3706    #[tokio::test]
3707    async fn sip_auth_service_rejects_basic_over_cleartext_by_default() {
3708        let mut service = SipAuthService::new().with_basic_realm("legacy");
3709        service.add_basic_user("alice", "secret");
3710        let token = BASE64_STANDARD.encode("alice:secret");
3711
3712        let decision = service
3713            .authenticate_authorization(
3714                Some(&format!("Basic {token}")),
3715                "OPTIONS",
3716                "sip:bob@example.test",
3717                None,
3718                SipAuthSource::Origin,
3719                false,
3720            )
3721            .await
3722            .expect("basic validation");
3723
3724        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3725    }
3726
3727    #[tokio::test]
3728    async fn sip_auth_service_accepts_basic_with_secure_transport_context() {
3729        let mut service = SipAuthService::new().with_basic_realm("legacy");
3730        service.add_basic_user("alice", "secret");
3731        let token = BASE64_STANDARD.encode("alice:secret");
3732
3733        let decision = service
3734            .authenticate_authorization_with_transport_context(
3735                Some(&format!("Basic {token}")),
3736                "OPTIONS",
3737                "sip:bob@example.test",
3738                None,
3739                SipAuthSource::Origin,
3740                &SipTransportSecurityContext::from_transport_name("WSS"),
3741            )
3742            .await
3743            .expect("basic validation with transport context");
3744
3745        assert!(matches!(
3746            decision,
3747            SipAuthDecision::Authorized(AuthIdentity {
3748                scheme: SipAuthScheme::Basic,
3749                ..
3750            })
3751        ));
3752    }
3753
3754    #[tokio::test]
3755    async fn sip_auth_service_accepts_basic_password_verifier() {
3756        let service = SipAuthService::new()
3757            .with_basic_verifier("legacy", Arc::new(StaticPasswordVerifier))
3758            .allow_basic_over_cleartext(true);
3759        let token = BASE64_STANDARD.encode("alice:secret");
3760
3761        let decision = service
3762            .authenticate_authorization(
3763                Some(&format!("Basic {token}")),
3764                "OPTIONS",
3765                "sip:bob@example.test",
3766                None,
3767                SipAuthSource::Origin,
3768                false,
3769            )
3770            .await
3771            .expect("provider-backed basic validation");
3772
3773        match decision {
3774            SipAuthDecision::Authorized(identity) => {
3775                assert_eq!(identity.scheme, SipAuthScheme::Basic);
3776                assert_eq!(identity.username.as_deref(), Some("alice"));
3777                assert_eq!(identity.realm.as_deref(), Some("legacy"));
3778                assert_eq!(identity.scopes, vec!["sip.register".to_string()]);
3779            }
3780            other => panic!("expected provider-backed Basic authorization, got {other:?}"),
3781        }
3782    }
3783
3784    #[tokio::test]
3785    async fn sip_auth_service_accepts_bearer_validator_identity() {
3786        let service =
3787            SipAuthService::new().with_bearer_validator("api", rvoip_auth_core::bearer_stub());
3788
3789        let decision = service
3790            .authenticate_authorization_with_transport_context(
3791                Some("Bearer token-123"),
3792                "MESSAGE",
3793                "sip:bob@example.test",
3794                None,
3795                SipAuthSource::Proxy,
3796                &SipTransportSecurityContext::from_transport_name("TLS"),
3797            )
3798            .await
3799            .expect("bearer validation");
3800
3801        match decision {
3802            SipAuthDecision::Authorized(identity) => {
3803                assert_eq!(identity.scheme, SipAuthScheme::Bearer);
3804                assert_eq!(identity.realm.as_deref(), Some("api"));
3805                assert_eq!(identity.source, SipAuthSource::Proxy);
3806            }
3807            other => panic!("expected bearer authorization, got {other:?}"),
3808        }
3809    }
3810
3811    #[tokio::test]
3812    async fn sip_auth_service_rejects_bearer_over_cleartext_by_default() {
3813        let service =
3814            SipAuthService::new().with_bearer_validator("api", rvoip_auth_core::bearer_stub());
3815
3816        let decision = service
3817            .authenticate_authorization(
3818                Some("Bearer token-123"),
3819                "MESSAGE",
3820                "sip:bob@example.test",
3821                None,
3822                SipAuthSource::Origin,
3823                false,
3824            )
3825            .await
3826            .expect("bearer cleartext policy");
3827
3828        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3829    }
3830
3831    #[tokio::test]
3832    async fn sip_auth_service_accepts_bearer_cleartext_when_explicitly_allowed() {
3833        let service = SipAuthService::new()
3834            .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
3835            .allow_bearer_over_cleartext(true);
3836
3837        let decision = service
3838            .authenticate_authorization(
3839                Some("Bearer token-123"),
3840                "MESSAGE",
3841                "sip:bob@example.test",
3842                None,
3843                SipAuthSource::Origin,
3844                false,
3845            )
3846            .await
3847            .expect("bearer cleartext opt-in");
3848
3849        assert!(matches!(
3850            decision,
3851            SipAuthDecision::Authorized(AuthIdentity {
3852                scheme: SipAuthScheme::Bearer,
3853                ..
3854            })
3855        ));
3856    }
3857
3858    #[tokio::test]
3859    async fn sip_auth_policy_filters_challenges_and_rejects_disabled_scheme() {
3860        let mut service = SipAuthService::digest("example.test")
3861            .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
3862            .with_basic_realm("legacy")
3863            .with_policy(SipAuthPolicy::new().allow_only([SipAuthScheme::Bearer]));
3864        service.add_digest_user("alice", "secret");
3865        service.add_basic_user("alice", "secret");
3866
3867        let challenges = service.challenges(SipAuthSource::Origin);
3868        assert_eq!(challenges.len(), 1);
3869        assert_eq!(challenges[0].scheme, SipAuthScheme::Bearer);
3870
3871        let token = BASE64_STANDARD.encode("alice:secret");
3872        let decision = service
3873            .authenticate_authorization_with_transport_context(
3874                Some(&format!("Basic {token}")),
3875                "OPTIONS",
3876                "sip:bob@example.test",
3877                None,
3878                SipAuthSource::Origin,
3879                &SipTransportSecurityContext::from_transport_name("TLS"),
3880            )
3881            .await
3882            .expect("policy rejection");
3883        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3884    }
3885
3886    #[tokio::test]
3887    async fn sip_auth_policy_rejects_digest_below_minimum_algorithm() {
3888        let mut service = SipAuthService::digest("example.test").with_policy(
3889            SipAuthPolicy::new().with_minimum_digest_algorithm(DigestAlgorithm::SHA256),
3890        );
3891        service.add_digest_user("alice", "secret");
3892        let challenge = SipDigestAuthService::new("example.test").challenge();
3893        let authorization = authorization_for(
3894            "alice",
3895            "secret",
3896            &challenge,
3897            "OPTIONS",
3898            "sip:bob@example.test",
3899            None,
3900        );
3901
3902        let decision = service
3903            .authenticate_authorization(
3904                Some(&authorization),
3905                "OPTIONS",
3906                "sip:bob@example.test",
3907                None,
3908                SipAuthSource::Origin,
3909                false,
3910            )
3911            .await
3912            .expect("minimum digest policy");
3913
3914        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3915    }
3916
3917    #[tokio::test]
3918    async fn sip_auth_policy_can_require_digest_replay_store() {
3919        let mut service = SipAuthService::digest("example.test")
3920            .with_policy(SipAuthPolicy::new().require_digest_replay_store(true));
3921        service.add_digest_user("alice", "secret");
3922        let challenge = SipDigestAuthService::new("example.test").challenge();
3923        let authorization = authorization_for(
3924            "alice",
3925            "secret",
3926            &challenge,
3927            "OPTIONS",
3928            "sip:bob@example.test",
3929            None,
3930        );
3931
3932        let decision = service
3933            .authenticate_authorization(
3934                Some(&authorization),
3935                "OPTIONS",
3936                "sip:bob@example.test",
3937                None,
3938                SipAuthSource::Origin,
3939                false,
3940            )
3941            .await
3942            .expect("required replay-store policy");
3943
3944        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
3945    }
3946
3947    #[tokio::test]
3948    async fn sip_auth_service_accepts_digest_secret_provider() {
3949        let service = SipAuthService::new()
3950            .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
3951            .with_digest_provider_algorithm(DigestAlgorithm::SHA256);
3952        let challenge = service
3953            .challenges(SipAuthSource::Origin)
3954            .into_iter()
3955            .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
3956            .expect("digest challenge");
3957        let digest_challenge =
3958            DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
3959        assert_eq!(digest_challenge.algorithm, DigestAlgorithm::SHA256);
3960        let authorization = authorization_for(
3961            "alice",
3962            "secret",
3963            &digest_challenge,
3964            "OPTIONS",
3965            "sip:bob@example.test",
3966            None,
3967        );
3968
3969        let decision = service
3970            .authenticate_authorization(
3971                Some(&authorization),
3972                "OPTIONS",
3973                "sip:bob@example.test",
3974                None,
3975                SipAuthSource::Origin,
3976                false,
3977            )
3978            .await
3979            .expect("provider-backed digest validation");
3980
3981        assert_eq!(
3982            decision,
3983            SipAuthDecision::Authorized(AuthIdentity {
3984                scheme: SipAuthScheme::Digest,
3985                username: Some("alice".to_string()),
3986                subject: None,
3987                realm: Some("example.test".to_string()),
3988                scopes: Vec::new(),
3989                source: SipAuthSource::Origin,
3990            })
3991        );
3992    }
3993
3994    #[tokio::test]
3995    async fn sip_auth_service_emits_redacted_audit_and_rate_results() {
3996        let sink = RecordingAuditSink::default();
3997        let limiter = TestRateLimiter::allow();
3998        let mut service = SipAuthService::new()
3999            .with_basic_realm("legacy")
4000            .allow_basic_over_cleartext(true)
4001            .with_audit_sink(sink.clone().into_arc())
4002            .with_rate_limiter(limiter.clone().into_arc());
4003        service.add_basic_user("alice", "secret");
4004        let context = SipAuthContext::new()
4005            .with_peer("192.0.2.10")
4006            .with_metadata("tenant", "acme");
4007
4008        let valid = BASE64_STANDARD.encode("alice:secret");
4009        let wrong = BASE64_STANDARD.encode("alice:wrong");
4010
4011        service
4012            .authenticate_authorization_with_context(
4013                Some(&format!("Basic {valid}")),
4014                "OPTIONS",
4015                "sip:bob@example.test",
4016                None,
4017                SipAuthSource::Origin,
4018                false,
4019                &context,
4020            )
4021            .await
4022            .expect("valid Basic auth");
4023        service
4024            .authenticate_authorization_with_context(
4025                Some(&format!("Basic {wrong}")),
4026                "OPTIONS",
4027                "sip:bob@example.test",
4028                None,
4029                SipAuthSource::Origin,
4030                false,
4031                &context,
4032            )
4033            .await
4034            .expect("invalid Basic auth");
4035        service
4036            .authenticate_authorization_with_context(
4037                None,
4038                "OPTIONS",
4039                "sip:bob@example.test",
4040                None,
4041                SipAuthSource::Origin,
4042                false,
4043                &context,
4044            )
4045            .await
4046            .expect("missing auth");
4047
4048        let events = sink.events();
4049        assert_eq!(events.len(), 3);
4050        assert_eq!(events[0].scheme, AuthAuditScheme::Basic);
4051        assert_eq!(events[0].outcome, AuthAuditOutcome::Success);
4052        assert_eq!(events[0].subject.as_deref(), Some("alice"));
4053        assert_eq!(events[0].peer.as_deref(), Some("192.0.2.10"));
4054        assert_eq!(
4055            events[0].metadata.get("tenant").map(String::as_str),
4056            Some("acme")
4057        );
4058        assert_eq!(
4059            events[1].outcome,
4060            AuthAuditOutcome::Failure(AuthFailureReason::InvalidCredential)
4061        );
4062        assert_eq!(
4063            events[2].outcome,
4064            AuthAuditOutcome::Failure(AuthFailureReason::MissingCredential)
4065        );
4066        assert_eq!(
4067            limiter.results(),
4068            vec![
4069                AuthAuditOutcome::Success,
4070                AuthAuditOutcome::Failure(AuthFailureReason::InvalidCredential),
4071                AuthAuditOutcome::Failure(AuthFailureReason::MissingCredential)
4072            ]
4073        );
4074        for event in events {
4075            assert!(
4076                !event
4077                    .metadata
4078                    .values()
4079                    .any(|value| value.contains("secret")),
4080                "audit metadata must not contain credentials: {event:?}"
4081            );
4082        }
4083    }
4084
4085    #[tokio::test]
4086    async fn sip_auth_service_audits_basic_cleartext_rejection() {
4087        let sink = RecordingAuditSink::default();
4088        let mut service = SipAuthService::new()
4089            .with_basic_realm("legacy")
4090            .with_audit_sink(sink.clone().into_arc());
4091        service.add_basic_user("alice", "secret");
4092        let token = BASE64_STANDARD.encode("alice:secret");
4093
4094        let decision = service
4095            .authenticate_authorization(
4096                Some(&format!("Basic {token}")),
4097                "OPTIONS",
4098                "sip:bob@example.test",
4099                None,
4100                SipAuthSource::Origin,
4101                false,
4102            )
4103            .await
4104            .expect("Basic cleartext rejection");
4105
4106        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
4107        assert_eq!(
4108            sink.events()[0].outcome,
4109            AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)
4110        );
4111    }
4112
4113    #[tokio::test]
4114    async fn sip_auth_service_rate_limiter_denies_before_validation() {
4115        let sink = RecordingAuditSink::default();
4116        let limiter = TestRateLimiter::deny();
4117        let mut service = SipAuthService::new()
4118            .with_basic_realm("legacy")
4119            .allow_basic_over_cleartext(true)
4120            .with_audit_sink(sink.clone().into_arc())
4121            .with_rate_limiter(limiter.clone().into_arc());
4122        service.add_basic_user("alice", "secret");
4123        let token = BASE64_STANDARD.encode("alice:secret");
4124
4125        let decision = service
4126            .authenticate_authorization(
4127                Some(&format!("Basic {token}")),
4128                "OPTIONS",
4129                "sip:bob@example.test",
4130                None,
4131                SipAuthSource::Origin,
4132                false,
4133            )
4134            .await
4135            .expect("rate-limit denial");
4136
4137        assert!(matches!(decision, SipAuthDecision::Rejected { .. }));
4138        assert_eq!(
4139            sink.events()[0].outcome,
4140            AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)
4141        );
4142        assert_eq!(
4143            limiter.results(),
4144            vec![AuthAuditOutcome::Failure(AuthFailureReason::PolicyRejected)]
4145        );
4146    }
4147
4148    #[tokio::test]
4149    async fn sip_auth_service_rate_limiter_failure_fails_closed() {
4150        let sink = RecordingAuditSink::default();
4151        let limiter = TestRateLimiter::fail_check();
4152        let service = SipAuthService::new()
4153            .with_bearer_validator("api", rvoip_auth_core::bearer_stub())
4154            .with_audit_sink(sink.clone().into_arc())
4155            .with_rate_limiter(limiter.into_arc());
4156
4157        let err = service
4158            .authenticate_authorization(
4159                Some("Bearer token"),
4160                "MESSAGE",
4161                "sip:bob@example.test",
4162                None,
4163                SipAuthSource::Origin,
4164                false,
4165            )
4166            .await
4167            .expect_err("rate limiter failure should fail closed");
4168
4169        assert!(matches!(err, SessionError::AuthError(_)));
4170        assert_eq!(
4171            sink.events()[0].outcome,
4172            AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable)
4173        );
4174    }
4175
4176    #[tokio::test]
4177    async fn sip_auth_service_audits_bearer_provider_unavailable() {
4178        let sink = RecordingAuditSink::default();
4179        let service = SipAuthService::new()
4180            .with_bearer_validator("api", Arc::new(UnavailableBearer))
4181            .with_audit_sink(sink.clone().into_arc());
4182
4183        let err = service
4184            .authenticate_authorization_with_transport_context(
4185                Some("Bearer token"),
4186                "MESSAGE",
4187                "sip:bob@example.test",
4188                None,
4189                SipAuthSource::Origin,
4190                &SipTransportSecurityContext::from_transport_name("TLS"),
4191            )
4192            .await
4193            .expect_err("provider failure should return error");
4194
4195        assert!(matches!(err, SessionError::AuthError(_)));
4196        assert_eq!(
4197            sink.events()[0].outcome,
4198            AuthAuditOutcome::Failure(AuthFailureReason::ProviderUnavailable)
4199        );
4200    }
4201
4202    #[tokio::test]
4203    async fn sip_auth_service_uses_digest_replay_store() {
4204        let replay_store = Arc::new(MemoryDigestReplayStore::default());
4205        let sink = RecordingAuditSink::default();
4206        let service = SipAuthService::new()
4207            .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
4208            .with_digest_replay_store(replay_store.clone())
4209            .with_audit_sink(sink.clone().into_arc());
4210        let challenge = service
4211            .challenges_async(SipAuthSource::Origin)
4212            .await
4213            .expect("async challenges")
4214            .into_iter()
4215            .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4216            .expect("Digest challenge");
4217        let digest_challenge =
4218            DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
4219        assert_eq!(
4220            replay_store
4221                .nonce_status(&digest_challenge.nonce, SystemTime::now())
4222                .await
4223                .unwrap(),
4224            DigestNonceStatus::Active
4225        );
4226        let authorization = authorization_for(
4227            "alice",
4228            "secret",
4229            &digest_challenge,
4230            "OPTIONS",
4231            "sip:bob@example.test",
4232            None,
4233        );
4234
4235        let first = service
4236            .authenticate_authorization(
4237                Some(&authorization),
4238                "OPTIONS",
4239                "sip:bob@example.test",
4240                None,
4241                SipAuthSource::Origin,
4242                false,
4243            )
4244            .await
4245            .expect("first Digest auth");
4246        assert!(matches!(first, SipAuthDecision::Authorized(_)));
4247
4248        let replay = service
4249            .authenticate_authorization(
4250                Some(&authorization),
4251                "OPTIONS",
4252                "sip:bob@example.test",
4253                None,
4254                SipAuthSource::Origin,
4255                false,
4256            )
4257            .await
4258            .expect("replayed Digest auth");
4259        assert!(matches!(replay, SipAuthDecision::Rejected { .. }));
4260        assert_eq!(
4261            sink.events().last().unwrap().outcome,
4262            AuthAuditOutcome::Failure(AuthFailureReason::ReplayRejected)
4263        );
4264    }
4265
4266    #[tokio::test]
4267    async fn sip_auth_service_preserves_digest_stale_challenge() {
4268        let replay_store = Arc::new(MemoryDigestReplayStore::default());
4269        let sink = RecordingAuditSink::default();
4270        let service = SipAuthService::new()
4271            .with_digest_provider("example.test", Arc::new(StaticDigestProvider))
4272            .with_digest_replay_store(replay_store.clone())
4273            .with_audit_sink(sink.clone().into_arc());
4274        let challenge = service
4275            .challenges_async(SipAuthSource::Origin)
4276            .await
4277            .expect("async challenges")
4278            .into_iter()
4279            .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4280            .expect("Digest challenge");
4281        let digest_challenge =
4282            DigestAuthenticator::parse_challenge(&challenge.value).expect("parse challenge");
4283        replay_store.set_force_expired(true);
4284        let authorization = authorization_for(
4285            "alice",
4286            "secret",
4287            &digest_challenge,
4288            "OPTIONS",
4289            "sip:bob@example.test",
4290            None,
4291        );
4292
4293        let decision = service
4294            .authenticate_authorization(
4295                Some(&authorization),
4296                "OPTIONS",
4297                "sip:bob@example.test",
4298                None,
4299                SipAuthSource::Origin,
4300                false,
4301            )
4302            .await
4303            .expect("stale Digest auth");
4304
4305        match decision {
4306            SipAuthDecision::Rejected { challenges } => {
4307                let digest = challenges
4308                    .into_iter()
4309                    .find(|challenge| challenge.scheme == SipAuthScheme::Digest)
4310                    .expect("Digest challenge");
4311                assert!(
4312                    digest.value.contains("stale=true"),
4313                    "stale challenge must be preserved: {}",
4314                    digest.value
4315                );
4316            }
4317            other => panic!("expected stale rejection, got {other:?}"),
4318        }
4319        assert_eq!(
4320            sink.events().last().unwrap().outcome,
4321            AuthAuditOutcome::Failure(AuthFailureReason::StaleNonce)
4322        );
4323    }
4324
4325    #[tokio::test]
4326    async fn sip_digest_auth_service_supports_replay_store_helper() {
4327        let replay_store = Arc::new(MemoryDigestReplayStore::default());
4328        let service = SipDigestAuthService::new("example.test");
4329        service.add_user("alice", "secret");
4330        let challenge = service
4331            .challenge_with_replay_store(replay_store.clone())
4332            .await
4333            .expect("challenge with replay store");
4334        let authorization = authorization_for(
4335            "alice",
4336            "secret",
4337            &challenge,
4338            "OPTIONS",
4339            "sip:bob@example.test",
4340            None,
4341        );
4342
4343        let first = service
4344            .authenticate_authorization_with_replay_store(
4345                Some(&authorization),
4346                "OPTIONS",
4347                "sip:bob@example.test",
4348                None,
4349                replay_store.clone(),
4350            )
4351            .await
4352            .expect("first Digest auth");
4353        assert!(matches!(first, AuthDecision::Authorized { .. }));
4354
4355        let replay = service
4356            .authenticate_authorization_with_replay_store(
4357                Some(&authorization),
4358                "OPTIONS",
4359                "sip:bob@example.test",
4360                None,
4361                replay_store,
4362            )
4363            .await
4364            .expect("replayed Digest auth");
4365        assert!(matches!(replay, AuthDecision::Rejected { .. }));
4366    }
4367
4368    #[test]
4369    fn sip_transport_security_context_classifies_secure_transports() {
4370        assert!(SipTransportSecurityContext::from_transport_name("TLS").is_secure());
4371        assert!(SipTransportSecurityContext::from_transport_name("wss").is_secure());
4372        assert!(
4373            SipTransportSecurityContext::from_request_uri_hint("sips:bob@example.test").is_secure()
4374        );
4375        assert!(
4376            SipTransportSecurityContext::from_request_uri_transport_hint(
4377                "sip:bob@example.test;transport=tls"
4378            )
4379            .is_secure()
4380        );
4381        assert!(
4382            SipTransportSecurityContext::from_request_uri_transport_hint(
4383                "sip:bob@example.test;transport=wss"
4384            )
4385            .is_secure()
4386        );
4387        assert!(!SipTransportSecurityContext::from_transport_name("UDP").is_secure());
4388        assert!(
4389            !SipTransportSecurityContext::from_request_uri_hint("sip:bob@example.test").is_secure()
4390        );
4391    }
4392
4393    #[test]
4394    fn sip_client_auth_basic_uses_transport_context_policy() {
4395        let auth = SipClientAuth::basic("alice", "secret");
4396        let cleartext = auth.authorization_for_challenge_with_transport_context(
4397            r#"Basic realm="legacy""#,
4398            "OPTIONS",
4399            "sip:bob@example.test",
4400            1,
4401            None,
4402            &SipTransportSecurityContext::unknown(),
4403        );
4404        assert!(
4405            format!("{:?}", cleartext.expect_err("cleartext Basic must fail"))
4406                .contains("cleartext")
4407        );
4408
4409        let secure = auth
4410            .authorization_for_challenge_with_transport_context(
4411                r#"Basic realm="legacy""#,
4412                "OPTIONS",
4413                "sip:bob@example.test",
4414                1,
4415                None,
4416                &SipTransportSecurityContext::from_transport_name("TLS"),
4417            )
4418            .expect("secure transport permits Basic");
4419        assert_eq!(secure.scheme, SipAuthScheme::Basic);
4420        assert!(secure.value.starts_with("Basic "));
4421    }
4422
4423    #[test]
4424    fn sip_client_auth_bearer_uses_transport_context_policy() {
4425        let auth = SipClientAuth::bearer_token("token-123");
4426        let cleartext = auth.authorization_for_challenge_with_transport_context(
4427            r#"Bearer realm="api""#,
4428            "OPTIONS",
4429            "sip:bob@example.test",
4430            1,
4431            None,
4432            &SipTransportSecurityContext::unknown(),
4433        );
4434        assert!(
4435            format!("{:?}", cleartext.expect_err("cleartext Bearer must fail"))
4436                .contains("cleartext")
4437        );
4438
4439        let secure = auth
4440            .authorization_for_challenge_with_transport_context(
4441                r#"Bearer realm="api""#,
4442                "OPTIONS",
4443                "sip:bob@example.test",
4444                1,
4445                None,
4446                &SipTransportSecurityContext::from_transport_name("TLS"),
4447            )
4448            .expect("secure transport permits Bearer");
4449        assert_eq!(secure.scheme, SipAuthScheme::Bearer);
4450        assert_eq!(secure.value, "Bearer token-123");
4451
4452        let cleartext_allowed = SipClientAuth::bearer_token("token-123")
4453            .allow_bearer_over_cleartext(true)
4454            .authorization_for_challenge_with_transport_context(
4455                r#"Bearer realm="api""#,
4456                "OPTIONS",
4457                "sip:bob@example.test",
4458                1,
4459                None,
4460                &SipTransportSecurityContext::unknown(),
4461            )
4462            .expect("explicit cleartext opt-in permits Bearer");
4463        assert_eq!(cleartext_allowed.value, "Bearer token-123");
4464    }
4465
4466    #[test]
4467    fn sip_client_auth_matches_schemes_case_insensitively() {
4468        let bearer = SipClientAuth::bearer_token("token-123")
4469            .authorization_for_challenge_with_transport_context(
4470                r#"bearer realm="api""#,
4471                "OPTIONS",
4472                "sip:bob@example.test",
4473                1,
4474                None,
4475                &SipTransportSecurityContext::from_transport_name("TLS"),
4476            )
4477            .expect("lowercase bearer challenge must match");
4478        assert_eq!(bearer.scheme, SipAuthScheme::Bearer);
4479
4480        let basic = SipClientAuth::basic("alice", "secret")
4481            .allow_basic_over_cleartext(true)
4482            .authorization_for_challenge(
4483                r#"bAsIc realm="legacy""#,
4484                "OPTIONS",
4485                "sip:bob@example.test",
4486                1,
4487                None,
4488                false,
4489            )
4490            .expect("mixed-case basic challenge must match");
4491        assert_eq!(basic.scheme, SipAuthScheme::Basic);
4492    }
4493
4494    #[test]
4495    fn sip_client_auth_composite_selects_strongest_compatible_scheme() {
4496        let auth = SipClientAuth::any([
4497            SipClientAuth::digest("alice", "secret"),
4498            SipClientAuth::bearer_token("token-123"),
4499            SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4500        ]);
4501        let header = auth
4502            .authorization_for_challenge_with_transport_context(
4503                r#"Digest realm="pbx", nonce="n1", algorithm=MD5, Bearer realm="api", Basic realm="legacy""#,
4504                "OPTIONS",
4505                "sip:bob@example.test",
4506                1,
4507                None,
4508                &SipTransportSecurityContext::from_transport_name("TLS"),
4509            )
4510            .expect("composite auth");
4511
4512        assert_eq!(header.scheme, SipAuthScheme::Bearer);
4513        assert_eq!(header.value, "Bearer token-123");
4514    }
4515
4516    #[test]
4517    fn sip_client_auth_composite_handles_quoted_commas_in_challenge_lists() {
4518        let auth = SipClientAuth::any([
4519            SipClientAuth::digest("alice", "secret"),
4520            SipClientAuth::bearer_token("token-123"),
4521            SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4522        ]);
4523        let header = auth
4524            .authorization_for_challenge_with_transport_context(
4525                r#"Basic realm="legacy,with,commas", Digest realm="pbx", nonce="n1", algorithm=MD5, qop="auth,auth-int", Bearer realm="api", scope="sip.invite,sip.message""#,
4526                "OPTIONS",
4527                "sip:bob@example.test",
4528                1,
4529                None,
4530                &SipTransportSecurityContext::from_transport_name("TLS"),
4531            )
4532            .expect("composite auth with quoted commas");
4533
4534        assert_eq!(header.scheme, SipAuthScheme::Bearer);
4535        assert_eq!(header.value, "Bearer token-123");
4536    }
4537
4538    proptest! {
4539        #[test]
4540        fn auth_challenge_splitter_preserves_quoted_commas(
4541            basic_realm in quoted_auth_value_strategy(),
4542            digest_realm in quoted_auth_value_strategy(),
4543            nonce in auth_token_strategy(),
4544            bearer_scope in quoted_auth_value_strategy(),
4545        ) {
4546            let header = format!(
4547                r#"Basic realm="{basic_realm}", Digest realm="{digest_realm}", nonce="{nonce}", algorithm=SHA-256, qop="auth,auth-int", Bearer realm="api", scope="{bearer_scope}""#
4548            );
4549
4550            let challenges = split_auth_challenges(&header);
4551            prop_assert_eq!(
4552                challenges.len(),
4553                3,
4554                "challenge splitter must not split quoted commas: {:?}",
4555                challenges
4556            );
4557            prop_assert!(challenges[0].starts_with("Basic "));
4558            prop_assert!(challenges[1].starts_with("Digest "));
4559            prop_assert!(challenges[2].starts_with("Bearer "));
4560
4561            let digest = extract_digest_challenge(&header).expect("Digest challenge");
4562            let parsed = DigestAuthenticator::parse_challenge(&digest).expect("parse Digest challenge");
4563            prop_assert_eq!(parsed.realm, digest_realm);
4564            prop_assert_eq!(parsed.nonce, nonce);
4565            prop_assert_eq!(parsed.algorithm, DigestAlgorithm::SHA256);
4566        }
4567    }
4568
4569    #[test]
4570    fn sip_client_auth_composite_rejects_basic_downgrade_when_digest_is_offered() {
4571        let auth = SipClientAuth::any([
4572            SipClientAuth::basic("alice", "secret").allow_basic_over_cleartext(true),
4573            SipClientAuth::digest("alice", "secret"),
4574        ]);
4575        let header = auth
4576            .authorization_for_challenge(
4577                r#"Basic realm="legacy", Digest realm="pbx", nonce="n1", algorithm=SHA-256"#,
4578                "OPTIONS",
4579                "sip:bob@example.test",
4580                1,
4581                None,
4582                false,
4583            )
4584            .expect("composite auth");
4585
4586        assert_eq!(header.scheme, SipAuthScheme::Digest);
4587        assert!(header.value.starts_with("Digest "));
4588    }
4589
4590    #[test]
4591    fn sip_client_auth_digest_selects_strongest_supported_challenge() {
4592        let auth = SipClientAuth::digest("alice", "secret");
4593        let header = auth
4594            .authorization_for_challenge(
4595                r#"Digest realm="pbx", nonce="weak", algorithm=MD5, Digest realm="pbx", nonce="strong", algorithm=SHA-512-256, qop="auth""#,
4596                "OPTIONS",
4597                "sip:bob@example.test",
4598                1,
4599                None,
4600                false,
4601            )
4602            .expect("digest auth");
4603        let response =
4604            DigestAuthenticator::parse_authorization(&header.value).expect("parse authorization");
4605
4606        assert_eq!(header.scheme, SipAuthScheme::Digest);
4607        assert_eq!(response.algorithm, DigestAlgorithm::SHA512256);
4608        assert_eq!(response.nonce, "strong");
4609    }
4610
4611    #[test]
4612    fn sip_client_auth_digest_skips_malformed_challenge_when_valid_digest_exists() {
4613        let auth = SipClientAuth::digest("alice", "secret");
4614        let header = auth
4615            .authorization_for_challenge(
4616                r#"Digest realm="pbx", algorithm=SHA-512-256, Digest realm="pbx", nonce="valid", algorithm=SHA-256, qop="auth""#,
4617                "OPTIONS",
4618                "sip:bob@example.test",
4619                1,
4620                None,
4621                false,
4622            )
4623            .expect("valid Digest alternative should be selected");
4624        let response =
4625            DigestAuthenticator::parse_authorization(&header.value).expect("parse authorization");
4626
4627        assert_eq!(header.scheme, SipAuthScheme::Digest);
4628        assert_eq!(response.algorithm, DigestAlgorithm::SHA256);
4629        assert_eq!(response.nonce, "valid");
4630    }
4631
4632    #[test]
4633    fn sip_client_auth_digest_rejects_malformed_only_challenge() {
4634        let err = SipClientAuth::digest("alice", "secret")
4635            .authorization_for_challenge(
4636                r#"Digest realm="pbx", algorithm=SHA-512-256"#,
4637                "OPTIONS",
4638                "sip:bob@example.test",
4639                1,
4640                None,
4641                false,
4642            )
4643            .expect_err("malformed-only Digest challenge must fail");
4644
4645        assert!(
4646            format!("{err:?}").contains("Invalid digest challenge")
4647                || format!("{err:?}").contains("nonce"),
4648            "unexpected error for malformed Digest challenge: {err:?}"
4649        );
4650    }
4651}