use super::*;
use base64::{
Engine as _, alphabet,
engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig},
};
use josekit::jws::JwsVerifier;
impl JwtPlugin {
pub async fn verify<S: AuthSchema>(
&self,
token: &str,
issuer: Option<&str>,
ctx: &AuthContext<S>,
) -> AuthResult<Option<Map<String, Value>>> {
ctx.with_native_context(Default::default(), |resolved| async move {
let body = verification_body(token, issuer);
let mut endpoint = EndpointContext::new(
None,
better_auth_core::FieldValue::from_json(body)?,
&resolved,
);
endpoint.path = Some("virtual:");
self.verify_in_endpoint(token, issuer, &endpoint).await
})
.await
}
pub async fn verify_in_endpoint<S: AuthSchema>(
&self,
token: &str,
issuer: Option<&str>,
endpoint: &EndpointContext<'_, S>,
) -> AuthResult<Option<Map<String, Value>>> {
let Some(header) = raw_header(token).filter(Header::has_key_id) else {
return Ok(None);
};
let verified = async {
let keys = self.read_keys(endpoint).await?;
if !keys.is_truthy() || keys.model_property("length")?.strict_equals(&0.into()) {
return Ok(None);
}
let key = keys::find(&keys, header.field("kid")?)?;
if !key.is_truthy() {
return Ok(None);
}
let public = keys::parse_json(&key.model_property("publicKey")?)?;
let algorithm = keys::algorithm(&key, self.config.primary_algorithm().name().into())?;
let ctx = endpoint.auth;
let issuer = issuer
.filter(|issuer| !issuer.is_empty())
.or(self.config.issuer.as_deref())
.or(ctx.config.base_url.as_static());
let audience = self
.config
.audience
.as_ref()
.map(JwtAudience::recipients)
.or_else(|| ctx.config.base_url.as_static().map(|base| vec![base]));
verify_local(token, public, &algorithm, issuer, audience.as_deref(), 0)
}
.await;
let verified = match verified {
Ok(verified) => verified,
Err(error) => {
better_auth_core::observability::logger::current().debug(
"JWT verification failed",
&[better_auth_core::observability::LogArgument::Error(&error)],
)?;
return Ok(None);
}
};
Ok(verified.filter(|payload| {
["sub", "aud"].iter().all(|claim| {
payload
.get(*claim)
.is_some_and(crate::plugins::json_body::is_truthy)
})
}))
}
}
pub(super) fn verification_body(token: &str, issuer: Option<&str>) -> Value {
let mut body = Map::from_iter([("token".into(), token.into())]);
if let Some(issuer) = issuer {
let _ = body.insert("issuer".into(), issuer.into());
}
body.into()
}
#[serde_with::serde_as]
#[derive(serde::Deserialize)]
#[serde(transparent)]
pub(super) struct Header(
#[serde_as(as = "std::collections::HashMap<serde_with::Bytes, _>")]
std::collections::HashMap<Vec<u8>, Box<serde_json::value::RawValue>>,
);
impl Header {
pub(super) fn has_key_id(&self) -> bool {
self.0
.get(b"kid".as_slice())
.is_some_and(|kid| match kid.get().as_bytes().first() {
Some(b'"') => kid.get() != "\"\"",
Some(b'[' | b'{') => true,
_ => FieldValue::parse_json(kid.get()).is_ok_and(|value| value.is_truthy()),
})
}
pub(super) fn has_type(&self, expected: &str) -> bool {
self.0.get(b"typ".as_slice()).is_some_and(|typ| {
serde_json::from_str::<String>(typ.get()).is_ok_and(|typ| typ == expected)
})
}
pub(super) fn field(&self, name: &str) -> AuthResult<FieldValue> {
self.0
.get(name.as_bytes())
.map_or(Ok(FieldValue::Undefined), |value| {
FieldValue::parse_json(value.get())
})
}
}
pub(super) fn raw_header(token: &str) -> Option<Header> {
if token.split('.').count() != 3 {
return None;
}
let encoded = token.split('.').next()?;
let alphabet = if encoded.contains(['-', '_']) {
&alphabet::URL_SAFE
} else {
&alphabet::STANDARD
};
let engine = GeneralPurpose::new(
alphabet,
GeneralPurposeConfig::new()
.with_decode_allow_trailing_bits(true)
.with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
let encoded = encoded.split('=').next()?;
let encoded = if encoded.len() % 4 == 1 {
let _ = engine
.decode([*encoded.as_bytes().last()?, b'A', b'A', b'A'])
.ok()?;
encoded.get(..encoded.len() - 1)?
} else {
encoded
};
let bytes = engine.decode(encoded).ok()?;
let text = String::from_utf8_lossy(&bytes);
serde_json::from_str(text.strip_prefix('\u{feff}').unwrap_or(&text)).ok()
}
pub(super) fn verify_local(
token: &str,
public: FieldValue,
algorithm: &FieldValue,
issuer: Option<&str>,
audience: Option<&[&str]>,
tolerance: i64,
) -> AuthResult<Option<Map<String, Value>>> {
let (algorithm, public) = keys::import(public, algorithm)?;
Ok(verify_imported(
token, &public, &algorithm, issuer, audience, tolerance,
))
}
fn verify_imported(
token: &str,
public: &Jwk,
algorithm: &str,
issuer: Option<&str>,
audience: Option<&[&str]>,
tolerance: i64,
) -> Option<Map<String, Value>> {
if public.parameter("d").is_some() {
return None;
}
let verifier: Box<dyn JwsVerifier> = match algorithm {
"EdDSA" => Box::new(jws::EdDSA.verifier_from_jwk(public).ok()?),
"RS256" => Box::new(jws::RS256.verifier_from_jwk(public).ok()?),
"ES256" => Box::new(jws::ES256.verifier_from_jwk(public).ok()?),
"ES512" => Box::new(jws::ES512.verifier_from_jwk(public).ok()?),
"PS256" => Box::new(jws::PS256.verifier_from_jwk(public).ok()?),
_ => return None,
};
let protected = jose::header(token)?;
let payload = jose::verify(
token,
&protected,
verifier.algorithm().name(),
|input, signature| verifier.verify(input, signature).is_ok(),
)
.ok()?;
let claims: Map<String, Value> = serde_json::from_slice(&payload).ok()?;
if issuer.is_some_and(|issuer| claims.get("iss").and_then(Value::as_str) != Some(issuer)) {
return None;
}
if let Some(audience) = audience {
let aud = claims.get("aud")?;
if !aud.as_str().is_some_and(|value| audience.contains(&value))
&& !aud.as_array().is_some_and(|values| {
values.iter().any(|value| {
value
.as_str()
.is_some_and(|value| audience.contains(&value))
})
})
{
return None;
}
}
let now = Utc::now().timestamp() as f64;
for field in ["iat", "exp", "nbf"] {
if let Some(value) = claims.get(field) {
let value = value.as_f64()?;
if (field == "exp" && value <= now - tolerance as f64)
|| (field == "nbf" && value > now + tolerance as f64)
{
return None;
}
}
}
Some(claims)
}
#[cfg(test)]
#[path = "verification_native_tests.rs"]
mod native_tests;