use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
use jsonwebtoken::Algorithm;
use serde::Deserialize;
use serde_json::Value;
#[derive(Debug, PartialEq)]
pub(crate) struct DecodedJwt {
pub header: DecodedJwtHeader,
pub claims: DecodedJwtClaims,
}
#[derive(Debug, Deserialize, PartialEq)]
pub(crate) struct DecodedJwtHeader {
pub typ: Option<String>,
pub cty: Option<String>,
pub kid: Option<String>,
pub alg: Algorithm,
}
#[derive(Debug, Deserialize, PartialEq)]
pub(crate) struct DecodedJwtClaims {
#[serde(flatten)]
pub inner: Value,
}
pub(crate) fn decode_jwt(jwt: &str) -> Result<DecodedJwt, DecodeJwtError> {
let split = jwt.split('.').collect::<Vec<&str>>();
if split.len() != 3 {
return Err(DecodeJwtError::InvalidJwtFormat);
}
let decoded_header = BASE64_URL_SAFE_NO_PAD
.decode(split[0])
.map_err(DecodeJwtError::DecodeHeader)?;
let header = serde_json::from_slice::<DecodedJwtHeader>(&decoded_header)
.map_err(DecodeJwtError::DeserializeHeader)?;
let decoded_claims = BASE64_URL_SAFE_NO_PAD
.decode(split[1])
.map_err(DecodeJwtError::DecodeClaims)?;
let claims = serde_json::from_slice::<DecodedJwtClaims>(&decoded_claims)
.map_err(DecodeJwtError::DeserializeClaims)?;
Ok(DecodedJwt { header, claims })
}
#[derive(Debug, thiserror::Error)]
pub enum DecodeJwtError {
#[error("the JWT is not in the form 'header.payload.signature'")]
InvalidJwtFormat,
#[error("error while decoding the JWT's header: {0}")]
DecodeHeader(#[source] base64::DecodeError),
#[error("error while deserializing the JWT's header: {0}")]
DeserializeHeader(#[source] serde_json::Error),
#[error("error while decoding the JWT's claims: {0}")]
DecodeClaims(#[source] base64::DecodeError),
#[error("error while deserializing the JWT's claims: {0}")]
DeserializeClaims(#[source] serde_json::Error),
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::json;
use test_utils::assert_eq;
#[test]
fn can_decode_jwt() {
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let jwt = decode_jwt(jwt).expect("decode jwt successfully");
assert_eq!(
jwt,
DecodedJwt {
header: DecodedJwtHeader {
typ: Some("JWT".into()),
alg: Algorithm::HS256,
cty: None,
kid: None,
},
claims: DecodedJwtClaims {
inner: json!({
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022,
})
}
}
);
}
#[test]
fn errors_on_invalid_jwt_format() {
let cases = [
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ",
];
for (i, jwt) in cases.into_iter().enumerate() {
let err = decode_jwt(jwt).expect_err("should error while decoding jwt");
assert!(
matches!(err, DecodeJwtError::InvalidJwtFormat),
"failed assertion in case: {i}"
);
}
}
}