Skip to main content

forest/auth/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use crate::key_management::KeyInfo;
5use crate::shim::crypto::SignatureType;
6use crate::utils::misc::env::is_env_truthy;
7use chrono::{Duration, Utc};
8use jsonwebtoken::{DecodingKey, EncodingKey, Header, decode, encode, errors::Result as JWTResult};
9use rand::Rng;
10use serde::{Deserialize, Serialize};
11
12/// constant string that is used to identify the JWT secret key in `KeyStore`
13pub const JWT_IDENTIFIER: &str = "auth-jwt-private";
14/// Admin permissions
15pub const ADMIN: &[&str] = &["read", "write", "sign", "admin"];
16/// Signing permissions
17pub const SIGN: &[&str] = &["read", "write", "sign"];
18/// Writing permissions
19pub const WRITE: &[&str] = &["read", "write"];
20/// Reading permissions
21pub const READ: &[&str] = &["read"];
22
23/// Claim structure for JWT Tokens
24#[derive(Debug, Clone, Serialize, Deserialize)]
25struct Claims {
26    #[serde(rename = "Allow")]
27    allow: Vec<String>,
28    // Expiration time (as UTC timestamp)
29    #[serde(default)]
30    exp: Option<usize>,
31}
32
33/// Create a new JWT Token
34pub fn create_token(perms: Vec<String>, key: &[u8], token_exp: Duration) -> JWTResult<String> {
35    let exp_time = Utc::now() + token_exp;
36    let payload = Claims {
37        allow: perms,
38        exp: Some(exp_time.timestamp() as usize),
39    };
40    encode(&Header::default(), &payload, &EncodingKey::from_secret(key))
41}
42
43/// Verify JWT Token and return the allowed permissions from token
44pub fn verify_token(token: &str, key: &[u8]) -> JWTResult<Vec<String>> {
45    let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::default());
46    if is_env_truthy("FOREST_JWT_DISABLE_EXP_VALIDATION") {
47        let mut claims = validation.required_spec_claims.clone();
48        claims.remove("exp");
49        let buff: Vec<_> = claims.iter().collect();
50        validation.set_required_spec_claims(&buff);
51        validation.validate_exp = false;
52    }
53    let token = decode::<Claims>(token, &DecodingKey::from_secret(key), &validation)?;
54    Ok(token.claims.allow)
55}
56
57pub fn generate_priv_key() -> KeyInfo {
58    let priv_key = crate::utils::rand::forest_os_rng().r#gen::<[u8; 32]>();
59    // This is temporary use of bls key as placeholder, need to update keyinfo to use string
60    // instead of keyinfo for key type
61    KeyInfo::new(SignatureType::Bls, priv_key.to_vec())
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use serial_test::serial;
68
69    /// Create a new JWT Token without expiration
70    fn create_token_without_exp(perms: Vec<String>, key: &[u8]) -> JWTResult<String> {
71        let payload = Claims {
72            allow: perms,
73            exp: None,
74        };
75        encode(&Header::default(), &payload, &EncodingKey::from_secret(key))
76    }
77
78    #[test]
79    #[serial]
80    fn create_and_verify_token() {
81        let perms_expected = vec![
82            "Ph'nglui mglw'nafh Cthulhu".to_owned(),
83            "R'lyeh wgah'nagl fhtagn".to_owned(),
84        ];
85        let key = generate_priv_key();
86
87        // Token duration of 1 hour. Validation must pass.
88        let token = create_token(
89            perms_expected.clone(),
90            key.private_key(),
91            Duration::try_hours(1).expect("Infallible"),
92        )
93        .unwrap();
94        let perms = verify_token(&token, key.private_key()).unwrap();
95        assert_eq!(perms_expected, perms);
96
97        // Token duration of -1 hour (already expired). Validation must fail.
98        let token = create_token(
99            perms_expected.clone(),
100            key.private_key(),
101            -Duration::try_hours(1).expect("Infallible"),
102        )
103        .unwrap();
104        assert!(verify_token(&token, key.private_key()).is_err());
105
106        // Token duration of -10 seconds (already expired, slightly). There is leeway of 60 seconds
107        // by default, so validation must pass.
108        let token = create_token(
109            perms_expected.clone(),
110            key.private_key(),
111            -Duration::try_seconds(10).expect("Infallible"),
112        )
113        .unwrap();
114        let perms = verify_token(&token, key.private_key()).unwrap();
115        assert_eq!(perms_expected, perms);
116    }
117
118    #[test]
119    #[serial]
120    fn create_and_verify_token_without_exp() {
121        let perms_expected = vec![
122            "Ia! Ia! Cthulhu fhtagn".to_owned(),
123            "Zin-Mi-Yak, dread lord of the deep".to_owned(),
124        ];
125        let key = generate_priv_key();
126
127        // Disable expiration validation via env var
128        unsafe {
129            std::env::set_var("FOREST_JWT_DISABLE_EXP_VALIDATION", "1");
130        }
131
132        // No exp at all in the token. Validation must pass.
133        let token = create_token_without_exp(perms_expected.clone(), key.private_key()).unwrap();
134        let perms = verify_token(&token, key.private_key()).unwrap();
135        assert_eq!(perms_expected, perms);
136
137        // Token duration of -1 hour (already expired). Validation must pass.
138        let token = create_token(
139            perms_expected.clone(),
140            key.private_key(),
141            -Duration::try_hours(1).expect("Infallible"),
142        )
143        .unwrap();
144        let perms = verify_token(&token, key.private_key()).unwrap();
145        assert_eq!(perms_expected, perms);
146
147        unsafe {
148            std::env::remove_var("FOREST_JWT_DISABLE_EXP_VALIDATION");
149        }
150    }
151}