use jsonwebtoken::{decode, DecodingKey, Validation};
use rust_webx_core::auth::{IAuthenticationHandler, IClaims};
use rust_webx_core::error::Result;
use rust_webx_core::http::IHttpContext;
use rust_webx_core::middleware::IMiddleware;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::ControlFlow;
use std::sync::{Arc, OnceLock};
pub struct JwtClaims {
sub: String,
roles: Vec<String>,
permissions: Vec<String>,
raw: HashMap<String, String>,
}
#[derive(Debug, Deserialize, Serialize)]
struct RawClaims {
#[serde(default)]
sub: String,
#[serde(default)]
roles: Vec<String>,
#[serde(default)]
permissions: Vec<String>,
#[serde(flatten)]
extra: HashMap<String, serde_json::Value>,
}
impl JwtClaims {
pub fn new(subject: impl Into<String>) -> Self {
let sub = subject.into();
let mut raw = HashMap::new();
raw.insert("sub".to_string(), sub.clone());
Self {
sub,
roles: Vec::new(),
permissions: Vec::new(),
raw,
}
}
}
impl From<RawClaims> for JwtClaims {
fn from(rc: RawClaims) -> Self {
let mut raw = HashMap::new();
raw.insert("sub".to_string(), rc.sub.clone());
for (k, v) in &rc.extra {
match v {
serde_json::Value::String(s) => {
raw.insert(k.clone(), s.clone());
}
other => {
raw.insert(k.clone(), other.to_string());
}
}
}
Self {
sub: rc.sub,
roles: rc.roles,
permissions: rc.permissions,
raw,
}
}
}
impl IClaims for JwtClaims {
fn subject(&self) -> &str {
&self.sub
}
fn roles(&self) -> &[String] {
&self.roles
}
fn permissions(&self) -> &[String] {
&self.permissions
}
fn claims(&self) -> &HashMap<String, String> {
&self.raw
}
fn clone_box(&self) -> Box<dyn IClaims> {
Box::new(Self {
sub: self.sub.clone(),
roles: self.roles.clone(),
permissions: self.permissions.clone(),
raw: self.raw.clone(),
})
}
}
pub struct JwtAuth {
decoding_key: DecodingKey,
validation: Validation,
}
impl JwtAuth {
pub fn new(decoding_key: DecodingKey, validation: Validation) -> Self {
Self {
decoding_key,
validation,
}
}
}
#[async_trait::async_trait]
impl IAuthenticationHandler for JwtAuth {
async fn authenticate(&self, ctx: &mut dyn IHttpContext) -> Result<Option<Box<dyn IClaims>>> {
let header = match ctx.request().header("authorization") {
Some(h) => h,
None => return Ok(None),
};
let token = if header.len() >= 7 && header[..7].eq_ignore_ascii_case("Bearer ") {
header[7..].trim().to_string()
} else {
return Ok(None);
};
if token.is_empty() {
return Ok(None);
}
let token_data = decode::<RawClaims>(&token, &self.decoding_key, &self.validation)
.map_err(|e| rust_webx_core::error::Error::Unauthorized(format!(
"invalid or expired token: {}", e
)))?;
let claims: JwtClaims = token_data.claims.into();
Ok(Some(Box::new(claims)))
}
}
struct AuthMiddleware {
handler: Arc<dyn IAuthenticationHandler>,
}
#[async_trait::async_trait]
impl IMiddleware for AuthMiddleware {
async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
if let Some(claims) = self.handler.authenticate(ctx).await? {
ctx.set_claims(claims);
}
Ok(ControlFlow::Continue(()))
}
}
pub fn jwt_middleware(handler: Arc<dyn IAuthenticationHandler>) -> impl IMiddleware {
AuthMiddleware { handler }
}
static JWT_ENCODING_SECRET: OnceLock<String> = OnceLock::new();
pub fn init_jwt_secret(secret: &str) {
let _ = JWT_ENCODING_SECRET.set(secret.to_owned());
}
pub fn jwt_secret() -> &'static str {
JWT_ENCODING_SECRET
.get()
.expect("init_jwt_secret must be called before jwt_secret")
}