better-auth-api 1.0.0-alpha.4

Plugin implementations for better-auth
Documentation
use super::{AuthError, AuthResult, DEFAULT_TOTP_PERIOD_SECS, TOTP, Utc};

/// Keep the configured period separate from the library's integer counter clock.
pub(super) struct Totp {
    inner: TOTP,
    period: f64,
}

impl Totp {
    pub(super) fn new(mut inner: TOTP, period: f64) -> Self {
        inner.step = 1;
        Self { inner, period }
    }

    pub(super) fn with_default_period(mut self) -> Self {
        if self.period == 0.0 || self.period.is_nan() {
            self.period = DEFAULT_TOTP_PERIOD_SECS;
        }
        self
    }

    fn counter(&self, time_millis: i64) -> f64 {
        (time_millis as f64 / (self.period * 1000.0)).floor()
    }

    fn hotp_counter(counter: f64) -> AuthResult<u64> {
        if !counter.is_finite() {
            return Err(AuthError::config("Not an integer"));
        }
        // Upstream converts the integral Number to BigInt, then stores it modulo 2^64.
        let remainder = counter % 18_446_744_073_709_551_616.0;
        Ok(if remainder < 0.0 {
            // Adding 2^64 as a float would round small negative remainders incorrectly.
            0_u64.wrapping_sub((-remainder) as u64)
        } else {
            remainder as u64
        })
    }

    pub(super) fn generate_at(&self, time_millis: i64) -> AuthResult<String> {
        Ok(self
            .inner
            .generate(Self::hotp_counter(self.counter(time_millis))?))
    }

    pub(super) fn generate_current(&self) -> AuthResult<String> {
        self.generate_at(Utc::now().timestamp_millis())
    }

    pub(super) fn check_current(&self, token: &str) -> AuthResult<bool> {
        self.check_at(token, Utc::now().timestamp_millis())
    }

    fn check_at(&self, token: &str, time_millis: i64) -> AuthResult<bool> {
        let counter = self.counter(time_millis);
        let skew = i16::from(self.inner.skew);
        let mut verifier = self.inner.clone();
        verifier.skew = 0;
        let mut matched = false;
        for offset in -skew..=skew {
            // Upstream adds each window offset as a Number before converting to BigInt.
            let candidate = Self::hotp_counter(counter + f64::from(offset))?;
            matched |= verifier.check(token, candidate);
        }
        Ok(matched)
    }

    pub(super) fn get_url(&self) -> AuthResult<String> {
        let mut uri = url::Url::parse(&self.inner.get_url())
            .map_err(|error| AuthError::internal(format!("Build TOTP URI: {error}")))?;
        let parameters: Vec<_> = uri
            .query_pairs()
            .filter(|(name, _)| name != "period" && name != "digits")
            .map(|(name, value)| (name.into_owned(), value.into_owned()))
            .collect();
        let _ = uri
            .query_pairs_mut()
            .clear()
            .extend_pairs(parameters)
            .append_pair("digits", &self.inner.digits.to_string())
            .append_pair(
                "period",
                &better_auth_core::schema_value::number_string(self.period),
            );
        Ok(uri.into())
    }
}

#[cfg(test)]
mod tests;