From be8ada4b1a78146fa86cd61b531870a038cfde9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 23 Mar 2026 13:37:06 +0100 Subject: [PATCH 1/2] add type to claims struct --- crates/defguard_common/src/auth/claims.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/defguard_common/src/auth/claims.rs b/crates/defguard_common/src/auth/claims.rs index bca84e18e7..bfd07b8448 100644 --- a/crates/defguard_common/src/auth/claims.rs +++ b/crates/defguard_common/src/auth/claims.rs @@ -13,7 +13,7 @@ pub static AUTH_SECRET_ENV: &str = "DEFGUARD_AUTH_SECRET"; pub static GATEWAY_SECRET_ENV: &str = "DEFGUARD_GATEWAY_SECRET"; pub static YUBIBRIDGE_SECRET_ENV: &str = "DEFGUARD_YUBIBRIDGE_SECRET"; -#[derive(Clone, Copy, Default)] +#[derive(Clone, Copy, Default, Deserialize, Serialize)] pub enum ClaimsType { #[default] Auth, @@ -37,6 +37,7 @@ pub struct Claims { pub exp: u64, // not before pub nbf: u64, + pub claims_type: ClaimsType, } impl Claims { @@ -60,6 +61,7 @@ impl Claims { client_id, exp, nbf, + claims_type, } } From f624fd09754828da233574c3fe63a6598a3e6111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Mon, 23 Mar 2026 16:06:18 +0100 Subject: [PATCH 2/2] use secret_key setting for generating tokens --- crates/defguard_common/src/auth/claims.rs | 78 +++++++++++++---------- crates/defguard_core/src/grpc/auth.rs | 34 +++++++--- 2 files changed, 71 insertions(+), 41 deletions(-) diff --git a/crates/defguard_common/src/auth/claims.rs b/crates/defguard_common/src/auth/claims.rs index bfd07b8448..bd1e36809c 100644 --- a/crates/defguard_common/src/auth/claims.rs +++ b/crates/defguard_common/src/auth/claims.rs @@ -1,19 +1,16 @@ -use std::{ - env, - time::{Duration, SystemTime}, -}; +use std::time::{Duration, SystemTime}; use jsonwebtoken::{ DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::Error as JWTError, }; use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::db::models::{Settings, settings::SettingsInitializationError}; pub static JWT_ISSUER: &str = "DefGuard"; -pub static AUTH_SECRET_ENV: &str = "DEFGUARD_AUTH_SECRET"; -pub static GATEWAY_SECRET_ENV: &str = "DEFGUARD_GATEWAY_SECRET"; -pub static YUBIBRIDGE_SECRET_ENV: &str = "DEFGUARD_YUBIBRIDGE_SECRET"; -#[derive(Clone, Copy, Default, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] pub enum ClaimsType { #[default] Auth, @@ -22,11 +19,22 @@ pub enum ClaimsType { DesktopClient, } +#[derive(Debug, Error)] +pub enum ClaimsError { + #[error("Failed to read JWT signing key from settings: {0}")] + Settings(#[from] SettingsInitializationError), + #[error("JWT processing failed: {0}")] + Jwt(#[from] JWTError), + #[error("JWT claims type mismatch: expected {expected:?}, got {actual:?}")] + UnexpectedClaimsType { + expected: ClaimsType, + actual: ClaimsType, + }, +} + /// Standard claims: https://www.iana.org/assignments/jwt/jwt.xhtml #[derive(Deserialize, Serialize)] pub struct Claims { - #[serde(skip_serializing, skip_deserializing)] - secret: String, // issuer pub iss: String, // subject @@ -55,7 +63,6 @@ impl Claims { .expect("valid timestamp") .as_secs(); Self { - secret: Self::get_secret(claims_type), iss: JWT_ISSUER.to_string(), sub, client_id, @@ -65,36 +72,43 @@ impl Claims { } } - fn get_secret(claims_type: ClaimsType) -> String { - let env_var = match claims_type { - ClaimsType::Auth | ClaimsType::DesktopClient => AUTH_SECRET_ENV, - ClaimsType::Gateway => GATEWAY_SECRET_ENV, - ClaimsType::YubiBridge => YUBIBRIDGE_SECRET_ENV, - }; - env::var(env_var).unwrap_or_default() + fn encoding_key() -> Result { + let settings = Settings::get_current_settings(); + Ok(EncodingKey::from_secret( + settings.secret_key_required()?.as_bytes(), + )) + } + + fn decoding_key() -> Result { + let settings = Settings::get_current_settings(); + Ok(DecodingKey::from_secret( + settings.secret_key_required()?.as_bytes(), + )) } /// Convert claims to JWT. - pub fn to_jwt(&self) -> Result { - encode( - &Header::default(), - self, - &EncodingKey::from_secret(self.secret.as_bytes()), - ) + pub fn to_jwt(&self) -> Result { + let encoding_key = Self::encoding_key()?; + + encode(&Header::default(), self, &encoding_key).map_err(ClaimsError::from) } /// Verify JWT and, if successful, convert it to claims. - pub fn from_jwt(claims_type: ClaimsType, token: &str) -> Result { - let secret = Self::get_secret(claims_type); + pub fn from_jwt(expected_claims_type: ClaimsType, token: &str) -> Result { + let decoding_key = Self::decoding_key()?; let mut validation = Validation::default(); validation.validate_nbf = true; validation.set_issuer(&[JWT_ISSUER]); validation.set_required_spec_claims(&["iss", "sub", "exp", "nbf"]); - decode::( - token, - &DecodingKey::from_secret(secret.as_bytes()), - &validation, - ) - .map(|data| data.claims) + let claims = decode::(token, &decoding_key, &validation).map(|data| data.claims)?; + + if claims.claims_type != expected_claims_type { + return Err(ClaimsError::UnexpectedClaimsType { + expected: expected_claims_type, + actual: claims.claims_type, + }); + } + + Ok(claims) } } diff --git a/crates/defguard_core/src/grpc/auth.rs b/crates/defguard_core/src/grpc/auth.rs index b68ce63556..72acbf722a 100644 --- a/crates/defguard_core/src/grpc/auth.rs +++ b/crates/defguard_core/src/grpc/auth.rs @@ -1,11 +1,10 @@ use std::sync::{Arc, Mutex}; use defguard_common::{ - auth::claims::{Claims, ClaimsType}, + auth::claims::{Claims, ClaimsError, ClaimsType}, db::models::{Settings, User}, }; use defguard_proto::auth::{AuthenticateRequest, AuthenticateResponse, auth_service_server}; -use jsonwebtoken::errors::Error as JWTError; use sqlx::PgPool; use tonic::{Request, Response, Status}; @@ -26,7 +25,7 @@ impl AuthServer { } /// Creates JWT token for specified user - fn create_jwt(uid: &str) -> Result { + fn create_jwt(uid: &str) -> Result { let settings = Settings::get_current_settings(); let timeout = settings.authentication_timeout(); Claims::new( @@ -37,6 +36,27 @@ impl AuthServer { ) .to_jwt() } + + fn create_auth_token(uid: &str) -> Result { + Self::create_jwt(uid).map_err(|err| match err { + ClaimsError::Settings(err) => { + error!( + "Failed to create gRPC auth token for user {uid}: JWT signing is misconfigured: {err}" + ); + Status::failed_precondition("JWT signing is not configured") + } + ClaimsError::Jwt(err) => { + error!("Failed to create gRPC auth token for user {uid}: {err}"); + Status::internal("failed to create JWT token") + } + ClaimsError::UnexpectedClaimsType { expected, actual } => { + error!( + "Failed to create gRPC auth token for user {uid}: unexpected claims type mismatch while minting token (expected {expected:?}, got {actual:?})" + ); + Status::internal("failed to create JWT token") + } + }) + } } #[tonic::async_trait] @@ -55,13 +75,9 @@ impl auth_service_server::AuthService for AuthServer { if let Ok(Some(user)) = User::find_by_username(&self.pool, &request.username).await { if user.verify_password(&request.password).is_ok() { + let token = Self::create_auth_token(&request.username)?; info!("Authentication successful for user {}", request.username); - Ok(Response::new(AuthenticateResponse { - token: Self::create_jwt(&request.username).map_err(|_| { - log_failed_login_attempt(&self.failed_logins, &request.username); - Status::unauthenticated("error creating JWT token") - })?, - })) + Ok(Response::new(AuthenticateResponse { token })) } else { warn!("Invalid login credentials for user {}", request.username); log_failed_login_attempt(&self.failed_logins, &request.username);