Fix panic-on-missing-env in auth middleware and ticket provider

require_auth and NxtgaugeTicketProvider::create_ticket both called
env::var(...).expect(...) on every request, crashing instead of
returning a proper error when JWT_SECRET/AI_SERVICE_KEY were unset.
Both are now loaded once into AppConfig at startup and checked with
a graceful 401/500 response.
This commit is contained in:
Ashwin Kumar Sivakumar 2026-07-02 18:31:35 +05:30
parent 2e0b986e20
commit 6decf8dce2
6 changed files with 55 additions and 10 deletions

View file

@ -10,3 +10,10 @@ OLLAMA_EMBED_MODEL=nomic-embed-text
HELP_CENTER_SEED_PATH=./seeds/help_articles.json HELP_CENTER_SEED_PATH=./seeds/help_articles.json
TICKETS_SOURCE=chatbot TICKETS_SOURCE=chatbot
NXTGAUGE_USERS_URL=http://localhost:9101
# Required: must match the JWT signing secret used by nxtgauge-backend-rust,
# and the AI service key nxtgauge-backend-rust expects on X-AI-Service-Key.
# Requests fail closed (401/500) if these are unset, rather than crashing.
JWT_SECRET=
AI_SERVICE_KEY=

View file

@ -1,5 +1,5 @@
use axum::{ use axum::{
extract::Request, extract::{Request, State},
http::{header::AUTHORIZATION, StatusCode}, http::{header::AUTHORIZATION, StatusCode},
middleware::Next, middleware::Next,
response::{IntoResponse, Response}, response::{IntoResponse, Response},
@ -8,6 +8,8 @@ use axum::{
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims { pub struct Claims {
pub sub: String, pub sub: String,
@ -25,7 +27,23 @@ pub struct AuthUser {
pub claims: Claims, pub claims: Claims,
} }
pub async fn require_auth(mut request: Request, next: Next) -> Response { pub async fn require_auth(
State(state): State<AppState>,
mut request: Request,
next: Next,
) -> Response {
if state.config.jwt_secret.is_empty() {
tracing::error!("rejecting request: JWT_SECRET is not configured");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "Authentication is misconfigured",
"code": "AUTH_MISCONFIGURED"
})),
)
.into_response();
}
let auth_header = request let auth_header = request
.headers() .headers()
.get(AUTHORIZATION) .get(AUTHORIZATION)
@ -46,11 +64,9 @@ pub async fn require_auth(mut request: Request, next: Next) -> Response {
} }
}; };
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
let token_data = match decode::<Claims>( let token_data = match decode::<Claims>(
&token, &token,
&DecodingKey::from_secret(jwt_secret.as_bytes()), &DecodingKey::from_secret(state.config.jwt_secret.as_bytes()),
&Validation::new(Algorithm::HS256), &Validation::new(Algorithm::HS256),
) { ) {
Ok(data) => data, Ok(data) => data,

View file

@ -15,6 +15,8 @@ pub struct AppConfig {
pub help_center_seed_path: String, pub help_center_seed_path: String,
pub tickets_source: String, pub tickets_source: String,
pub nxtgauge_users_url: String, pub nxtgauge_users_url: String,
pub jwt_secret: String,
pub ai_service_key: String,
} }
impl AppConfig { impl AppConfig {
@ -47,6 +49,20 @@ impl AppConfig {
"NXTGAUGE_USERS_URL", "NXTGAUGE_USERS_URL",
"http://nxtgauge-rust-users:9101", "http://nxtgauge-rust-users:9101",
), ),
jwt_secret: {
let v = std::env::var("JWT_SECRET").unwrap_or_default();
if v.is_empty() {
tracing::error!("JWT_SECRET is not set; all authenticated requests will be rejected");
}
v
},
ai_service_key: {
let v = std::env::var("AI_SERVICE_KEY").unwrap_or_default();
if v.is_empty() {
tracing::error!("AI_SERVICE_KEY is not set; ticket creation will fail");
}
v
},
} }
} }

View file

@ -85,6 +85,7 @@ async fn main() {
http_client, http_client,
cfg.nxtgauge_users_url.clone(), cfg.nxtgauge_users_url.clone(),
cfg.tickets_source.clone(), cfg.tickets_source.clone(),
cfg.ai_service_key.clone(),
)); ));
let state = AppState::new( let state = AppState::new(

View file

@ -12,6 +12,7 @@ pub struct NxtgaugeTicketProvider {
client: Client, client: Client,
base_url: String, base_url: String,
source: String, source: String,
ai_service_key: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -31,11 +32,12 @@ struct NxtgaugeTicketRequest {
} }
impl NxtgaugeTicketProvider { impl NxtgaugeTicketProvider {
pub fn new(client: Client, base_url: String, source: String) -> Self { pub fn new(client: Client, base_url: String, source: String, ai_service_key: String) -> Self {
Self { Self {
client, client,
base_url, base_url,
source, source,
ai_service_key,
} }
} }
} }
@ -56,13 +58,16 @@ impl TicketProvider for NxtgaugeTicketProvider {
user_id: Some(payload.user_id), user_id: Some(payload.user_id),
}; };
let ai_service_key = std::env::var("AI_SERVICE_KEY") if self.ai_service_key.is_empty() {
.expect("AI_SERVICE_KEY must be set"); return Err(AppError::ExternalService(
"AI_SERVICE_KEY is not configured".to_string(),
));
}
let response = self let response = self
.client .client
.post(&url) .post(&url)
.header("X-AI-Service-Key", ai_service_key) .header("X-AI-Service-Key", &self.ai_service_key)
.json(&req) .json(&req)
.send() .send()
.await .await

View file

@ -44,7 +44,7 @@ pub fn build_router(state: AppState) -> Router {
.route("/tickets/create", post(handlers::tickets::create)) .route("/tickets/create", post(handlers::tickets::create))
.route("/help/search", post(handlers::help::search)) .route("/help/search", post(handlers::help::search))
.route("/actions/confirm", post(handlers::confirm_action::confirm_action)) .route("/actions/confirm", post(handlers::confirm_action::confirm_action))
.layer(middleware::from_fn(require_auth)), .layer(middleware::from_fn_with_state(state.clone(), require_auth)),
) )
.layer(cors) .layer(cors)
.layer(TraceLayer::new_for_http()) .layer(TraceLayer::new_for_http())