axum_gate/codecs/jwt/
mod.rs

1//! JWT infrastructure components.
2//!
3use super::Codec;
4use crate::errors::JwtError;
5use crate::errors::JwtOperation;
6use crate::errors::{Error, Result};
7use crate::jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
8pub use validation_result::JwtValidationResult;
9pub use validation_service::JwtValidationService;
10
11use std::collections::HashSet;
12use std::marker::PhantomData;
13
14use chrono::Utc;
15use serde::{Deserialize, Serialize, de::DeserializeOwned};
16use serde_with::skip_serializing_none;
17
18mod validation_result;
19mod validation_service;
20
21/// Registered/reserved claims by IANA/JWT spec, see
22/// [auth0](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims) for more
23/// information.
24#[derive(Serialize, Deserialize, Clone, Debug)]
25#[skip_serializing_none]
26pub struct RegisteredClaims {
27    /// Issuer of the JWT
28    #[serde(rename = "iss")]
29    pub issuer: String,
30    /// Subject of the JWT (the user)
31    #[serde(rename = "sub")]
32    pub subject: Option<String>,
33    /// Recipient for which the JWT is intended
34    #[serde(rename = "aud")]
35    pub audience: Option<HashSet<String>>,
36    /// Time after which the JWT expires
37    #[serde(rename = "exp")]
38    pub expiration_time: u64,
39    /// Time before which the JWT must not be accepted for processing
40    #[serde(rename = "nbf")]
41    pub not_before_time: Option<u64>,
42    /// Time at which the JWT was issued; can be used to determine age of the JWT
43    #[serde(rename = "iat")]
44    pub issued_at_time: u64,
45    /// Unique identifier; can be used to prevent the JWT from being replayed (allows a token to be used only once)
46    #[serde(rename = "jti")]
47    pub jwt_id: Option<String>,
48}
49
50impl RegisteredClaims {
51    /// Initializes the claims. Automatically sets the `issued_at_time` to `Utc::now`.
52    pub fn new(issuer: &str, expiration_time: u64) -> Self {
53        Self {
54            issuer: issuer.to_string(),
55            subject: None,
56            audience: None,
57            expiration_time,
58            not_before_time: None,
59            issued_at_time: Utc::now().timestamp() as u64,
60            jwt_id: None,
61        }
62    }
63}
64
65/// Combination of claims used within `axum-gate` and encoded with [JsonWebToken] codec.
66#[derive(Serialize, Deserialize, Clone, Debug)]
67pub struct JwtClaims<CustomClaims> {
68    /// The registered claims of a JWT.
69    #[serde(flatten)]
70    pub registered_claims: RegisteredClaims,
71    /// Your custom claims that are added to the JWT.
72    #[serde(flatten)]
73    pub custom_claims: CustomClaims,
74}
75
76impl<CustomClaims> JwtClaims<CustomClaims> {
77    /// Creates new claims with the given registered claims.
78    pub fn new(custom_claims: CustomClaims, registered_claims: RegisteredClaims) -> Self {
79        Self {
80            custom_claims,
81            registered_claims,
82        }
83    }
84
85    /// Checks whether the issuer equals to the given value.
86    pub fn has_issuer(&self, issuer: &str) -> bool {
87        self.registered_claims.issuer == issuer
88    }
89}
90
91/// Options to configure the [JsonWebToken] codec.
92pub struct JsonWebTokenOptions {
93    /// Key for encoding.
94    pub enc_key: EncodingKey,
95    /// Key for decoding.
96    pub dec_key: DecodingKey,
97    /// The header used for encoding.
98    pub header: Option<Header>,
99    /// Validation options.
100    pub validation: Option<Validation>,
101}
102
103impl Default for JsonWebTokenOptions {
104    /// Creates a random, alphanumeric 60 char key and uses it for en- and decoding (symmetric).
105    /// [Header] and [Validation] are set with its default values.
106    fn default() -> Self {
107        use rand::{Rng, distr::Alphanumeric, rng};
108
109        let authentication_secret: String = rng()
110            .sample_iter(&Alphanumeric)
111            .take(60)
112            .map(char::from)
113            .collect();
114        Self {
115            enc_key: EncodingKey::from_secret(authentication_secret.as_bytes()),
116            dec_key: DecodingKey::from_secret(authentication_secret.as_bytes()),
117            header: Some(Header::default()),
118            validation: Some(Validation::default()),
119        }
120    }
121}
122
123impl JsonWebTokenOptions {
124    /// Adds the given encoding key.
125    pub fn with_encoding_key(self, enc_key: EncodingKey) -> Self {
126        Self { enc_key, ..self }
127    }
128
129    /// Adds the given decoding key.
130    pub fn with_decoding_key(self, dec_key: DecodingKey) -> Self {
131        Self { dec_key, ..self }
132    }
133
134    /// Adds the given header.
135    pub fn with_header(self, header: Header) -> Self {
136        Self {
137            header: Some(header),
138            ..self
139        }
140    }
141
142    /// Adds the given validation.
143    pub fn with_validation(self, validation: Validation) -> Self {
144        Self {
145            validation: Some(validation),
146            ..self
147        }
148    }
149}
150
151/// Encrypts and validates JWTs using the configured keys and the `jsonwebtoken` crate.
152///
153/// # Key Management (IMPORTANT)
154///
155/// The default `JsonWebToken` (and its underlying `JsonWebTokenOptions::default()`) generates
156/// a fresh, random 60-character symmetric signing key every time a new instance is created.
157/// This is convenient for tests or ephemeral development sessions, but it also means that
158/// previously issued tokens become invalid whenever you create a NEW `JsonWebToken` via
159/// `JsonWebToken::default()` (or `JsonWebTokenOptions::default()`), because each call
160/// generates a fresh random key. If you construct exactly one instance at startup and
161/// reuse it for the whole process lifetime, tokens remain valid for that lifetime;
162/// but creating additional instances later (including, but not limited to, during a
163/// process restart or horizontal scaling) invalidates tokens produced by earlier instances.
164///
165/// If you need session continuity beyond a single in-memory instance (e.g. across process
166/// restarts, deployments, horizontal scaling, or any re-instantiation), you MUST provide
167/// a stable (persistent) key. Do this by constructing a `JsonWebToken` with explicit
168/// `JsonWebTokenOptions` using a key loaded from an environment variable, file, KMS,
169/// or another secret management system.
170///
171/// ## Providing a Persistent Symmetric Key
172/// ```rust
173/// use std::sync::Arc;
174/// use axum_gate::codecs::jwt::{JsonWebToken, JwtClaims, RegisteredClaims, JsonWebTokenOptions};
175/// use axum_gate::accounts::Account;
176/// use axum_gate::prelude::{Role, Group};
177/// use jsonwebtoken::{EncodingKey, DecodingKey};
178///
179/// // For the example we define a stable secret. In real code, load from env or secret manager.
180/// let secret = "test-secret".to_string();
181///
182/// // Construct symmetric encoding/decoding keys
183/// let enc_key = EncodingKey::from_secret(secret.as_bytes());
184/// let dec_key = DecodingKey::from_secret(secret.as_bytes());
185///
186/// // Build options manually (do NOT call `JsonWebTokenOptions::default()` here)
187/// let options = JsonWebTokenOptions {
188///     enc_key,
189///     dec_key,
190///     header: None,       // Use default header
191///     validation: None,   // Use default validation
192/// };
193///
194/// // Create a codec that will survive restarts as long as JWT_SECRET stays the same
195/// let jwt_codec = Arc::new(
196///     JsonWebToken::<JwtClaims<Account<Role, Group>>>::new_with_options(options)
197/// );
198/// ```
199///
200/// ## When It Is Safe to Use the Default
201/// - Unit / integration tests
202/// - Short-lived local development where logout on restart is acceptable
203/// - Disposable preview environments
204///
205/// ## When You Should NOT Use the Default
206/// - Production services
207/// - Any environment where user sessions must persist across restarts
208/// - Multi-instance / horizontally scaled deployments
209///
210/// In those cases always supply a deterministic key source.
211#[derive(Clone)]
212pub struct JsonWebToken<P> {
213    /// Key for encoding.
214    enc_key: EncodingKey,
215    /// Key for decoding.
216    dec_key: DecodingKey,
217    /// The header used for encoding.
218    pub header: Header,
219    /// Validation options for the JWT.
220    pub validation: Validation,
221    phantom_payload: PhantomData<P>,
222}
223
224impl<P> JsonWebToken<P> {
225    /// Creates a new instance with the given options.
226    pub fn new_with_options(options: JsonWebTokenOptions) -> Self {
227        let JsonWebTokenOptions {
228            enc_key,
229            dec_key,
230            header,
231            validation,
232        } = options;
233        Self {
234            enc_key,
235            dec_key,
236            header: header.unwrap_or(Header::default()),
237            validation: validation.unwrap_or(Validation::default()),
238            phantom_payload: PhantomData,
239        }
240    }
241}
242
243impl<P> Default for JsonWebToken<P> {
244    fn default() -> Self {
245        Self::new_with_options(JsonWebTokenOptions::default())
246    }
247}
248
249impl<P> Codec for JsonWebToken<P>
250where
251    P: Serialize + DeserializeOwned + Clone,
252{
253    type Payload = P;
254    fn encode(&self, payload: &Self::Payload) -> Result<Vec<u8>> {
255        let web_token =
256            jsonwebtoken::encode(&self.header, payload, &self.enc_key).map_err(|e| {
257                Error::Jwt(JwtError::processing(
258                    JwtOperation::Encode,
259                    format!("JWT encoding failed: {e}"),
260                ))
261            })?;
262        Ok(web_token.as_bytes().to_vec())
263    }
264    /// Decodes the given value.
265    ///
266    /// # Errors
267    /// Returns an error if the header stored in [JsonWebToken] does not match the decoded value.
268    /// The header can be retrieved from [JsonWebToken::header].
269    fn decode(&self, encoded_value: &[u8]) -> Result<Self::Payload> {
270        let claims =
271            jsonwebtoken::decode::<Self::Payload>(&encoded_value, &self.dec_key, &self.validation)
272                .map_err(|e| {
273                    Error::Jwt(JwtError::processing_with_preview(
274                        JwtOperation::Decode,
275                        format!("JWT decoding failed: {e}"),
276                        Some(
277                            String::from_utf8_lossy(encoded_value)
278                                .chars()
279                                .take(20)
280                                .collect::<String>()
281                                + "...",
282                        ),
283                    ))
284                })?;
285
286        if self.header != claims.header {
287            return Err(Error::Jwt(JwtError::processing(
288                JwtOperation::Validate,
289                "Header of the decoded value does not match the one used for encoding".to_string(),
290            )));
291        }
292
293        Ok(claims.claims)
294    }
295}