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};
91
92use crate::core::error::RiError;
93
94/// Represents the claims payload in a JWT token.
95///
96/// This structure contains all the standard and custom claims for a Ri JWT.
97/// It follows the JWT standard specification with additional custom claims
98/// for role-based access control (RBAC).
99///
100/// ## Standard Claims
101///
102/// - **sub**: Subject claim identifying the principal (user ID)
103/// - **exp**: Expiration time claim (Unix timestamp)
104/// - **iat**: Issued at claim (Unix timestamp)
105///
106/// ## Custom Claims
107///
108/// - **roles**: Role-based access control roles assigned to the subject
109/// - **permissions**: Specific permissions granted to the subject
110///
111/// ## Serialization
112///
113/// This struct uses serde with custom field names to ensure compatibility
114/// with standard JWT libraries across different programming languages.
115#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
116#[derive(Debug, Serialize, Deserialize, Clone)]
117pub struct RiJWTClaims {
118    /// Subject claim - identifies the principal (user ID)
119    #[serde(rename = "sub")]
120    pub sub: String,
121
122    /// Expiration time claim - Unix timestamp when the token expires
123    #[serde(rename = "exp")]
124    pub exp: u64,
125
126    /// Issued at claim - Unix timestamp when the token was created
127    #[serde(rename = "iat")]
128    pub iat: u64,
129
130    /// Custom claim - list of role identifiers for RBAC
131    #[serde(rename = "roles")]
132    pub roles: Vec<String>,
133
134    /// Custom claim - list of permission identifiers for fine-grained access control
135    #[serde(rename = "permissions")]
136    pub permissions: Vec<String>,
137}
138
139/// Configuration options for JWT token validation.
140///
141/// This structure provides configurable validation parameters that control
142/// how tokens are validated during the authentication process. Default values
143/// are provided for all options, making the struct suitable for common use cases.
144///
145/// ## Validation Options
146///
147/// - **validate_exp**: Verify the expiration claim is valid (not expired)
148/// - **validate_iat**: Verify the issued-at claim is valid (not issued in future)
149/// - **required_roles**: Minimum roles required for token to be valid
150/// - **required_permissions**: Minimum permissions required for token to be valid
151///
152/// ## Usage
153///
154/// ```rust,ignore
155/// use ri::auth::jwt::RiJWTValidationOptions;
156///
157/// let options = RiJWTValidationOptions {
158///     validate_exp: true,
159///     validate_iat: true,
160///     required_roles: vec!["user".to_string()],
161///     required_permissions: vec!["read".to_string()],
162/// };
163/// ```
164#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
165#[derive(Debug, Clone)]
166#[allow(dead_code)]
167pub struct RiJWTValidationOptions {
168    /// Whether to validate the expiration time claim
169    pub validate_exp: bool,
170
171    /// Whether to validate the issued-at time claim
172    pub validate_iat: bool,
173
174    /// Minimum roles required for the token to be valid
175    pub required_roles: Vec<String>,
176
177    /// Minimum permissions required for the token to be valid
178    pub required_permissions: Vec<String>,
179}
180
181#[cfg(feature = "pyo3")]
182#[pyo3::prelude::pymethods]
183impl RiJWTValidationOptions {
184    #[new]
185    fn py_new(
186        validate_exp: bool,
187        validate_iat: bool,
188        required_roles: Vec<String>,
189        required_permissions: Vec<String>,
190    ) -> Self {
191        Self {
192            validate_exp,
193            validate_iat,
194            required_roles,
195            required_permissions,
196        }
197    }
198}
199
200impl Default for RiJWTValidationOptions {
201    fn default() -> Self {
202        Self {
203            validate_exp: true,
204            validate_iat: true,
205            required_roles: vec![],
206            required_permissions: vec![],
207        }
208    }
209}
210
211/// Core JWT management structure.
212///
213/// The `RiJWTManager` handles all JWT-related operations including token
214/// generation, validation, and secret key management. It uses the HS256
215/// (HMAC SHA-256) algorithm for signing tokens.
216///
217/// ## Thread Safety
218///
219/// This structure is designed to be shared across threads when wrapped in
220/// an Arc. All methods are stateless regarding the token content and only
221/// read the configuration (secret and expiry).
222///
223/// ## Algorithm
224///
225/// Uses HMAC-SHA256 (HS256) for token signing. This symmetric algorithm
226/// uses the same secret key for both signing and verification.
227///
228/// ## Performance
229///
230/// Token generation and validation are designed to be fast operations.
231/// The encoding/decoding operations are primarily CPU-bound due to the
232/// HMAC computation.
233#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
234pub struct RiJWTManager {
235    /// The secret key used for signing and verifying tokens
236    secret: String,
237
238    /// Default expiry time in seconds for generated tokens
239    expiry_secs: u64,
240
241    /// Pre-computed encoding key for faster token generation
242    encoding_key: EncodingKey,
243
244    /// Pre-computed decoding key for faster token validation
245    decoding_key: DecodingKey,
246}
247
248#[cfg(feature = "pyo3")]
249#[pyo3::prelude::pymethods]
250impl RiJWTManager {
251    /// Creates a new JWT manager with the specified secret and expiry time.
252    ///
253    /// This constructor is used for Python bindings and creates a JWT manager
254    /// that can generate and validate tokens with the given configuration.
255    ///
256    /// # Parameters
257    ///
258    /// - `secret`: The secret key used for signing and verifying JWT tokens
259    /// - `expiry_secs`: The default expiry time in seconds for generated tokens
260    ///
261    /// # Returns
262    ///
263    /// A new instance of `RiJWTManager`
264    #[new]
265    pub fn new(secret: String, expiry_secs: u64) -> Self {
266        let secret_bytes = secret.as_bytes().to_vec();
267        Self {
268            secret,
269            expiry_secs,
270            encoding_key: EncodingKey::from_secret(&secret_bytes),
271            decoding_key: DecodingKey::from_secret(&secret_bytes),
272        }
273    }
274
275    /// Generates a new JWT token for the specified user with roles and permissions.
276    ///
277    /// # Parameters
278    ///
279    /// - `user_id`: The unique identifier of the user (subject claim)
280    /// - `roles`: A list of role identifiers assigned to the user
281    /// - `permissions`: A list of permission identifiers granted to the user
282    ///
283    /// # Returns
284    ///
285    /// The encoded JWT token string
286    pub fn py_generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> Result<String, RiError> {
287        self.generate_token(user_id, roles, permissions)
288    }
289
290    /// Validates a JWT token and returns the decoded claims.
291    ///
292    /// # Parameters
293    ///
294    /// - `token`: The JWT token string to validate
295    ///
296    /// # Returns
297    ///
298    /// The decoded RiJWTClaims if validation succeeds
299    pub fn py_validate_token(&self, token: &str) -> Result<RiJWTClaims, RiError> {
300        self.validate_token(token)
301    }
302
303    /// Returns the default token expiry time in seconds.
304    pub fn py_get_token_expiry(&self) -> u64 {
305        self.expiry_secs
306    }
307
308    /// Generates a new JWT token for the specified user with roles and permissions.
309    ///
310    /// This is an alias for `py_generate_token` providing a more Pythonic API.
311    ///
312    /// # Parameters
313    ///
314    /// - `user_id`: The unique identifier of the user (subject claim)
315    /// - `roles`: A list of role identifiers assigned to the user
316    /// - `permissions`: A list of permission identifiers granted to the user
317    ///
318    /// # Returns
319    ///
320    /// The encoded JWT token string
321    #[pyo3(name = "generate_token")]
322    pub fn generate_token_py(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> Result<String, RiError> {
323        self.py_generate_token(user_id, roles, permissions)
324    }
325
326    /// Validates a JWT token and returns the decoded claims.
327    ///
328    /// This is an alias for `py_validate_token` providing a more Pythonic API.
329    ///
330    /// # Parameters
331    ///
332    /// - `token`: The JWT token string to validate
333    ///
334    /// # Returns
335    ///
336    /// The decoded RiJWTClaims if validation succeeds
337    #[pyo3(name = "validate_token")]
338    pub fn validate_token_py(&self, token: &str) -> Result<RiJWTClaims, RiError> {
339        self.py_validate_token(token)
340    }
341
342    /// Returns the default token expiry time in seconds.
343    ///
344    /// This is an alias for `py_get_token_expiry` providing a more Pythonic API.
345    #[pyo3(name = "get_token_expiry")]
346    pub fn get_token_expiry_py(&self) -> u64 {
347        self.py_get_token_expiry()
348    }
349}
350
351impl RiJWTManager {
352    /// Creates a new JWT manager with the specified secret and expiry time.
353    ///
354    /// This is the primary constructor for creating a JWT manager. It initializes
355    /// the manager with a secret key and default token expiry time. The secret key
356    /// is used for both signing new tokens and validating existing ones.
357    ///
358    /// ## Performance
359    ///
360    /// This constructor pre-computes the encoding and decoding keys for optimal
361    /// performance during token generation and validation operations.
362    ///
363    /// # Parameters
364    ///
365    /// - `secret`: The secret key used for signing and verifying JWT tokens
366    /// - `expiry_secs`: The default expiry time in seconds for generated tokens
367    ///
368    /// # Returns
369    ///
370    /// A new instance of `RiJWTManager`
371    ///
372    /// # Examples
373    ///
374    /// ```rust,ignore
375    /// use ri::auth::jwt::RiJWTManager;
376    ///
377    /// let manager = RiJWTManager::create(
378    ///     "your-secret-key".to_string(),
379    ///     3600  // 1 hour expiry
380    /// );
381    /// ```
382    pub fn create(secret: String, expiry_secs: u64) -> Self {
383        let secret_bytes = secret.as_bytes().to_vec();
384        Self {
385            secret,
386            expiry_secs,
387            encoding_key: EncodingKey::from_secret(&secret_bytes),
388            decoding_key: DecodingKey::from_secret(&secret_bytes),
389        }
390    }
391
392    /// Generates a new JWT token for the specified user with roles and permissions.
393    ///
394    /// This method creates a signed JWT token containing the user's subject identifier,
395    /// assigned roles, and permissions. The token is signed using HMAC-SHA256 algorithm.
396    ///
397    /// ## Token Claims
398    ///
399    /// The generated token includes the following claims:
400    /// - `sub`: The user identifier
401    /// - `exp`: Expiration time (current time + expiry_secs)
402    /// - `iat`: Issued at time (current time)
403    /// - `roles`: List of role identifiers
404    /// - `permissions`: List of permission identifiers
405    ///
406    /// # Parameters
407    ///
408    /// - `user_id`: The unique identifier of the user (subject claim)
409    /// - `roles`: A vector of role identifiers assigned to the user
410    /// - `permissions`: A vector of permission identifiers granted to the user
411    ///
412    /// # Returns
413    ///
414    /// A Result containing the encoded JWT token string, or a RiError if encoding fails
415    ///
416    /// # Examples
417    ///
418    /// ```rust,ignore
419    /// use ri::auth::jwt::RiJWTManager;
420    ///
421    /// let manager = RiJWTManager::create("secret".to_string(), 3600);
422    ///
423    /// let token = manager.generate_token(
424    ///     "user123",
425    ///     vec!["admin".to_string()],
426    ///     vec!["read:data".to_string(), "write:data".to_string()]
427    /// );
428    ///
429    /// match token {
430    ///     Ok(t) => println!("Generated token: {}", t),
431    ///     Err(e) => println!("Failed to generate token: {:?}", e),
432    /// }
433    /// ```
434    pub fn generate_token(&self, user_id: &str, roles: Vec<String>, permissions: Vec<String>) -> Result<String, RiError> {
435        let now = SystemTime::now()
436            .duration_since(UNIX_EPOCH)
437            .map_err(|e| RiError::Other(format!("System time error: {}", e)))?
438            .as_secs();
439
440        let claims = RiJWTClaims {
441            sub: user_id.to_string(),
442            exp: now + self.expiry_secs,
443            iat: now,
444            roles,
445            permissions,
446        };
447
448        encode(&Header::default(), &claims, &self.encoding_key)
449            .map_err(|e| RiError::Other(format!("JWT encoding failed: {}", e)))
450    }
451
452    /// Validates a JWT token and returns the decoded claims.
453    ///
454    /// This method verifies the token's signature and decodes the claims payload.
455    /// It validates the token structure and signature using the configured secret key.
456    ///
457    /// ## Validation Performed
458    ///
459    /// - Verifies the token signature using HMAC-SHA256
460    /// - Validates the token structure (header, payload, signature)
461    /// - Checks token expiration if validation is enabled
462    ///
463    /// # Parameters
464    ///
465    /// - `token`: The JWT token string to validate
466    ///
467    /// # Returns
468    ///
469    /// A Result containing the decoded RiJWTClaims if validation succeeds,
470    /// or a RiError if validation fails (invalid signature, expired token, etc.)
471    ///
472    /// # Examples
473    ///
474    /// ```rust,ignore
475    /// use ri::auth::jwt::RiJWTManager;
476    ///
477    /// let manager = RiJWTManager::create("secret".to_string(), 3600);
478    ///
479    /// // First generate a token
480    /// let token = manager.generate_token("user123", vec![], vec![]).unwrap();
481    ///
482    /// // Then validate it
483    /// let claims = manager.validate_token(&token);
484    ///
485    /// match claims {
486    ///     Ok(c) => println!("User: {}, Roles: {:?}", c.sub, c.roles),
487    ///     Err(e) => println!("Invalid token: {:?}", e),
488    /// }
489    /// ```
490    pub fn validate_token(&self, token: &str) -> Result<RiJWTClaims, RiError> {
491        let validation = Validation::default();
492        decode::<RiJWTClaims>(token, &self.decoding_key, &validation)
493            .map_err(|e| RiError::Other(format!("JWT decoding failed: {}", e)))
494            .map(|token_data| token_data.claims)
495    }
496
497    /// Returns the default token expiry time in seconds.
498    ///
499    /// This method returns the configured default expiry time that is used
500    /// when generating new tokens.
501    ///
502    /// # Returns
503    ///
504    /// The default token expiry time in seconds
505    pub fn get_token_expiry(&self) -> u64 {
506        self.expiry_secs
507    }
508
509    /// Returns a reference to the secret key.
510    ///
511    /// This method provides read-only access to the configured secret key.
512    /// The secret key is used for both signing and verifying tokens.
513    ///
514    /// # Returns
515    ///
516    /// A string slice reference to the secret key
517    ///
518    /// # Security Note
519    ///
520    /// Be cautious when exposing the secret key. In production, the secret
521    /// should be stored securely and never logged or exposed to unauthorized parties.
522    pub fn get_secret(&self) -> &str {
523        &self.secret
524    }
525}