alibi_api/plugins/jwt/
verification.rs1use super::*;
2impl JwtPlugin {
3 pub async fn verify_jwt(
10 &self,
11 token: &str,
12 issuer: Option<&str>,
13 request: Option<&AuthRequest>,
14 ctx: &AuthContext<impl AuthSchema>,
15 ) -> AuthResult<Option<Map<String, Value>>> {
16 let claims = self.config.claims.clone();
17 let issuer = issuer
18 .filter(|issuer| !issuer.is_empty())
19 .or(claims.issuer.as_deref())
20 .unwrap_or(&ctx.config.base_url);
21 let audience = claims
22 .audience
23 .unwrap_or_else(|| JwtAudience::One(ctx.config.base_url.clone()));
24 let policy = JwtVerifyPolicy {
25 issuer,
26 audience: &audience,
27 tolerance: 0,
28 require_nonempty_subject: true,
29 };
30 Ok(self
31 .verify_internal(token, &policy, request, ctx)
32 .await
33 .unwrap_or(None))
34 }
35
36 #[expect(
37 clippy::as_conversions,
38 clippy::cast_precision_loss,
39 reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
40 )]
41 pub(in crate::plugins::jwt) async fn verify_internal(
42 &self,
43 token: &str,
44 policy: &JwtVerifyPolicy<'_>,
45 request: Option<&AuthRequest>,
46 ctx: &AuthContext<impl AuthSchema>,
47 ) -> AuthResult<Option<Map<String, Value>>> {
48 let parts = token.split('.').collect::<Vec<_>>();
49 let [header, payload, signature] = parts.as_slice() else {
50 return Ok(None);
51 };
52 let header = decode_compact_json(header, false)?;
53 let Some(header_object) = header.as_object() else {
54 return Ok(None);
55 };
56 if validate_critical_header(header_object, false).is_err() {
57 return Ok(None);
58 }
59 let Some(kid) = header
60 .get("kid")
61 .and_then(Value::as_str)
62 .filter(|value| !value.is_empty())
63 else {
64 return Ok(None);
65 };
66 let Some(key) = self
67 .keys_at_path(request, "virtual:", ctx)
68 .await?
69 .into_iter()
70 .find(|key| key.id == kid)
71 else {
72 return Ok(None);
73 };
74 let algorithm = key
75 .alg
76 .as_deref()
77 .map(JwtAlgorithm::from_str)
78 .unwrap_or(Ok(self.config.key_pair.algorithm))?;
79 if header.get("alg").and_then(Value::as_str) != Some(algorithm.as_str()) {
80 return Ok(None);
81 }
82 let public: Value = serde_json::from_str(&key.public_key)?;
83 let input = format!("{}.{}", parts.first().copied().unwrap_or_default(), payload);
84 if !crypto::verify(
85 algorithm,
86 &public,
87 input.as_bytes(),
88 &decode_compact_part(signature, true)?,
89 )? {
90 return Ok(None);
91 }
92 let Some(payload) = decode_compact_json(payload, true)?.as_object().cloned() else {
93 return Ok(None);
94 };
95 let now = Utc::now().timestamp();
96 for field in ["iat", "exp", "nbf"] {
97 if payload.get(field).is_some_and(|value| !value.is_number()) {
98 return Ok(None);
99 }
100 }
101 if payload
102 .get("exp")
103 .and_then(Value::as_f64)
104 .is_some_and(|exp| exp <= (now - policy.tolerance) as f64)
105 || payload
106 .get("nbf")
107 .and_then(Value::as_f64)
108 .is_some_and(|nbf| nbf > (now + policy.tolerance) as f64)
109 || payload.get("iss").and_then(Value::as_str) != Some(policy.issuer)
110 || !payload
111 .get("aud")
112 .is_some_and(|value| policy.audience.matches(value))
113 || payload.get("aud").is_none_or(|value| !js_truthy(value))
114 || (policy.require_nonempty_subject
115 && payload.get("sub").is_none_or(|value| !js_truthy(value)))
116 {
117 return Ok(None);
118 }
119 Ok(Some(payload))
120 }
121}