mod template;
pub use axess_factors::Fido2Config;
pub use axess_factors::{
EmailOtpConfig, HotpConfig, OtpAlgorithm, PasswordConfig, PasswordRules, TotpConfig,
ZeroizedString,
};
pub use template::{FactorTemplate, default_catalog};
#[cfg(feature = "fido2")]
pub use axess_factors::{
AuthenticationResult, AuthenticatorAttachment, CredentialID, Fido2Credential, Fido2Options,
};
use serde::{Deserialize, Serialize};
use std::{fmt, sync::Arc};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum FactorKind {
Password,
Totp,
Hotp,
EmailOtp,
Fido2,
LdapBind,
Federated(FederatedProvider),
}
impl FactorKind {
pub fn as_str(&self) -> &str {
match self {
FactorKind::Password => "password",
FactorKind::Totp => "totp",
FactorKind::Hotp => "hotp",
FactorKind::EmailOtp => "email_otp",
FactorKind::Fido2 => "fido2",
FactorKind::LdapBind => "ldap_bind",
FactorKind::Federated(p) => p.as_str(),
}
}
}
impl fmt::Display for FactorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FactorStep {
Required(FactorKind),
AnyOf(Vec<FactorKind>),
}
impl FactorStep {
pub fn as_required(&self) -> Option<&FactorKind> {
match self {
FactorStep::Required(k) => Some(k),
FactorStep::AnyOf(_) => None,
}
}
pub fn accepts(&self, kind: &FactorKind) -> bool {
match self {
FactorStep::Required(k) => k == kind,
FactorStep::AnyOf(choices) => choices.contains(kind),
}
}
}
impl From<FactorKind> for FactorStep {
fn from(kind: FactorKind) -> Self {
FactorStep::Required(kind)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(
feature = "rkyv",
derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
)]
pub enum FederatedProvider {
Github,
Google,
Microsoft,
Custom(String),
}
impl FederatedProvider {
pub fn as_str(&self) -> &str {
match self {
FederatedProvider::Github => "github",
FederatedProvider::Google => "google",
FederatedProvider::Microsoft => "microsoft",
FederatedProvider::Custom(s) => s.as_ref(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FactorConfig {
Password(PasswordConfig),
Totp(TotpConfig),
Hotp(HotpConfig),
EmailOtp(EmailOtpConfig),
Fido2(Fido2Config),
LdapBind(LdapBindFactorConfig),
}
impl FactorConfig {
pub fn kind(&self) -> FactorKind {
match self {
FactorConfig::Password(_) => FactorKind::Password,
FactorConfig::Totp(_) => FactorKind::Totp,
FactorConfig::Hotp(_) => FactorKind::Hotp,
FactorConfig::EmailOtp(_) => FactorKind::EmailOtp,
FactorConfig::Fido2(_) => FactorKind::Fido2,
FactorConfig::LdapBind(_) => FactorKind::LdapBind,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LdapBindFactorConfig {
pub bind_dn: Option<String>,
}
#[derive(Debug)]
pub enum FactorCredential {
Password(ZeroizedString),
OtpCode(Arc<str>),
Fido2Assertion(serde_json::Value),
}
impl FactorCredential {
#[cfg(feature = "fido2")]
pub fn fido2_assertion(
cred: &webauthn_rs::prelude::PublicKeyCredential,
) -> Result<Self, serde_json::Error> {
serde_json::to_value(cred).map(Self::Fido2Assertion)
}
#[cfg(feature = "fido2")]
pub fn as_public_key_credential(&self) -> Option<webauthn_rs::prelude::PublicKeyCredential> {
match self {
Self::Fido2Assertion(v) => serde_json::from_value(v.clone()).ok(),
_ => None,
}
}
}
#[cfg(test)]
mod factor_tests {
use super::*;
#[test]
fn factor_kind_display_matches_as_str() {
assert_eq!(FactorKind::Password.to_string(), "password");
assert_eq!(FactorKind::Totp.to_string(), "totp");
assert_eq!(FactorKind::EmailOtp.to_string(), "email_otp");
assert_eq!(
FactorKind::Federated(FederatedProvider::Github).to_string(),
"github"
);
}
#[test]
fn factor_step_as_required_returns_some_for_required_variant() {
let step = FactorStep::Required(FactorKind::Totp);
assert_eq!(step.as_required(), Some(&FactorKind::Totp));
}
#[test]
fn factor_step_as_required_returns_none_for_anyof_variant() {
let step = FactorStep::AnyOf(vec![FactorKind::Password, FactorKind::Totp]);
assert!(step.as_required().is_none());
}
#[test]
fn factor_step_accepts_required_only_for_matching_kind() {
let step = FactorStep::Required(FactorKind::Totp);
assert!(step.accepts(&FactorKind::Totp));
assert!(!step.accepts(&FactorKind::Password));
}
#[test]
fn factor_step_accepts_anyof_member_only() {
let step = FactorStep::AnyOf(vec![FactorKind::Password, FactorKind::Totp]);
assert!(step.accepts(&FactorKind::Password));
assert!(step.accepts(&FactorKind::Totp));
assert!(!step.accepts(&FactorKind::EmailOtp));
}
#[test]
fn federated_provider_as_str_per_variant() {
assert_eq!(FederatedProvider::Github.as_str(), "github");
assert_eq!(FederatedProvider::Google.as_str(), "google");
assert_eq!(FederatedProvider::Microsoft.as_str(), "microsoft");
assert_eq!(
FederatedProvider::Custom("okta-prod".to_string()).as_str(),
"okta-prod"
);
assert_ne!(
FederatedProvider::Custom("a".to_string()).as_str(),
FederatedProvider::Custom("b".to_string()).as_str()
);
}
#[cfg(feature = "fido2")]
#[test]
fn as_public_key_credential_yields_some_for_fido2_assertion() {
let pkc_json = serde_json::json!({
"id": "AAAA",
"rawId": "AAAA",
"type": "public-key",
"response": {
"authenticatorData": "AAAA",
"clientDataJSON": "AAAA",
"signature": "AAAA",
"userHandle": null,
},
"extensions": {},
});
let cred = FactorCredential::Fido2Assertion(pkc_json);
assert!(
cred.as_public_key_credential().is_some(),
"Fido2Assertion with a valid PublicKeyCredential JSON must yield Some"
);
let non_fido2 = FactorCredential::OtpCode(Arc::from("123456"));
assert!(
non_fido2.as_public_key_credential().is_none(),
"non-Fido2 variants must yield None"
);
}
}