use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum KeyError {
Algorithm(String),
SecretMissing,
KeyMissing(&'static str),
KeyInvalid(String),
}
impl std::fmt::Display for KeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KeyError::Algorithm(name) => write!(f, "unsupported JWT algorithm '{name}'"),
KeyError::SecretMissing => write!(f, "jwt_signing_key credential missing 'secret' field"),
KeyError::KeyMissing(field) => write!(f, "jwt_signing_key credential missing '{field}' field"),
KeyError::KeyInvalid(reason) => write!(f, "jwt_signing_key key is invalid: {reason}"),
}
}
}
pub fn algorithm(secret: &Value) -> Result<Algorithm, KeyError> {
let name = secret.get("algorithm").and_then(Value::as_str).unwrap_or("HS256");
match name.trim().to_ascii_uppercase().as_str() {
"HS256" => Ok(Algorithm::HS256),
"HS384" => Ok(Algorithm::HS384),
"HS512" => Ok(Algorithm::HS512),
"RS256" => Ok(Algorithm::RS256),
"RS384" => Ok(Algorithm::RS384),
"RS512" => Ok(Algorithm::RS512),
_ => Err(KeyError::Algorithm(name.to_string())),
}
}
fn is_hmac(algorithm: Algorithm) -> bool {
matches!(algorithm, Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512)
}
fn hmac_secret(secret: &Value) -> Result<&str, KeyError> {
secret.get("secret").and_then(Value::as_str).ok_or(KeyError::SecretMissing)
}
pub fn encoding_key(secret: &Value) -> Result<(Algorithm, EncodingKey), KeyError> {
let algorithm = algorithm(secret)?;
if is_hmac(algorithm) {
return Ok((algorithm, EncodingKey::from_secret(hmac_secret(secret)?.as_bytes())));
}
let pem = secret.get("private_key").and_then(Value::as_str).ok_or(KeyError::KeyMissing("private_key"))?;
let key = EncodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| KeyError::KeyInvalid(e.to_string()))?;
Ok((algorithm, key))
}
pub fn decoding_key(secret: &Value) -> Result<(Algorithm, DecodingKey), KeyError> {
let algorithm = algorithm(secret)?;
if is_hmac(algorithm) {
return Ok((algorithm, DecodingKey::from_secret(hmac_secret(secret)?.as_bytes())));
}
let pem = secret
.get("public_key")
.or_else(|| secret.get("private_key"))
.and_then(Value::as_str)
.ok_or(KeyError::KeyMissing("public_key"))?;
let key = DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| KeyError::KeyInvalid(e.to_string()))?;
Ok((algorithm, key))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
Key(KeyError),
Token(String),
}
pub fn verify_app_token(secret: &Value, token: &str) -> Result<Value, VerifyError> {
let (algorithm, key) = decoding_key(secret).map_err(VerifyError::Key)?;
let mut validation = Validation::new(algorithm);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.validate_aud = false;
jsonwebtoken::decode::<Value>(token, &key, &validation)
.map(|data| data.claims)
.map_err(|e| VerifyError::Token(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn the_algorithm_is_read_one_way() {
assert_eq!(algorithm(&json!({})).unwrap(), Algorithm::HS256);
assert_eq!(algorithm(&json!({ "algorithm": "hs512" })).unwrap(), Algorithm::HS512);
assert_eq!(algorithm(&json!({ "algorithm": "ES256" })), Err(KeyError::Algorithm("ES256".into())));
assert!(matches!(encoding_key(&json!({ "algorithm": "HS256" })), Err(KeyError::SecretMissing)));
assert!(matches!(decoding_key(&json!({ "algorithm": "RS256" })), Err(KeyError::KeyMissing("public_key"))));
}
#[test]
fn an_app_token_verifies_whatever_its_audience() {
let secret = json!({ "algorithm": "HS256", "secret": uuid::Uuid::new_v4().to_string() });
let (algorithm, key) = encoding_key(&secret).unwrap();
let now = chrono::Utc::now().timestamp();
let sign = |claims: Value| jsonwebtoken::encode(&jsonwebtoken::Header::new(algorithm), &claims, &key).unwrap();
let claims = verify_app_token(&secret, &sign(json!({ "sub": "u1", "aud": "app", "exp": now + 60 }))).unwrap();
assert_eq!(claims["sub"], "u1");
assert!(matches!(verify_app_token(&secret, &sign(json!({ "sub": "u1", "exp": now - 120 }))), Err(VerifyError::Token(_))));
let other = json!({ "secret": uuid::Uuid::new_v4().to_string() });
assert!(matches!(verify_app_token(&other, &sign(json!({ "exp": now + 60 }))), Err(VerifyError::Token(_))));
}
}