rvoip-auth-core 0.2.4

OAuth2 and token-based authentication for RVoIP services
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
//! Auth provider contracts for RVoIP services.
//!
//! Protocol crates use these traits to authenticate credentials without
//! depending on a specific user database or identity provider. `users-core`
//! implements these traits behind its `auth-core` feature, while applications
//! can implement them for external services such as LDAP, OIDC, IMS, or a
//! custom database.

use std::collections::BTreeMap;
use std::time::{Duration, SystemTime};

use async_trait::async_trait;
use rvoip_core_traits::identity::IdentityAssurance;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::sip_digest::DigestAlgorithm;

/// Error returned by provider-backed credential checks.
#[derive(Debug, Error)]
pub enum CredentialAuthError {
    /// Credentials were present but did not authenticate.
    #[error("invalid credentials")]
    Invalid,

    /// The backing provider could not answer the request.
    #[error("credential provider unavailable: {0}")]
    Unavailable(String),

    /// A configured security policy rejected the credential or request.
    #[error("credential policy rejected request: {0}")]
    PolicyRejected(String),
}

/// Password verifier for Basic-style username/password authentication.
///
/// This trait intentionally verifies credentials without issuing access or
/// refresh tokens. Token issuance remains a user-service concern.
#[async_trait]
pub trait PasswordVerifier: Send + Sync {
    /// Verify a username/password pair and return the authenticated identity.
    async fn verify_password(
        &self,
        username: &str,
        password: &str,
    ) -> Result<IdentityAssurance, CredentialAuthError>;
}

/// Secret material usable for SIP Digest validation.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum DigestSecret {
    /// Plaintext SIP Digest password.
    PlaintextPassword(String),

    /// Precomputed HA1 value for `username:realm:password`.
    ///
    /// For `-sess` algorithms this is the base HA1 before nonce/cnonce folding.
    Ha1(String),
}

/// Provider for SIP Digest credential material.
///
/// Implementations should prefer returning [`DigestSecret::Ha1`] so the
/// backing store does not retain plaintext SIP secrets. This is separate from
/// login password storage; Argon2 login hashes are not valid SIP Digest
/// secrets.
#[async_trait]
pub trait DigestSecretProvider: Send + Sync {
    /// Look up SIP Digest credential material for a username and realm.
    async fn lookup_digest_secret(
        &self,
        username: &str,
        realm: &str,
        algorithm: DigestAlgorithm,
    ) -> Result<Option<DigestSecret>, CredentialAuthError>;
}

/// API key verifier for services that accept first-party API keys directly.
#[async_trait]
pub trait ApiKeyVerifier: Send + Sync {
    /// Verify an API key and return the authenticated identity.
    async fn verify_api_key(&self, api_key: &str)
        -> Result<IdentityAssurance, CredentialAuthError>;
}

/// Revocation state for an access token identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenRevocationStatus {
    /// Token identifier is not revoked.
    Active,
    /// Token identifier has been revoked and must be rejected.
    Revoked,
}

/// Redacted context supplied to a token revocation checker.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenRevocationContext {
    /// Token identifier, usually the JWT `jti` claim.
    pub token_id: String,
    /// Token subject, usually the JWT `sub` claim.
    pub subject: Option<String>,
    /// Token issuer, usually the JWT `iss` claim.
    pub issuer: Option<String>,
    /// Token issued-at time when present.
    pub issued_at: Option<SystemTime>,
    /// Token expiry time when present.
    pub expires_at: Option<SystemTime>,
}

impl TokenRevocationContext {
    /// Create a revocation context for a token identifier.
    pub fn new(token_id: impl Into<String>) -> Self {
        Self {
            token_id: token_id.into(),
            subject: None,
            issuer: None,
            issued_at: None,
            expires_at: None,
        }
    }

    /// Attach a token subject.
    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
        self.subject = Some(subject.into());
        self
    }

    /// Attach a token issuer.
    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
        self.issuer = Some(issuer.into());
        self
    }

    /// Attach token issued-at and expiry times.
    pub fn with_times(
        mut self,
        issued_at: Option<SystemTime>,
        expires_at: Option<SystemTime>,
    ) -> Self {
        self.issued_at = issued_at;
        self.expires_at = expires_at;
        self
    }
}

/// Checks whether an access token identifier has been revoked.
///
/// JWT validators call this with the token's `jti` claim when a revocation
/// checker is configured. Opaque-token validators can use the same contract
/// with provider-specific token identifiers.
#[async_trait]
pub trait TokenRevocationChecker: Send + Sync {
    /// Return revocation state for a token.
    async fn check_token(
        &self,
        context: &TokenRevocationContext,
    ) -> Result<TokenRevocationStatus, CredentialAuthError>;
}

/// SIP Digest nonce state from a replay store.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestNonceStatus {
    /// The nonce is known and not expired.
    Active,
    /// The nonce was issued by this service but has expired.
    Expired,
    /// The nonce is unknown to this service.
    Unknown,
}

/// Shared replay store for clustered SIP Digest UAS deployments.
///
/// Implementations should key nonce-count replay by `(username, nonce)`, not by
/// cnonce, because clients can change cnonce while replaying an old nonce-count.
#[async_trait]
pub trait DigestReplayStore: Send + Sync {
    /// Record an issued nonce with its expiry time.
    async fn record_nonce(
        &self,
        nonce: &str,
        expires_at: SystemTime,
    ) -> Result<(), CredentialAuthError>;

    /// Return current nonce state.
    async fn nonce_status(
        &self,
        nonce: &str,
        now: SystemTime,
    ) -> Result<DigestNonceStatus, CredentialAuthError>;

    /// Atomically accept a nonce-count only if it is greater than the last
    /// accepted value for `(username, nonce)`.
    async fn accept_nonce_count(
        &self,
        username: &str,
        nonce: &str,
        nonce_count: u32,
    ) -> Result<bool, CredentialAuthError>;
}

/// Auth scheme associated with an audit event.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthAuditScheme {
    /// SIP Digest authentication.
    Digest,
    /// Bearer token authentication.
    Bearer,
    /// Basic username/password authentication.
    Basic,
    /// IMS AKA authentication.
    Aka,
    /// API key authentication.
    ApiKey,
    /// Direct password verification.
    Password,
    /// Token issuance, refresh, or revocation.
    Token,
    /// External or future scheme.
    Other(String),
}

/// Security-relevant reason for an authentication failure.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthFailureReason {
    /// No credential was supplied.
    MissingCredential,
    /// Credential was malformed.
    MalformedCredential,
    /// Credential was present but invalid.
    InvalidCredential,
    /// Credential used an unsupported auth scheme or algorithm.
    UnsupportedScheme,
    /// Credential was rejected by transport or deployment policy.
    PolicyRejected,
    /// Token was expired.
    TokenExpired,
    /// Token identifier was revoked.
    TokenRevoked,
    /// Digest nonce was stale and should be re-challenged.
    StaleNonce,
    /// Digest nonce-count or proof replay was rejected.
    ReplayRejected,
    /// Backing provider was unavailable.
    ProviderUnavailable,
    /// External or future failure reason.
    Other(String),
}

/// Result captured by an auth audit event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthAuditOutcome {
    /// Authentication succeeded.
    Success,
    /// Authentication failed with a categorized reason.
    Failure(AuthFailureReason),
}

/// Redacted audit event for auth/security logging.
///
/// Events intentionally carry identifiers and metadata, not credential values.
/// Do not put passwords, HA1 values, bearer tokens, API keys, full
/// Authorization headers, or full JWTs into `metadata`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthAuditEvent {
    /// Scheme or auth subsystem involved.
    pub scheme: AuthAuditScheme,
    /// Success/failure result.
    pub outcome: AuthAuditOutcome,
    /// User, subject, token id, SIP username, or API key id when known.
    pub subject: Option<String>,
    /// Auth realm, issuer, tenant, or provider name when known.
    pub realm: Option<String>,
    /// Source peer, IP, connection id, or SIP source when known.
    pub peer: Option<String>,
    /// Additional non-secret attributes.
    pub metadata: BTreeMap<String, String>,
}

impl AuthAuditEvent {
    /// Create an audit event without optional identifiers.
    pub fn new(scheme: AuthAuditScheme, outcome: AuthAuditOutcome) -> Self {
        Self {
            scheme,
            outcome,
            subject: None,
            realm: None,
            peer: None,
            metadata: BTreeMap::new(),
        }
    }

    /// Attach a non-secret subject identifier.
    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
        self.subject = Some(subject.into());
        self
    }

    /// Attach a non-secret realm, issuer, tenant, or provider name.
    pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
        self.realm = Some(realm.into());
        self
    }

    /// Attach a peer identifier.
    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
        self.peer = Some(peer.into());
        self
    }

    /// Attach non-secret metadata.
    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }
}

/// Sink for security audit events.
///
/// Production implementations usually write to structured logging, SIEM, an
/// audit database, or a message bus. Applications decide whether an unavailable
/// sink is fail-open or fail-closed at the call site.
#[async_trait]
pub trait AuthAuditSink: Send + Sync {
    /// Record a redacted auth audit event.
    async fn record_auth_event(&self, event: AuthAuditEvent) -> Result<(), CredentialAuthError>;
}

/// Authentication operation subject to rate limits or lockout policy.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthRateLimitKind {
    /// SIP REGISTER attempts.
    SipRegister,
    /// SIP request authentication outside REGISTER.
    SipRequest,
    /// Basic username/password verification.
    BasicPassword,
    /// Direct login password verification.
    Password,
    /// API key verification.
    ApiKey,
    /// Bearer token validation.
    BearerToken,
    /// Token issuance or refresh.
    TokenIssuance,
    /// SIP Digest validation.
    Digest,
    /// External or future operation.
    Other(String),
}

/// Rate-limit key. Fields are optional so applications can key by peer, realm,
/// subject, or any combination their deployment supports.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthRateLimitKey {
    /// Operation category.
    pub kind: AuthRateLimitKind,
    /// Subject or username when known.
    pub subject: Option<String>,
    /// Realm, issuer, tenant, or provider name when known.
    pub realm: Option<String>,
    /// Source peer, IP, connection id, or SIP source when known.
    pub peer: Option<String>,
}

impl AuthRateLimitKey {
    /// Create a key for an auth operation.
    pub fn new(kind: AuthRateLimitKind) -> Self {
        Self {
            kind,
            subject: None,
            realm: None,
            peer: None,
        }
    }

    /// Attach a subject or username.
    pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
        self.subject = Some(subject.into());
        self
    }

    /// Attach a realm, issuer, tenant, or provider name.
    pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
        self.realm = Some(realm.into());
        self
    }

    /// Attach a peer identifier.
    pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
        self.peer = Some(peer.into());
        self
    }
}

/// Rate-limit decision.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthRateLimitVerdict {
    /// Request is allowed.
    Allowed,
    /// Request is denied by rate-limit or lockout policy.
    Denied {
        /// Suggested retry delay when known.
        retry_after: Option<Duration>,
    },
}

/// Provider contract for rate-limit and lockout policy.
#[async_trait]
pub trait AuthRateLimiter: Send + Sync {
    /// Check whether an auth attempt is allowed before validating credentials.
    async fn check_auth_attempt(
        &self,
        key: &AuthRateLimitKey,
    ) -> Result<AuthRateLimitVerdict, CredentialAuthError>;

    /// Record the outcome after an auth attempt is evaluated.
    async fn record_auth_result(
        &self,
        key: &AuthRateLimitKey,
        outcome: &AuthAuditOutcome,
    ) -> Result<(), CredentialAuthError>;
}