alibi_plugins/jwt/
verification.rs1use super::JwtAlgorithm;
2use super::JwtAudience;
3use super::JwtPlugin;
4use super::JwtVerifyPolicy;
5use super::claims::decode_compact_json;
6use super::claims::decode_compact_part;
7use super::claims::js_truthy;
8use super::claims::validate_critical_header;
9use super::crypto;
10use alibi_core::AuthContext;
11use alibi_core::AuthRequest;
12use alibi_core::AuthResult;
13use alibi_core::AuthSchema;
14use chrono::Utc;
15use serde_json::Map;
16use serde_json::Value;
17use std::str::FromStr;
18impl JwtPlugin {
19 pub async fn verify_jwt(
22 &self,
23 token: &str,
24 issuer: Option<&str>,
25 request: Option<&AuthRequest>,
26 ctx: &AuthContext<impl AuthSchema>,
27 ) -> AuthResult<Option<Map<String, Value>>> {
28 let claims = self.config.claims.clone();
29 let issuer = issuer
30 .filter(|issuer| !issuer.is_empty())
31 .or(claims.issuer.as_deref())
32 .unwrap_or(&ctx.config.base_url);
33 let audience = claims
34 .audience
35 .unwrap_or_else(|| JwtAudience::One(ctx.config.base_url.clone()));
36 let policy = JwtVerifyPolicy {
37 issuer,
38 audience: &audience,
39 tolerance: 0,
40 require_nonempty_subject: true,
41 };
42 Ok(self
43 .verify_internal(token, &policy, request, ctx)
44 .await
45 .unwrap_or(None))
46 }
47
48 #[expect(
49 clippy::as_conversions,
50 clippy::cast_precision_loss,
51 reason = "JavaScript-compatible numbers deliberately retain IEEE754 rounding and guarded integer coercion at the wire or adapter boundary"
52 )]
53 pub(in crate::jwt) async fn verify_internal(
54 &self,
55 token: &str,
56 policy: &JwtVerifyPolicy<'_>,
57 request: Option<&AuthRequest>,
58 ctx: &AuthContext<impl AuthSchema>,
59 ) -> AuthResult<Option<Map<String, Value>>> {
60 let Some(payload) = self.verify_signed_payload(token, request, ctx).await? else {
61 return Ok(None);
62 };
63 let now = Utc::now().timestamp();
64 for field in ["iat", "exp", "nbf"] {
65 if payload.get(field).is_some_and(|value| !value.is_number()) {
66 return Ok(None);
67 }
68 }
69 if payload
70 .get("exp")
71 .and_then(Value::as_f64)
72 .is_some_and(|exp| exp <= (now - policy.tolerance) as f64)
73 || payload
74 .get("nbf")
75 .and_then(Value::as_f64)
76 .is_some_and(|nbf| nbf > (now + policy.tolerance) as f64)
77 || payload.get("iss").and_then(Value::as_str) != Some(policy.issuer)
78 || !payload
79 .get("aud")
80 .is_some_and(|value| policy.audience.matches(value))
81 || payload.get("aud").is_none_or(|value| !js_truthy(value))
82 || (policy.require_nonempty_subject
83 && payload.get("sub").is_none_or(|value| !js_truthy(value)))
84 {
85 return Ok(None);
86 }
87 Ok(Some(payload))
88 }
89 pub(crate) async fn verify_signed_payload(
92 &self,
93 token: &str,
94 request: Option<&AuthRequest>,
95 ctx: &AuthContext<impl AuthSchema>,
96 ) -> AuthResult<Option<Map<String, Value>>> {
97 self.verify_signed_payload_inner(token, request, ctx, true)
98 .await
99 }
100 pub(crate) async fn verify_signed_payload_with_optional_kid(
102 &self,
103 token: &str,
104 request: Option<&AuthRequest>,
105 ctx: &AuthContext<impl AuthSchema>,
106 ) -> AuthResult<Option<Map<String, Value>>> {
107 self.verify_signed_payload_inner(token, request, ctx, false)
108 .await
109 }
110 async fn verify_signed_payload_inner(
111 &self,
112 token: &str,
113 request: Option<&AuthRequest>,
114 ctx: &AuthContext<impl AuthSchema>,
115 require_kid: bool,
116 ) -> AuthResult<Option<Map<String, Value>>> {
117 let parts = token.split('.').collect::<Vec<_>>();
118 let [header, payload, signature] = parts.as_slice() else {
119 return Ok(None);
120 };
121 let header = decode_compact_json(header, false)?;
122 let Some(header_object) = header.as_object() else {
123 return Ok(None);
124 };
125 if validate_critical_header(header_object, false).is_err() {
126 return Ok(None);
127 }
128 let kid = header
129 .get("kid")
130 .and_then(Value::as_str)
131 .filter(|kid| !kid.is_empty());
132 if require_kid && kid.is_none() {
133 return Ok(None);
134 }
135 let input = format!("{}.{}", parts.first().copied().unwrap_or_default(), payload);
136 let signature = decode_compact_part(signature, true)?;
137 let mut verified = false;
138 for key in self.keys_at_path(request, "virtual:", ctx).await? {
139 if kid.is_some_and(|kid| key.id != kid) {
140 continue;
141 }
142 let algorithm = key
143 .alg
144 .as_deref()
145 .map(JwtAlgorithm::from_str)
146 .unwrap_or(Ok(self.config.key_pair.algorithm))?;
147 if header.get("alg").and_then(Value::as_str) != Some(algorithm.as_str()) {
148 continue;
149 }
150 let public: Value = serde_json::from_str(&key.public_key)?;
151 if crypto::verify(algorithm, &public, input.as_bytes(), &signature)? {
152 verified = true;
153 break;
154 }
155 }
156 if !verified {
157 return Ok(None);
158 }
159 let Some(payload) = decode_compact_json(payload, true)?.as_object().cloned() else {
160 return Ok(None);
161 };
162 Ok(Some(payload))
163 }
164}