Skip to main content

alibi_plugins/jwt/
verification.rs

1use 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    /// Verify a token against the persisted keyring and configured claims.
20    /// Invalid signatures, malformed tokens and claim failures return `None`.
21    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    /// Verify the compact signature without enforcing JWT claim lifetime.
90    /// Logout hints deliberately accept expired ID tokens and apply their own claims.
91    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    /// Resolve algorithm-compatible keys when an OAuth access JWS omits `kid`.
101    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}