Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 48 additions & 32 deletions crates/defguard_common/src/auth/claims.rs
Original file line number Diff line number Diff line change
@@ -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)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum ClaimsType {
#[default]
Auth,
Expand All @@ -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
Expand All @@ -37,6 +45,7 @@ pub struct Claims {
pub exp: u64,
// not before
pub nbf: u64,
pub claims_type: ClaimsType,
}

impl Claims {
Expand All @@ -54,45 +63,52 @@ impl Claims {
.expect("valid timestamp")
.as_secs();
Self {
secret: Self::get_secret(claims_type),
iss: JWT_ISSUER.to_string(),
sub,
client_id,
exp,
nbf,
claims_type,
}
}

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<EncodingKey, ClaimsError> {
let settings = Settings::get_current_settings();
Ok(EncodingKey::from_secret(
settings.secret_key_required()?.as_bytes(),
))
}

fn decoding_key() -> Result<DecodingKey, ClaimsError> {
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<String, JWTError> {
encode(
&Header::default(),
self,
&EncodingKey::from_secret(self.secret.as_bytes()),
)
pub fn to_jwt(&self) -> Result<String, ClaimsError> {
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<Self, JWTError> {
let secret = Self::get_secret(claims_type);
pub fn from_jwt(expected_claims_type: ClaimsType, token: &str) -> Result<Self, ClaimsError> {
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::<Self>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&validation,
)
.map(|data| data.claims)
let claims = decode::<Self>(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)
}
}
34 changes: 25 additions & 9 deletions crates/defguard_core/src/grpc/auth.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -26,7 +25,7 @@ impl AuthServer {
}

/// Creates JWT token for specified user
fn create_jwt(uid: &str) -> Result<String, JWTError> {
fn create_jwt(uid: &str) -> Result<String, ClaimsError> {
let settings = Settings::get_current_settings();
let timeout = settings.authentication_timeout();
Claims::new(
Expand All @@ -37,6 +36,27 @@ impl AuthServer {
)
.to_jwt()
}

fn create_auth_token(uid: &str) -> Result<String, Status> {
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]
Expand All @@ -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);
Expand Down
Loading