pub mod error;
use std::{collections::HashMap, sync::Arc};
use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD};
use clap::ValueEnum;
use glob::Pattern;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
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, Debug)]
pub struct Jwt<P> {
pub iss: Option<String>,
pub aud: Vec<String>,
pub exp: i64,
pub nbf: i64,
pub iat: i64,
pub jti: Uuid,
#[serde(flatten)]
pub payload: P,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Permission {
pub methods: Vec<HttpMethod>,
pub resource_pattern: Option<String>,
pub max_size: Option<u64>,
pub allowed_content_types: Vec<String>,
}
#[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,
}
impl<P: Serialize + for<'de> Deserialize<'de>> Jwt<P> {
#[inline]
pub fn encode(claims: &Jwt<P>, config: &JwtConfig) -> Result<String, AuthError> {
Ok(jsonwebtoken::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(jsonwebtoken::decode::<Jwt<P>>(token, key, &config.validation)?.claims)
}
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::TokenInvalid)?;
let decoded_payload = BASE64_STANDARD_NO_PAD.decode(payload)?;
let json_value = serde_json::from_slice(&decoded_payload).map_err(Arc::new)?;
Ok(json_value)
}
#[inline]
pub fn new(payload: P) -> Self {
Self {
iss: None,
aud: vec![],
exp: i32::MAX as i64,
nbf: 0,
iat: chrono::Utc::now().timestamp(),
jti: Uuid::new_v4(),
payload,
}
}
#[inline]
pub fn issue_as<T>(mut self, iss: T) -> Self
where
T: Into<String>,
{
self.iss = Some(iss.into());
self
}
#[inline]
pub fn issue_as_option<T>(mut self, iss: Option<T>) -> Self
where
T: Into<String>,
{
self.iss = iss.map(|val| val.into());
self
}
#[inline]
pub fn audiences<'a, T>(mut self, aud: &'a [T]) -> Self
where
String: From<&'a T>,
{
self.aud = aud.iter().map(|aud| aud.into()).collect();
self
}
#[inline]
pub fn audiences_option<'a, T>(mut self, aud: Option<&'a [T]>) -> Self
where
String: From<&'a T>,
{
self.aud = aud
.map(|aud| aud.iter().map(String::from).collect())
.unwrap_or_default();
self
}
#[inline]
pub fn expires_in(mut self, duration: chrono::Duration) -> Self {
self.exp = (chrono::Utc::now() + duration).timestamp();
self
}
#[inline]
pub fn not_valid_in(mut self, duration: chrono::Duration) -> Self {
self.nbf = (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 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 Permission {
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 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_access_url<T>(mut self, pattern: T) -> Self
where
T: Into<String>,
{
self.resource_pattern = Some(pattern.into());
self
}
#[inline]
pub fn permit_access_url_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: u64) -> Self {
self.max_size = Some(max);
self
}
#[inline]
pub fn restrict_maximum_size_option(mut self, max: Option<u64>) -> 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
}
pub fn can_perform_method<T>(&self, method: T) -> bool
where
T: Into<HttpMethod>,
{
self.methods.contains(&HttpMethod::All) || self.methods.contains(&method.into())
}
pub fn can_access(&self, path: &str) -> bool {
match &self.resource_pattern {
Some(pat) => Pattern::new(pat)
.map(|pattern| pattern.matches(path))
.unwrap_or(false),
None => false,
}
}
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
}
}
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 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 HttpMethod {
pub fn safe(self) -> bool {
match self {
HttpMethod::Connect
| HttpMethod::Get
| HttpMethod::Head
| HttpMethod::Options
| HttpMethod::Trace => true,
HttpMethod::Post
| HttpMethod::Put
| HttpMethod::Patch
| HttpMethod::Delete
| HttpMethod::Other
| HttpMethod::All => false,
}
}
}