pdk-jwt-lib 1.8.0

PDK JWT Library
Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

//! JWT claims parsing
//!
//! This module provides functionality to parse JWT tokens and extract claims
//! without signature validation. It handles JWT token structure validation
//! and extracts both header and payload claims.
//!
//! ## Primary types
//!
//! - [`JWTClaimsParser`]: parser for extracting claims from JWT tokens
//!
//! ## Usage
//!
//! This parser should be used after signature validation with [`SignatureValidator`]
//! to ensure the token is trusted before parsing claims.
//!

use std::collections::HashMap;

use base64::{decode_config, URL_SAFE_NO_PAD};
use jwt_compact::Claims;
use serde_json::Value;

use crate::{
    error::jwt_error::JWTError,
    model::{
        claims::{CustomClaims, JWTClaims, TryFromUntrustedJWTCompactClaims},
        untrusted_token::untrusted_jwt_compact::UntrustedJWT,
    },
};

/// This parser receives an untrusted JWT token (validation should be done previously using
/// a [SignatureValidator](crate::api::SignatureValidator)) and results in the content of the JWT
/// extracted into a [`JWTClaims`]. If parsing fails, it results in a [`JWTError`].
pub struct JWTClaimsParser {}

impl JWTClaimsParser {
    /// Parse a JWT token and return the claims.
    pub fn parse(token: String) -> Result<JWTClaims, JWTError> {
        UntrustedJWT::new(token.clone().as_str()).and_then(|_| {
            let token_parts: Vec<_> = token.splitn(4, '.').collect();

            match &token_parts[..] {
                [_, claims, _] => {
                    let serialized_claims = decode_config(claims, URL_SAFE_NO_PAD)?;
                    let claims: Claims<CustomClaims> = serde_json::from_slice(&serialized_claims)?;

                    JWTClaims::try_from_untrusted_jwt_compact_claims(claims, token)
                }
                _ => Err(JWTError::TokenParseFailed(String::from(
                    "A JWT should have xxxxx.yyyyy.zzzzz or xxxxx.yyyyy. format",
                ))),
            }
        })
    }

    /// Parse the headers of a JWT token and return them as a HashMap.
    pub fn parse_headers(token: String) -> Result<HashMap<String, Value>, JWTError> {
        UntrustedJWT::new(token.as_str()).and_then(|_| {
            let mut parts = token.split('.');
            let jwt_header_b64 = parts.next().ok_or(JWTError::TokenParseFailed(String::from(
                "Error parsing token header",
            )))?;

            Self::decode_base64_to_hashmap(jwt_header_b64)
        })
    }

    fn decode_base64_to_hashmap(base64: &str) -> Result<HashMap<String, Value>, JWTError> {
        let jwt_header_vec = decode_config(base64, URL_SAFE_NO_PAD)
            .map_err(|_| JWTError::TokenParseFailed(String::from("Error parsing token header")))?;

        let headers: HashMap<String, Value> = serde_json::from_slice(&jwt_header_vec)?;

        Ok(headers)
    }
}

#[cfg(test)]
mod tests {
    use crate::error::jwt_error::JWTErrorEnumKind;
    use crate::validator::signature_validator::tests::assert_error_kind;

    use super::JWTClaimsParser;

    #[test]
    fn parse_fails_when_token_not_sent() {
        let token = "";

        let jwt_claims = JWTClaimsParser::parse(token.to_string());

        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
    }

    #[test]
    fn unparseable_token_is_rejected() {
        let token = "WhatAmI?AnInvalidToken!";

        let jwt_claims = JWTClaimsParser::parse(token.to_string());

        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
    }

    #[test]
    fn parse_doesnt_fail_on_invalid_signature() {
        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.Ba3m9DcgHHeLKAh7KthRASIDNm7G_nZDkVJ_gh1wXQ3IJFS5moG_qohHl8ZoJI5WZOSSklnFWzEYPTbFtgeUzg";

        let jwt_claims = JWTClaimsParser::parse(token.to_string());

        assert!(jwt_claims.is_ok());
    }

    #[test]
    fn parse_doesnt_fail_with_exp_in_float_and_result_is_correct() {
        let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsImV4cCI6MTY5Mzk4ODY1Ni41OTcsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.EswBSyD9976PVC6o4tWwsT5rGTf2RJcvL7hdpcTwXUo";

        let jwt_claims = JWTClaimsParser::parse(token.to_string()).unwrap();
        assert_eq!(jwt_claims.get_header("alg").unwrap(), "HS256");
        assert_eq!(
            jwt_claims.expiration().unwrap().to_string(),
            "2023-09-06 08:24:16 UTC"
        );
    }

    #[test]
    fn parse_doesnt_fail_on_missing_signature() {
        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.";

        let jwt_claims = JWTClaimsParser::parse(token.to_string());

        assert!(jwt_claims.is_ok());
    }

    #[test]
    fn parse_fails_on_missing_signature_with_no_ending_point() {
        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30";

        let jwt_claims = JWTClaimsParser::parse(token.to_string());

        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
    }

    #[test]
    fn headers_unparseable_token_is_rejected() {
        let token = "WhatAmI?AnInvalidToken!";
        let headers = JWTClaimsParser::parse_headers(token.to_string());

        assert_error_kind(&headers, JWTErrorEnumKind::TokenParseFailed);
    }

    #[test]
    fn headers_parse_fails_when_token_not_sent() {
        let token = "";
        let headers = JWTClaimsParser::parse_headers(token.to_string());

        assert_error_kind(&headers, JWTErrorEnumKind::TokenParseFailed);
    }

    #[test]
    fn headers_parse_successful() {
        let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsImV4cCI6MTY5Mzk4ODY1Ni41OTcsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.EswBSyD9976PVC6o4tWwsT5rGTf2RJcvL7hdpcTwXUo";
        let headers = JWTClaimsParser::parse_headers(token.to_string());

        assert!(headers.is_ok());
        assert_eq!(
            headers.unwrap().get("alg").unwrap().as_str().unwrap(),
            "HS256"
        );
    }
}