use super::*;
pub(super) fn build_totp(
config: &TwoFactorConfig,
secret: &str,
request_issuer: Option<&str>,
user: &impl AuthUser,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<totp::Totp> {
let issuer = request_issuer
.map(str::to_owned)
.unwrap_or_else(|| ctx.config.app_name.clone());
let account_name = user.email().unwrap_or("user").to_string();
TOTP::new(
Algorithm::SHA1,
if config.totp_digits == 0 {
6
} else {
config.totp_digits
},
1,
1,
secret.as_bytes().to_vec(),
Some(issuer),
account_name,
)
.map_err(|error| AuthError::internal(format!("Failed to create TOTP: {}", error)))
.map(|inner| totp::Totp::new(inner, config.totp_period))
}
pub(super) async fn verify_user_password(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
user: &impl AuthUser,
password: Option<&str>,
allow_passwordless: bool,
hash_missing_password: bool,
) -> AuthResult<()> {
let stored_hash = get_credential_password_hash(ctx, user).await?;
if allow_passwordless && stored_hash.as_deref().is_none_or(str::is_empty) {
return Ok(());
}
let password = password
.filter(|password| !password.is_empty())
.ok_or_else(|| AuthError::bad_request("Invalid password"))?;
if password.encode_utf16().count() > ctx.password_policy.max_length {
return Err(AuthError::bad_request("Password too long"));
}
let Some(stored_hash) = stored_hash.filter(|hash| !hash.is_empty()) else {
if hash_missing_password {
_ = better_auth_core::hash_password(ctx.password_policy.hasher.as_ref(), password)
.await?;
}
return Err(AuthError::bad_request("Invalid password"));
};
match better_auth_core::verify_password(
ctx.password_policy.hasher.as_ref(),
password,
&stored_hash,
)
.await
{
Ok(()) => Ok(()),
Err(AuthError::InvalidCredentials) => Err(AuthError::bad_request("Invalid password")),
Err(error) => Err(error),
}
}
pub(super) fn generate_secret() -> String {
rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect()
}
pub(super) fn otp_verification_identifier(key: &str) -> String {
format!("2fa-otp-{}", key)
}
pub(super) fn two_factor_cookie_max_age(
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> f64 {
ctx.extensions
.get::<TwoFactorConfig>()
.map_or(DEFAULT_TWO_FACTOR_COOKIE_MAX_AGE_SECS, |config| {
config.two_factor_cookie_max_age
})
}
pub(super) fn trust_device_max_age(ctx: &AuthContext<impl better_auth_core::AuthSchema>) -> f64 {
ctx.extensions
.get::<TwoFactorConfig>()
.map_or(DEFAULT_TRUST_DEVICE_MAX_AGE_SECS, |config| {
config.trust_device_max_age
})
}
pub(super) fn cookie_expires_at(seconds: f64) -> AuthResult<chrono::DateTime<Utc>> {
better_auth_core::utils::date::from_milliseconds(
Utc::now().timestamp_millis() as f64 + seconds * 1000.0,
)
.ok_or_else(|| AuthError::config("Two Factor cookie expiry is out of range"))
}
pub(super) fn clear_cookie_header(
req: &AuthRequest,
config: &better_auth_core::AuthConfig,
suffix: &str,
) -> AuthResult<String> {
let name = related_cookie_name(config, suffix);
better_auth_core::utils::cookie_utils::remove_set_cookie_entries(req, None, &name)?;
create_clear_cookie(&name, config)
}
pub(super) async fn create_trust_device_cookie_header(
user: &impl AuthUser,
ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<String> {
let identifier = format!("trust-device-{}", uuid::Uuid::new_v4());
let token = sign_value(
ctx.config.signing_secret(),
&format!("{}!{}", user.id().display_string()?, identifier),
)?;
let value = format!("{}!{}", token, identifier);
let max_age = trust_device_max_age(ctx);
let expires_at = cookie_expires_at(max_age)?;
_ = ctx
.database
.create_verification(CreateVerification {
identifier: (identifier.clone()).into(),
value: user.id().into_owned(),
expires_at: (expires_at).into(),
..Default::default()
})
.await?;
create_signed_cookie_header(
ctx.config.signing_secret(),
&ctx.config,
TRUST_DEVICE_COOKIE_SUFFIX,
&value,
Some(max_age),
)
}
pub(super) fn create_signed_cookie_header(
secret: &str,
config: &better_auth_core::AuthConfig,
suffix: &str,
value: &str,
max_age_seconds: Option<f64>,
) -> AuthResult<String> {
let cookie_name = related_cookie_name(config, suffix);
let signed_value = sign_cookie_value(secret, value)?;
create_session_like_cookie(&cookie_name, &signed_value, max_age_seconds, config)
}
pub(super) fn read_signed_cookie<S: better_auth_core::AuthSchema>(
req: &AuthRequest,
suffix: &str,
ctx: &AuthContext<S>,
) -> AuthResult<Option<String>> {
let cookie_name = related_cookie_name(&ctx.config, suffix);
let Some(raw_cookie) = get_cookie(req, &cookie_name) else {
return Ok(None);
};
verify_signed_cookie_value(ctx.config.signing_secret(), &raw_cookie)
}
pub(super) fn sign_cookie_value(secret: &str, value: &str) -> AuthResult<String> {
Ok(better_auth_core::utils::cookie_utils::sign_cookie_value(
value, secret,
))
}
pub(super) fn verify_signed_cookie_value(
secret: &str,
signed_value: &str,
) -> AuthResult<Option<String>> {
Ok(better_auth_core::utils::cookie_utils::verify_cookie_value(
signed_value,
secret,
))
}
pub(super) fn sign_value(secret: &str, value: &str) -> AuthResult<String> {
let mut mac = <HmacSha256 as Mac>::new_from_slice(secret.as_bytes())
.map_err(|error| AuthError::internal(format!("Failed to initialize HMAC: {}", error)))?;
mac.update(value.as_bytes());
Ok(URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes()))
}
pub(super) fn encrypt_value<'a>(
secret: impl Into<better_auth_core::SecretKey<'a>>,
plaintext: &str,
) -> AuthResult<String> {
crate::plugins::symmetric::encrypt(secret, plaintext)
}
pub(super) fn decrypt_value<'a>(
secret: impl Into<better_auth_core::SecretKey<'a>>,
encrypted: &str,
) -> AuthResult<String> {
crate::plugins::symmetric::decrypt(secret, encrypted)
}
pub(super) fn require_totp(config: &TwoFactorConfig) -> AuthResult<()> {
if config.totp_disabled {
return Err(AuthError::Upstream {
status: 400,
code: "TOTP_NOT_CONFIGURED",
message: "totp isn't configured",
});
}
Ok(())
}