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}