pub mod error;
use std::{collections::HashMap, i64, sync::Arc};
use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD};
use clap::ValueEnum;
use glob::Pattern;
use jsonwebtoken::*;
use serde::{Deserialize, Serialize};
use crate::error::AuthError;
pub struct JwtConfig {
pub encoding_key: EncodingKey,
pub decoding_key: HashMap<Algorithm, DecodingKey>,
pub header: Header,
pub validation: Validation,
}
#[derive(Serialize, Deserialize)]
pub struct Jwt<P> {
pub iss: Option<String>,
pub aud: Vec<String>,
pub exp: i64,
pub nbf: i64,
pub iat: i64,
pub jti: u128,
#[serde(flatten)]
pub payload: P,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Permission {
pub operations: Vec<HttpMethod>,
pub resource_pattern: String,
#[serde(default)]
pub conditions: Conditions,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, ValueEnum)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
Trace,
Connect,
Other,
All,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Conditions {
pub max_size: Option<u64>,
pub allowed_content_types: Vec<String>,
}
impl<P: Serialize + for<'de> Deserialize<'de>> Jwt<P> {
pub fn encode(claims: &Jwt<P>, config: &JwtConfig) -> Result<String, AuthError> {
Ok(encode(&config.header, claims, &config.encoding_key)?)
}
pub fn decode(token: &str, config: &JwtConfig) -> Result<Jwt<P>, AuthError> {
let header = jsonwebtoken::decode_header(token)?;
let key = config
.decoding_key
.get(&header.alg)
.ok_or(AuthError::InvalidAlgorithm(header.alg))?;
Ok(decode::<Jwt<P>>(token, key, &config.validation)?.claims)
}
pub fn decode_unchecked(token: &str) -> Result<serde_json::Value, AuthError> {
for (idx, piece) in token.split('.').enumerate() {
if idx == 1 {
let mut decoded = "".to_string();
for chunk in BASE64_STANDARD_NO_PAD.decode(piece)?.utf8_chunks() {
decoded.push_str(chunk.valid());
for bt in chunk.invalid() {
decoded.push_str(&format!("0x{:X}", bt));
}
}
return Ok(serde_json::from_str(&decoded).map_err(|e| Arc::new(e))?);
}
}
Err(AuthError::TokenInvalid)
}
pub fn new(load: P) -> Self {
Self {
iss: None,
aud: vec![],
exp: i64::MAX,
nbf: 0,
iat: chrono::Utc::now().timestamp(),
jti: uuid::Uuid::new_v4().as_u128(),
payload: load,
}
}
#[inline(always)]
pub fn issue_as<T: ToString>(mut self, iss: T) -> Self {
self.iss = Some(iss.to_string());
self
}
#[inline(always)]
pub fn issue_as_option<T: ToString>(mut self, iss: Option<T>) -> Self {
self.iss = iss.map(|val| val.to_string());
self
}
#[inline(always)]
pub fn audiences<T: ToString>(mut self, aud: &[T]) -> Self {
self.aud = aud.iter().map(|aud| aud.to_string()).collect();
self
}
#[inline(always)]
pub fn expires_in(mut self, duration: chrono::Duration) -> Self {
self.exp = (chrono::Utc::now() + duration).timestamp();
self
}
#[inline(always)]
pub fn not_valid_in(mut self, duration: chrono::Duration) -> Self {
self.nbf = (chrono::Utc::now() + duration).timestamp();
self
}
#[inline(always)]
pub fn expires_at<T>(mut self, when: chrono::DateTime<T>) -> Self
where
T: chrono::TimeZone,
{
self.exp = when.timestamp();
self
}
#[inline(always)]
pub fn not_valid_till<T>(mut self, when: chrono::DateTime<T>) -> Self
where
T: chrono::TimeZone
{
self.nbf = when.timestamp();
self
}
}
impl Permission {
pub fn new_root() -> Self {
Self {
operations: vec![HttpMethod::All],
resource_pattern: "*".to_string(),
conditions: Conditions {
max_size: None,
allowed_content_types: vec!["*".to_string()],
},
}
}
pub fn can_perform(&self, method: HttpMethod) -> bool {
self.operations.contains(&HttpMethod::All) || self.operations.contains(&method)
}
pub fn can_access(&self, path: &str) -> bool {
Pattern::new(&self.resource_pattern)
.map(|pattern| pattern.matches(path))
.unwrap_or(false)
}
pub fn check_size(&self, size: u64) -> bool {
self.conditions.check_size(size)
}
pub fn check_content_type(&self, content_type: &str) -> bool {
self.conditions.check_content_type(content_type)
}
}
impl From<&axum::http::Method> for HttpMethod {
fn from(value: &axum::http::Method) -> Self {
match *value {
axum::http::Method::GET => Self::Get,
axum::http::Method::POST => Self::Post,
axum::http::Method::PUT => Self::Put,
axum::http::Method::PATCH => Self::Patch,
axum::http::Method::DELETE => Self::Delete,
axum::http::Method::HEAD => Self::Head,
axum::http::Method::OPTIONS => Self::Options,
axum::http::Method::TRACE => Self::Trace,
axum::http::Method::CONNECT => Self::Connect,
_ => Self::Other,
}
}
}
impl Default for Conditions {
fn default() -> Self {
Self {
max_size: Some(0),
allowed_content_types: vec![],
}
}
}
impl Conditions {
pub fn check_size(&self, size: u64) -> bool {
self.max_size.is_none_or(|limit| size <= limit)
}
pub fn check_content_type(&self, content_type: &str) -> bool {
for allows in &self.allowed_content_types {
if Pattern::new(allows)
.map(|e| e.matches(content_type))
.unwrap_or(false)
{
return true;
}
}
false
}
}