rust_webx_host/auth_jwt.rs
1//! JWT authentication module for the rust-webx framework.
2//!
3//! Provides `JwtAuth` —an `IAuthenticationHandler` implementation that reads
4//! a Bearer token from the `Authorization` header and validates it using
5//! the `jsonwebtoken` crate.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use rust_webx_host::auth_jwt::{JwtAuth, jwt_middleware};
11//! use jsonwebtoken::{DecodingKey, Validation};
12//!
13//! let auth = JwtAuth::new(
14//! DecodingKey::from_secret(b"my-secret"),
15//! Validation::default(),
16//! );
17//! let middleware = jwt_middleware(Arc::new(auth));
18//! ```
19
20use jsonwebtoken::{decode, DecodingKey, Validation};
21use rust_webx_core::auth::{IAuthenticationHandler, IClaims};
22use rust_webx_core::error::Result;
23use rust_webx_core::http::IHttpContext;
24use rust_webx_core::middleware::IMiddleware;
25use serde::{Deserialize, Serialize};
26use std::collections::HashMap;
27use std::ops::ControlFlow;
28use std::sync::{Arc, OnceLock};
29
30// ---------------------------------------------------------------------------
31// JwtClaims —IClaims implementation backed by JWT payload
32// ---------------------------------------------------------------------------
33
34/// Claims extracted from a JWT token.
35///
36/// By default, looks for `sub`, `roles`, and `permissions` claims.
37/// All other claims are accessible via `claims()` as stringified values.
38pub struct JwtClaims {
39 sub: String,
40 roles: Vec<String>,
41 permissions: Vec<String>,
42 /// Pre-flattened raw claims map for the `claims()` method.
43 raw: HashMap<String, String>,
44}
45
46/// Intermediate deserialization target. After deserialization we convert
47/// into `JwtClaims` which stores pre-computed data for `IClaims`.
48#[derive(Debug, Deserialize, Serialize)]
49struct RawClaims {
50 #[serde(default)]
51 sub: String,
52 #[serde(default)]
53 roles: Vec<String>,
54 #[serde(default)]
55 permissions: Vec<String>,
56 #[serde(flatten)]
57 extra: HashMap<String, serde_json::Value>,
58}
59
60impl JwtClaims {
61 /// Create empty claims with just a subject.
62 pub fn new(subject: impl Into<String>) -> Self {
63 let sub = subject.into();
64 let mut raw = HashMap::new();
65 raw.insert("sub".to_string(), sub.clone());
66 Self {
67 sub,
68 roles: Vec::new(),
69 permissions: Vec::new(),
70 raw,
71 }
72 }
73}
74
75impl From<RawClaims> for JwtClaims {
76 fn from(rc: RawClaims) -> Self {
77 let mut raw = HashMap::new();
78 raw.insert("sub".to_string(), rc.sub.clone());
79 for (k, v) in &rc.extra {
80 match v {
81 serde_json::Value::String(s) => {
82 raw.insert(k.clone(), s.clone());
83 }
84 other => {
85 raw.insert(k.clone(), other.to_string());
86 }
87 }
88 }
89 Self {
90 sub: rc.sub,
91 roles: rc.roles,
92 permissions: rc.permissions,
93 raw,
94 }
95 }
96}
97
98impl IClaims for JwtClaims {
99 fn subject(&self) -> &str {
100 &self.sub
101 }
102
103 fn roles(&self) -> &[String] {
104 &self.roles
105 }
106
107 fn permissions(&self) -> &[String] {
108 &self.permissions
109 }
110
111 fn claims(&self) -> &HashMap<String, String> {
112 &self.raw
113 }
114
115 fn clone_box(&self) -> Box<dyn IClaims> {
116 Box::new(Self {
117 sub: self.sub.clone(),
118 roles: self.roles.clone(),
119 permissions: self.permissions.clone(),
120 raw: self.raw.clone(),
121 })
122 }
123}
124
125// ---------------------------------------------------------------------------
126// JwtAuth —IAuthenticationHandler implementation
127// ---------------------------------------------------------------------------
128
129/// JWT-based authentication handler.
130///
131/// Reads the `Authorization: Bearer <token>` header, decodes and validates
132/// the JWT, and returns the resulting claims.
133pub struct JwtAuth {
134 decoding_key: DecodingKey,
135 validation: Validation,
136}
137
138impl JwtAuth {
139 /// Create a new JWT authenticator.
140 pub fn new(decoding_key: DecodingKey, validation: Validation) -> Self {
141 Self {
142 decoding_key,
143 validation,
144 }
145 }
146}
147
148#[async_trait::async_trait]
149impl IAuthenticationHandler for JwtAuth {
150 async fn authenticate(&self, ctx: &mut dyn IHttpContext) -> Result<Option<Box<dyn IClaims>>> {
151 // Extract token synchronously —avoids holding &dyn IHttpContext
152 // across any async boundary, keeping the future Send.
153 let header = match ctx.request().header("authorization") {
154 Some(h) => h,
155 None => return Ok(None),
156 };
157
158 // RFC 6750: scheme is case-insensitive
159 let token = if header.len() >= 7 && header[..7].eq_ignore_ascii_case("Bearer ") {
160 header[7..].trim().to_string()
161 } else {
162 return Ok(None);
163 };
164
165 if token.is_empty() {
166 return Ok(None);
167 }
168
169 let token_data = decode::<RawClaims>(&token, &self.decoding_key, &self.validation)
170 .map_err(|e| rust_webx_core::error::Error::Unauthorized(format!(
171 "invalid or expired token: {}", e
172 )))?;
173
174 let claims: JwtClaims = token_data.claims.into();
175 Ok(Some(Box::new(claims)))
176 }
177}
178
179// ---------------------------------------------------------------------------
180// Middleware
181// ---------------------------------------------------------------------------
182
183/// Authentication middleware that uses the given `IAuthenticationHandler`
184/// to extract claims from the request and store them in the context.
185struct AuthMiddleware {
186 handler: Arc<dyn IAuthenticationHandler>,
187}
188
189#[async_trait::async_trait]
190impl IMiddleware for AuthMiddleware {
191 async fn invoke(&self, ctx: &mut dyn IHttpContext) -> Result<ControlFlow<()>> {
192 if let Some(claims) = self.handler.authenticate(ctx).await? {
193 // IHttpContext extends IClaimsExt, so set_claims is available directly.
194 ctx.set_claims(claims);
195 }
196 Ok(ControlFlow::Continue(()))
197 }
198}
199
200/// Create a JWT authentication middleware from an authentication handler.
201///
202/// Reads the Bearer token from `Authorization` header, validates it, and
203/// stores the resulting claims in the HTTP context for downstream use.
204pub fn jwt_middleware(handler: Arc<dyn IAuthenticationHandler>) -> impl IMiddleware {
205 AuthMiddleware { handler }
206}
207
208// ---------------------------------------------------------------------------
209// Global JWT encoding secret (for token creation in handlers)
210// ---------------------------------------------------------------------------
211
212static JWT_ENCODING_SECRET: OnceLock<String> = OnceLock::new();
213
214/// Initialize the global JWT encoding secret from the configured secret.
215/// This is called automatically by `.add_authentication()` on the `HostBuilder`,
216/// but can also be called manually if needed.
217///
218/// Idempotent: subsequent calls are no-ops (the first secret wins).
219pub fn init_jwt_secret(secret: &str) {
220 let _ = JWT_ENCODING_SECRET.set(secret.to_owned());
221}
222
223/// Retrieve the global JWT encoding secret previously set via [`init_jwt_secret`].
224///
225/// # Panics
226/// Panics if [`init_jwt_secret`] has not been called yet.
227pub fn jwt_secret() -> &'static str {
228 JWT_ENCODING_SECRET
229 .get()
230 .expect("init_jwt_secret must be called before jwt_secret")
231}