use chrono::{DateTime, Utc};
use jwt_simple::prelude::{
EdDSAKeyPairLike, EdDSAPublicKeyLike,
RSAKeyPairLike, RSAPublicKeyLike,
ECDSAP256KeyPairLike, ECDSAP256PublicKeyLike,
};
use serde::{Deserialize, Serialize};
use crate::ConfidentialText;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JwtAlgorithm {
EdDSA,
RS256,
ES256,
}
#[derive(Debug, Clone)]
pub enum PublicKeySource {
Pem(String),
}
#[derive(Debug, Clone)]
pub struct JwtVerifierConfig {
pub allowed_algorithms: Vec<JwtAlgorithm>,
pub issuer: Option<String>,
pub audience: Option<String>,
pub key_id: Option<String>,
pub public_key: PublicKeySource,
}
#[derive(Clone)]
enum VerifiedPublicKey {
Ed25519(jwt_simple::prelude::Ed25519PublicKey),
RS256(jwt_simple::prelude::RS256PublicKey),
ES256(jwt_simple::prelude::ES256PublicKey),
}
impl VerifiedPublicKey {
fn from_pem(pem: &str) -> Result<Self, JwtError> {
if let Ok(pk) = jwt_simple::prelude::Ed25519PublicKey::from_pem(pem) {
return Ok(Self::Ed25519(pk));
}
if let Ok(pk) = jwt_simple::prelude::RS256PublicKey::from_pem(pem) {
return Ok(Self::RS256(pk));
}
if let Ok(pk) = jwt_simple::prelude::ES256PublicKey::from_pem(pem) {
return Ok(Self::ES256(pk));
}
Err(JwtError::InvalidPublicKey(
"could not parse PEM as Ed25519, RSA, or EC public key".into(),
))
}
fn algorithm(&self) -> JwtAlgorithm {
match self {
Self::Ed25519(_) => JwtAlgorithm::EdDSA,
Self::RS256(_) => JwtAlgorithm::RS256,
Self::ES256(_) => JwtAlgorithm::ES256,
}
}
fn alg_name(&self) -> &'static str {
match self {
Self::Ed25519(_) => "EdDSA",
Self::RS256(_) => "RS256",
Self::ES256(_) => "ES256",
}
}
fn verify_token(
&self,
token: &str,
options: Option<jwt_simple::prelude::VerificationOptions>,
) -> Result<jwt_simple::prelude::JWTClaims<serde_json::Value>, JwtError> {
match self {
Self::Ed25519(pk) => pk
.verify_token(token, options)
.map_err(|e| JwtError::SignatureVerificationFailed(e.to_string())),
Self::RS256(pk) => pk
.verify_token(token, options)
.map_err(|e| JwtError::SignatureVerificationFailed(e.to_string())),
Self::ES256(pk) => pk
.verify_token(token, options)
.map_err(|e| JwtError::SignatureVerificationFailed(e.to_string())),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedClaims {
pub sub: String,
pub iss: String,
pub aud: String,
pub exp: DateTime<Utc>,
pub iat: DateTime<Utc>,
pub nbf: Option<DateTime<Utc>>,
pub jti: Option<String>,
pub scope: Option<String>,
pub nonce: Option<String>,
#[serde(flatten)]
pub custom: serde_json::Map<String, serde_json::Value>,
}
impl VerifiedClaims {
#[must_use]
pub fn verify_nonce(&self, expected_nonce: &str) -> bool {
self.nonce
.as_ref()
.is_some_and(|n| n == expected_nonce)
}
}
pub struct JwtVerifier {
config: JwtVerifierConfig,
public_key: VerifiedPublicKey,
}
impl JwtVerifier {
pub fn from_config(config: JwtVerifierConfig) -> Result<Self, JwtError> {
let public_key = match &config.public_key {
PublicKeySource::Pem(pem) => VerifiedPublicKey::from_pem(pem)?,
};
Ok(Self { config, public_key })
}
fn build_options(&self) -> jwt_simple::prelude::VerificationOptions {
let mut opts = jwt_simple::prelude::VerificationOptions::default();
if let Some(ref issuer) = self.config.issuer {
let mut issuers = std::collections::HashSet::new();
issuers.insert(issuer.clone());
opts.allowed_issuers = Some(issuers);
}
if let Some(ref audience) = self.config.audience {
let mut audiences = std::collections::HashSet::new();
audiences.insert(audience.clone());
opts.allowed_audiences = Some(audiences);
}
if let Some(ref key_id) = self.config.key_id {
opts.required_key_id = Some(key_id.clone());
}
opts
}
pub fn verify(&self, token: &str) -> Result<VerifiedClaims, JwtError> {
let header_alg = decode_header_algorithm(token)?;
let expected_alg = self.public_key.alg_name();
if header_alg != expected_alg {
return Err(JwtError::AlgorithmNotAllowed(format!(
"token uses {header_alg}, expected {expected_alg}"
)));
}
if !self
.config
.allowed_algorithms
.contains(&self.public_key.algorithm())
{
return Err(JwtError::AlgorithmNotAllowed(format!(
"{expected_alg} is not in the allowed algorithms list"
)));
}
let options = self.build_options();
let claims = self.public_key.verify_token(token, Some(options))?;
to_verified_claims(&claims)
}
pub fn verify_with_key(&self, token: &str, _kid: &str) -> Result<VerifiedClaims, JwtError> {
self.verify(token)
}
#[must_use]
pub fn key_id(&self) -> Option<&str> {
self.config.key_id.as_deref()
}
}
pub enum JwtSigningKey {
Ed25519(jwt_simple::prelude::Ed25519KeyPair),
RS256(jwt_simple::prelude::RS256KeyPair),
ES256(jwt_simple::prelude::ES256KeyPair),
}
impl JwtSigningKey {
#[must_use]
pub fn generate_ed25519() -> Self {
Self::Ed25519(jwt_simple::prelude::Ed25519KeyPair::generate())
}
pub fn sign_claims(&self, claims: &serde_json::Value) -> Result<String, JwtError> {
use jwt_simple::prelude::{Claims, Duration};
let duration = claims
.get("exp")
.and_then(serde_json::Value::as_u64)
.map(|exp| {
let now = jwt_simple::prelude::Clock::now_since_epoch().as_secs();
Duration::from_secs(exp.saturating_sub(now).max(1))
})
.unwrap_or_else(|| Duration::from_secs(3600));
let mut custom = claims.clone();
if let Some(obj) = custom.as_object_mut() {
for key in ["sub", "iss", "aud", "exp", "iat", "nbf", "jti"] {
obj.remove(key);
}
}
let mut builder = Claims::with_custom_claims(custom, duration);
if let Some(sub) = claims.get("sub").and_then(|v| v.as_str()) {
builder = builder.with_subject(sub);
}
if let Some(iss) = claims.get("iss").and_then(|v| v.as_str()) {
builder = builder.with_issuer(iss);
}
if let Some(aud) = claims.get("aud").and_then(|v| v.as_str()) {
builder = builder.with_audience(aud);
}
match self {
Self::Ed25519(kp) => kp
.sign(builder)
.map_err(|e| JwtError::GenerationError(e.to_string())),
Self::RS256(kp) => kp
.sign(builder)
.map_err(|e| JwtError::GenerationError(e.to_string())),
Self::ES256(kp) => kp
.sign(builder)
.map_err(|e| JwtError::GenerationError(e.to_string())),
}
}
pub fn public_key_pem(&self) -> Result<String, JwtError> {
match self {
Self::Ed25519(kp) => Ok(kp.public_key().to_pem()),
Self::RS256(kp) => kp.public_key().to_pem()
.map_err(|e| JwtError::GenerationError(e.to_string())),
Self::ES256(kp) => kp.public_key().to_pem()
.map_err(|e| JwtError::GenerationError(e.to_string())),
}
}
pub fn to_pem(&self) -> Result<String, JwtError> {
match self {
Self::Ed25519(kp) => Ok(kp.to_pem()),
Self::RS256(kp) => kp.to_pem()
.map_err(|e| JwtError::GenerationError(e.to_string())),
Self::ES256(kp) => kp.to_pem()
.map_err(|e| JwtError::GenerationError(e.to_string())),
}
}
pub fn from_pem(pem: &str) -> Result<Self, JwtError> {
use jwt_simple::prelude::Ed25519KeyPair;
if let Ok(kp) = Ed25519KeyPair::from_pem(pem) {
return Ok(Self::Ed25519(kp));
}
Err(JwtError::InvalidPrivateKey(
"could not parse PEM as Ed25519, RS256, or ES256 key".into(),
))
}
}
fn decode_header_algorithm(token: &str) -> Result<String, JwtError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(JwtError::InvalidTokenFormat);
}
let payload = base64_decode(parts[0])?;
let header: serde_json::Value =
serde_json::from_slice(&payload).map_err(|e| JwtError::Json(e))?;
header
.get("alg")
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| JwtError::AlgorithmNotAllowed("missing alg in header".into()))
}
fn to_verified_claims(
claims: &jwt_simple::prelude::JWTClaims<serde_json::Value>,
) -> Result<VerifiedClaims, JwtError> {
let sub = claims
.subject
.clone()
.ok_or(JwtError::InvalidSubject)?;
let raw: serde_json::Value =
serde_json::to_value(claims).map_err(|e| JwtError::Json(e))?;
let scope = raw
.get("scope")
.and_then(|v| v.as_str())
.map(String::from);
let custom = if let serde_json::Value::Object(map) = raw {
let standard_keys = [
"sub", "iss", "aud", "exp", "iat", "nbf", "jti", "nonce",
];
map.into_iter()
.filter(|(k, _)| !standard_keys.contains(&k.as_str()))
.collect()
} else {
serde_json::Map::new()
};
let unix_ts_to_datetime = |ts: Option<jwt_simple::prelude::UnixTimeStamp>| {
ts.map(|t| DateTime::from_timestamp(t.as_secs() as i64, 0))
.flatten()
};
Ok(VerifiedClaims {
sub,
iss: claims.issuer.clone().unwrap_or_default(),
aud: match claims.audiences.as_ref() {
Some(jwt_simple::prelude::Audiences::AsString(s)) => s.clone(),
Some(jwt_simple::prelude::Audiences::AsSet(set)) => {
set.iter().next().cloned().unwrap_or_default()
}
None => String::new(),
},
exp: unix_ts_to_datetime(claims.expires_at).unwrap_or_else(Utc::now),
iat: unix_ts_to_datetime(claims.issued_at).unwrap_or_else(Utc::now),
nbf: unix_ts_to_datetime(claims.invalid_before),
jti: claims.jwt_id.clone(),
scope,
nonce: claims.nonce.clone(),
custom,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtToken {
pub token: ConfidentialText,
pub refresh_token: Option<ConfidentialText>,
pub expires_at: i64,
pub scope: Option<String>,
pub audience: Option<String>,
pub issuer: Option<String>,
pub created_at: i64,
}
impl JwtToken {
#[must_use]
pub fn from_parts(
token: String,
refresh_token: Option<String>,
expires_at: i64,
scope: Option<String>,
audience: Option<String>,
issuer: Option<String>,
) -> Self {
let now = Utc::now().timestamp();
Self {
token: ConfidentialText::new(token),
refresh_token: refresh_token.map(ConfidentialText::new),
expires_at,
scope,
audience,
issuer,
created_at: now,
}
}
pub fn from_token(token: String) -> Result<Self, JwtError> {
let claims = decode_claims(&token)?;
let expires_at = claims.exp.ok_or(JwtError::MissingExpiration)?;
Ok(Self {
token: ConfidentialText::new(token),
refresh_token: None, expires_at,
scope: claims
.custom
.get("scope")
.and_then(|v| v.as_str().map(String::from)),
audience: claims
.custom
.get("aud")
.and_then(|v| v.as_str().map(String::from)),
issuer: claims
.custom
.get("iss")
.and_then(|v| v.as_str().map(String::from)),
created_at: Utc::now().timestamp(),
})
}
#[must_use]
pub fn with_refresh_token(mut self, refresh_token: String) -> Self {
self.refresh_token = Some(ConfidentialText::new(refresh_token));
self
}
#[must_use]
pub fn is_expired(&self) -> bool {
let now = Utc::now().timestamp();
now >= self.expires_at
}
#[must_use]
pub fn expires_within(&self, buffer_seconds: i64) -> bool {
let now = Utc::now().timestamp();
now + buffer_seconds >= self.expires_at
}
#[must_use]
pub fn expires_in(&self) -> i64 {
let now = Utc::now().timestamp();
(self.expires_at - now).max(0)
}
#[must_use]
pub fn refresh_token(&self) -> Option<String> {
self.refresh_token.as_ref().map(ConfidentialText::get)
}
#[must_use]
pub fn access_token(&self) -> String {
self.token.get()
}
}
pub struct JwtManager {
token: Option<JwtToken>,
refresh_buffer: i64,
storage_key: String,
}
impl Default for JwtManager {
fn default() -> Self {
Self::new()
}
}
impl JwtManager {
#[must_use]
pub fn new() -> Self {
Self {
token: None,
refresh_buffer: 300, storage_key: String::from("jwt:token"),
}
}
#[must_use]
pub fn with_refresh_buffer(mut self, buffer_seconds: i64) -> Self {
self.refresh_buffer = buffer_seconds;
self
}
#[must_use]
pub fn with_storage_key(mut self, key: impl Into<String>) -> Self {
self.storage_key = key.into();
self
}
pub fn set_token(&mut self, token: JwtToken) {
self.token = Some(token);
}
#[must_use]
pub fn get_token(&self) -> Option<&JwtToken> {
self.token.as_ref()
}
pub fn clear_token(&mut self) {
self.token = None;
}
pub fn get_valid_token<F>(&mut self, refresh_fn: F) -> Result<String, JwtError>
where
F: FnOnce(String) -> Result<JwtToken, JwtError>,
{
let Some(token) = &self.token else {
return Err(JwtError::NoToken);
};
if token.is_expired() || token.expires_within(self.refresh_buffer) {
let Some(refresh_token) = token.refresh_token() else {
return Err(JwtError::NoRefreshToken);
};
let new_token = refresh_fn(refresh_token)?;
self.token = Some(new_token);
}
self.token
.as_ref()
.map(JwtToken::access_token)
.ok_or(JwtError::NoToken)
}
pub fn refresh_if_needed<F>(&mut self, refresh_fn: F) -> Result<bool, JwtError>
where
F: FnOnce(String) -> Result<JwtToken, JwtError>,
{
let Some(token) = &self.token else {
return Ok(false);
};
if !token.is_expired() && !token.expires_within(self.refresh_buffer) {
return Ok(false); }
let Some(refresh_token) = token.refresh_token() else {
return Err(JwtError::NoRefreshToken);
};
let new_token = refresh_fn(refresh_token)?;
self.token = Some(new_token);
Ok(true)
}
#[must_use]
pub fn has_valid_token(&self) -> bool {
self.token
.as_ref()
.is_some_and(|t| !t.is_expired() && !t.expires_within(self.refresh_buffer))
}
#[must_use]
pub fn storage_key(&self) -> &str {
&self.storage_key
}
}
fn decode_claims(token: &str) -> Result<Claims, JwtError> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err(JwtError::InvalidTokenFormat);
}
let payload = base64_decode(parts[1])?;
let claims: Claims = serde_json::from_slice(&payload)?;
Ok(claims)
}
fn base64_decode(input: &str) -> Result<Vec<u8>, JwtError> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
URL_SAFE_NO_PAD
.decode(input)
.map_err(|_| JwtError::DecodeError)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
#[serde(rename = "iss", skip_serializing_if = "Option::is_none")]
pub iss: Option<String>,
#[serde(rename = "sub", skip_serializing_if = "Option::is_none")]
pub sub: Option<String>,
#[serde(rename = "aud", skip_serializing_if = "Option::is_none")]
pub aud: Option<String>,
#[serde(rename = "exp", skip_serializing_if = "Option::is_none")]
pub exp: Option<i64>,
#[serde(rename = "nbf", skip_serializing_if = "Option::is_none")]
pub nbf: Option<i64>,
#[serde(rename = "iat", skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
#[serde(rename = "jti", skip_serializing_if = "Option::is_none")]
pub jti: Option<String>,
#[serde(flatten)]
pub custom: serde_json::Map<String, serde_json::Value>,
}
impl Claims {
#[must_use]
pub fn expires_at(&self) -> Option<DateTime<Utc>> {
self.exp.and_then(|ts| DateTime::from_timestamp(ts, 0))
}
}
#[derive(derive_more::From, Debug)]
pub enum JwtError {
NoToken,
NoRefreshToken,
#[from(ignore)]
RefreshFailed(String),
InvalidTokenFormat,
TokenExpired,
MissingExpiration,
#[from(ignore)]
DecodeError,
Json(serde_json::Error),
#[from(ignore)]
GenerationError(String),
#[from(ignore)]
SignatureVerificationFailed(String),
#[from(ignore)]
AlgorithmNotAllowed(String),
#[from(ignore)]
InvalidIssuer(String),
#[from(ignore)]
InvalidAudience(String),
#[from(ignore)]
TokenNotYetValid(String),
#[from(ignore)]
InvalidPublicKey(String),
#[from(ignore)]
InvalidPrivateKey(String),
#[from(ignore)]
InvalidSubject,
}
impl core::fmt::Display for JwtError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
JwtError::NoToken => write!(f, "No JWT token available"),
JwtError::NoRefreshToken => write!(f, "No refresh token available"),
JwtError::RefreshFailed(s) => write!(f, "Token refresh failed: {s}"),
JwtError::InvalidTokenFormat => write!(f, "Invalid JWT token format"),
JwtError::TokenExpired => write!(f, "JWT token is expired"),
JwtError::MissingExpiration => write!(f, "JWT token missing expiration claim"),
JwtError::DecodeError => write!(f, "Base64 decode error"),
JwtError::Json(e) => write!(f, "JSON parse error: {e}"),
JwtError::GenerationError(s) => write!(f, "Token generation error: {s}"),
JwtError::SignatureVerificationFailed(s) => {
write!(f, "JWT signature verification failed: {s}")
}
JwtError::AlgorithmNotAllowed(s) => {
write!(f, "JWT algorithm not allowed: {s}")
}
JwtError::InvalidIssuer(s) => write!(f, "JWT invalid issuer: {s}"),
JwtError::InvalidAudience(s) => write!(f, "JWT invalid audience: {s}"),
JwtError::TokenNotYetValid(s) => write!(f, "JWT not yet valid: {s}"),
JwtError::InvalidPublicKey(s) => write!(f, "JWT invalid public key: {s}"),
JwtError::InvalidPrivateKey(s) => write!(f, "JWT invalid private key: {s}"),
JwtError::InvalidSubject => write!(f, "JWT missing subject claim"),
}
}
}
impl std::error::Error for JwtError {}