Skip to content

Commit 9ce3d77

Browse files
committed
wip username
1 parent a5aa81a commit 9ce3d77

File tree

9 files changed

+757
-41
lines changed

9 files changed

+757
-41
lines changed

Cargo.lock

Lines changed: 238 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dasp_ring_buffer = "0.11.0" # Ring buffer for audio
2121
clap = { version = "4.5.38", features = ["derive"] }
2222

2323
# Web interface
24-
rocket = { version = "0.5", features = ["json", "tls"] }
24+
rocket = { version = "0.5", features = ["json", "tls", "secrets"] }
2525
rocket_cors = "0.6.0"
2626
rocket_okapi = { version = "0.9.0", features = [
2727
"rapidoc",
@@ -53,6 +53,7 @@ time = "0.3.41"
5353
rsa = {version = "0.9.8", features=["pem","sha2"]}
5454
tokio = { version = "1.45.0", features = ["rt", "macros", "rt-multi-thread", "time"] }
5555
tokio-modbus = { version = "0.16.1", features = ["tcp", "tcp-server", "server"] }
56+
pwhash = "1.0.0" # Add this dependency for password hashing
5657

5758
[dev-dependencies]
5859
criterion = "0.6" # Benchmarking

config.example.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ visualization:
99
address: 127.0.0.1
1010
# The server name
1111
name: LaserSmartApiServer/0.1.0
12+
13+
# The session secret key for the visualization server
14+
# This key is used to sign the session cookie
15+
# It can be securely generated with `openssl rand -base64 32`
16+
session_secret: 6wcVSUhxt1+YPEondChFXtesCL1boh57gqHv2gnEH7U=
17+
1218
# SSL certificate PEM data (Base64 encoded) - Optional
1319
# generated with `cat rust-photoacoustic.local.pem | base64 -w0`
1420
# cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t...

resources/config.schema.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
"null"
5757
],
5858
"description": "RS256 public key for JWT token verification (base64 encoded)"
59+
},
60+
"session_secret": {
61+
"type": [
62+
"string"
63+
],
64+
"description": "Session secret for cookie signing (base64 encoded) can be generated with openssl rand -base64 32"
5965
}
6066
},
6167
"required": [
@@ -64,7 +70,8 @@
6470
"name",
6571
"hmac_secret",
6672
"rs256_private_key",
67-
"rs256_public_key"
73+
"rs256_public_key",
74+
"session_secret"
6875
]
6976
},
7077
"modbus": {

src/config.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,10 @@ pub struct VisualizationConfig {
350350
/// without removing the configuration. Default is `true`.
351351
#[serde(default = "default_enabled")]
352352
pub enabled: bool,
353+
354+
/// Session secret key for cookie-based authentication.
355+
#[serde(default = "default_session_secret")]
356+
pub session_secret: String,
353357
}
354358

355359
/// Provides the default TCP port (8080) for the visualization server.
@@ -448,6 +452,14 @@ fn default_enabled() -> bool {
448452
true
449453
}
450454

455+
/// Generate a random session secret key for cookie-based authentication.
456+
fn default_session_secret() -> String {
457+
use rand::Rng;
458+
let mut rng = rand::rng();
459+
let secret: [u8; 32] = rng.random();
460+
base64::engine::general_purpose::STANDARD.encode(&secret)
461+
}
462+
451463
impl Default for VisualizationConfig {
452464
fn default() -> Self {
453465
Self {
@@ -460,6 +472,7 @@ impl Default for VisualizationConfig {
460472
rs256_private_key: default_rs256_private_key(),
461473
rs256_public_key: default_rs256_public_key(),
462474
enabled: default_enabled(),
475+
session_secret: default_session_secret(),
463476
}
464477
}
465478
}

src/daemon/launch_daemon.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,8 @@ impl Daemon {
216216
.merge(("limits", Limits::new().limit("json", 2.mebibytes())))
217217
.merge(("address", config.visualization.address.clone()))
218218
.merge(("port", config.visualization.port))
219-
.merge(("log_level", LogLevel::Normal));
219+
.merge(("log_level", LogLevel::Normal))
220+
.merge(("secret_key", config.visualization.session_secret.clone()));
220221

221222
// Add RS256 keys to figment
222223
if !config.visualization.rs256_public_key.is_empty()

src/visualization/jwt.rs

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,18 @@ pub struct JwtTokenMap {
239239
/// The algorithm used to sign and verify JWT tokens.
240240
/// Default is HS256 (HMAC with SHA-256).
241241
algorithm: Algorithm,
242+
243+
/// Additional claims to include in tokens
244+
///
245+
/// This allows adding extra claims to the JWT tokens being generated.
246+
/// Used for including user-specific information in tokens.
247+
claims: HashMap<String, Value>,
248+
249+
/// Token hook for extensibility
250+
///
251+
/// A callback function that allows modifying token responses during issuance.
252+
/// Used for enriching tokens with additional data.
253+
token_hook: Option<Box<dyn FnMut(&mut oxide_auth::code_grant::accesstoken::TokenResponse) + Send>>,
242254
}
243255

244256
impl JwtTokenMap {
@@ -254,6 +266,8 @@ impl JwtTokenMap {
254266
issuer: "rust-photoacoustic".to_string(),
255267
usage_counter: 0,
256268
algorithm: Algorithm::HS256, // Default to HMAC-SHA256
269+
claims: HashMap::new(),
270+
token_hook: None,
257271
}
258272
}
259273

@@ -273,6 +287,8 @@ impl JwtTokenMap {
273287
issuer: "rust-photoacoustic".to_string(),
274288
usage_counter: 0,
275289
algorithm,
290+
claims: HashMap::new(),
291+
token_hook: None,
276292
}
277293
}
278294

@@ -303,6 +319,8 @@ impl JwtTokenMap {
303319
issuer: "rust-photoacoustic".to_string(),
304320
usage_counter: 0,
305321
algorithm: Algorithm::RS256,
322+
claims: HashMap::new(),
323+
token_hook: None,
306324
})
307325
}
308326

@@ -338,6 +356,15 @@ impl JwtTokenMap {
338356
}
339357
}
340358

359+
// Add any additional claims
360+
for (key, value) in &self.claims {
361+
if let Some(val) = value {
362+
metadata.insert(key.to_string(), val.to_string());
363+
} else {
364+
metadata.insert(key.to_string(), "true".to_string());
365+
}
366+
}
367+
341368
// Store the redirect URI in the metadata
342369
metadata.insert("redirect_uri".to_string(), grant.redirect_uri.to_string());
343370

@@ -360,6 +387,27 @@ impl JwtTokenMap {
360387
},
361388
}
362389
}
390+
391+
/// Add user information to token claims
392+
pub fn add_user_claims(&mut self, username: &str, permissions: &[String]) -> &mut Self {
393+
// Add user information to claims
394+
self.claims.insert("sub".to_string(), Value::public(Some(username.to_string())));
395+
396+
// Add permissions as a space-separated string
397+
let perms_str = permissions.join(" ");
398+
self.claims.insert("permissions".to_string(), Value::public(Some(perms_str)));
399+
400+
// Common identity claims
401+
self.claims.insert("preferred_username".to_string(), Value::public(Some(username.to_string())));
402+
self.claims.insert("name".to_string(), Value::public(Some(username.to_string())));
403+
404+
self
405+
}
406+
407+
/// Set a token hook function
408+
pub fn set_token_hook(&mut self, hook: Box<dyn FnMut(&mut oxide_auth::code_grant::accesstoken::TokenResponse) + Send>) {
409+
self.token_hook = Some(hook);
410+
}
363411
}
364412

365413
impl Issuer for JwtTokenMap {
@@ -397,13 +445,27 @@ impl Issuer for JwtTokenMap {
397445
.insert(refresh.clone(), Arc::clone(&token_entry));
398446
}
399447

400-
// Return the issued token
401-
Ok(IssuedToken {
448+
// Create the token response
449+
let mut token = IssuedToken {
402450
token: access_token,
403451
refresh: refresh_token,
404452
until: grant.until,
405453
token_type: TokenType::Bearer,
406-
})
454+
};
455+
456+
// Apply token hook if present
457+
if let Some(hook) = &mut self.token_hook {
458+
// Convert IssuedToken to TokenResponse for the hook
459+
let mut token_response = oxide_auth::code_grant::accesstoken::TokenResponse::from(token.clone());
460+
461+
// Call the hook with our token response
462+
hook(&mut token_response);
463+
464+
// No need to convert back as we already have our IssuedToken
465+
}
466+
467+
// Return the issued token
468+
Ok(token)
407469
}
408470

409471
fn refresh(&mut self, refresh: &str, mut grant: Grant) -> Result<RefreshedToken, ()> {
@@ -464,13 +526,27 @@ impl Issuer for JwtTokenMap {
464526
.insert(refresh.clone(), Arc::clone(&new_token_entry));
465527
}
466528

467-
// Return the refreshed token
468-
Ok(RefreshedToken {
529+
// Create the refreshed token
530+
let mut token = RefreshedToken {
469531
token: new_access_token,
470532
refresh: new_refresh_token,
471533
until: grant.until,
472534
token_type: TokenType::Bearer,
473-
})
535+
};
536+
537+
// Apply token hook if present
538+
if let Some(hook) = &mut self.token_hook {
539+
// Convert RefreshedToken to TokenResponse for the hook
540+
let mut token_response = oxide_auth::code_grant::accesstoken::TokenResponse::from(token.clone());
541+
542+
// Call the hook with our token response
543+
hook(&mut token_response);
544+
545+
// No need to convert back as we already have our RefreshedToken
546+
}
547+
548+
// Return the refreshed token
549+
Ok(token)
474550
}
475551

476552
fn recover_token<'a>(&'a self, token: &'a str) -> Result<Option<Grant>, ()> {
@@ -627,6 +703,21 @@ impl JwtIssuer {
627703
self
628704
}
629705

706+
/// Add user information to token claims
707+
pub fn add_user_claims(&mut self, username: &str, permissions: &[String]) -> &mut Self {
708+
{
709+
let mut map = self.0.lock().unwrap();
710+
map.add_user_claims(username, permissions);
711+
}
712+
self
713+
}
714+
715+
/// Set a token hook function
716+
pub fn set_token_hook(&mut self, hook: Box<dyn FnMut(&mut oxide_auth::code_grant::accesstoken::TokenResponse) + Send>) {
717+
let mut map = self.0.lock().unwrap();
718+
map.set_token_hook(hook);
719+
}
720+
630721
/// Print the decoded contents of a JWT token for debugging purposes
631722
/// Returns Ok if the token could be decoded, Err otherwise
632723
pub fn debug_token(&self, token: &str) -> Result<JwtClaims, String> {

0 commit comments

Comments
 (0)