use std::marker::PhantomData;
use http::StatusCode;
use jsonwebtoken::{decode, Algorithm, DecodingKey, TokenData, Validation};
use serde::de::DeserializeOwned;
use crate::{
extract::auth::parse_bearer, extract::auth::BearerTokenError, middleware::auth::Authenticator,
Request,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum KeyFamily {
Hmac,
Rsa,
Ec,
}
impl KeyFamily {
const fn permits(self, algorithm: Algorithm) -> bool {
match self {
Self::Hmac => matches!(
algorithm,
Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
),
Self::Rsa => matches!(
algorithm,
Algorithm::RS256
| Algorithm::RS384
| Algorithm::RS512
| Algorithm::PS256
| Algorithm::PS384
| Algorithm::PS512
),
Self::Ec => matches!(algorithm, Algorithm::ES256 | Algorithm::ES384),
}
}
}
#[derive(Clone)]
pub struct JwtConfig {
decoding_key: DecodingKey,
validation: Validation,
key_family: KeyFamily,
}
impl std::fmt::Debug for JwtConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwtConfig")
.field("validation", &self.validation)
.finish_non_exhaustive()
}
}
impl JwtConfig {
#[must_use]
pub fn with_secret(secret: &[u8]) -> Self {
Self {
decoding_key: DecodingKey::from_secret(secret),
validation: Validation::default(),
key_family: KeyFamily::Hmac,
}
}
pub fn with_rsa_pem(pem: &[u8]) -> Result<Self, jsonwebtoken::errors::Error> {
Ok(Self {
decoding_key: DecodingKey::from_rsa_pem(pem)?,
validation: {
let mut v = Validation::default();
v.algorithms = vec![Algorithm::RS256];
v
},
key_family: KeyFamily::Rsa,
})
}
pub fn with_ec_pem(pem: &[u8]) -> Result<Self, jsonwebtoken::errors::Error> {
Ok(Self {
decoding_key: DecodingKey::from_ec_pem(pem)?,
validation: {
let mut v = Validation::default();
v.algorithms = vec![Algorithm::ES256];
v
},
key_family: KeyFamily::Ec,
})
}
#[must_use]
pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
self.validation.set_issuer(&[issuer.into()]);
self
}
#[must_use]
pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
self.validation.set_audience(&[audience.into()]);
self
}
#[must_use]
pub fn with_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
for algorithm in &algorithms {
assert!(
self.key_family.permits(*algorithm),
"JWT algorithm {algorithm:?} is incompatible with the configured {:?} key",
self.key_family
);
}
self.validation.algorithms = algorithms;
self
}
#[must_use]
pub const fn without_expiration_validation(mut self) -> Self {
self.validation.validate_exp = false;
self
}
#[must_use]
pub const fn with_leeway(mut self, leeway: u64) -> Self {
self.validation.leeway = leeway;
self
}
fn decode<C: DeserializeOwned>(
&self,
token: &str,
) -> Result<TokenData<C>, jsonwebtoken::errors::Error> {
decode::<C>(token, &self.decoding_key, &self.validation)
}
}
#[skyzen::error(status = StatusCode::UNAUTHORIZED)]
pub enum JwtError {
#[error("Invalid Authorization header")]
Header(#[from] BearerTokenError),
#[error("Invalid token signature")]
InvalidSignature,
#[error("Unsupported or disallowed token algorithm")]
InvalidAlgorithm,
#[error("Token has expired")]
Expired,
#[error("Token is not yet valid")]
NotYetValid,
#[error("Invalid token claims")]
InvalidClaims,
#[error("Malformed token")]
Malformed,
}
impl From<jsonwebtoken::errors::Error> for JwtError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
use jsonwebtoken::errors::ErrorKind;
match err.kind() {
ErrorKind::InvalidSignature => Self::InvalidSignature,
ErrorKind::ExpiredSignature => Self::Expired,
ErrorKind::ImmatureSignature => Self::NotYetValid,
ErrorKind::InvalidIssuer
| ErrorKind::InvalidAudience
| ErrorKind::InvalidSubject
| ErrorKind::MissingRequiredClaim(_) => Self::InvalidClaims,
ErrorKind::InvalidAlgorithm
| ErrorKind::InvalidAlgorithmName
| ErrorKind::MissingAlgorithm => Self::InvalidAlgorithm,
_ => Self::Malformed,
}
}
}
#[derive(Clone)]
pub struct JwtAuthenticator<C> {
config: JwtConfig,
_claims: PhantomData<fn() -> C>,
}
impl<C> std::fmt::Debug for JwtAuthenticator<C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwtAuthenticator")
.field("config", &self.config)
.finish()
}
}
impl<C> JwtAuthenticator<C> {
#[must_use]
pub const fn new(config: JwtConfig) -> Self {
Self {
config,
_claims: PhantomData,
}
}
}
impl<C> Authenticator for JwtAuthenticator<C>
where
C: DeserializeOwned + Clone + Send + Sync + 'static,
{
type User = C;
type Error = JwtError;
async fn authenticate(&self, req: &Request) -> Result<Self::User, Self::Error> {
let token = parse_bearer(req)?;
let token_data = self.config.decode::<C>(token)?;
Ok(token_data.claims)
}
}
#[cfg(test)]
mod tests {
use http::header::AUTHORIZATION;
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use super::{JwtAuthenticator, JwtConfig, JwtError};
use crate::extract::auth::BearerTokenError;
use crate::middleware::auth::Authenticator;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct TestClaims {
sub: String,
exp: u64,
roles: Vec<String>,
}
fn create_token(claims: &TestClaims, secret: &[u8]) -> String {
encode(
&Header::default(),
claims,
&EncodingKey::from_secret(secret),
)
.unwrap()
}
#[tokio::test]
async fn test_jwt_authentication_success() {
let secret = b"test-secret-key";
let claims = TestClaims {
sub: "user123".to_owned(),
exp: u64::MAX, roles: vec!["user".to_owned()],
};
let token = create_token(&claims, secret);
let config = JwtConfig::with_secret(secret);
let authenticator = JwtAuthenticator::<TestClaims>::new(config);
let request = http::Request::builder()
.header(AUTHORIZATION, format!("Bearer {token}"))
.body(http_kit::Body::empty())
.unwrap();
let result = authenticator.authenticate(&request).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), claims);
}
#[tokio::test]
async fn test_jwt_missing_header() {
let config = JwtConfig::with_secret(b"secret");
let authenticator = JwtAuthenticator::<TestClaims>::new(config);
let request = http::Request::builder()
.body(http_kit::Body::empty())
.unwrap();
let result = authenticator.authenticate(&request).await;
assert!(matches!(
result,
Err(JwtError::Header(BearerTokenError::MissingHeader))
));
}
#[tokio::test]
async fn test_jwt_invalid_signature() {
let claims = TestClaims {
sub: "user123".to_owned(),
exp: u64::MAX,
roles: vec![],
};
let token = create_token(&claims, b"secret1");
let config = JwtConfig::with_secret(b"secret2");
let authenticator = JwtAuthenticator::<TestClaims>::new(config);
let request = http::Request::builder()
.header(AUTHORIZATION, format!("Bearer {token}"))
.body(http_kit::Body::empty())
.unwrap();
let result = authenticator.authenticate(&request).await;
assert!(matches!(result, Err(JwtError::InvalidSignature)));
}
#[tokio::test]
async fn test_jwt_expired() {
let secret = b"secret";
let claims = TestClaims {
sub: "user123".to_owned(),
exp: 0, roles: vec![],
};
let token = create_token(&claims, secret);
let config = JwtConfig::with_secret(secret);
let authenticator = JwtAuthenticator::<TestClaims>::new(config);
let request = http::Request::builder()
.header(AUTHORIZATION, format!("Bearer {token}"))
.body(http_kit::Body::empty())
.unwrap();
let result = authenticator.authenticate(&request).await;
assert!(matches!(result, Err(JwtError::Expired)));
}
#[tokio::test]
async fn test_jwt_not_bearer() {
let config = JwtConfig::with_secret(b"secret");
let authenticator = JwtAuthenticator::<TestClaims>::new(config);
let request = http::Request::builder()
.header(AUTHORIZATION, "Basic dXNlcjpwYXNz")
.body(http_kit::Body::empty())
.unwrap();
let result = authenticator.authenticate(&request).await;
assert!(matches!(
result,
Err(JwtError::Header(BearerTokenError::NotBearer))
));
}
#[tokio::test]
async fn test_jwt_accepts_case_insensitive_scheme() {
let secret = b"test-secret-key";
let claims = TestClaims {
sub: "user123".to_owned(),
exp: u64::MAX,
roles: vec![],
};
let token = create_token(&claims, secret);
let authenticator = JwtAuthenticator::<TestClaims>::new(JwtConfig::with_secret(secret));
let request = http::Request::builder()
.header(AUTHORIZATION, format!("bearer {token}"))
.body(http_kit::Body::empty())
.unwrap();
assert_eq!(authenticator.authenticate(&request).await.unwrap(), claims);
}
#[test]
#[should_panic(expected = "incompatible with the configured Hmac key")]
fn test_with_algorithms_rejects_cross_family() {
let _ =
JwtConfig::with_secret(b"secret").with_algorithms(vec![jsonwebtoken::Algorithm::RS256]);
}
}