use core::fmt;
use crate::crypto::constant_time::constant_time_eq;
use crate::crypto::{HmacSha256, HmacSha512};
use crate::encoding::base64url_decode;
use super::claims::{JwtClaims, JwtClaimsError};
use super::decode::MAX_SIGNATURE_SEGMENT_LEN;
use super::header::{JwtAlgorithm, JwtHeader, JwtHeaderError};
#[derive(Debug, Clone, PartialEq, Eq)]
enum JwtSignatureErrorKind {
MalformedToken,
HeaderError(JwtHeaderError),
ClaimsError(JwtClaimsError),
NoneAlgorithm,
#[cfg(feature = "asym-jwt")]
AlgorithmMismatch,
#[cfg(feature = "asym-jwt")]
NoMatchingKey,
InvalidSignature,
InvalidSignatureEncoding,
}
#[doc(alias = "signature_error")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JwtSignatureError {
kind: JwtSignatureErrorKind,
}
impl fmt::Display for JwtSignatureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
JwtSignatureErrorKind::MalformedToken => {
write!(f, "jwt: malformed token structure")
}
JwtSignatureErrorKind::HeaderError(_) => {
write!(f, "jwt: invalid header")
}
JwtSignatureErrorKind::ClaimsError(_) => {
write!(f, "jwt: invalid claims")
}
JwtSignatureErrorKind::NoneAlgorithm => {
write!(f, "jwt: 'none' algorithm is not permitted")
}
#[cfg(feature = "asym-jwt")]
JwtSignatureErrorKind::AlgorithmMismatch => {
write!(f, "jwt: algorithm does not match the supplied key")
}
#[cfg(feature = "asym-jwt")]
JwtSignatureErrorKind::NoMatchingKey => {
write!(f, "jwt: no key matched the token's key id")
}
JwtSignatureErrorKind::InvalidSignature => {
write!(f, "jwt: signature verification failed")
}
JwtSignatureErrorKind::InvalidSignatureEncoding => {
write!(f, "jwt: invalid signature encoding")
}
}
}
}
impl JwtSignatureError {
#[must_use]
#[inline]
pub fn is_invalid_signature(&self) -> bool {
self.kind == JwtSignatureErrorKind::InvalidSignature
}
#[must_use]
#[inline]
pub fn is_malformed_token(&self) -> bool {
self.kind == JwtSignatureErrorKind::MalformedToken
}
#[must_use]
#[inline]
pub fn is_none_algorithm(&self) -> bool {
self.kind == JwtSignatureErrorKind::NoneAlgorithm
}
#[cfg(feature = "asym-jwt")]
#[must_use]
#[inline]
pub fn is_algorithm_mismatch(&self) -> bool {
self.kind == JwtSignatureErrorKind::AlgorithmMismatch
}
#[cfg(feature = "asym-jwt")]
#[must_use]
#[inline]
pub fn is_no_matching_key(&self) -> bool {
self.kind == JwtSignatureErrorKind::NoMatchingKey
}
#[cfg(feature = "asym-jwt")]
#[must_use]
#[inline]
pub(crate) fn no_matching_key() -> Self {
Self {
kind: JwtSignatureErrorKind::NoMatchingKey,
}
}
#[must_use]
#[inline]
pub fn is_header_error(&self) -> bool {
matches!(self.kind, JwtSignatureErrorKind::HeaderError(_))
}
#[must_use]
#[inline]
pub fn is_claims_error(&self) -> bool {
matches!(self.kind, JwtSignatureErrorKind::ClaimsError(_))
}
#[must_use]
#[inline]
pub fn is_invalid_signature_encoding(&self) -> bool {
self.kind == JwtSignatureErrorKind::InvalidSignatureEncoding
}
}
impl std::error::Error for JwtSignatureError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
JwtSignatureErrorKind::HeaderError(err) => Some(err),
JwtSignatureErrorKind::ClaimsError(err) => Some(err),
JwtSignatureErrorKind::MalformedToken
| JwtSignatureErrorKind::NoneAlgorithm
| JwtSignatureErrorKind::InvalidSignature
| JwtSignatureErrorKind::InvalidSignatureEncoding => None,
#[cfg(feature = "asym-jwt")]
JwtSignatureErrorKind::AlgorithmMismatch | JwtSignatureErrorKind::NoMatchingKey => None,
}
}
}
impl From<JwtHeaderError> for JwtSignatureError {
fn from(err: JwtHeaderError) -> Self {
Self {
kind: JwtSignatureErrorKind::HeaderError(err),
}
}
}
impl From<JwtClaimsError> for JwtSignatureError {
fn from(err: JwtClaimsError) -> Self {
Self {
kind: JwtSignatureErrorKind::ClaimsError(err),
}
}
}
pub(super) fn split_compact(token: &str) -> Result<(&str, &str, &str), JwtSignatureError> {
let parts: Vec<&str> = token.splitn(4, '.').collect();
if parts.len() != 3 {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::MalformedToken,
});
}
Ok((parts[0], parts[1], parts[2]))
}
fn decode_signature(signature_b64: &str) -> Result<Vec<u8>, JwtSignatureError> {
if signature_b64.as_bytes().contains(&b'=') {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignatureEncoding,
});
}
if signature_b64.len() > MAX_SIGNATURE_SEGMENT_LEN {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignatureEncoding,
});
}
base64url_decode(signature_b64).map_err(|_| JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignatureEncoding,
})
}
pub(super) fn signing_input(header_b64: &str, payload_b64: &str) -> String {
let mut input = String::with_capacity(header_b64.len() + 1 + payload_b64.len());
input.push_str(header_b64);
input.push('.');
input.push_str(payload_b64);
input
}
#[must_use = "verification may fail; handle the Result"]
pub fn verify_jwt(token: &str, key: &[u8]) -> Result<(JwtHeader, JwtClaims), JwtSignatureError> {
let (header_b64, payload_b64, signature_b64) = split_compact(token)?;
let header = JwtHeader::parse(header_b64)?;
if header.alg() == JwtAlgorithm::None {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::NoneAlgorithm,
});
}
let provided_sig = decode_signature(signature_b64)?;
let signing_input = signing_input(header_b64, payload_b64);
let signature_valid = match header.alg() {
JwtAlgorithm::HS256 => {
let computed = HmacSha256::mac(key, signing_input.as_bytes());
constant_time_eq(&computed, &provided_sig)
}
JwtAlgorithm::HS512 => {
let computed = HmacSha512::mac(key, signing_input.as_bytes());
constant_time_eq(&computed, &provided_sig)
}
#[cfg(feature = "asym-jwt")]
JwtAlgorithm::EdDSA | JwtAlgorithm::ES256 | JwtAlgorithm::RS256 | JwtAlgorithm::RS512 => {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::AlgorithmMismatch,
});
}
JwtAlgorithm::None => {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::NoneAlgorithm,
});
}
};
if !signature_valid {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignature,
});
}
let claims = JwtClaims::parse(payload_b64)?;
Ok((header, claims))
}
#[cfg(feature = "asym-jwt")]
#[must_use = "verification may fail; handle the Result"]
pub fn verify_jwt_asymmetric(
token: &str,
key: &super::AsymmetricVerifyingKey,
) -> Result<(JwtHeader, JwtClaims), JwtSignatureError> {
let (header_b64, payload_b64, signature_b64) = split_compact(token)?;
let header = JwtHeader::parse(header_b64)?;
if header.alg() == JwtAlgorithm::None {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::NoneAlgorithm,
});
}
if header.alg() != key.algorithm().to_jwt_algorithm() {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::AlgorithmMismatch,
});
}
let provided_sig = decode_signature(signature_b64)?;
let signing_input = signing_input(header_b64, payload_b64);
if !key.verify(signing_input.as_bytes(), &provided_sig) {
return Err(JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignature,
});
}
let claims = JwtClaims::parse(payload_b64)?;
Ok((header, claims))
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
use crate::encoding::base64url_encode;
fn make_jwt(header_json: &str, claims_json: &str, key: &[u8], alg: &str) -> String {
let header_b64 = base64url_encode(header_json.as_bytes());
let payload_b64 = base64url_encode(claims_json.as_bytes());
let signing_input = format!("{header_b64}.{payload_b64}");
let sig = match alg {
"HS256" => {
let mac = HmacSha256::mac(key, signing_input.as_bytes());
base64url_encode(&mac)
}
"HS512" => {
let mac = HmacSha512::mac(key, signing_input.as_bytes());
base64url_encode(&mac)
}
_ => String::new(),
};
format!("{header_b64}.{payload_b64}.{sig}")
}
const TEST_KEY: &[u8] = b"super-secret-key-for-testing-only";
#[test]
fn verify_valid_hs256() {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
let claims = r#"{"iss":"test","sub":"user-1","aud":"my-app","exp":9999999999}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (hdr, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert_eq!(hdr.alg(), JwtAlgorithm::HS256);
assert_eq!(clm.iss(), Some("test"));
assert_eq!(clm.sub(), Some("user-1"));
assert!(clm.validate_aud("my-app"));
}
#[test]
fn verify_valid_hs512() {
let header = r#"{"alg":"HS512","typ":"JWT"}"#;
let claims = r#"{"iss":"test","sub":"user-2"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS512");
let (hdr, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert_eq!(hdr.alg(), JwtAlgorithm::HS512);
assert_eq!(clm.sub(), Some("user-2"));
}
#[test]
fn reject_invalid_signature() {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
let claims = r#"{"sub":"user-1"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let err = verify_jwt(&token, b"wrong-key").unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignature,
},
);
assert!(err.to_string().contains("signature verification failed"));
}
#[test]
fn reject_tampered_payload() {
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
let claims = r#"{"sub":"user-1"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let parts: Vec<&str> = token.splitn(3, '.').collect();
let tampered_payload = base64url_encode(br#"{"sub":"admin"}"#);
let tampered = format!("{}.{}.{}", parts[0], tampered_payload, parts[2]);
assert!(verify_jwt(&tampered, TEST_KEY).is_err());
}
#[test]
fn reject_none_algorithm() {
let header = r#"{"alg":"none"}"#;
let claims = r#"{"sub":"attacker"}"#;
let header_b64 = base64url_encode(header.as_bytes());
let payload_b64 = base64url_encode(claims.as_bytes());
let token = format!("{header_b64}.{payload_b64}.");
let err = verify_jwt(&token, TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::NoneAlgorithm,
},
);
assert!(err.to_string().contains("none"));
}
#[cfg(feature = "asym-jwt")]
#[test]
fn reject_asymmetric_alg_on_hmac_verifier() {
for alg in ["RS256", "RS512", "ES256", "EdDSA"] {
let header = format!(r#"{{"alg":"{alg}"}}"#);
let header_b64 = base64url_encode(header.as_bytes());
let payload_b64 = base64url_encode(br#"{"sub":"attacker"}"#);
let token = format!("{header_b64}.{payload_b64}.AAAA");
let err = verify_jwt(&token, b"public-key-bytes-as-hmac-secret").unwrap_err();
assert!(
err.is_algorithm_mismatch(),
"{alg} token must be rejected as algorithm mismatch on the HMAC verifier"
);
}
}
#[cfg(feature = "asym-jwt")]
#[test]
fn reject_hmac_alg_on_asymmetric_verifier() {
use crate::jwt::{AsymmetricAlgorithm, AsymmetricSigningKey};
let signing = AsymmetricSigningKey::generate(AsymmetricAlgorithm::EdDsa).unwrap();
let verifying = signing.to_verifying_key();
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"sub":"attacker"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let err = verify_jwt_asymmetric(&token, &verifying).unwrap_err();
assert!(err.is_algorithm_mismatch());
}
#[test]
fn expired_claims_validation() {
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"exp": 1000}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (_, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert!(!clm.validate_exp(2000, 0));
}
#[test]
fn not_expired_claims_validation() {
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"exp": 9999999999}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (_, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert!(clm.validate_exp(1000, 0));
}
#[test]
fn wrong_audience() {
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"aud":"expected-client"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (_, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert!(!clm.validate_aud("wrong-client"));
assert!(clm.validate_aud("expected-client"));
}
#[test]
fn wrong_issuer() {
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"iss":"https://auth.example.com"}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (_, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert!(!clm.validate_iss("https://evil.example.com"));
assert!(clm.validate_iss("https://auth.example.com"));
}
#[test]
fn reject_missing_parts() {
let err = verify_jwt("onlyonepart", TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::MalformedToken,
},
);
}
#[test]
fn reject_two_parts() {
let err = verify_jwt("part1.part2", TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::MalformedToken,
},
);
}
#[test]
fn reject_four_parts() {
let err = verify_jwt("a.b.c.d", TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::MalformedToken,
},
);
}
#[test]
fn reject_empty_token() {
let err = verify_jwt("", TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::MalformedToken,
},
);
}
#[test]
fn reject_invalid_header_encoding() {
let err = verify_jwt("!!!.payload.sig", TEST_KEY).unwrap_err();
assert!(
err.to_string().contains("invalid header"),
"expected 'invalid header' in '{err}'",
);
assert!(
err.source().is_some(),
"expected source error for HeaderError"
);
}
#[test]
fn reject_invalid_signature_encoding() {
let header = r#"{"alg":"HS256"}"#;
let claims = r#"{"sub":"user"}"#;
let header_b64 = base64url_encode(header.as_bytes());
let payload_b64 = base64url_encode(claims.as_bytes());
let token = format!("{header_b64}.{payload_b64}.!!!invalid!!!");
let err = verify_jwt(&token, TEST_KEY).unwrap_err();
assert_eq!(
err,
JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignatureEncoding,
},
);
}
#[test]
fn error_display_messages() {
let simple_cases = [
(
JwtSignatureErrorKind::MalformedToken,
"malformed token structure",
),
(JwtSignatureErrorKind::NoneAlgorithm, "none"),
(
JwtSignatureErrorKind::InvalidSignature,
"signature verification failed",
),
(
JwtSignatureErrorKind::InvalidSignatureEncoding,
"invalid signature encoding",
),
];
for (kind, expected_substr) in simple_cases {
let err = JwtSignatureError { kind };
assert!(
err.to_string().contains(expected_substr),
"expected '{expected_substr}' in '{err}'",
);
}
let header_err = JwtSignatureError::from(JwtHeader::parse("!!!invalid!!!").unwrap_err());
assert!(
header_err.to_string().contains("invalid header"),
"expected 'invalid header' in '{header_err}'",
);
let claims_err = JwtSignatureError::from(JwtClaims::parse("!!!invalid!!!").unwrap_err());
assert!(
claims_err.to_string().contains("invalid claims"),
"expected 'invalid claims' in '{claims_err}'",
);
}
#[test]
fn error_implements_std_error() {
let err: Box<dyn std::error::Error> = Box::new(JwtSignatureError {
kind: JwtSignatureErrorKind::InvalidSignature,
});
let _ = err.to_string();
}
#[test]
fn end_to_end_full_claim_set() {
let header = r#"{"alg":"HS256","typ":"JWT","kid":"test-key-1"}"#;
let claims = r#"{
"iss": "https://auth.example.com",
"sub": "user-42",
"aud": ["app-1", "app-2"],
"exp": 9999999999,
"nbf": 1000,
"iat": 1000,
"jti": "token-id-xyz",
"custom": "value"
}"#;
let token = make_jwt(header, claims, TEST_KEY, "HS256");
let (hdr, clm) = verify_jwt(&token, TEST_KEY).unwrap();
assert_eq!(hdr.alg(), JwtAlgorithm::HS256);
assert_eq!(hdr.typ(), Some("JWT"));
assert_eq!(hdr.kid(), Some("test-key-1"));
assert_eq!(clm.iss(), Some("https://auth.example.com"));
assert_eq!(clm.sub(), Some("user-42"));
assert_eq!(clm.aud(), &["app-1", "app-2"]);
assert_eq!(clm.exp(), Some(9_999_999_999));
assert_eq!(clm.nbf(), Some(1000));
assert_eq!(clm.iat(), Some(1000));
assert_eq!(clm.jti(), Some("token-id-xyz"));
assert_eq!(
clm.get_claim("custom").and_then(|v| v.as_str()),
Some("value"),
);
assert!(clm.validate_iss("https://auth.example.com"));
assert!(clm.validate_aud("app-1"));
assert!(clm.validate_aud("app-2"));
assert!(clm.validate_exp(5000, 0));
}
}