1
use base64::{Engine as _, engine::general_purpose};
2
use jsonwebtoken::errors::{Error as JwtError, ErrorKind};
3
use serde::{Deserialize, Serialize};
4
use uuid::Uuid;
5

            
6
#[derive(Debug, Serialize, Deserialize)]
7
pub 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")]
23
pub enum TokenType {
24
    Access,
25
    Refresh,
26
}
27

            
28
#[derive(Debug, Serialize, Deserialize, Clone)]
29
pub 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.
44
15
fn decode_pem(b64: &str) -> Result<Vec<u8>, JwtError> {
45
15
    general_purpose::STANDARD
46
15
        .decode(b64)
47
15
        .map_err(|_| JwtError::from(ErrorKind::InvalidKeyFormat))
48
15
}
49

            
50
5
pub fn generate_jwt_token(
51
5
    user_id: uuid::Uuid,
52
5
    ttl: i64,
53
5
    private_key: &str,
54
5
    token_type: TokenType,
55
5
    generation: i32,
56
5
) -> Result<TokenDetails, JwtError> {
57
5
    generate_jwt_token_with_uuid(
58
5
        user_id,
59
5
        Uuid::new_v4(),
60
5
        ttl,
61
5
        private_key,
62
5
        token_type,
63
5
        generation,
64
    )
65
5
}
66

            
67
8
pub fn generate_jwt_token_with_uuid(
68
8
    user_id: uuid::Uuid,
69
8
    token_uuid: uuid::Uuid,
70
8
    ttl: i64,
71
8
    private_key: &str,
72
8
    token_type: TokenType,
73
8
    generation: i32,
74
8
) -> Result<TokenDetails, JwtError> {
75
8
    let decoded_private_key = decode_pem(private_key)?;
76

            
77
8
    let now = chrono::Utc::now();
78
8
    let mut token_details = TokenDetails {
79
8
        user_id,
80
8
        token_uuid,
81
8
        token_type: Some(token_type),
82
8
        expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()),
83
8
        token: None,
84
8
        generation,
85
8
    };
86

            
87
8
    let claims = TokenClaims {
88
8
        sub: token_details.user_id.to_string(),
89
8
        token_uuid: token_details.token_uuid.to_string(),
90
8
        generation,
91
8
        token_type: Some(token_type),
92
8
        exp: token_details.expires_in.unwrap_or_default(),
93
8
        iat: now.timestamp(),
94
8
        nbf: now.timestamp(),
95
8
    };
96

            
97
8
    let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
98
8
    let token = jsonwebtoken::encode(
99
8
        &header,
100
8
        &claims,
101
8
        &jsonwebtoken::EncodingKey::from_rsa_pem(&decoded_private_key)?,
102
    )?;
103
8
    token_details.token = Some(token);
104
8
    Ok(token_details)
105
8
}
106

            
107
7
pub fn verify_jwt_token(public_key: &str, token: &str) -> Result<TokenDetails, JwtError> {
108
7
    let decoded_public_key = decode_pem(public_key)?;
109

            
110
7
    let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
111

            
112
7
    let decoded = jsonwebtoken::decode::<TokenClaims>(
113
7
        token,
114
7
        &jsonwebtoken::DecodingKey::from_rsa_pem(&decoded_public_key)?,
115
7
        &validation,
116
1
    )?;
117

            
118
6
    let token_details = claims_to_details(&decoded.claims)?;
119
6
    Ok(token_details)
120
7
}
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.
132
4
pub fn unverified_user_id(token: &str) -> Result<Uuid, JwtError> {
133
4
    let decoded = jsonwebtoken::dangerous::insecure_decode::<TokenClaims>(token)?;
134
3
    Uuid::parse_str(&decoded.claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))
135
4
}
136

            
137
6
fn claims_to_details(claims: &TokenClaims) -> Result<TokenDetails, JwtError> {
138
6
    let user_id =
139
6
        Uuid::parse_str(&claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))?;
140
6
    let token_uuid =
141
6
        Uuid::parse_str(&claims.token_uuid).map_err(|_| JwtError::from(ErrorKind::InvalidToken))?;
142
6
    Ok(TokenDetails {
143
6
        token: None,
144
6
        token_uuid,
145
6
        user_id,
146
6
        token_type: claims.token_type,
147
6
        // The signed `exp`, not `None`: callers bound work by a token's
148
6
        // remaining life, and dropping it here silently disabled them.
149
6
        expires_in: Some(claims.exp),
150
6
        generation: claims.generation,
151
6
    })
152
6
}