better-auth-api 1.0.0-alpha.5

Plugin implementations for better-auth
#![expect(
    clippy::unwrap_used,
    clippy::panic_in_result_fn,
    reason = "JWT consumer contracts assert complete access order, receiver identity, and original logged errors."
)]

use super::super::native_test_support::{ObservedFields, Trace};
use super::*;
use better_auth_core::{
    FieldFunction, LoggerConfig,
    observability::LogLevel,
    session::{SessionCookieContext, SessionCookieSigner},
    store::{AuthStore, EphemeralStore, StatelessSchema},
};
use std::sync::Arc;

fn signed_key() -> AuthResult<(FieldMap, String)> {
    let private = JwtAlgorithm::EdDsa.generate(2048).map_err(jose_error)?;
    let public = private.to_public_key().map_err(jose_error)?.to_string();
    let signer = jws::EdDSA.signer_from_jwk(&private).map_err(jose_error)?;
    let now = Utc::now().timestamp();
    let token = jose::sign(
        FieldMap::from_json(serde_json::from_value(json!({
            "sub":"owner", "sid":"session-token", "user":{"id":"owner"},
            "session":{"token":"session-token"}, "iss":"http://localhost:3000",
            "aud":"better-auth:session-cache", "iat":now, "exp":now + 60
        }))?)?,
        FieldMap::from([
            ("alg".into(), "EdDSA".into()),
            ("kid".into(), "selected".into()),
            ("typ".into(), "better-auth.session-cache+jwt".into()),
        ]),
        &signer,
    )?;
    Ok((
        FieldMap::from([
            ("id".into(), "selected".into()),
            ("publicKey".into(), public.into()),
            ("alg".into(), "EdDSA".into()),
        ]),
        token,
    ))
}

async fn verify(
    token: &str,
    keyring: FieldValue,
    trace: Arc<Trace>,
    cookie: bool,
) -> AuthResult<Option<Map<String, Value>>> {
    let mut config = crate::plugins::test_helpers::create_test_config();
    config.telemetry.enabled = false;
    config.logger = LoggerConfig {
        level: Some(LogLevel::Debug),
        log: Some(trace.clone()),
        ..Default::default()
    }
    .into();
    let config = Arc::new(config);
    let store: Arc<dyn AuthStore<StatelessSchema>> = Arc::new(EphemeralStore::new(config.clone()));
    let mut context = AuthContext::new(config, store)?;
    context.extensions.insert(Arc::new(
        JwtCallbacks::<StatelessSchema>::default().get_jwks_native(move |_| {
            trace.record("callback");
            let keyring = keyring.clone();
            Box::pin(async move { Ok(keyring) })
        }),
    ));
    let context = Arc::new(context);
    let plugin = JwtPlugin::with_config(JwtPluginConfig {
        audience: Some("better-auth:session-cache".into()),
        ..Default::default()
    });
    if !cookie {
        return plugin.verify(token, None, &context).await;
    }
    let init = AuthInitContext::new(context.config.clone(), context.database.clone())?;
    let runtime = init.runtime();
    runtime.bind(&context)?;
    let signer = cache::CookieSigner { plugin, runtime };
    context
        .with_native_context(Default::default(), |active| async move {
            let request = AuthRequest::new(HttpMethod::Get, "/get-session");
            signer
                .verify(
                    token,
                    SessionCookieContext {
                        request: &request,
                        config: &active.config,
                        transaction: None,
                    },
                )
                .await
        })
        .await
}

#[tokio::test]
async fn verification_reads_native_key_fields_in_each_consumers_source_order() -> AuthResult<()> {
    let (fields, token) = signed_key()?;
    for cookie in [false, true] {
        let trace = Arc::new(Trace::default());
        let key = ObservedFields::new(fields.clone(), trace.clone(), None);
        let payload = verify(&token, vec![key.value()].into(), trace.clone(), cookie)
            .await?
            .unwrap();
        assert_eq!(payload["sub"], "owner");
        assert_eq!(
            *trace.events.lock().unwrap(),
            if cookie {
                ["callback", "get:id", "get:alg", "get:publicKey"]
            } else {
                ["callback", "get:id", "get:publicKey", "get:alg"]
            }
        );
    }
    Ok(())
}

#[tokio::test]
async fn native_getter_errors_reach_each_logger_and_logger_failures_escape() -> AuthResult<()> {
    let (fields, token) = signed_key()?;
    for cookie in [false, true] {
        for property in ["id", "publicKey", "alg"] {
            for logger_fails in [false, true] {
                let original: FieldValue =
                    FieldMap::from([("error".into(), property.into())]).into();
                let logger_error: FieldValue =
                    FieldMap::from([("logger".into(), true.into())]).into();
                let trace = Arc::new(Trace {
                    logger_failure: logger_fails.then(|| logger_error.clone()),
                    ..Default::default()
                });
                let key = ObservedFields::new(
                    fields.clone(),
                    trace.clone(),
                    Some((property, original.clone())),
                );
                let result = verify(&token, vec![key.value()].into(), trace.clone(), cookie).await;
                if logger_fails {
                    assert!(
                        result
                            .unwrap_err()
                            .into_thrown_value()?
                            .strict_equals(&logger_error)
                    );
                } else {
                    assert!(result?.is_none());
                }
                let logged = trace.errors.lock().unwrap();
                assert_eq!(logged.len(), 1);
                assert!(logged[0].strict_equals(&original));
                let order = if cookie {
                    ["id", "alg", "publicKey"]
                } else {
                    ["id", "publicKey", "alg"]
                };
                let mut expected = vec!["callback".to_owned()];
                for name in order {
                    expected.push(format!("get:{name}"));
                    if name == property {
                        break;
                    }
                }
                expected.push(
                    if cookie {
                        "log:Cookie-cache JWT verification failed"
                    } else {
                        "log:JWT verification failed"
                    }
                    .into(),
                );
                assert_eq!(*trace.events.lock().unwrap(), expected);
            }
        }
    }
    Ok(())
}

#[tokio::test]
async fn native_keyring_methods_keep_the_receiver_and_consumer_specific_length_checks()
-> AuthResult<()> {
    let (fields, token) = signed_key()?;
    for cookie in [false, true] {
        for length in [
            FieldValue::Undefined,
            FieldValue::Null,
            false.into(),
            0.into(),
            "0".into(),
            1.into(),
        ] {
            let trace = Arc::new(Trace::default());
            let key = ObservedFields::new(fields.clone(), trace.clone(), None);
            let keyring = ObservedFields::new(
                FieldMap::from([("length".into(), length.clone())]),
                trace.clone(),
                None,
            );
            let owner = Arc::downgrade(&keyring);
            let selected = key.value();
            let events = trace.clone();
            let _ = keyring.fields.set(
                "find",
                FieldFunction::contextual(move |receiver, arguments| {
                    assert!(receiver.strict_equals(&owner.upgrade().unwrap().value()));
                    events.record("find");
                    let FieldValue::Function(predicate) = &arguments[0] else {
                        return Err(AuthError::internal("Native find requires its predicate"));
                    };
                    assert!(
                        predicate
                            .call_with(
                                &FieldValue::Undefined,
                                &[selected.clone(), 0.into(), receiver.clone()]
                            )?
                            .is_truthy()
                    );
                    Ok(selected.clone())
                })
                .into(),
            )?;
            let expected = if cookie {
                length.is_truthy()
            } else {
                !length.strict_equals(&0.into())
            };
            let result = verify(&token, keyring.value(), trace.clone(), cookie).await?;
            assert_eq!(result.is_some(), expected);
            let mut events = vec!["callback", "get:length"];
            if expected {
                events.extend(["get:find", "find", "get:id"]);
                events.extend(if cookie {
                    ["get:alg", "get:publicKey"]
                } else {
                    ["get:publicKey", "get:alg"]
                });
            }
            assert_eq!(*trace.events.lock().unwrap(), events);
        }
    }
    Ok(())
}