From c341f4462ee0570f7b44c0b2a4197944ce819be9 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Sat, 25 Apr 2026 03:36:52 +0300 Subject: [PATCH] feat: Add JWT authentication middleware and token management for charging engine - Add Claims struct with sub and exp fields for JWT payload - Add AuthConfig struct with secret field loaded from JWT_SECRET environment variable - Add create_token function generating 24-hour expiration tokens - Add validate_token function for JWT verification - Add auth_middleware for Axum with Authorization header Bearer token validation - Allow unauthenticated requests temporarily for packet-gateway integration --- apps/charging-engine/src/auth.rs | 81 ++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 apps/charging-engine/src/auth.rs diff --git a/apps/charging-engine/src/auth.rs b/apps/charging-engine/src/auth.rs new file mode 100644 index 0000000..73ffbb4 --- /dev/null +++ b/apps/charging-engine/src/auth.rs @@ -0,0 +1,81 @@ +use axum::{ + extract::{Request, State}, + http::StatusCode, + middleware::Next, + response::Response, +}; +use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; +use std::env; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Claims { + pub sub: String, + pub exp: usize, +} + +#[derive(Clone)] +pub struct AuthConfig { + pub secret: String, +} + +impl AuthConfig { + pub fn from_env() -> Self { + let secret = env::var("JWT_SECRET").unwrap_or_else(|_| "default_secret_key_change_in_production".to_string()); + Self { secret } + } +} + +pub fn create_token(user_id: &str, config: &AuthConfig) -> Result { + let expiration = chrono::Utc::now() + .checked_add_signed(chrono::Duration::hours(24)) + .expect("valid timestamp") + .timestamp() as usize; + + let claims = Claims { + sub: user_id.to_string(), + exp: expiration, + }; + + encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(config.secret.as_ref()), + ) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) +} + +pub fn validate_token(token: &str, config: &AuthConfig) -> Result { + decode::( + token, + &DecodingKey::from_secret(config.secret.as_ref()), + &Validation::default(), + ) + .map(|data| data.claims) + .map_err(|_| StatusCode::UNAUTHORIZED) +} + +pub async fn auth_middleware( + State(config): State, + request: Request, + next: Next, +) -> Result { + let auth_header = request + .headers() + .get("Authorization") + .and_then(|h| h.to_str().ok()); + + if let Some(auth_header) = auth_header { + if let Some(token) = auth_header.strip_prefix("Bearer ") { + match validate_token(token, &config) { + Ok(_claims) => return Ok(next.run(request).await), + Err(_) => return Err(StatusCode::UNAUTHORIZED), + } + } + } + + // For now, allow requests without auth for packet-gateway integration + // In production, you would want to enable strict authentication + // or use API keys for service-to-service communication + Ok(next.run(request).await) +}