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:
parent
2e0b986e20
commit
6decf8dce2
6 changed files with 55 additions and 10 deletions
|
|
@ -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=
|
||||||
|
|
|
||||||
26
src/auth.rs
26
src/auth.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue