use std::collections::BTreeMap;
use std::fmt;
use std::time::{Duration, SystemTime};
use async_trait::async_trait;
use rvoip_core_traits::identity::IdentityAssurance;
use serde::{Deserialize, Serialize};
use crate::sip_digest::DigestAlgorithm;
pub enum CredentialAuthError {
Invalid,
Unavailable(String),
PolicyRejected(String),
}
impl CredentialAuthError {
fn diagnostic_class(&self) -> &'static str {
match self {
Self::Invalid => "invalid",
Self::Unavailable(_) => "provider-unavailable",
Self::PolicyRejected(_) => "policy-rejected",
}
}
}
impl fmt::Display for CredentialAuthError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"credential authentication failed (class={})",
self.diagnostic_class()
)
}
}
impl fmt::Debug for CredentialAuthError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CredentialAuthError")
.field("class", &self.diagnostic_class())
.finish()
}
}
impl std::error::Error for CredentialAuthError {}
#[async_trait]
pub trait PasswordVerifier: Send + Sync {
async fn verify_password(
&self,
username: &str,
password: &str,
) -> Result<IdentityAssurance, CredentialAuthError>;
}
#[derive(Clone, Eq, PartialEq)]
pub enum DigestSecret {
PlaintextPassword(String),
Ha1(String),
}
impl fmt::Debug for DigestSecret {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::PlaintextPassword(value) => formatter
.debug_struct("PlaintextPassword")
.field("secret_bytes", &value.len())
.finish(),
Self::Ha1(value) => formatter
.debug_struct("Ha1")
.field("secret_bytes", &value.len())
.finish(),
}
}
}
#[async_trait]
pub trait DigestSecretProvider: Send + Sync {
async fn lookup_digest_secret(
&self,
username: &str,
realm: &str,
algorithm: DigestAlgorithm,
) -> Result<Option<DigestSecret>, CredentialAuthError>;
}
#[async_trait]
pub trait ApiKeyVerifier: Send + Sync {
async fn verify_api_key(&self, api_key: &str)
-> Result<IdentityAssurance, CredentialAuthError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TokenRevocationStatus {
Active,
Revoked,
}
#[derive(Clone, PartialEq, Eq)]
pub struct TokenRevocationContext {
pub token_id: String,
pub subject: Option<String>,
pub issuer: Option<String>,
pub issued_at: Option<SystemTime>,
pub expires_at: Option<SystemTime>,
}
impl fmt::Debug for TokenRevocationContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("TokenRevocationContext")
.field("token_id_present", &!self.token_id.is_empty())
.field("token_id_bytes", &self.token_id.len())
.field("subject_present", &self.subject.is_some())
.field("issuer_present", &self.issuer.is_some())
.field("issued_at_present", &self.issued_at.is_some())
.field("expires_at_present", &self.expires_at.is_some())
.finish()
}
}
impl TokenRevocationContext {
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,
}
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.issuer = Some(issuer.into());
self
}
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
}
}
#[async_trait]
pub trait TokenRevocationChecker: Send + Sync {
async fn check_token(
&self,
context: &TokenRevocationContext,
) -> Result<TokenRevocationStatus, CredentialAuthError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DigestNonceStatus {
Active,
Expired,
Unknown,
}
#[async_trait]
pub trait DigestReplayStore: Send + Sync {
async fn record_nonce(
&self,
nonce: &str,
expires_at: SystemTime,
) -> Result<(), CredentialAuthError>;
async fn nonce_status(
&self,
nonce: &str,
now: SystemTime,
) -> Result<DigestNonceStatus, CredentialAuthError>;
async fn accept_nonce_count(
&self,
username: &str,
nonce: &str,
nonce_count: u32,
) -> Result<bool, CredentialAuthError>;
async fn admit_nonce(
&self,
_proposed_nonce: &str,
_expires_at: SystemTime,
) -> Result<String, CredentialAuthError> {
Err(CredentialAuthError::PolicyRejected(
"bounded Digest nonce admission is not implemented".to_string(),
))
}
async fn accept_client_nonce_count(
&self,
_username: &str,
_nonce: &str,
_cnonce: &str,
_nonce_count: u32,
_now: SystemTime,
) -> Result<bool, CredentialAuthError> {
Err(CredentialAuthError::PolicyRejected(
"client-aware Digest replay protection is not implemented".to_string(),
))
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthAuditScheme {
Digest,
Bearer,
Basic,
Aka,
ApiKey,
Password,
Token,
Other(String),
}
impl fmt::Debug for AuthAuditScheme {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Digest => formatter.write_str("Digest"),
Self::Bearer => formatter.write_str("Bearer"),
Self::Basic => formatter.write_str("Basic"),
Self::Aka => formatter.write_str("Aka"),
Self::ApiKey => formatter.write_str("ApiKey"),
Self::Password => formatter.write_str("Password"),
Self::Token => formatter.write_str("Token"),
Self::Other(value) => formatter
.debug_struct("Other")
.field("value_len", &value.len())
.finish(),
}
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthFailureReason {
MissingCredential,
MalformedCredential,
InvalidCredential,
UnsupportedScheme,
PolicyRejected,
TokenExpired,
TokenRevoked,
StaleNonce,
ReplayRejected,
ProviderUnavailable,
Other(String),
}
impl fmt::Debug for AuthFailureReason {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingCredential => formatter.write_str("MissingCredential"),
Self::MalformedCredential => formatter.write_str("MalformedCredential"),
Self::InvalidCredential => formatter.write_str("InvalidCredential"),
Self::UnsupportedScheme => formatter.write_str("UnsupportedScheme"),
Self::PolicyRejected => formatter.write_str("PolicyRejected"),
Self::TokenExpired => formatter.write_str("TokenExpired"),
Self::TokenRevoked => formatter.write_str("TokenRevoked"),
Self::StaleNonce => formatter.write_str("StaleNonce"),
Self::ReplayRejected => formatter.write_str("ReplayRejected"),
Self::ProviderUnavailable => formatter.write_str("ProviderUnavailable"),
Self::Other(value) => formatter
.debug_struct("Other")
.field("value_len", &value.len())
.finish(),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthAuditOutcome {
Success,
Failure(AuthFailureReason),
}
impl fmt::Debug for AuthAuditOutcome {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Success => formatter.write_str("Success"),
Self::Failure(reason) => formatter.debug_tuple("Failure").field(reason).finish(),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthAuditEvent {
pub scheme: AuthAuditScheme,
pub outcome: AuthAuditOutcome,
pub subject: Option<String>,
pub realm: Option<String>,
pub peer: Option<String>,
pub metadata: BTreeMap<String, String>,
}
impl fmt::Debug for AuthAuditEvent {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthAuditEvent")
.field("scheme", &self.scheme)
.field("outcome", &self.outcome)
.field("subject_present", &self.subject.is_some())
.field("realm_present", &self.realm.is_some())
.field("peer_present", &self.peer.is_some())
.field("metadata_entry_count", &self.metadata.len())
.finish()
}
}
impl AuthAuditEvent {
pub fn new(scheme: AuthAuditScheme, outcome: AuthAuditOutcome) -> Self {
Self {
scheme,
outcome,
subject: None,
realm: None,
peer: None,
metadata: BTreeMap::new(),
}
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
self.realm = Some(realm.into());
self
}
pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
self.peer = Some(peer.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
}
#[async_trait]
pub trait AuthAuditSink: Send + Sync {
async fn record_auth_event(&self, event: AuthAuditEvent) -> Result<(), CredentialAuthError>;
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq)]
pub enum AuthRateLimitKind {
SipChallenge,
SipRegister,
SipRequest,
BasicPassword,
Password,
ApiKey,
BearerToken,
TokenIssuance,
Digest,
Other(String),
}
impl fmt::Debug for AuthRateLimitKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SipChallenge => formatter.write_str("SipChallenge"),
Self::SipRegister => formatter.write_str("SipRegister"),
Self::SipRequest => formatter.write_str("SipRequest"),
Self::BasicPassword => formatter.write_str("BasicPassword"),
Self::Password => formatter.write_str("Password"),
Self::ApiKey => formatter.write_str("ApiKey"),
Self::BearerToken => formatter.write_str("BearerToken"),
Self::TokenIssuance => formatter.write_str("TokenIssuance"),
Self::Digest => formatter.write_str("Digest"),
Self::Other(value) => formatter
.debug_struct("Other")
.field("value_len", &value.len())
.finish(),
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct AuthRateLimitKey {
pub kind: AuthRateLimitKind,
pub subject: Option<String>,
pub realm: Option<String>,
pub peer: Option<String>,
}
impl fmt::Debug for AuthRateLimitKey {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthRateLimitKey")
.field("kind", &self.kind)
.field("subject_present", &self.subject.is_some())
.field("realm_present", &self.realm.is_some())
.field("peer_present", &self.peer.is_some())
.finish()
}
}
impl AuthRateLimitKey {
pub fn new(kind: AuthRateLimitKind) -> Self {
Self {
kind,
subject: None,
realm: None,
peer: None,
}
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
self.realm = Some(realm.into());
self
}
pub fn with_peer(mut self, peer: impl Into<String>) -> Self {
self.peer = Some(peer.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthRateLimitVerdict {
Allowed,
Denied {
retry_after: Option<Duration>,
},
}
#[derive(Clone, PartialEq, Eq)]
pub struct AuthAttemptReservation {
opaque_id: String,
}
impl AuthAttemptReservation {
pub fn new(opaque_id: impl Into<String>) -> Result<Self, CredentialAuthError> {
let opaque_id = opaque_id.into();
if opaque_id.is_empty()
|| opaque_id.len() > 128
|| opaque_id.trim() != opaque_id
|| opaque_id.chars().any(char::is_control)
{
return Err(CredentialAuthError::PolicyRejected(
"invalid auth-attempt reservation identifier".to_string(),
));
}
Ok(Self { opaque_id })
}
pub fn opaque_id(&self) -> &str {
&self.opaque_id
}
}
impl fmt::Debug for AuthAttemptReservation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("AuthAttemptReservation")
.field("opaque_id_len", &self.opaque_id.len())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthAttemptAdmission {
Reserved(AuthAttemptReservation),
Denied {
retry_after: Option<Duration>,
},
}
#[async_trait]
pub trait AuthRateLimiter: Send + Sync {
async fn check_auth_attempt(
&self,
key: &AuthRateLimitKey,
) -> Result<AuthRateLimitVerdict, CredentialAuthError>;
async fn record_auth_result(
&self,
key: &AuthRateLimitKey,
outcome: &AuthAuditOutcome,
) -> Result<(), CredentialAuthError>;
async fn reserve_auth_attempt(
&self,
_key: &AuthRateLimitKey,
) -> Result<AuthAttemptAdmission, CredentialAuthError> {
Err(CredentialAuthError::PolicyRejected(
"atomic auth-attempt admission is not implemented".to_string(),
))
}
async fn complete_auth_attempt(
&self,
_reservation: &AuthAttemptReservation,
_outcome: &AuthAuditOutcome,
) -> Result<(), CredentialAuthError> {
Err(CredentialAuthError::PolicyRejected(
"atomic auth-attempt completion is not implemented".to_string(),
))
}
}