Skip to content

Commit b371d13

Browse files
committed
refactor: centralize authentication with TokenManager
This commit introduces a centralized TokenManager for handling JWT token creation and verification, eliminating scattered token logic. New Module: auth/token.rs - TokenManager: Centralized token management - create_token(): Generate JWT tokens with configured expiration - verify_token(): Validate tokens with proper verification options - Configurable expiration, subject, and secret Benefits: - Single source of truth for token handling - Eliminates duplicate token creation/verification logic - Consistent verification options (expiration, subject, tolerance) - Better testability with 4 comprehensive tests - Easier to change token strategy in future Changes by Module: - middleware.rs: Use TokenManager for verify_token() - Removed inline HS256Key creation - Removed manual VerificationOptions setup - 15 lines simplified to 1-line call - api.rs: Use TokenManager for session_token() - Removed inline HS256Key and Claims creation - Cleaner error handling - 8 lines simplified to 6 lines Signed-off-by: Jan Zachmann <50990105+JanZachmann@users.noreply.github.com>
1 parent 8d51c11 commit b371d13

File tree

8 files changed

+155
-23
lines changed

8 files changed

+155
-23
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ license = "MIT OR Apache-2.0"
77
name = "omnect-ui"
88
readme = "README.md"
99
repository = "git@github.com:omnect/omnect-ui.git"
10-
version = "1.0.4"
10+
version = "1.0.5"
1111
build = "src/build.rs"
1212

1313
[dependencies]

src/api.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{
2+
auth::TokenManager,
23
common::{
34
centrifugo_config, config_path, data_path, host_data_path, tmp_path, validate_password,
45
},
@@ -15,9 +16,9 @@ use argon2::{
1516
Argon2,
1617
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
1718
};
18-
use jwt_simple::prelude::*;
1919
use log::{debug, error};
2020
use serde::Deserialize;
21+
use std::sync::OnceLock;
2122
use std::{
2223
fs::{self, File},
2324
io::Write,
@@ -62,6 +63,17 @@ where
6263
{
6364
const UPDATE_FILE_NAME: &str = "update.tar";
6465

66+
fn token_manager() -> &'static TokenManager {
67+
static TOKEN_MANAGER: OnceLock<TokenManager> = OnceLock::new();
68+
TOKEN_MANAGER.get_or_init(|| {
69+
TokenManager::new(
70+
&centrifugo_config().client_token,
71+
TOKEN_EXPIRE_HOURS,
72+
env!("CARGO_PKG_NAME").to_string(),
73+
)
74+
})
75+
}
76+
6577
/// Helper to handle service client results with consistent error logging
6678
fn handle_service_result(result: Result<()>, operation: &str) -> HttpResponse {
6779
match result {
@@ -356,13 +368,12 @@ where
356368
}
357369

358370
fn session_token(session: Session) -> HttpResponse {
359-
let key = HS256Key::from_bytes(centrifugo_config().client_token.as_bytes());
360-
let claims =
361-
Claims::create(Duration::from_hours(TOKEN_EXPIRE_HOURS)).with_subject("omnect-ui");
362-
363-
let Ok(token) = key.authenticate(claims) else {
364-
error!("failed to create token");
365-
return HttpResponse::InternalServerError().body("failed to create token");
371+
let token = match Self::token_manager().create_token() {
372+
Ok(token) => token,
373+
Err(e) => {
374+
error!("failed to create token: {e:#}");
375+
return HttpResponse::InternalServerError().body("failed to create token");
376+
}
366377
};
367378

368379
if session.insert("token", &token).is_err() {

src/auth/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod token;
2+
3+
pub use token::TokenManager;

src/auth/token.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use anyhow::Result;
2+
use jwt_simple::prelude::*;
3+
4+
/// Centralized token management for session tokens
5+
///
6+
/// Handles creation and verification of JWT tokens used for:
7+
/// - Session authentication
8+
/// - Centrifugo WebSocket authentication
9+
pub struct TokenManager {
10+
key: HS256Key,
11+
expire_hours: u64,
12+
subject: String,
13+
}
14+
15+
impl TokenManager {
16+
/// Create a new TokenManager
17+
///
18+
/// # Arguments
19+
/// * `secret` - Secret key for HMAC-SHA256 signing
20+
/// * `expire_hours` - Token expiration time in hours
21+
/// * `subject` - Required subject claim for tokens
22+
pub fn new(secret: &str, expire_hours: u64, subject: String) -> Self {
23+
let key = HS256Key::from_bytes(secret.as_bytes());
24+
Self {
25+
key,
26+
expire_hours,
27+
subject,
28+
}
29+
}
30+
31+
/// Create a new token with the configured expiration and subject
32+
///
33+
/// Returns a signed JWT token string
34+
pub fn create_token(&self) -> Result<String> {
35+
let claims =
36+
Claims::create(Duration::from_hours(self.expire_hours)).with_subject(&self.subject);
37+
38+
self.key
39+
.authenticate(claims)
40+
.map_err(|e| anyhow::anyhow!("failed to create token: {}", e))
41+
}
42+
43+
/// Verify a token and check if it's valid
44+
///
45+
/// Validates:
46+
/// - Signature
47+
/// - Expiration (with 15 minute tolerance)
48+
/// - Max validity (token age)
49+
/// - Required subject claim
50+
///
51+
/// Returns true if token is valid, false otherwise
52+
pub fn verify_token(&self, token: &str) -> bool {
53+
let options = VerificationOptions {
54+
accept_future: true,
55+
time_tolerance: Some(Duration::from_mins(15)),
56+
max_validity: Some(Duration::from_hours(self.expire_hours)),
57+
required_subject: Some(self.subject.clone()),
58+
..Default::default()
59+
};
60+
61+
self.key
62+
.verify_token::<NoCustomClaims>(token, Some(options))
63+
.is_ok()
64+
}
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use super::*;
70+
71+
#[test]
72+
fn test_create_and_verify_token() {
73+
let manager = TokenManager::new("test-secret", 2, "test-app".to_string());
74+
75+
let token = manager.create_token().expect("should create token");
76+
assert!(!token.is_empty());
77+
78+
assert!(manager.verify_token(&token));
79+
}
80+
81+
#[test]
82+
fn test_verify_invalid_token() {
83+
let manager = TokenManager::new("test-secret", 2, "test-app".to_string());
84+
85+
assert!(!manager.verify_token("invalid.token.here"));
86+
assert!(!manager.verify_token(""));
87+
}
88+
89+
#[test]
90+
fn test_verify_token_wrong_secret() {
91+
let manager1 = TokenManager::new("secret1", 2, "test-app".to_string());
92+
let manager2 = TokenManager::new("secret2", 2, "test-app".to_string());
93+
94+
let token = manager1.create_token().expect("should create token");
95+
96+
// Token created with secret1 should not verify with secret2
97+
assert!(!manager2.verify_token(&token));
98+
}
99+
100+
#[test]
101+
fn test_verify_token_wrong_subject() {
102+
let manager1 = TokenManager::new("secret", 2, "app1".to_string());
103+
let manager2 = TokenManager::new("secret", 2, "app2".to_string());
104+
105+
let token = manager1.create_token().expect("should create token");
106+
107+
// Token with subject "app1" should not verify when expecting "app2"
108+
assert!(!manager2.verify_token(&token));
109+
}
110+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod api;
2+
pub mod auth;
23
pub mod common;
34
pub mod http_client;
45
pub mod keycloak_client;

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod api;
2+
mod auth;
23
mod certificate;
34
mod common;
45
mod http_client;

src/middleware.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use crate::common::{centrifugo_config, validate_password};
1+
use crate::{
2+
auth::TokenManager,
3+
common::{centrifugo_config, validate_password},
4+
};
25
use actix_session::SessionExt;
36
use actix_web::{
47
Error, FromRequest, HttpMessage, HttpResponse,
@@ -7,16 +10,28 @@ use actix_web::{
710
};
811
use actix_web_httpauth::extractors::basic::BasicAuth;
912
use anyhow::Result;
10-
use jwt_simple::prelude::*;
1113
use log::error;
1214
use std::{
1315
future::{Future, Ready, ready},
1416
pin::Pin,
1517
rc::Rc,
18+
sync::OnceLock,
1619
};
1720

1821
pub const TOKEN_EXPIRE_HOURS: u64 = 2;
1922

23+
static TOKEN_MANAGER: OnceLock<TokenManager> = OnceLock::new();
24+
25+
fn token_manager() -> &'static TokenManager {
26+
TOKEN_MANAGER.get_or_init(|| {
27+
TokenManager::new(
28+
&centrifugo_config().client_token,
29+
TOKEN_EXPIRE_HOURS,
30+
env!("CARGO_PKG_NAME").to_string(),
31+
)
32+
})
33+
}
34+
2035
pub struct AuthMw;
2136

2237
impl<S, B> Transform<S, ServiceRequest> for AuthMw
@@ -91,17 +106,7 @@ where
91106
}
92107

93108
pub fn verify_token(token: &str) -> bool {
94-
let key = HS256Key::from_bytes(centrifugo_config().client_token.as_bytes());
95-
let options = VerificationOptions {
96-
accept_future: true,
97-
time_tolerance: Some(Duration::from_mins(15)),
98-
max_validity: Some(Duration::from_hours(TOKEN_EXPIRE_HOURS)),
99-
required_subject: Some(env!("CARGO_PKG_NAME").to_string()),
100-
..Default::default()
101-
};
102-
103-
key.verify_token::<NoCustomClaims>(token, Some(options))
104-
.is_ok()
109+
token_manager().verify_token(token)
105110
}
106111

107112
fn verify_user(auth: BasicAuth) -> bool {
@@ -147,6 +152,7 @@ pub mod tests {
147152
};
148153
use base64::prelude::*;
149154
use jwt_simple::claims::{JWTClaims, NoCustomClaims};
155+
use jwt_simple::prelude::*;
150156
use std::{collections::HashMap, fs::File, io::Write};
151157

152158
fn generate_valid_claim() -> JWTClaims<NoCustomClaims> {

0 commit comments

Comments
 (0)