Skip to main content

karbon_framework/security/
jwt.rs

1use chrono::Utc;
2use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
3use serde::{Deserialize, Serialize};
4
5/// JWT claims stored in the token
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Claims {
8    /// Subject (username or email)
9    pub sub: String,
10    /// Username
11    pub username: String,
12    /// User roles
13    pub roles: Vec<String>,
14    /// Numeric user ID (optional, for apps using i64 primary keys)
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub user_id: Option<i64>,
17    /// User UUID (optional, for apps using a separate UUID column)
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub user_uuid: Option<String>,
20    /// Audience (optional, for multi-audience token validation)
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub aud: Option<String>,
23    /// Expiration timestamp
24    pub exp: i64,
25    /// Issued at
26    pub iat: i64,
27}
28
29/// JWT token manager
30#[derive(Clone)]
31pub struct JwtManager {
32    encoding_key: EncodingKey,
33    decoding_key: DecodingKey,
34    expiration: i64,
35}
36
37impl JwtManager {
38    /// Create a new JWT manager with the given secret and expiration (in seconds)
39    pub fn new(secret: &str, expiration: i64) -> Self {
40        Self {
41            encoding_key: EncodingKey::from_secret(secret.as_bytes()),
42            decoding_key: DecodingKey::from_secret(secret.as_bytes()),
43            expiration,
44        }
45    }
46
47    /// Generate a JWT token for a user (basic — sub only)
48    pub fn generate(&self, sub: &str, username: &str, roles: Vec<String>) -> Result<String, jsonwebtoken::errors::Error> {
49        self.generate_full(sub, username, roles, None, None, None)
50    }
51
52    /// Generate a JWT token with numeric user_id and/or UUID
53    pub fn generate_full(
54        &self,
55        sub: &str,
56        username: &str,
57        roles: Vec<String>,
58        user_id: Option<i64>,
59        user_uuid: Option<String>,
60        aud: Option<String>,
61    ) -> Result<String, jsonwebtoken::errors::Error> {
62        let now = Utc::now().timestamp();
63        let claims = Claims {
64            sub: sub.to_string(),
65            username: username.to_string(),
66            roles,
67            user_id,
68            user_uuid,
69            aud,
70            exp: now + self.expiration,
71            iat: now,
72        };
73
74        encode(&Header::default(), &claims, &self.encoding_key)
75    }
76
77    /// Validate and decode a JWT token
78    pub fn verify(&self, token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
79        let mut validation = Validation::default();
80        validation.validate_aud = false;
81        let token_data = decode::<Claims>(token, &self.decoding_key, &validation)?;
82        Ok(token_data.claims)
83    }
84
85    /// Generate an opaque refresh token (random 48-byte string, NOT a JWT).
86    /// The caller must hash it with `Crypto::hash_token()` before storing in DB.
87    /// Returns the raw token to send to the client.
88    pub fn generate_refresh_token() -> String {
89        super::Crypto::random_token(48)
90    }
91}