better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
use super::*;

pub(super) async fn issue_factor_session(
    req: &AuthRequest,
    owner: SchemaValue<String>,
    user: FieldValue,
    inherited_fields: FieldMap,
    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<better_auth_core::wire::SessionView> {
    admit_session_for_id(ctx, &owner, None)
        .await
        .map_err(SessionIssueError::into_auth_error)?;
    let meta = RequestMeta::from_request_with_config(req, &ctx.config.advanced.ip_address);
    let session = ctx
        .database
        .create_session_optional(better_auth_core::CreateSession {
            inherited_fields,
            additional_fields: Default::default(),
            user_id: owner,
            expires_at: (Utc::now() + ctx.config.session.expires_in()).into(),
            ip_address: meta.ip_address,
            user_agent: meta.user_agent,
            impersonated_by: None,
            active_organization_id: None,
        })
        .await?
        .ok_or_else(|| AuthError::internal("Cannot read properties of null (reading 'token')"))?;
    ctx.session_manager()
        .set_native_session_cookie(
            req,
            better_auth_core::session::NativeSessionData::new(session.clone(), user),
            None,
        )
        .await?;
    Ok(session)
}

pub(super) async fn update_two_factor_user(
    user_id: &FieldValue,
    enabled: bool,
    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<Option<UserView>> {
    ctx.database
        .update_user_by_id_value(
            user_id,
            UpdateUser {
                two_factor_enabled: Some(enabled),
                ..Default::default()
            },
        )
        .await
}

pub(super) async fn delete_factor_session(
    session: &NativeSessionData,
    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
) -> AuthResult<()> {
    ctx.database
        .delete_session_by_token_value(&session.session_property("token")?)
        .await
}

pub(super) fn build_totp(
    config: &TwoFactorConfig,
    secret: &str,
    request_issuer: Option<&str>,
    email: &FieldValue,
    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 = email
        .display_utf16()?
        .to_utf8()
        .map_err(|_| AuthError::internal("URI malformed"))?;
    let inner = TOTP::new_unchecked(
        Algorithm::SHA1,
        if config.totp_digits == 0 {
            6
        } else {
            config.totp_digits
        },
        1,
        1,
        secret.as_bytes().to_vec(),
        Some(issuer),
        account_name,
    );
    Ok(totp::Totp::new(inner, config.totp_period))
}

pub(super) fn totp_verifier(config: &TwoFactorConfig, secret: &str) -> totp::Totp {
    totp::Totp::new(
        TOTP::new_unchecked(
            Algorithm::SHA1,
            if config.totp_digits == 0 {
                6
            } else {
                config.totp_digits
            },
            1,
            1,
            secret.as_bytes().to_vec(),
            None,
            String::new(),
        ),
        config.totp_period,
    )
}

pub(super) async fn verify_user_password(
    ctx: &AuthContext<impl better_auth_core::AuthSchema>,
    user_id: &FieldValue,
    password: Option<&str>,
    allow_passwordless: bool,
    hash_missing_password: bool,
) -> AuthResult<()> {
    use crate::plugins::helpers::get_credential_password_hash;

    let user_id = SchemaValue::<String>::from_field(user_id.clone());
    if allow_passwordless
        && get_credential_password_hash(ctx, user_id.clone())
            .await?
            .is_none()
    {
        return Ok(());
    }
    let password = password
        .filter(|password| !password.is_empty())
        .ok_or_else(|| {
            AuthError::from_code("BAD_REQUEST", "INVALID_PASSWORD", "Invalid password")
        })?;
    if password.encode_utf16().count() > ctx.password_policy.max_length {
        return Err(AuthError::from_code(
            "BAD_REQUEST",
            "PASSWORD_TOO_LONG",
            "Password too long",
        ));
    }
    let Some(stored_hash) = get_credential_password_hash(ctx, user_id).await? else {
        if hash_missing_password {
            _ = better_auth_core::hash_password(ctx.password_policy.hasher.as_ref(), password)
                .await?;
        }
        return Err(AuthError::from_code(
            "BAD_REQUEST",
            "INVALID_PASSWORD",
            "Invalid password",
        ));
    };
    match better_auth_core::verify_password_native(
        ctx.password_policy.hasher.as_ref(),
        password,
        &stored_hash,
    )
    .await
    {
        Ok(()) => Ok(()),
        Err(AuthError::InvalidCredentials) => Err(AuthError::from_code(
            "BAD_REQUEST",
            "INVALID_PASSWORD",
            "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 split_otp_record(value: &FieldValue) -> AuthResult<(FieldValue, f64)> {
    if value.is_null() || value.is_undefined() {
        return Ok((FieldValue::Undefined, 0.0));
    }
    let units = better_auth_core::query::field_string_units(value)
        .ok_or_else(|| AuthError::internal("consumed.value?.split is not a function"))?;
    let mut parts = units.split(|unit| *unit == u16::from(b':'));
    let otp =
        better_auth_core::Utf16String::from_units(parts.next().unwrap_or_default().to_vec()).into();
    let counter = String::from_utf16_lossy(parts.next().unwrap_or_default());
    let counter = counter
        .trim_start_matches(|ch: char| (ch.is_whitespace() && ch != '\u{85}') || ch == '\u{feff}');
    let (sign, counter) = if let Some(counter) = counter.strip_prefix('-') {
        (-1.0, counter)
    } else {
        (1.0, counter.strip_prefix('+').unwrap_or(counter))
    };
    let count = counter
        .bytes()
        .take_while(u8::is_ascii_digit)
        .fold(0.0, |count, byte| count * 10.0 + f64::from(byte - b'0'));
    Ok((otp, sign * count))
}

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_optional(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)
}

#[cfg(test)]
pub(super) fn decrypt_value<'a>(
    secret: impl Into<better_auth_core::SecretKey<'a>>,
    encrypted: &SchemaValue<String>,
) -> AuthResult<String> {
    crate::plugins::symmetric::decrypt_field(secret, &encrypted.field_value())
}

pub(super) fn require_totp(config: &TwoFactorConfig) -> AuthResult<()> {
    if config.totp_disabled {
        return Err(AuthError::from_code(
            "BAD_REQUEST",
            "TOTP_NOT_CONFIGURED",
            "totp isn't configured",
        ));
    }
    Ok(())
}