better-auth-api 1.0.0-alpha.4

Plugin implementations for better-auth
Documentation
use super::*;
use better_auth_core::session::{SessionCookieContext, SessionCookieSigner};

const TYPE: &str = "better-auth.session-cache+jwt";
const AUDIENCE: &str = "better-auth:session-cache";

pub(super) struct CookieSigner<S: AuthSchema> {
    pub plugin: JwtPlugin,
    pub runtime: better_auth_core::plugin_runtime::PluginRuntime<S>,
}

#[async_trait::async_trait]
impl<S: AuthSchema> SessionCookieSigner<S> for CookieSigner<S> {
    async fn sign(
        &self,
        mut payload: Map<String, Value>,
        expires_in: f64,
        context: SessionCookieContext<'_, S>,
    ) -> AuthResult<String> {
        let runtime = self.runtime.context()?;
        let mut endpoint = EndpointContext::new(
            Some(context.request),
            request_body(context.request)?,
            &runtime,
        );
        endpoint.transaction = context.transaction;
        endpoint.session = context
            .request
            .session_snapshot()?
            .map(|data| (data.user, data.session));
        let options = JwtSigningOptions {
            header: serde_json::from_value(json!({"typ": TYPE}))?,
            ..Default::default()
        };
        let key = self
            .plugin
            .resolve_local_signing_key(&options, &endpoint)
            .await?;
        let sid = payload
            .get("session")
            .and_then(|session| session.get("token"))
            .cloned()
            .ok_or_else(|| AuthError::internal("Session cache requires a session token"))?;
        payload.extend([
            ("sid".into(), sid),
            ("iat".into(), Utc::now().timestamp().into()),
            (
                "exp".into(),
                better_auth_core::wire::serialize_optional_number(
                    &Some(Utc::now().timestamp() as f64 + expires_in),
                    serde_json::value::Serializer,
                )?,
            ),
            ("aud".into(), AUDIENCE.into()),
            ("iss".into(), issuer(&runtime).into()),
        ]);
        let _ = payload.remove("sub");
        claims::prepare_local_claims(&mut payload)?;
        let subject = payload
            .get("user")
            .and_then(|user| user.get("id"))
            .and_then(Value::as_str)
            .ok_or_else(|| AuthError::internal("\"sub\" claim must be a string"))?
            .to_owned();
        let _ = payload.insert("sub".into(), subject.into());
        key.sign(payload, &options)
    }

    async fn verify(
        &self,
        token: &str,
        context: SessionCookieContext<'_, S>,
    ) -> AuthResult<Option<Map<String, Value>>> {
        let Some(header) = verification::raw_header(token) else {
            return Ok(None);
        };
        if !header.has_type(TYPE) || !header.has_key_id() {
            return Ok(None);
        }
        let runtime = self.runtime.context()?;
        let mut endpoint = EndpointContext::new(
            Some(context.request),
            request_body(context.request)?,
            &runtime,
        );
        endpoint.transaction = context.transaction;
        endpoint.session = context
            .request
            .session_snapshot()?
            .map(|data| (data.user, data.session));
        let keys = match self.plugin.read_keys(&endpoint).await {
            Ok(Some(keys)) => keys,
            Ok(None) => return Ok(None),
            Err(error) => {
                better_auth_core::observability::logger::current().debug(
                    "Cookie-cache JWT verification failed",
                    &[better_auth_core::observability::LogArgument::Error(&error)],
                );
                return Ok(None);
            }
        };
        let payload = verification::verify_local(
            token,
            &keys,
            self.plugin.config.algorithm,
            Some(issuer(&runtime)),
            Some(&[AUDIENCE]),
            15,
        );
        Ok(payload.filter(|payload| {
            payload.get("sub").is_some_and(|sub| {
                sub.is_string() && Some(sub) == payload.get("user").and_then(|user| user.get("id"))
            }) && payload.get("sid").is_some_and(|sid| {
                sid.is_string()
                    && Some(sid)
                        == payload
                            .get("session")
                            .and_then(|session| session.get("token"))
            })
        }))
    }
}

fn issuer<S: AuthSchema>(context: &AuthContext<S>) -> &str {
    let base_url = context
        .config
        .base_url
        .as_static()
        .unwrap_or_else(|| context.base_url());
    if base_url.is_empty() {
        AUDIENCE
    } else {
        base_url
    }
}