better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![expect(
    clippy::expect_used,
    clippy::indexing_slicing,
    reason = "The paired JWT contract fails immediately on incomplete captured claims or expiration observations"
)]

use super::*;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use better_auth_core::AuthError;
use jsonwebtoken::Algorithm;
use serde_json::{Value, json};

const SECRET: &str = "email-verification-duration-contract-secret-at-least-32-characters";
const EMAIL: &str = "owner@email-verification-duration.test";

fn fixture() -> AuthResult<Value> {
    Ok(serde_json::from_str(include_str!(
        "../../../../../tests/fixtures/email-verification-duration-1.7.6.json"
    ))?)
}

fn date(value: &Value) -> DateTime<Utc> {
    DateTime::from_timestamp_millis(value.as_i64().expect("Captured millisecond clock"))
        .expect("Captured clock fits Chrono")
}

#[test]
fn email_verification_numeric_dates_match_all_pinned_jwts_and_expiry_boundaries() -> AuthResult<()>
{
    let fixture = fixture()?;
    assert_eq!(fixture["version"], "1.7.6");
    let cases = fixture["cases"]
        .as_array()
        .expect("Captured duration cases");
    assert_eq!(cases.len(), 14);
    let scenarios = fixture["scenarios"]
        .as_array()
        .expect("Captured duration scenarios");
    assert_eq!(scenarios.len(), 7);
    for (backend, cases) in ["memory", "sqlite"].into_iter().zip(cases.chunks_exact(7)) {
        for (case, scenario) in cases.iter().zip(scenarios) {
            assert_eq!(case["backend"], backend);
            assert_eq!(&case["scenario"], scenario);
            let lifetime = Duration::milliseconds(
                (scenario["expiresIn"].as_f64().expect("Captured lifetime") * 1000.0) as i64,
            );
            let token = create_email_verification_token_at(
                SECRET,
                EMAIL,
                None,
                lifetime,
                None,
                date(&case["issuedAt"]),
            )?;
            assert_eq!(
                token,
                case["jwt"]["token"].as_str().expect("Captured compact JWT"),
                "{backend} {} JWT bytes",
                scenario["name"]
            );
            let parts = token.split('.').collect::<Vec<_>>();
            assert_eq!(parts.len(), 3);
            for (part, expected) in [
                (parts[0], &case["jwt"]["protectedHeader"]),
                (parts[1], &case["jwt"]["claims"]),
            ] {
                let bytes = URL_SAFE_NO_PAD
                    .decode(part)
                    .expect("JWT uses base64url encoding");
                assert_eq!(&serde_json::from_slice::<Value>(&bytes)?, expected);
            }
            let read_at = date(&case["readAt"]);
            let result = decode_email_verification_token_at(SECRET, &token, read_at);
            let accepted = scenario["accepted"]
                .as_bool()
                .expect("Captured acceptance result");
            assert_eq!(
                result.is_ok(),
                accepted,
                "{backend} {}: {result:?}",
                scenario["name"]
            );
            if accepted {
                assert_eq!(
                    serde_json::to_value(result.expect("Accepted verification claims"))?,
                    case["jwt"]["claims"]
                );
            } else {
                assert!(
                    matches!(result, Err(AuthError::Jwt(ref error)) if error.kind() == &ErrorKind::ExpiredSignature),
                    "{backend} {} must expire at the captured integer clock: {result:?}",
                    scenario["name"]
                );
            }
            let signed = decode_email_verification_token_at(
                SECRET,
                &token,
                date(&case["issuedAt"]) - Duration::seconds(1),
            )?;
            assert_eq!(serde_json::to_value(signed)?, case["jwt"]["claims"]);
            assert_eq!(case["jwt"]["signatureVerified"], true);
        }
    }
    eprintln!(
        "Email verification duration boundary: JWT bytes, claims, signature validation, and fixed-time expiration are paired; captured HTTP, callbacks, cookies, and storage snapshots remain unpaired at the fixed clock"
    );
    Ok(())
}

#[test]
fn fractional_expiration_keeps_signature_algorithm_and_present_claim_guards() -> AuthResult<()> {
    let fixture = fixture()?;
    let case = &fixture["cases"][0];
    let token = case["jwt"]["token"]
        .as_str()
        .expect("Captured fractional token");
    let now = date(&case["issuedAt"]);
    assert!(matches!(
        decode_email_verification_token_at("wrong-signing-secret", token, now),
        Err(AuthError::Jwt(error)) if error.kind() == &ErrorKind::InvalidSignature
    ));
    let invalid_algorithm = encode(
        &Header::new(Algorithm::HS384),
        &case["jwt"]["claims"],
        &EncodingKey::from_secret(SECRET.as_bytes()),
    )?;
    assert!(matches!(
        decode_email_verification_token_at(SECRET, &invalid_algorithm, now),
        Err(AuthError::Jwt(error)) if error.kind() == &ErrorKind::InvalidAlgorithm
    ));
    for field in ["email", "iat", "exp"] {
        let mut claims = case["jwt"]["claims"]
            .as_object()
            .expect("Captured claims")
            .clone();
        let _ = claims.remove(field);
        let missing = encode(
            &Header::new(Algorithm::HS256),
            &claims,
            &EncodingKey::from_secret(SECRET.as_bytes()),
        )?;
        let decoded = decode_email_verification_token_at(SECRET, &missing, now);
        if field == "email" {
            assert!(matches!(decoded, Err(AuthError::Serialization(_))));
        } else {
            assert_eq!(serde_json::to_value(decoded?)?, Value::Object(claims));
        }
    }
    for expiration in [Value::Null, json!("2000000001.5")] {
        let mut claims = case["jwt"]["claims"].clone();
        claims["exp"] = expiration;
        let malformed = encode(
            &Header::new(Algorithm::HS256),
            &claims,
            &EncodingKey::from_secret(SECRET.as_bytes()),
        )?;
        assert!(decode_email_verification_token_at(SECRET, &malformed, now).is_err());
    }
    Ok(())
}