Lines
97.3 %
Functions
12 %
Branches
100 %
use base64::{Engine as _, engine::general_purpose};
use jsonwebtoken::errors::{Error as JwtError, ErrorKind};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize)]
pub struct TokenDetails {
pub token: Option<String>,
pub token_uuid: uuid::Uuid,
pub user_id: uuid::Uuid,
pub token_type: Option<TokenType>,
pub expires_in: Option<i64>,
}
/// Token class. A signed claim, so a refresh path can reject an access token
/// and vice-versa — without this, one per-user key signs both classes and an
/// access token could be replayed as a refresh token.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TokenType {
Access,
Refresh,
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TokenClaims {
pub sub: String,
pub token_uuid: String,
#[serde(default)]
pub exp: i64,
pub iat: i64,
pub nbf: i64,
/// Decodes a base64-encoded PEM key, mapping a malformed value to a JWT
/// `InvalidKeyFormat` error instead of panicking.
fn decode_pem(b64: &str) -> Result<Vec<u8>, JwtError> {
general_purpose::STANDARD
.decode(b64)
.map_err(|_| JwtError::from(ErrorKind::InvalidKeyFormat))
pub fn generate_jwt_token(
user_id: uuid::Uuid,
ttl: i64,
private_key: &str,
token_type: TokenType,
) -> Result<TokenDetails, JwtError> {
generate_jwt_token_with_uuid(user_id, Uuid::new_v4(), ttl, private_key, token_type)
pub fn generate_jwt_token_with_uuid(
token_uuid: uuid::Uuid,
let decoded_private_key = decode_pem(private_key)?;
let now = chrono::Utc::now();
let mut token_details = TokenDetails {
user_id,
token_uuid,
token_type: Some(token_type),
expires_in: Some((now + chrono::Duration::minutes(ttl)).timestamp()),
token: None,
};
let claims = TokenClaims {
sub: token_details.user_id.to_string(),
token_uuid: token_details.token_uuid.to_string(),
exp: token_details.expires_in.unwrap_or_default(),
iat: now.timestamp(),
nbf: now.timestamp(),
let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256);
let token = jsonwebtoken::encode(
&header,
&claims,
&jsonwebtoken::EncodingKey::from_rsa_pem(&decoded_private_key)?,
)?;
token_details.token = Some(token);
Ok(token_details)
pub fn verify_jwt_token(public_key: &str, token: &str) -> Result<TokenDetails, JwtError> {
let decoded_public_key = decode_pem(public_key)?;
let validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::RS256);
let decoded = jsonwebtoken::decode::<TokenClaims>(
token,
&jsonwebtoken::DecodingKey::from_rsa_pem(&decoded_public_key)?,
&validation,
let token_details = claims_to_details(&decoded.claims)?;
/// Reads the `sub` (user id) from a token WITHOUT verifying its signature.
///
/// This is a routing hint ONLY: it tells the verifier whose public key to fetch.
/// The token MUST then be re-verified with that key via [`verify_jwt_token`] —
/// an attacker can set any `sub`, but a token signed with the wrong key fails
/// that real verification. Never trust the result for authorization.
/// # Errors
/// Returns the underlying JWT decode error if the token is structurally invalid
/// or its `sub` is not a UUID.
pub fn unverified_user_id(token: &str) -> Result<Uuid, JwtError> {
let decoded = jsonwebtoken::dangerous::insecure_decode::<TokenClaims>(token)?;
Uuid::parse_str(&decoded.claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))
fn claims_to_details(claims: &TokenClaims) -> Result<TokenDetails, JwtError> {
let user_id =
Uuid::parse_str(&claims.sub).map_err(|_| JwtError::from(ErrorKind::InvalidSubject))?;
let token_uuid =
Uuid::parse_str(&claims.token_uuid).map_err(|_| JwtError::from(ErrorKind::InvalidToken))?;
Ok(TokenDetails {
token_type: claims.token_type,
expires_in: None,
})