Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

### Added

- [#6166](https://github.com/ChainSafe/forest/pull/6166) Gate `JWT` expiration validation behind environment variable `FOREST_JWT_DISABLE_EXP_VALIDATION`.

### Changed

- [#6145](https://github.com/ChainSafe/forest/pull/6145) Updated `forest-cli snapshot export` to use v2 format by default.
Expand Down
12 changes: 12 additions & 0 deletions docs/docs/users/reference/env_variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ process.
| `FOREST_SNAPSHOT_GC_CHECK_INTERVAL_SECONDS` | non-negative integer | 300 | 60 | The interval in seconds for checking if snapshot GC should run |
| `FOREST_DISABLE_BAD_BLOCK_CACHE` | 1 or true | empty | 1 | Whether or not to disable bad block cache |
| `FOREST_ZSTD_FRAME_CACHE_DEFAULT_MAX_SIZE` | positive integer | 268435456 | 536870912 | The default zstd frame cache max size in bytes |
| `FOREST_JWT_DISABLE_EXP_VALIDATION` | 1 or true | empty | 1 | Whether or not to disable JWT expiration validation |

### `FOREST_F3_SIDECAR_FFI_BUILD_OPT_OUT`

Expand All @@ -74,6 +75,17 @@ environmental variable.
The databases can be found, by default, under `<DATA_DIR>/<chain>/`, e.g.,
`$HOME/.local/share/forest/calibnet`.

### `FOREST_JWT_DISABLE_EXP_VALIDATION`

#### 🔧 Use Case

Intended for controlled cross-system token sharing where expiration validation must be bypassed (e.g., load balancing with Lotus).

> **⚠️ Warning**
>
> Disabling expiration checks for all JWTs will also allow expired tokens.
> This significantly weakens security and should only be used in tightly controlled environments. Not recommended for general use.

### Drand config format

```json
Expand Down
60 changes: 57 additions & 3 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use crate::key_management::KeyInfo;
use crate::shim::crypto::SignatureType;
use crate::utils::misc::env::is_env_truthy;
use chrono::{Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, decode, encode, errors::Result as JWTResult};
use rand::Rng;
Expand All @@ -25,22 +26,30 @@ struct Claims {
#[serde(rename = "Allow")]
allow: Vec<String>,
// Expiration time (as UTC timestamp)
exp: usize,
#[serde(default)]
exp: Option<usize>,
}

/// Create a new JWT Token
pub fn create_token(perms: Vec<String>, key: &[u8], token_exp: Duration) -> JWTResult<String> {
let exp_time = Utc::now() + token_exp;
let payload = Claims {
allow: perms,
exp: exp_time.timestamp() as usize,
exp: Some(exp_time.timestamp() as usize),
};
encode(&Header::default(), &payload, &EncodingKey::from_secret(key))
}

/// Verify JWT Token and return the allowed permissions from token
pub fn verify_token(token: &str, key: &[u8]) -> JWTResult<Vec<String>> {
let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::default());
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::default());
if is_env_truthy("FOREST_JWT_DISABLE_EXP_VALIDATION") {
let mut claims = validation.required_spec_claims.clone();
claims.remove("exp");
let buff: Vec<_> = claims.iter().collect();
validation.set_required_spec_claims(&buff);
validation.validate_exp = false;
}
Comment thread
elmattic marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
let token = decode::<Claims>(token, &DecodingKey::from_secret(key), &validation)?;
Ok(token.claims.allow)
}
Expand All @@ -55,8 +64,19 @@ pub fn generate_priv_key() -> KeyInfo {
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;

/// Create a new JWT Token without expiration
fn create_token_without_exp(perms: Vec<String>, key: &[u8]) -> JWTResult<String> {
let payload = Claims {
allow: perms,
exp: None,
};
encode(&Header::default(), &payload, &EncodingKey::from_secret(key))
}

#[test]
#[serial]
fn create_and_verify_token() {
let perms_expected = vec![
"Ph'nglui mglw'nafh Cthulhu".to_owned(),
Expand Down Expand Up @@ -94,4 +114,38 @@ mod tests {
let perms = verify_token(&token, key.private_key()).unwrap();
assert_eq!(perms_expected, perms);
}

#[test]
#[serial]
fn create_and_verify_token_without_exp() {
let perms_expected = vec![
"Ia! Ia! Cthulhu fhtagn".to_owned(),
"Zin-Mi-Yak, dread lord of the deep".to_owned(),
];
let key = generate_priv_key();

// Disable expiration validation via env var
unsafe {
std::env::set_var("FOREST_JWT_DISABLE_EXP_VALIDATION", "1");
}
Comment thread
elmattic marked this conversation as resolved.

// No exp at all in the token. Validation must pass.
let token = create_token_without_exp(perms_expected.clone(), key.private_key()).unwrap();
let perms = verify_token(&token, key.private_key()).unwrap();
assert_eq!(perms_expected, perms);

// Token duration of -1 hour (already expired). Validation must pass.
let token = create_token(
perms_expected.clone(),
key.private_key(),
-Duration::try_hours(1).expect("Infallible"),
)
.unwrap();
let perms = verify_token(&token, key.private_key()).unwrap();
assert_eq!(perms_expected, perms);

unsafe {
std::env::remove_var("FOREST_JWT_DISABLE_EXP_VALIDATION");
}
}
Comment thread
elmattic marked this conversation as resolved.
}
Loading