Skip to main content

ri/auth/
jwt.rs

1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of Ri.
4//! The Ri project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//!     http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # JWT Authentication Module
21//!
22//! This module provides JSON Web Token (JWT) based authentication functionality
23//! for the Ri framework. It includes JWT token generation, validation, and
24//! claims management.
25//!
26//! ## JSON Web Tokens
27//!
28//! JWT is an open standard (RFC 7519) for securely transmitting information
29//! between parties as a JSON object. This module implements JWT-based stateless
30//! authentication, which is suitable for distributed systems and microservices
31//! architectures where session persistence is challenging.
32//!
33//! ## Key Components
34//!
35//! - **RiJWTClaims**: Standard JWT claims including subject, expiration, issued at,
36//!   roles, and permissions
37//! - **RiJWTValidationOptions**: Configuration options for token validation
38//! - **RiJWTManager**: Core manager for token generation and validation
39//!
40//! ## Token Structure
41//!
42//! A JWT consists of three parts separated by dots:
43//!
44//! 1. **Header**: Contains token type (JWT) and signing algorithm (HS256)
45//! 2. **Payload**: Contains the claims (subject, expiration, roles, permissions)
46//! 3. **Signature**: Verifies the token's integrity
47//!
48//! ## Usage Example
49//!
50//! ```rust,ignore
51//! use ri::auth::jwt::RiJWTManager;
52//!
53//! fn authenticate_user() {
54//!     let manager = RiJWTManager::create(
55//!         "your-secret-key".to_string(),
56//!         3600  // 1 hour expiry
57//!     );
58//!
59//!     // Generate token
60//!     let token = manager.generate_token(
61//!         "user123",
62//!         vec!["admin".to_string()],
63//!         vec!["read".to_string(), "write".to_string()]
64//!     );
65//!
66//!     // Validate token
67//!     let claims = manager.validate_token(&token);
68//!     println!("User: {}", claims.sub);
69//!     println!("Roles: {:?}", claims.roles);
70//! }
71//! ```
72//!
73//! ## Security Considerations
74//!
75//! - **Secret Key**: Keep the secret key secure and never expose it in client code
76//! - **Expiration**: Always set appropriate expiration times for tokens
77//! - **HTTPS**: Transmit tokens only over HTTPS connections
78//! - **Token Storage**: Store tokens securely on the client side
79//!
80//! ## Claims Reference
81//!
82//! - **sub (Subject)**: The user identifier or principal
83//! - **exp (Expiration)**: Token expiration time in Unix timestamp
84//! - **iat (Issued At)**: Token creation time in Unix timestamp
85//! - **roles**: List of role identifiers assigned to the user
86//! - **permissions**: List of permission identifiers granted to the subject
87
88use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
89use serde::{Deserialize, Serialize};
90use std::time::{SystemTime, UNIX_EPOCH};
91use zeroize::Zeroize;
92
93use crate::core::error::RiError;
94
95/// Represents the claims payload in a JWT token.
96///
97/// This structure contains all the standard and custom claims for a Ri JWT.
98/// It follows the JWT standard specification with additional custom claims
99/// for role-based access control (RBAC).
100///
101/// ## Standard Claims
102///
103/// - **sub**: Subject claim identifying the principal (user ID)
104/// - **exp**: Expiration time claim (Unix timestamp)
105/// - **iat**: Issued at claim (Unix timestamp)
106///
107/// ## Custom Claims
108///
109/// - **roles**: Role-based access control roles assigned to the subject
110/// - **permissions**: Specific permissions granted to the subject
111///
112/// ## Serialization
113///
114/// This struct uses serde with custom field names to ensure compatibility
115/// with standard JWT libraries across different programming languages.
116#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
117#[derive(Debug, Serialize, Deserialize, Clone)]
118pub struct RiJWTClaims {
119    /// Subject claim - identifies the principal (user ID)
120    #[serde(rename = "sub")]
121    pub sub: String,
122
123    /// Expiration time claim - Unix timestamp when the token expires
124    #[serde(rename = "exp")]
125    pub exp: u64,
126
127    /// Issued at claim - Unix timestamp when the token was created
128    #[serde(rename = "iat")]
129    pub iat: u64,
130
131    /// Custom claim - list of role identifiers for RBAC
132    #[serde(rename = "roles")]
133    pub roles: Vec<String>,
134
135    /// Custom claim - list of permission identifiers for fine-grained access control
136    #[serde(rename = "permissions")]
137    pub permissions: Vec<String>,
138}
139
140/// Configuration options for JWT token validation.
141///
142/// This structure provides configurable validation parameters that control
143/// how tokens are validated during the authentication process. Default values
144/// are provided for all options, making the struct suitable for common use cases.
145///
146/// ## Validation Options
147///
148/// - **validate_exp**: Verify the expiration claim is valid (not expired)
149/// - **validate_iat**: Verify the issued-at claim is valid (not issued in future)
150/// - **required_roles**: Minimum roles required for token to be valid
151/// - **required_permissions**: Minimum permissions required for token to be valid
152///
153/// ## Usage
154///
155/// ```rust,ignore
156/// use ri::auth::jwt::RiJWTValidationOptions;
157///
158/// let options = RiJWTValidationOptions {
159///     validate_exp: true,
160///     validate_iat: true,
161///     required_roles: vec!["user".to_string()],
162///     required_permissions: vec!["read".to_string()],
163/// };
164/// ```
165#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
166#[derive(Debug, Clone)]
167#[allow(dead_code)]
168pub struct RiJWTValidationOptions {
169    /// Whether to validate the expiration time claim
170    pub validate_exp: bool,
171
172    /// Whether to validate the issued-at time claim
173    pub validate_iat: bool,
174
175    /// Minimum roles required for the token to be valid
176    pub required_roles: Vec<String>,
177
178    /// Minimum permissions required for the token to be valid
179    pub required_permissions: Vec<String>,
180}
181
182#[cfg(feature = "pyo3")]
183#[pyo3::prelude::pymethods]
184impl RiJWTValidationOptions {
185    #[new]
186    fn py_new(
187        validate_exp: bool,
188        validate_iat: bool,
189        required_roles: Vec<String>,
190        required_permissions: Vec<String>,
191    ) -> Self {
192        Self {
193            validate_exp,
194            validate_iat,
195            required_roles,
196            required_permissions,
197        }
198    }
199}
200
201impl Default for RiJWTValidationOptions {
202    fn default() -> Self {
203        Self {
204            validate_exp: true,
205            validate_iat: true,
206            required_roles: vec![],
207            required_permissions: vec![],
208        }
209    }
210}
211
212/// Core JWT management structure.
213///
214/// The `RiJWTManager` handles all JWT-related operations including token
215/// generation, validation, and secret key management. It uses the HS256
216/// (HMAC SHA-256) algorithm for signing tokens.
217///
218/// ## Thread Safety
219///
220/// This structure is designed to be shared across threads when wrapped in
221/// an Arc. All methods are stateless regarding the token content and only
222/// read the configuration (secret and expiry).
223///
224/// ## Algorithm
225///
226/// Uses HMAC-SHA256 (HS256) for token signing. This symmetric algorithm
227/// uses the same secret key for both signing and verification.
228///
229/// ## Security
230///
231/// The secret key is protected with zeroize to ensure it is securely cleared
232/// from memory when the manager is dropped, preventing memory dump attacks.
233///
234/// ## Performance
235///
236/// Token generation and validation are designed to be fast operations.
237/// The encoding/decoding operations are primarily CPU-bound due to the
238/// HMAC computation.
239#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
240pub struct RiJWTManager {
241    /// The secret key used for signing and verifying tokens
242    /// This field is securely zeroized on drop
243    secret: String,
244
245    /// Default expiry time in seconds for generated tokens
246    expiry_secs: u64,
247
248    /// Pre-computed encoding key for faster token generation
249    encoding_key: EncodingKey,
250
251    /// Pre-computed decoding key for faster token validation
252    decoding_key: DecodingKey,
253}
254
255impl Drop for RiJWTManager {
256    fn drop(&mut self) {
257        // Securely zeroize the secret key to prevent memory dump attacks
258        self.secret.zeroize();
259    }
260}
261
262#[cfg(feature = "pyo3")]
263#[pyo3::prelude::pymethods]
264impl RiJWTManager {
265    /// Creates a new JWT manager with the specified secret and expiry time.
266    ///
267    /// This constructor is used for Python bindings and creates a JWT manager
268    /// that can generate and validate tokens with the given configuration.
269    ///
270    /// # Parameters
271    ///
272    /// - `secret`: The secret key used for signing and verifying JWT tokens
273    /// - `expiry_secs`: The default expiry time in seconds for generated tokens
274    ///
275    /// # Returns
276    ///
277    /// A new instance of `RiJWTManager`
278    #[new]
279    pub fn new(secret: String, expiry_secs: u64) -> Self {
280        let secret_bytes = secret.as_bytes().to_vec();
281        Self {
282            secret,
283            expiry_secs,
284            encoding_key: EncodingKey::from_secret(&secret_bytes),
285            decoding_key: DecodingKey::from_secret(&secret_bytes),
286        }
287    }
288
289    /// Generates a new JWT token for the specified user with roles and permissions.
290    ///
291    /// # Parameters
292    ///
293    /// - `user_id`: The unique identifier of the user (subject claim)
294    /// - `roles`: A list of role identifiers assigned to the user
295    /// - `permissions`: A list of permission identifiers granted to the user
296    ///
297    /// # Returns
298    ///
299    /// The encoded JWT token string
300    pub fn py_generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> pyo3::prelude::PyResult<String> {
301        self.generate_token(user_id, roles, permissions).map_err(crate::auth::security::ri_error_to_py_err)
302    }
303
304    /// Validates a JWT token and returns the decoded claims.
305    ///
306    /// # Parameters
307    ///
308    /// - `token`: The JWT token string to validate
309    ///
310    /// # Returns
311    ///
312    /// The decoded RiJWTClaims if validation succeeds
313    pub fn py_validate_token(&self, token: &str) -> pyo3::prelude::PyResult<pyo3::Py<pyo3::PyAny>> {
314        use pyo3::prelude::*;
315        
316        Python::attach(|py| {
317            self.validate_token(token)
318                .map_err(crate::auth::security::ri_error_to_py_err)
319                .map(|claims| {
320                    Py::new(py, claims).unwrap().into_any()
321                })
322        })
323    }
324
325    /// Returns the default token expiry time in seconds.
326    pub fn py_get_token_expiry(&self) -> u64 {
327        self.expiry_secs
328    }
329
330    /// Generates a new JWT token for the specified user with roles and permissions.
331    ///
332    /// This is an alias for `py_generate_token` providing a more Pythonic API.
333    ///
334    /// # Parameters
335    ///
336    /// - `user_id`: The unique identifier of the user (subject claim)
337    /// - `roles`: A list of role identifiers assigned to the user
338    /// - `permissions`: A list of permission identifiers granted to the user
339    ///
340    /// # Returns
341    ///
342    /// The encoded JWT token string
343    #[pyo3(name = "generate_token")]
344    pub fn generate_token_py(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> pyo3::prelude::PyResult<String> {
345        self.py_generate_token(user_id, roles, permissions)
346    }
347
348    /// Validates a JWT token and returns the decoded claims.
349    ///
350    /// This is an alias for `py_validate_token` providing a more Pythonic API.
351    ///
352    /// # Parameters
353    ///
354    /// - `token`: The JWT token string to validate
355    ///
356    /// # Returns
357    ///
358    /// The decoded RiJWTClaims if validation succeeds
359    #[pyo3(name = "validate_token")]
360    pub fn validate_token_py(&self, token: &str) -> pyo3::prelude::PyResult<pyo3::Py<pyo3::PyAny>> {
361        self.py_validate_token(token)
362    }
363
364    /// Returns the default token expiry time in seconds.
365    ///
366    /// This is an alias for `py_get_token_expiry` providing a more Pythonic API.
367    #[pyo3(name = "get_token_expiry")]
368    pub fn get_token_expiry_py(&self) -> u64 {
369        self.py_get_token_expiry()
370    }
371}
372
373impl RiJWTManager {
374    /// Creates a new JWT manager with the specified secret and expiry time.
375    ///
376    /// This is the primary constructor for creating a JWT manager. It initializes
377    /// the manager with a secret key and default token expiry time. The secret key
378    /// is used for both signing new tokens and validating existing ones.
379    ///
380    /// ## Performance
381    ///
382    /// This constructor pre-computes the encoding and decoding keys for optimal
383    /// performance during token generation and validation operations.
384    ///
385    /// # Parameters
386    ///
387    /// - `secret`: The secret key used for signing and verifying JWT tokens
388    /// - `expiry_secs`: The default expiry time in seconds for generated tokens
389    ///
390    /// # Returns
391    ///
392    /// A new instance of `RiJWTManager`
393    ///
394    /// # Examples
395    ///
396    /// ```rust,ignore
397    /// use ri::auth::jwt::RiJWTManager;
398    ///
399    /// let manager = RiJWTManager::create(
400    ///     "your-secret-key".to_string(),
401    ///     3600  // 1 hour expiry
402    /// );
403    /// ```
404    pub fn create(secret: String, expiry_secs: u64) -> Self {
405        let secret_bytes = secret.as_bytes().to_vec();
406        Self {
407            secret,
408            expiry_secs,
409            encoding_key: EncodingKey::from_secret(&secret_bytes),
410            decoding_key: DecodingKey::from_secret(&secret_bytes),
411        }
412    }
413
414    /// Generates a new JWT token for the specified user with roles and permissions.
415    ///
416    /// This method creates a signed JWT token containing the user's subject identifier,
417    /// assigned roles, and permissions. The token is signed using HMAC-SHA256 algorithm.
418    ///
419    /// ## Token Claims
420    ///
421    /// The generated token includes the following claims:
422    /// - `sub`: The user identifier
423    /// - `exp`: Expiration time (current time + expiry_secs)
424    /// - `iat`: Issued at time (current time)
425    /// - `roles`: List of role identifiers
426    /// - `permissions`: List of permission identifiers
427    ///
428    /// # Parameters
429    ///
430    /// - `user_id`: The unique identifier of the user (subject claim)
431    /// - `roles`: A vector of role identifiers assigned to the user
432    /// - `permissions`: A vector of permission identifiers granted to the user
433    ///
434    /// # Returns
435    ///
436    /// A Result containing the encoded JWT token string, or a RiError if encoding fails
437    ///
438    /// # Examples
439    ///
440    /// ```rust,ignore
441    /// use ri::auth::jwt::RiJWTManager;
442    ///
443    /// let manager = RiJWTManager::create("secret".to_string(), 3600);
444    ///
445    /// let token = manager.generate_token(
446    ///     "user123",
447    ///     vec!["admin".to_string()],
448    ///     vec!["read:data".to_string(), "write:data".to_string()]
449    /// );
450    ///
451    /// match token {
452    ///     Ok(t) => println!("Generated token: {}", t),
453    ///     Err(e) => println!("Failed to generate token: {:?}", e),
454    /// }
455    /// ```
456    pub fn generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> Result<String, RiError> {
457        let now = SystemTime::now()
458            .duration_since(UNIX_EPOCH)
459            .map_err(|e| RiError::Other(format!("System time error: {}", e)))?
460            .as_secs();
461
462        let claims = RiJWTClaims {
463            sub: user_id.to_string(),
464            exp: now + self.expiry_secs,
465            iat: now,
466            roles,
467            permissions,
468        };
469
470        encode(&Header::default(), &claims, &self.encoding_key)
471            .map_err(|e| RiError::Other(format!("JWT encoding failed: {}", e)))
472    }
473
474    /// Validates a JWT token and returns the decoded claims.
475    ///
476    /// This method verifies the token's signature and decodes the claims payload.
477    /// It validates the token structure and signature using the configured secret key.
478    ///
479    /// ## Validation Performed
480    ///
481    /// - Verifies the token signature using HMAC-SHA256
482    /// - Validates the token structure (header, payload, signature)
483    /// - Checks token expiration if validation is enabled
484    ///
485    /// # Parameters
486    ///
487    /// - `token`: The JWT token string to validate
488    ///
489    /// # Returns
490    ///
491    /// A Result containing the decoded RiJWTClaims if validation succeeds,
492    /// or a RiError if validation fails (invalid signature, expired token, etc.)
493    ///
494    /// # Examples
495    ///
496    /// ```rust,ignore
497    /// use ri::auth::jwt::RiJWTManager;
498    ///
499    /// let manager = RiJWTManager::create("secret".to_string(), 3600);
500    ///
501    /// // First generate a token
502    /// let token = manager.generate_token("user123", vec![], vec![]).unwrap();
503    ///
504    /// // Then validate it
505    /// let claims = manager.validate_token(&token);
506    ///
507    /// match claims {
508    ///     Ok(c) => println!("User: {}, Roles: {:?}", c.sub, c.roles),
509    ///     Err(e) => println!("Invalid token: {:?}", e),
510    /// }
511    /// ```
512    pub fn validate_token(&self, token: &str) -> Result<RiJWTClaims, RiError> {
513        let mut validation = Validation::default();
514        validation.set_required_spec_claims(&["exp"]);
515        validation.algorithms = vec![jsonwebtoken::Algorithm::HS256];
516        
517        decode::<RiJWTClaims>(token, &self.decoding_key, &validation)
518            .map_err(|e| {
519                // Security: Log detailed error internally, return generic message
520                log::warn!("[Ri.JWT] Token validation failed: {}", e);
521                RiError::Other("Invalid token".to_string())
522            })
523            .map(|token_data| token_data.claims)
524    }
525
526    /// Returns the default token expiry time in seconds.
527    ///
528    /// This method returns the configured default expiry time that is used
529    /// when generating new tokens.
530    ///
531    /// # Returns
532    ///
533    /// The default token expiry time in seconds
534    pub fn get_token_expiry(&self) -> u64 {
535        self.expiry_secs
536    }
537
538    /// Returns a fingerprint of the secret key for audit purposes.
539    ///
540    /// This method returns a SHA-256 hash of the secret key prefix,
541    /// useful for identifying which key was used for signing without
542    /// exposing the actual secret.
543    ///
544    /// # Returns
545    ///
546    /// A hexadecimal string representing the key fingerprint
547    pub fn get_key_fingerprint(&self) -> String {
548        use sha2::{Sha256, Digest};
549        let prefix = if self.secret.len() > 8 {
550            &self.secret[..8]
551        } else {
552            &self.secret
553        };
554        let mut hasher = Sha256::new();
555        hasher.update(prefix.as_bytes());
556        format!("{:x}", hasher.finalize())
557    }
558}