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