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
}
}