1use jsonwebtoken::errors::ErrorKind;
4use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
5use serde::de::{self, Deserializer};
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::error::{Result, TokenError};
10use crate::types::{AccountKind, Jwks};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14#[non_exhaustive]
15pub struct Claims {
16 #[serde(default)]
18 pub iss: String,
19 pub sub: String,
21 #[serde(deserialize_with = "one_or_many")]
23 pub aud: Vec<String>,
24 pub exp: i64,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub iat: Option<i64>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub nbf: Option<i64>,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub jti: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
37 pub kind: Option<AccountKind>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub id: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub mid: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub fid: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub scope: Option<String>,
50}
51
52impl Claims {
53 pub fn scopes(&self) -> Vec<&str> {
55 self.scope
56 .as_deref()
57 .map(|s| s.split_whitespace().collect())
58 .unwrap_or_default()
59 }
60
61 pub fn has_scope(&self, scope: &str) -> bool {
63 self.scopes().contains(&scope)
64 }
65}
66
67fn one_or_many<'de, D: Deserializer<'de>>(
68 deserializer: D,
69) -> std::result::Result<Vec<String>, D::Error> {
70 match Value::deserialize(deserializer)? {
71 Value::String(s) => Ok(vec![s]),
72 Value::Array(items) => items
73 .into_iter()
74 .map(|v| match v {
75 Value::String(s) => Ok(s),
76 _ => Err(de::Error::custom("aud entries must be strings")),
77 })
78 .collect(),
79 _ => Err(de::Error::custom(
80 "aud must be a string or a list of strings",
81 )),
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct VerifyOptions {
88 pub audiences: Vec<String>,
90 pub issuer: Option<String>,
92 pub leeway_seconds: u64,
94}
95
96impl VerifyOptions {
97 pub fn for_app(app_id: &str) -> Self {
99 Self {
100 audiences: vec![app_id.to_owned()],
101 issuer: None,
102 leeway_seconds: 30,
103 }
104 }
105
106 pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
108 self.issuer = Some(issuer.into());
109 self
110 }
111}
112
113pub fn verify_access_token(jwks: &Jwks, token: &str, options: &VerifyOptions) -> Result<Claims> {
119 let token = token.trim();
120 let header = decode_header(token).map_err(|e| TokenError::Malformed(describe(e.kind())))?;
121 if header.alg != Algorithm::EdDSA {
122 return Err(TokenError::UnsupportedAlgorithm(format!("{:?}", header.alg)).into());
123 }
124 let jwk = match header.kid.as_deref() {
125 Some(kid) => jwks.find(kid).ok_or_else(|| TokenError::UnknownKey {
126 kid: Some(kid.to_owned()),
127 })?,
128 None if jwks.keys.len() == 1 => &jwks.keys[0],
129 None => return Err(TokenError::UnknownKey { kid: None }.into()),
130 };
131 if jwk.kty != "OKP" || jwk.crv.as_deref() != Some("Ed25519") {
132 return Err(TokenError::InvalidKey(format!(
133 "it has kty `{}` and crv `{}`, expected OKP / Ed25519",
134 jwk.kty,
135 jwk.crv.as_deref().unwrap_or("none")
136 ))
137 .into());
138 }
139 let x = jwk
140 .x
141 .as_deref()
142 .ok_or_else(|| TokenError::InvalidKey("it has no `x` value".to_owned()))?;
143 let raw = base64::Engine::decode(
146 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
147 x.trim_end_matches('='),
148 )
149 .map_err(|e| TokenError::InvalidKey(format!("`x` is not base64url ({e})")))?;
150 if raw.len() != 32 {
151 return Err(TokenError::InvalidKey(format!(
152 "`x` is {} bytes, an Ed25519 public key is 32",
153 raw.len()
154 ))
155 .into());
156 }
157 let key = DecodingKey::from_ed_components(x)
158 .map_err(|e| TokenError::InvalidKey(describe(e.kind())))?;
159
160 if options.audiences.is_empty() {
161 return Err(crate::Error::invalid_input(
162 "Refusing to verify an access token without an expected audience: any app's token would pass.",
163 "Pass your app id as the audience (VerifyOptions::for_app).",
164 ));
165 }
166 let mut validation = Validation::new(Algorithm::EdDSA);
167 validation.leeway = options.leeway_seconds;
168 validation.validate_nbf = true;
169 validation.set_audience(&options.audiences);
170 let mut required = vec!["exp", "sub", "aud"];
171 if let Some(issuer) = &options.issuer {
172 validation.set_issuer(&[issuer]);
173 required.push("iss");
174 }
175 validation.set_required_spec_claims(&required);
176
177 match decode::<Claims>(token, &key, &validation) {
178 Ok(data) => Ok(data.claims),
179 Err(e) => Err(match e.kind() {
180 ErrorKind::InvalidSignature => TokenError::BadSignature,
181 ErrorKind::ExpiredSignature => TokenError::Expired {
182 exp: unverified_exp(token),
183 },
184 ErrorKind::ImmatureSignature => TokenError::NotYetValid,
185 ErrorKind::InvalidAudience => TokenError::WrongAudience {
186 expected: options.audiences.clone(),
187 },
188 ErrorKind::InvalidIssuer => TokenError::WrongIssuer {
189 expected: options.issuer.clone().unwrap_or_default(),
190 },
191 ErrorKind::MissingRequiredClaim(claim) | ErrorKind::InvalidClaimFormat(claim) => {
192 TokenError::MissingClaim(claim.clone())
193 }
194 ErrorKind::InvalidAlgorithm => {
195 TokenError::UnsupportedAlgorithm("a non-EdDSA algorithm".to_owned())
196 }
197 ErrorKind::InvalidEddsaKey | ErrorKind::InvalidKeyFormat => {
198 TokenError::InvalidKey(describe(e.kind()))
199 }
200 other => TokenError::Malformed(describe(other)),
201 }
202 .into()),
203 }
204}
205
206fn unverified_exp(token: &str) -> Option<i64> {
208 let payload = token.split('.').nth(1)?;
209 let bytes =
210 base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, payload).ok()?;
211 serde_json::from_slice::<Value>(&bytes)
212 .ok()?
213 .get("exp")?
214 .as_i64()
215}
216
217fn describe(kind: &ErrorKind) -> String {
218 match kind {
219 ErrorKind::InvalidToken => {
220 "it does not have three base64url parts separated by dots".to_owned()
221 }
222 ErrorKind::Base64(e) => format!("a part is not valid base64url ({e})"),
223 ErrorKind::Json(e) => format!("a part is not valid JSON ({e})"),
224 ErrorKind::Utf8(e) => format!("a part is not valid UTF-8 ({e})"),
225 ErrorKind::InvalidEddsaKey => "the key is not a 32-byte Ed25519 public key".to_owned(),
226 ErrorKind::InvalidKeyFormat => "the key has an invalid format".to_owned(),
227 other => format!("{other:?}"),
228 }
229}
230
231#[cfg(test)]
232#[allow(clippy::unwrap_used)]
233mod tests {
234 use base64::Engine;
235 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
236 use ed25519_dalek::SigningKey;
237 use ed25519_dalek::pkcs8::EncodePrivateKey;
238 use jsonwebtoken::{EncodingKey, Header, encode};
239 use serde_json::json;
240
241 use super::*;
242 use crate::error::Error;
243 use crate::types::Jwk;
244
245 fn keypair(seed: u8) -> (EncodingKey, Jwk) {
246 let signing = SigningKey::from_bytes(&[seed; 32]);
247 let der = signing.to_pkcs8_der().unwrap();
248 let encoding = EncodingKey::from_ed_der(der.as_bytes());
249 let x = URL_SAFE_NO_PAD.encode(signing.verifying_key().as_bytes());
250 (encoding, Jwk::ed25519(format!("k{seed}"), x))
251 }
252
253 fn token(key: &EncodingKey, kid: &str, claims: &Value) -> String {
254 let mut header = Header::new(Algorithm::EdDSA);
255 header.kid = Some(kid.to_owned());
256 encode(&header, claims, key).unwrap()
257 }
258
259 fn claims(aud: &str, exp_offset: i64) -> Value {
260 let now = time::OffsetDateTime::now_utc().unix_timestamp();
261 json!({
262 "iss": "http://127.0.0.1:8590", "sub": "a8K", "aud": aud, "exp": now + exp_offset, "iat": now,
263 "nbf": now, "jti": "j1", "kind": "silicon", "id": "si:scout", "mid": "remind:a8K", "fid": "f1",
264 "scope": "profile timezone"
265 })
266 }
267
268 #[test]
269 fn verifies_a_valid_token() {
270 let (key, jwk) = keypair(7);
271 let jwks = Jwks::new(vec![jwk]);
272 let t = token(&key, "k7", &claims("remind", 600));
273 let verified = verify_access_token(
274 &jwks,
275 &t,
276 &VerifyOptions::for_app("remind").with_issuer("http://127.0.0.1:8590"),
277 )
278 .unwrap();
279 assert_eq!(verified.sub, "a8K");
280 assert_eq!(verified.aud, vec!["remind".to_owned()]);
281 assert_eq!(verified.kind, Some(AccountKind::Silicon));
282 assert!(verified.has_scope("timezone"));
283 }
284
285 #[test]
286 fn rejects_wrong_audience_expiry_unknown_key_and_tampering() {
287 let (key, jwk) = keypair(7);
288 let (other_key, _) = keypair(9);
289 let jwks = Jwks::new(vec![jwk]);
290 let options = VerifyOptions::for_app("remind");
291
292 let wrong_aud = token(&key, "k7", &claims("briefcase", 600));
293 let err = verify_access_token(&jwks, &wrong_aud, &options).unwrap_err();
294 assert!(
295 matches!(err, Error::Token(TokenError::WrongAudience { .. })),
296 "{err}"
297 );
298
299 let expired = token(&key, "k7", &claims("remind", -3600));
300 let err = verify_access_token(&jwks, &expired, &options).unwrap_err();
301 assert!(
302 matches!(err, Error::Token(TokenError::Expired { exp: Some(_) })),
303 "{err}"
304 );
305
306 let unknown = token(&other_key, "k9", &claims("remind", 600));
307 let err = verify_access_token(&jwks, &unknown, &options).unwrap_err();
308 assert!(
309 matches!(err, Error::Token(TokenError::UnknownKey { .. })),
310 "{err}"
311 );
312
313 let forged = token(&other_key, "k7", &claims("remind", 600));
314 let err = verify_access_token(&jwks, &forged, &options).unwrap_err();
315 assert!(
316 matches!(err, Error::Token(TokenError::BadSignature)),
317 "{err}"
318 );
319
320 let err = verify_access_token(&jwks, "not-a-jwt", &options).unwrap_err();
321 assert!(
322 matches!(err, Error::Token(TokenError::Malformed(_))),
323 "{err}"
324 );
325 assert_eq!(err.code(), "token_malformed");
326
327 let err = verify_access_token(
328 &jwks,
329 &wrong_aud,
330 &VerifyOptions {
331 audiences: vec![],
332 issuer: None,
333 leeway_seconds: 0,
334 },
335 )
336 .unwrap_err();
337 assert_eq!(err.code(), "invalid_input");
338 }
339}