1use async_trait::async_trait;
17use base64::engine::general_purpose::URL_SAFE_NO_PAD;
18use base64::Engine;
19use bytes::Bytes;
20use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
21use std::collections::HashMap;
22use std::sync::Arc;
23
24use crate::consumers::attach_consumer;
25use crate::context::{Context, GatewayError};
26use crate::plugins::resources::PluginResources;
27use crate::plugins::{Plugin, PluginExecutionError, PluginOutput, PluginResult};
28
29fn parse_algorithm(name: Option<&str>) -> Result<Algorithm, String> {
34 match name {
35 None | Some("HS256") => Ok(Algorithm::HS256),
36 Some("HS384") => Ok(Algorithm::HS384),
37 Some("HS512") => Ok(Algorithm::HS512),
38 Some(other) => Err(format!(
39 "Unknown jwt-auth algorithm '{}' — supported: HS256, HS384, HS512",
40 other
41 )),
42 }
43}
44
45pub struct JwtAuthPlugin {
59 secret: Option<String>,
62 algorithm: Algorithm,
64 header_name: String,
66 use_consumers: bool,
69 resources: Arc<PluginResources>,
70}
71
72impl JwtAuthPlugin {
73 pub fn from_config(
97 config: &HashMap<String, serde_json::Value>,
98 resources: &Arc<PluginResources>,
99 ) -> Result<Self, String> {
100 let secret = config
101 .get("secret")
102 .and_then(|v| v.as_str())
103 .map(String::from);
104
105 let use_consumers = config
106 .get("use_consumers")
107 .and_then(|v| v.as_bool())
108 .unwrap_or(false);
109
110 if secret.is_none() && !use_consumers {
111 return Err("jwt-auth plugin requires 'secret' or 'use_consumers: true'".to_string());
112 }
113
114 let algorithm = parse_algorithm(config.get("algorithm").and_then(|v| v.as_str()))?;
115
116 let header_name = config
117 .get("header_name")
118 .and_then(|v| v.as_str())
119 .unwrap_or("authorization")
120 .to_lowercase();
121
122 Ok(Self {
123 secret,
124 algorithm,
125 header_name,
126 use_consumers,
127 resources: resources.clone(),
128 })
129 }
130
131 fn reject(ctx: Context, message: &str) -> PluginResult {
135 let mut ctx = ctx;
136 ctx.response.status_code = 401;
137 ctx.response.body = Bytes::from(format!(
138 r#"{{"error": "unauthorized", "message": "{}"}}"#,
139 message
140 ));
141 ctx.response.headers.insert(
142 "content-type".to_string(),
143 vec!["application/json".to_string()],
144 );
145 Err(PluginExecutionError {
146 context: ctx,
147 error: GatewayError {
148 node_id: String::new(),
149 code: "JWT_INVALID".to_string(),
150 message: message.to_string(),
151 metadata: HashMap::new(),
152 },
153 })
154 }
155
156 fn verify(
159 token: &str,
160 secret: &str,
161 algorithm: Algorithm,
162 ctx: &mut Context,
163 ) -> Result<HashMap<String, serde_json::Value>, String> {
164 let key = DecodingKey::from_secret(secret.as_bytes());
165 let mut validation = Validation::new(algorithm);
166 validation.validate_exp = true;
167
168 match decode::<HashMap<String, serde_json::Value>>(token, &key, &validation) {
169 Ok(token_data) => {
170 ctx.message.insert(
171 "jwt_claims".to_string(),
172 serde_json::to_value(&token_data.claims).unwrap_or_default(),
173 );
174 if let Some(sub) = token_data.claims.get("sub") {
175 ctx.message.insert("user_id".to_string(), sub.clone());
176 }
177 Ok(token_data.claims)
178 }
179 Err(e) => Err(format!("Invalid JWT: {}", e)),
180 }
181 }
182
183 fn peek_key_claim(token: &str) -> Option<String> {
190 let payload = token.split('.').nth(1)?;
191 let decoded = URL_SAFE_NO_PAD.decode(payload).ok()?;
192 let claims: serde_json::Value = serde_json::from_slice(&decoded).ok()?;
193 claims.get("key")?.as_str().map(String::from)
194 }
195}
196
197#[async_trait]
198impl Plugin for JwtAuthPlugin {
199 fn plugin_type(&self) -> &str {
200 "jwt-auth"
201 }
202
203 async fn execute(
204 &self,
205 mut ctx: Context,
206 _named_inputs: &HashMap<String, serde_json::Value>,
207 ) -> PluginResult {
208 let token = ctx
209 .request
210 .headers
211 .get(&self.header_name)
212 .and_then(|v| v.first())
213 .map(|v| {
214 v.strip_prefix("Bearer ")
215 .map(String::from)
216 .unwrap_or_else(|| v.clone())
217 });
218
219 let token = match token {
220 Some(t) => t,
221 None => return Self::reject(ctx, "Missing authorization token"),
222 };
223
224 if let Some(ref secret) = self.secret {
226 match Self::verify(&token, secret, self.algorithm, &mut ctx) {
227 Ok(_) => {
228 return Ok(PluginOutput {
229 context: ctx,
230 named_outputs: HashMap::new(),
231 })
232 }
233 Err(e) if !self.use_consumers => return Self::reject(ctx, &e),
236 Err(_) => {}
237 }
238 }
239
240 if self.use_consumers {
243 let key_claim = match Self::peek_key_claim(&token) {
244 Some(k) => k,
245 None => return Self::reject(ctx, "JWT missing 'key' claim"),
246 };
247 let store = self.resources.consumers.load();
248 if let Some(consumer) = store.find_by_credential("jwt-auth", &key_claim) {
249 let cred = consumer.credentials.get("jwt-auth");
250 let secret = cred.and_then(|c| c.get("secret")).and_then(|v| v.as_str());
251 let algorithm = parse_algorithm(
252 cred.and_then(|c| c.get("algorithm"))
253 .and_then(|v| v.as_str()),
254 );
255 match (secret, algorithm) {
256 (Some(secret), Ok(algorithm)) => {
257 match Self::verify(&token, secret, algorithm, &mut ctx) {
258 Ok(_) => {
259 attach_consumer(&mut ctx, &consumer, "jwt-auth");
260 return Ok(PluginOutput {
261 context: ctx,
262 named_outputs: HashMap::new(),
263 });
264 }
265 Err(e) => return Self::reject(ctx, &e),
266 }
267 }
268 _ => return Self::reject(ctx, "Consumer has no valid jwt-auth secret"),
269 }
270 }
271 return Self::reject(ctx, "Unknown JWT key");
272 }
273
274 Self::reject(ctx, "Invalid JWT")
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::consumers::{ConsumerConfig, ConsumerStore};
282 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
283 use jsonwebtoken::{encode, EncodingKey, Header};
284
285 fn config(pairs: &[(&str, &str)]) -> HashMap<String, serde_json::Value> {
286 pairs
287 .iter()
288 .map(|(k, v)| (k.to_string(), serde_json::Value::String(v.to_string())))
289 .collect()
290 }
291
292 fn ctx_with_token(token: Option<&str>) -> Context {
293 let mut headers = HashMap::new();
294 if let Some(t) = token {
295 headers.insert("authorization".to_string(), vec![format!("Bearer {}", t)]);
296 }
297 Context {
298 request: GatewayRequest {
299 method: "GET".to_string(),
300 path: "/".to_string(),
301 host: "h".to_string(),
302 scheme: "http".to_string(),
303 headers,
304 query_params: HashMap::new(),
305 body: Bytes::new(),
306 remote_addr: "1.2.3.4:5".to_string(),
307 protocol: Protocol::Http1,
308 },
309 response: GatewayResponse {
310 status_code: 0,
311 headers: HashMap::new(),
312 body: Bytes::new(),
313 },
314 message: HashMap::new(),
315 errors: Vec::new(),
316 }
317 }
318
319 fn make_token(claims: serde_json::Value, secret: &str, alg: Algorithm) -> String {
321 encode(
322 &Header::new(alg),
323 &claims,
324 &EncodingKey::from_secret(secret.as_bytes()),
325 )
326 .unwrap()
327 }
328
329 #[test]
330 fn test_requires_secret_or_consumers() {
331 assert!(JwtAuthPlugin::from_config(&HashMap::new(), &PluginResources::empty()).is_err());
332 }
333
334 #[test]
335 fn test_accepts_supported_algorithms() {
336 for alg in ["HS256", "HS384", "HS512"] {
337 let cfg = config(&[("secret", "s3cret"), ("algorithm", alg)]);
338 assert!(
339 JwtAuthPlugin::from_config(&cfg, &PluginResources::empty()).is_ok(),
340 "{alg} should be accepted"
341 );
342 }
343 }
344
345 #[test]
346 fn test_rejects_unknown_algorithm() {
347 for alg in ["RS256", "ES256", "none"] {
350 let cfg = config(&[("secret", "s3cret"), ("algorithm", alg)]);
351 assert!(
352 JwtAuthPlugin::from_config(&cfg, &PluginResources::empty()).is_err(),
353 "{alg} should be rejected"
354 );
355 }
356 }
357
358 #[tokio::test]
359 async fn test_inline_secret_still_works() {
360 let cfg = config(&[("secret", "s3cret")]);
361 let plugin = JwtAuthPlugin::from_config(&cfg, &PluginResources::empty()).unwrap();
362
363 let token = make_token(
364 serde_json::json!({ "sub": "u1", "exp": 9999999999u64 }),
365 "s3cret",
366 Algorithm::HS256,
367 );
368 let out = plugin
369 .execute(ctx_with_token(Some(&token)), &HashMap::new())
370 .await
371 .unwrap();
372 assert_eq!(
373 out.context.message.get("user_id"),
374 Some(&serde_json::json!("u1"))
375 );
376
377 let forged = make_token(
379 serde_json::json!({ "sub": "u1", "exp": 9999999999u64 }),
380 "other",
381 Algorithm::HS256,
382 );
383 assert!(plugin
384 .execute(ctx_with_token(Some(&forged)), &HashMap::new())
385 .await
386 .is_err());
387 }
388
389 fn resources_with_consumers() -> Arc<PluginResources> {
390 let resources = PluginResources::empty();
391 let consumers: Vec<ConsumerConfig> = serde_json::from_value(serde_json::json!([
392 {
393 "name": "alice",
394 "credentials": {
395 "jwt-auth": { "key": "alice-key", "secret": "alice-secret", "algorithm": "HS256" }
396 }
397 }
398 ]))
399 .unwrap();
400 resources
401 .consumers
402 .store(Arc::new(ConsumerStore::from_config(&consumers).unwrap()));
403 resources
404 }
405
406 #[tokio::test]
407 async fn test_consumer_mode_verifies_and_attaches() {
408 let resources = resources_with_consumers();
409 let mut cfg = HashMap::new();
410 cfg.insert("use_consumers".to_string(), serde_json::json!(true));
411 let plugin = JwtAuthPlugin::from_config(&cfg, &resources).unwrap();
412
413 let token = make_token(
414 serde_json::json!({ "key": "alice-key", "sub": "alice", "exp": 9999999999u64 }),
415 "alice-secret",
416 Algorithm::HS256,
417 );
418 let out = plugin
419 .execute(ctx_with_token(Some(&token)), &HashMap::new())
420 .await
421 .unwrap();
422 assert_eq!(
423 out.context.message.get("consumer.name"),
424 Some(&serde_json::json!("alice"))
425 );
426
427 let forged = make_token(
429 serde_json::json!({ "key": "alice-key", "exp": 9999999999u64 }),
430 "wrong-secret",
431 Algorithm::HS256,
432 );
433 assert!(plugin
434 .execute(ctx_with_token(Some(&forged)), &HashMap::new())
435 .await
436 .is_err());
437
438 let unknown = make_token(
440 serde_json::json!({ "key": "nobody", "exp": 9999999999u64 }),
441 "x",
442 Algorithm::HS256,
443 );
444 assert!(plugin
445 .execute(ctx_with_token(Some(&unknown)), &HashMap::new())
446 .await
447 .is_err());
448 }
449}