Skip to main content

web/
token.rs

1use base64::{Engine as _, engine::general_purpose};
2use jsonwebtoken::errors::{Error as JwtError, ErrorKind};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6#[derive(Debug, Serialize, Deserialize)]
7pub struct TokenDetails {
8    pub token: Option<String>,
9    pub token_uuid: uuid::Uuid,
10    pub user_id: uuid::Uuid,
11    pub token_type: Option<TokenType>,
12    pub expires_in: Option<i64>,
13    /// `users.session_generation` as of minting. A password change bumps the
14    /// column, so every token carrying an older value stops verifying.
15    pub generation: i32,
16}
17
18/// Token class. A signed claim, so a refresh path can reject an access token
19/// and vice-versa — without this, one per-user key signs both classes and an
20/// access token could be replayed as a refresh token.
21#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
22#[serde(rename_all = "lowercase")]
23pub enum TokenType {
24    Access,
25    Refresh,
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
29pub struct TokenClaims {
30    pub sub: String,
31    pub token_uuid: String,
32    /// Deliberately NOT `#[serde(default)]`: a token minted before this claim
33    /// existed must fail to decode, not default to generation 0 and survive.
34    pub generation: i32,
35    #[serde(default)]
36    pub token_type: Option<TokenType>,
37    pub exp: i64,
38    pub iat: i64,
39    pub nbf: i64,
40}
41
42/// Decodes a base64-encoded PEM key, mapping a malformed value to a JWT
43/// `InvalidKeyFormat` error instead of panicking.
44fn decode_pem(b64: &str) -> Result<Vec<u8>, JwtError> {
45    general_purpose::STANDARD
46        .decode(b64)
47        .map_err(|_| JwtError::from(ErrorKind::InvalidKeyFormat))
48}
49
50pub fn generate_jwt_token(
51    user_id: uuid::Uuid,
52    ttl: i64,
53    private_key: &str,
54    token_type: TokenType,
55    generation: i32,
56) -> Result<TokenDetails, JwtError> {
57    generate_jwt_token_with_uuid(
58        user_id,
59        Uuid::new_v4(),
60        ttl,
61        private_key,
62        token_type,
63        generation,
64    )
65}
66
67pub fn generate_jwt_token_with_uuid(
68    user_id: uuid::Uuid,
69    token_uuid: uuid::Uuid,
70    ttl: i64,
71    private_key: &str,
72    token_type: TokenType,
73    generation: i32,
74) -> Result<TokenDetails, JwtError> {
75    let decoded_private_key = decode_pem(private_key)?;
76
77    let now = chrono::Utc::now();
78    let mut token_details = TokenDetails {
79        user_id,
80        token_uuid,
81        token_type: Some(token_type),
82        expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()),
83        token: None,
84        generation,
85    };
86
87    let claims = TokenClaims {
88        sub: token_details.user_id.to_string(),
89        token_uuid: token_details.token_uuid.to_string(),
90        generation,
91        token_type: Some(token_type),
92        exp: token_details.expires_in.unwrap_or_default(),
93        iat: now.timestamp(),
94        nbf: now.timestamp(),
95    };
96
97    let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
98    let token = jsonwebtoken::encode(
99        &header,
100        &claims,
101        &jsonwebtoken::EncodingKey::from_rsa_pem(&decoded_private_key)?,
102    )?;
103    token_details.token = Some(token);
104    Ok(token_details)
105}
106
107pub fn verify_jwt_token(public_key: &str, token: &str) -> Result<TokenDetails, JwtError> {
108    let decoded_public_key = decode_pem(public_key)?;
109
110    let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
111
112    let decoded = jsonwebtoken::decode::<TokenClaims>(
113        token,
114        &jsonwebtoken::DecodingKey::from_rsa_pem(&decoded_public_key)?,
115        &validation,
116    )?;
117
118    let token_details = claims_to_details(&decoded.claims)?;
119    Ok(token_details)
120}
121
122/// Reads the `sub` (user id) from a token WITHOUT verifying its signature.
123///
124/// This is a routing hint ONLY: it tells the verifier whose public key to fetch.
125/// The token MUST then be re-verified with that key via [`verify_jwt_token`] —
126/// an attacker can set any `sub`, but a token signed with the wrong key fails
127/// that real verification. Never trust the result for authorization.
128///
129/// # Errors
130/// Returns the underlying JWT decode error if the token is structurally invalid
131/// or its `sub` is not a UUID.
132pub fn unverified_user_id(token: &str) -> Result<Uuid, JwtError> {
133    let decoded = jsonwebtoken::dangerous::insecure_decode::<TokenClaims>(token)?;
134    Uuid::parse_str(&decoded.claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))
135}
136
137fn claims_to_details(claims: &TokenClaims) -> Result<TokenDetails, JwtError> {
138    let user_id =
139        Uuid::parse_str(&claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))?;
140    let token_uuid =
141        Uuid::parse_str(&claims.token_uuid).map_err(|_| JwtError::from(ErrorKind::InvalidToken))?;
142    Ok(TokenDetails {
143        token: None,
144        token_uuid,
145        user_id,
146        token_type: claims.token_type,
147        // The signed `exp`, not `None`: callers bound work by a token's
148        // remaining life, and dropping it here silently disabled them.
149        expires_in: Some(claims.exp),
150        generation: claims.generation,
151    })
152}