Skip to main content

rvoip_auth_core/
providers.rs

1//! Auth provider contracts for RVoIP services.
2//!
3//! Protocol crates use these traits to authenticate credentials without
4//! depending on a specific user database or identity provider. `users-core`
5//! implements these traits behind its `auth-core` feature, while applications
6//! can implement them for external services such as LDAP, OIDC, IMS, or a
7//! custom database.
8
9use std::collections::BTreeMap;
10use std::time::{Duration, SystemTime};
11
12use async_trait::async_trait;
13use rvoip_core_traits::identity::IdentityAssurance;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16
17use crate::sip_digest::DigestAlgorithm;
18
19/// Error returned by provider-backed credential checks.
20#[derive(Debug, Error)]
21pub enum CredentialAuthError {
22    /// Credentials were present but did not authenticate.
23    #[error("invalid credentials")]
24    Invalid,
25
26    /// The backing provider could not answer the request.
27    #[error("credential provider unavailable: {0}")]
28    Unavailable(String),
29
30    /// A configured security policy rejected the credential or request.
31    #[error("credential policy rejected request: {0}")]
32    PolicyRejected(String),
33}
34
35/// Password verifier for Basic-style username/password authentication.
36///
37/// This trait intentionally verifies credentials without issuing access or
38/// refresh tokens. Token issuance remains a user-service concern.
39#[async_trait]
40pub trait PasswordVerifier: Send + Sync {
41    /// Verify a username/password pair and return the authenticated identity.
42    async fn verify_password(
43        &self,
44        username: &str,
45        password: &str,
46    ) -> Result<IdentityAssurance, CredentialAuthError>;
47}
48
49/// Secret material usable for SIP Digest validation.
50#[derive(Debug, Clone, Eq, PartialEq)]
51pub enum DigestSecret {
52    /// Plaintext SIP Digest password.
53    PlaintextPassword(String),
54
55    /// Precomputed HA1 value for `username:realm:password`.
56    ///
57    /// For `-sess` algorithms this is the base HA1 before nonce/cnonce folding.
58    Ha1(String),
59}
60
61/// Provider for SIP Digest credential material.
62///
63/// Implementations should prefer returning [`DigestSecret::Ha1`] so the
64/// backing store does not retain plaintext SIP secrets. This is separate from
65/// login password storage; Argon2 login hashes are not valid SIP Digest
66/// secrets.
67#[async_trait]
68pub trait DigestSecretProvider: Send + Sync {
69    /// Look up SIP Digest credential material for a username and realm.
70    async fn lookup_digest_secret(
71        &self,
72        username: &str,
73        realm: &str,
74        algorithm: DigestAlgorithm,
75    ) -> Result<Option<DigestSecret>, CredentialAuthError>;
76}
77
78/// API key verifier for services that accept first-party API keys directly.
79#[async_trait]
80pub trait ApiKeyVerifier: Send + Sync {
81    /// Verify an API key and return the authenticated identity.
82    async fn verify_api_key(&self, api_key: &str)
83        -> Result<IdentityAssurance, CredentialAuthError>;
84}
85
86/// Revocation state for an access token identifier.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum TokenRevocationStatus {
89    /// Token identifier is not revoked.
90    Active,
91    /// Token identifier has been revoked and must be rejected.
92    Revoked,
93}
94
95/// Redacted context supplied to a token revocation checker.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct TokenRevocationContext {
98    /// Token identifier, usually the JWT `jti` claim.
99    pub token_id: String,
100    /// Token subject, usually the JWT `sub` claim.
101    pub subject: Option<String>,
102    /// Token issuer, usually the JWT `iss` claim.
103    pub issuer: Option<String>,
104    /// Token issued-at time when present.
105    pub issued_at: Option<SystemTime>,
106    /// Token expiry time when present.
107    pub expires_at: Option<SystemTime>,
108}
109
110impl TokenRevocationContext {
111    /// Create a revocation context for a token identifier.
112    pub fn new(token_id: impl Into<String>) -> Self {
113        Self {
114            token_id: token_id.into(),
115            subject: None,
116            issuer: None,
117            issued_at: None,
118            expires_at: None,
119        }
120    }
121
122    /// Attach a token subject.
123    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
124        self.subject = Some(subject.into());
125        self
126    }
127
128    /// Attach a token issuer.
129    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
130        self.issuer = Some(issuer.into());
131        self
132    }
133
134    /// Attach token issued-at and expiry times.
135    pub fn with_times(
136        mut self,
137        issued_at: Option<SystemTime>,
138        expires_at: Option<SystemTime>,
139    ) -> Self {
140        self.issued_at = issued_at;
141        self.expires_at = expires_at;
142        self
143    }
144}
145
146/// Checks whether an access token identifier has been revoked.
147///
148/// JWT validators call this with the token's `jti` claim when a revocation
149/// checker is configured. Opaque-token validators can use the same contract
150/// with provider-specific token identifiers.
151#[async_trait]
152pub trait TokenRevocationChecker: Send + Sync {
153    /// Return revocation state for a token.
154    async fn check_token(
155        &self,
156        context: &TokenRevocationContext,
157    ) -> Result<TokenRevocationStatus, CredentialAuthError>;
158}
159
160/// SIP Digest nonce state from a replay store.
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum DigestNonceStatus {
163    /// The nonce is known and not expired.
164    Active,
165    /// The nonce was issued by this service but has expired.
166    Expired,
167    /// The nonce is unknown to this service.
168    Unknown,
169}
170
171/// Shared replay store for clustered SIP Digest UAS deployments.
172///
173/// Implementations should key nonce-count replay by `(username, nonce)`, not by
174/// cnonce, because clients can change cnonce while replaying an old nonce-count.
175#[async_trait]
176pub trait DigestReplayStore: Send + Sync {
177    /// Record an issued nonce with its expiry time.
178    async fn record_nonce(
179        &self,
180        nonce: &str,
181        expires_at: SystemTime,
182    ) -> Result<(), CredentialAuthError>;
183
184    /// Return current nonce state.
185    async fn nonce_status(
186        &self,
187        nonce: &str,
188        now: SystemTime,
189    ) -> Result<DigestNonceStatus, CredentialAuthError>;
190
191    /// Atomically accept a nonce-count only if it is greater than the last
192    /// accepted value for `(username, nonce)`.
193    async fn accept_nonce_count(
194        &self,
195        username: &str,
196        nonce: &str,
197        nonce_count: u32,
198    ) -> Result<bool, CredentialAuthError>;
199}
200
201/// Auth scheme associated with an audit event.
202#[non_exhaustive]
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204pub enum AuthAuditScheme {
205    /// SIP Digest authentication.
206    Digest,
207    /// Bearer token authentication.
208    Bearer,
209    /// Basic username/password authentication.
210    Basic,
211    /// IMS AKA authentication.
212    Aka,
213    /// API key authentication.
214    ApiKey,
215    /// Direct password verification.
216    Password,
217    /// Token issuance, refresh, or revocation.
218    Token,
219    /// External or future scheme.
220    Other(String),
221}
222
223/// Security-relevant reason for an authentication failure.
224#[non_exhaustive]
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226pub enum AuthFailureReason {
227    /// No credential was supplied.
228    MissingCredential,
229    /// Credential was malformed.
230    MalformedCredential,
231    /// Credential was present but invalid.
232    InvalidCredential,
233    /// Credential used an unsupported auth scheme or algorithm.
234    UnsupportedScheme,
235    /// Credential was rejected by transport or deployment policy.
236    PolicyRejected,
237    /// Token was expired.
238    TokenExpired,
239    /// Token identifier was revoked.
240    TokenRevoked,
241    /// Digest nonce was stale and should be re-challenged.
242    StaleNonce,
243    /// Digest nonce-count or proof replay was rejected.
244    ReplayRejected,
245    /// Backing provider was unavailable.
246    ProviderUnavailable,
247    /// External or future failure reason.
248    Other(String),
249}
250
251/// Result captured by an auth audit event.
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub enum AuthAuditOutcome {
254    /// Authentication succeeded.
255    Success,
256    /// Authentication failed with a categorized reason.
257    Failure(AuthFailureReason),
258}
259
260/// Redacted audit event for auth/security logging.
261///
262/// Events intentionally carry identifiers and metadata, not credential values.
263/// Do not put passwords, HA1 values, bearer tokens, API keys, full
264/// Authorization headers, or full JWTs into `metadata`.
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct AuthAuditEvent {
267    /// Scheme or auth subsystem involved.
268    pub scheme: AuthAuditScheme,
269    /// Success/failure result.
270    pub outcome: AuthAuditOutcome,
271    /// User, subject, token id, SIP username, or API key id when known.
272    pub subject: Option<String>,
273    /// Auth realm, issuer, tenant, or provider name when known.
274    pub realm: Option<String>,
275    /// Source peer, IP, connection id, or SIP source when known.
276    pub peer: Option<String>,
277    /// Additional non-secret attributes.
278    pub metadata: BTreeMap<String, String>,
279}
280
281impl AuthAuditEvent {
282    /// Create an audit event without optional identifiers.
283    pub fn new(scheme: AuthAuditScheme, outcome: AuthAuditOutcome) -> Self {
284        Self {
285            scheme,
286            outcome,
287            subject: None,
288            realm: None,
289            peer: None,
290            metadata: BTreeMap::new(),
291        }
292    }
293
294    /// Attach a non-secret subject identifier.
295    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
296        self.subject = Some(subject.into());
297        self
298    }
299
300    /// Attach a non-secret realm, issuer, tenant, or provider name.
301    pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
302        self.realm = Some(realm.into());
303        self
304    }
305
306    /// Attach a peer identifier.
307    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
308        self.peer = Some(peer.into());
309        self
310    }
311
312    /// Attach non-secret metadata.
313    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
314        self.metadata.insert(key.into(), value.into());
315        self
316    }
317}
318
319/// Sink for security audit events.
320///
321/// Production implementations usually write to structured logging, SIEM, an
322/// audit database, or a message bus. Applications decide whether an unavailable
323/// sink is fail-open or fail-closed at the call site.
324#[async_trait]
325pub trait AuthAuditSink: Send + Sync {
326    /// Record a redacted auth audit event.
327    async fn record_auth_event(&self, event: AuthAuditEvent) -> Result<(), CredentialAuthError>;
328}
329
330/// Authentication operation subject to rate limits or lockout policy.
331#[non_exhaustive]
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub enum AuthRateLimitKind {
334    /// SIP REGISTER attempts.
335    SipRegister,
336    /// SIP request authentication outside REGISTER.
337    SipRequest,
338    /// Basic username/password verification.
339    BasicPassword,
340    /// Direct login password verification.
341    Password,
342    /// API key verification.
343    ApiKey,
344    /// Bearer token validation.
345    BearerToken,
346    /// Token issuance or refresh.
347    TokenIssuance,
348    /// SIP Digest validation.
349    Digest,
350    /// External or future operation.
351    Other(String),
352}
353
354/// Rate-limit key. Fields are optional so applications can key by peer, realm,
355/// subject, or any combination their deployment supports.
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct AuthRateLimitKey {
358    /// Operation category.
359    pub kind: AuthRateLimitKind,
360    /// Subject or username when known.
361    pub subject: Option<String>,
362    /// Realm, issuer, tenant, or provider name when known.
363    pub realm: Option<String>,
364    /// Source peer, IP, connection id, or SIP source when known.
365    pub peer: Option<String>,
366}
367
368impl AuthRateLimitKey {
369    /// Create a key for an auth operation.
370    pub fn new(kind: AuthRateLimitKind) -> Self {
371        Self {
372            kind,
373            subject: None,
374            realm: None,
375            peer: None,
376        }
377    }
378
379    /// Attach a subject or username.
380    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
381        self.subject = Some(subject.into());
382        self
383    }
384
385    /// Attach a realm, issuer, tenant, or provider name.
386    pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
387        self.realm = Some(realm.into());
388        self
389    }
390
391    /// Attach a peer identifier.
392    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
393        self.peer = Some(peer.into());
394        self
395    }
396}
397
398/// Rate-limit decision.
399#[derive(Debug, Clone, PartialEq, Eq)]
400pub enum AuthRateLimitVerdict {
401    /// Request is allowed.
402    Allowed,
403    /// Request is denied by rate-limit or lockout policy.
404    Denied {
405        /// Suggested retry delay when known.
406        retry_after: Option<Duration>,
407    },
408}
409
410/// Provider contract for rate-limit and lockout policy.
411#[async_trait]
412pub trait AuthRateLimiter: Send + Sync {
413    /// Check whether an auth attempt is allowed before validating credentials.
414    async fn check_auth_attempt(
415        &self,
416        key: &AuthRateLimitKey,
417    ) -> Result<AuthRateLimitVerdict, CredentialAuthError>;
418
419    /// Record the outcome after an auth attempt is evaluated.
420    async fn record_auth_result(
421        &self,
422        key: &AuthRateLimitKey,
423        outcome: &AuthAuditOutcome,
424    ) -> Result<(), CredentialAuthError>;
425}