pub mod error;
use clap::ValueEnum;
use jsonwebtoken::{EncodingKey, Header};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "server-side")]
use std::collections::HashSet;
use std::vec;
use uuid::Uuid;
use validator::{Validate, ValidationError};
#[cfg(feature = "server-side")]
use base64::Engine;
#[cfg(feature = "server-side")]
use glob::Pattern;
#[cfg(feature = "server-side")]
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use crate::error::AuthError;
pub struct JwtEncoder {
pub encoding_key: HashMap<String, EncodingKey>,
}
#[cfg(feature = "server-side")]
pub struct JwtDecoder {
#[cfg(feature = "server-side")]
decoding_keys: HashMap<(String, String), DecodingKey>,
#[cfg(feature = "server-side")]
validation: Validation,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Jwt<P> {
pub iss: String,
pub aud: Vec<String>,
pub exp: i64,
pub nbf: i64,
pub iat: i64,
pub jti: Uuid,
pub load: P,
}
#[derive(Serialize, Deserialize, Validate, Clone, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Permission {
pub methods: Vec<HttpMethod>,
#[validate(length(max = 128))]
pub resource_pattern: Option<String>,
pub max_size: Option<usize>,
#[validate(custom(function = "Self::validate_content_type_pattern"))]
pub allowed_content_types: Vec<String>,
}
#[cfg(feature = "server-side")]
pub struct CompiledPermission {
pub methods: HashSet<HttpMethod>,
pub resource_pattern: Option<String>,
pub max_size: Option<usize>,
pub allowed_content_types: Vec<String>,
resource_pattern_cache: Option<Pattern>,
allowed_content_types_cache: Vec<Pattern>,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug, ValueEnum)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
Trace,
Connect,
Other,
All,
Safe,
Unsafe,
}
impl JwtEncoder {
pub fn new(encoding_key: HashMap<String, EncodingKey>) -> Self {
Self { encoding_key }
}
#[inline]
pub fn encode<P: Serialize>(
&self,
header: &Header,
claims: &Jwt<P>,
kid: &str,
) -> Result<String, AuthError> {
use AuthError::InternalError;
let key = self
.encoding_key
.get(kid)
.ok_or(InternalError("No such kid found in your encoder".into()))?;
Ok(jsonwebtoken::encode(header, claims, key)?)
}
}
#[cfg(feature = "server-side")]
impl JwtDecoder {
pub fn new<T: ToString, U: ToString>(
mapping: HashMap<(String, String), DecodingKey>,
algorithms: &[Algorithm],
iss: &[T],
aud: &[U],
) -> Self {
let mut validation =
Validation::new(*algorithms.first().expect(
"You should provide at least one algorithm in your accepted algorithm slice!",
));
validation.validate_aud = true;
validation.validate_exp = true;
validation.validate_nbf = true;
validation.algorithms = algorithms.to_vec();
validation.reject_tokens_expiring_in_less_than = 0;
validation.leeway = 60;
validation.set_issuer(iss);
validation.set_audience(aud);
validation.set_required_spec_claims(&["aud", "exp", "nbf", "iss"]);
Self {
decoding_keys: mapping,
validation,
}
}
pub fn iss_kid_dec(mut self, mapping: HashMap<(String, String), DecodingKey>) -> Self {
self.decoding_keys = mapping;
self
}
pub fn algorithms(mut self, algorithms: &[Algorithm]) -> Self {
self.validation.algorithms = algorithms.to_vec();
self
}
#[inline]
pub fn authorized_issuer<T: ToString>(mut self, iss: &[T]) -> Self {
self.validation.set_issuer(iss);
self
}
#[inline]
pub fn possible_audience<T: ToString>(mut self, aud: &[T]) -> Self {
self.validation.set_audience(aud);
self
}
#[inline]
pub fn leeway(mut self, leeway: u64) -> Self {
self.validation.leeway = leeway;
self
}
#[inline]
pub fn reject_tokens_expiring_in_less_than(mut self, tolerance: u64) -> Self {
self.validation.reject_tokens_expiring_in_less_than = tolerance;
self
}
#[cfg(feature = "server-side")]
pub fn decode<P>(&self, token: &str) -> Result<Jwt<P>, AuthError>
where
for<'de> P: Deserialize<'de>,
{
let kid = jsonwebtoken::decode_header(token)?
.kid
.ok_or(AuthError::MissingClaim("kid".to_string()))?;
let body_unchecked: Jwt<P> = serde_json::from_value(Self::decode_unchecked(token)?)?;
let key = self
.decoding_keys
.get(&(body_unchecked.iss, kid))
.ok_or(AuthError::InvalidIssuer)?;
Ok(jsonwebtoken::decode::<Jwt<P>>(token, key, &self.validation)?.claims)
}
#[cfg(feature = "server-side")]
pub fn decode_unchecked(token: &str) -> Result<serde_json::Value, AuthError> {
let mut parts = token.split('.');
let _header = parts.next();
let payload = parts.next().ok_or(AuthError::InvalidToken)?;
let decoded_payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(payload)?;
let json_value = serde_json::from_slice(&decoded_payload)?;
Ok(json_value)
}
}
impl<P: Serialize + for<'de> Deserialize<'de>> Jwt<P> {
#[inline]
pub fn new<T: ToString, U: ToString>(iss: T, aud: &[U], payload: P) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
iss: iss.to_string(),
aud: aud.iter().map(|s| s.to_string()).collect(),
exp: now + 3600,
nbf: now,
iat: now,
jti: Uuid::new_v4(),
load: payload,
}
}
#[inline]
pub fn expires_in(mut self, duration: chrono::Duration) -> Self {
self.exp = (chrono::Utc::now() + duration).timestamp();
self
}
#[inline]
pub fn expires_at<T>(mut self, when: chrono::DateTime<T>) -> Self
where
T: chrono::TimeZone,
{
self.exp = when.timestamp();
self
}
#[inline]
pub fn never_expires(mut self) -> Self {
self.exp = i32::MAX as i64;
self
}
#[inline]
pub fn not_valid_in(mut self, duration: chrono::Duration) -> Self {
self.nbf = (chrono::Utc::now() + duration).timestamp();
self
}
#[inline]
pub fn not_valid_till<T>(mut self, when: chrono::DateTime<T>) -> Self
where
T: chrono::TimeZone,
{
self.nbf = when.timestamp();
self
}
#[inline]
pub fn uuid(mut self, id: Uuid) -> Self {
self.jti = id;
self
}
}
impl Default for Permission {
fn default() -> Self {
Self::new_minimum()
}
}
impl Permission {
fn validate_content_type_pattern(patterns: &[String]) -> Result<(), ValidationError> {
if patterns.len() <= 8 && patterns.iter().all(|s| s.len() <= 128) {
Ok(())
} else {
Err(ValidationError::new("pattern too long/much for parsing"))
}
}
#[inline]
pub const fn new() -> Self {
Self::new_minimum()
}
pub fn new_root() -> Self {
Self {
methods: vec![HttpMethod::All],
resource_pattern: Some("*".to_string()),
max_size: None,
allowed_content_types: vec!["*".to_string()],
}
}
pub const fn new_minimum() -> Self {
Self {
methods: vec![],
resource_pattern: None,
max_size: Some(0),
allowed_content_types: vec![],
}
}
#[inline]
pub fn permit_method(mut self, methods: Vec<HttpMethod>) -> Self {
self.methods = methods;
self
}
#[inline]
pub fn permit_resource_pattern<T>(mut self, pattern: T) -> Self
where
T: Into<String>,
{
self.resource_pattern = Some(pattern.into());
self
}
#[inline]
pub fn permit_resource_pattern_option<T>(mut self, pattern: Option<T>) -> Self
where
T: Into<String>,
{
self.resource_pattern = pattern.map(T::into);
self
}
#[inline]
pub fn restrict_maximum_size(mut self, max: usize) -> Self {
self.max_size = Some(max);
self
}
#[inline]
pub fn restrict_maximum_size_option(mut self, max: Option<usize>) -> Self {
self.max_size = max;
self
}
#[inline]
pub fn permit_content_type(mut self, content_type: Vec<String>) -> Self {
self.allowed_content_types = content_type;
self
}
#[cfg(feature = "server-side")]
pub fn compile(self) -> CompiledPermission {
let Permission {
methods,
resource_pattern,
max_size,
allowed_content_types,
} = self;
let resource_pattern_cache = match &resource_pattern {
Some(pat) => Pattern::new(pat).ok(),
None => None,
};
let mut allowed_content_types_cache = vec![];
for pat in &allowed_content_types {
if let Ok(pat) = Pattern::new(pat) {
allowed_content_types_cache.push(pat)
}
}
CompiledPermission {
methods: methods.iter().copied().collect(),
resource_pattern,
max_size,
allowed_content_types,
resource_pattern_cache,
allowed_content_types_cache,
}
}
}
#[cfg(feature = "server-side")]
impl CompiledPermission {
pub fn can_perform_method(&self, method: HttpMethod) -> bool {
self.methods.contains(&HttpMethod::All)
|| self.methods.contains(&method)
|| (self.methods.contains(&HttpMethod::Safe) && method.safe())
|| (self.methods.contains(&HttpMethod::Unsafe) && !method.safe())
}
pub fn can_access(&self, path: &str) -> bool {
match &self.resource_pattern_cache {
Some(pat) => pat.matches(path),
None => false,
}
}
pub fn check_size(&self, size: usize) -> bool {
self.max_size.is_none_or(|limit| size <= limit)
}
pub fn check_content_type(&self, content_type: &str) -> bool {
self.allowed_content_types_cache
.iter()
.any(|allow_pat| allow_pat.matches(content_type))
}
}
impl From<&axum::http::Method> for HttpMethod {
fn from(value: &axum::http::Method) -> Self {
use axum::http::Method;
match *value {
Method::GET => Self::Get,
Method::POST => Self::Post,
Method::PUT => Self::Put,
Method::PATCH => Self::Patch,
Method::DELETE => Self::Delete,
Method::HEAD => Self::Head,
Method::OPTIONS => Self::Options,
Method::TRACE => Self::Trace,
Method::CONNECT => Self::Connect,
_ => Self::Other,
}
}
}
impl From<axum::http::Method> for HttpMethod {
fn from(value: axum::http::Method) -> Self {
Self::from(&value)
}
}
impl HttpMethod {
pub fn safe(self) -> bool {
match self {
HttpMethod::Safe
| HttpMethod::Connect
| HttpMethod::Get
| HttpMethod::Head
| HttpMethod::Options
| HttpMethod::Trace => true,
HttpMethod::Unsafe
| HttpMethod::Post
| HttpMethod::Put
| HttpMethod::Patch
| HttpMethod::Delete
| HttpMethod::Other
| HttpMethod::All => false,
}
}
}