Skip to main content

pdk_jwt_lib/parser/
jwt_claims_parser.rs

1// Copyright (c) 2026, Salesforce, Inc.,
2// All rights reserved.
3// For full license text, see the LICENSE.txt file
4
5//! JWT claims parsing
6//!
7//! This module provides functionality to parse JWT tokens and extract claims
8//! without signature validation. It handles JWT token structure validation
9//! and extracts both header and payload claims.
10//!
11//! ## Primary types
12//!
13//! - [`JWTClaimsParser`]: parser for extracting claims from JWT tokens
14//!
15//! ## Usage
16//!
17//! This parser should be used after signature validation with [`SignatureValidator`]
18//! to ensure the token is trusted before parsing claims.
19//!
20
21use std::collections::HashMap;
22
23use base64::{decode_config, URL_SAFE_NO_PAD};
24use jwt_compact::Claims;
25use serde_json::Value;
26
27use crate::{
28    error::jwt_error::JWTError,
29    model::{
30        claims::{CustomClaims, JWTClaims, TryFromUntrustedJWTCompactClaims},
31        untrusted_token::untrusted_jwt_compact::UntrustedJWT,
32    },
33};
34
35/// This parser receives an untrusted JWT token (validation should be done previously using
36/// a [SignatureValidator](crate::api::SignatureValidator)) and results in the content of the JWT
37/// extracted into a [`JWTClaims`]. If parsing fails, it results in a [`JWTError`].
38pub struct JWTClaimsParser {}
39
40impl JWTClaimsParser {
41    /// Parse a JWT token and return the claims.
42    pub fn parse(token: String) -> Result<JWTClaims, JWTError> {
43        UntrustedJWT::new(token.clone().as_str()).and_then(|_| {
44            let token_parts: Vec<_> = token.splitn(4, '.').collect();
45
46            match &token_parts[..] {
47                [_, claims, _] => {
48                    let serialized_claims = decode_config(claims, URL_SAFE_NO_PAD)?;
49                    let claims: Claims<CustomClaims> = serde_json::from_slice(&serialized_claims)?;
50
51                    JWTClaims::try_from_untrusted_jwt_compact_claims(claims, token)
52                }
53                _ => Err(JWTError::TokenParseFailed(String::from(
54                    "A JWT should have xxxxx.yyyyy.zzzzz or xxxxx.yyyyy. format",
55                ))),
56            }
57        })
58    }
59
60    /// Parse the headers of a JWT token and return them as a HashMap.
61    pub fn parse_headers(token: String) -> Result<HashMap<String, Value>, JWTError> {
62        UntrustedJWT::new(token.as_str()).and_then(|_| {
63            let mut parts = token.split('.');
64            let jwt_header_b64 = parts.next().ok_or(JWTError::TokenParseFailed(String::from(
65                "Error parsing token header",
66            )))?;
67
68            Self::decode_base64_to_hashmap(jwt_header_b64)
69        })
70    }
71
72    fn decode_base64_to_hashmap(base64: &str) -> Result<HashMap<String, Value>, JWTError> {
73        let jwt_header_vec = decode_config(base64, URL_SAFE_NO_PAD)
74            .map_err(|_| JWTError::TokenParseFailed(String::from("Error parsing token header")))?;
75
76        let headers: HashMap<String, Value> = serde_json::from_slice(&jwt_header_vec)?;
77
78        Ok(headers)
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use crate::error::jwt_error::JWTErrorEnumKind;
85    use crate::validator::signature_validator::tests::assert_error_kind;
86
87    use super::JWTClaimsParser;
88
89    #[test]
90    fn parse_fails_when_token_not_sent() {
91        let token = "";
92
93        let jwt_claims = JWTClaimsParser::parse(token.to_string());
94
95        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
96    }
97
98    #[test]
99    fn unparseable_token_is_rejected() {
100        let token = "WhatAmI?AnInvalidToken!";
101
102        let jwt_claims = JWTClaimsParser::parse(token.to_string());
103
104        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
105    }
106
107    #[test]
108    fn parse_doesnt_fail_on_invalid_signature() {
109        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.Ba3m9DcgHHeLKAh7KthRASIDNm7G_nZDkVJ_gh1wXQ3IJFS5moG_qohHl8ZoJI5WZOSSklnFWzEYPTbFtgeUzg";
110
111        let jwt_claims = JWTClaimsParser::parse(token.to_string());
112
113        assert!(jwt_claims.is_ok());
114    }
115
116    #[test]
117    fn parse_doesnt_fail_with_exp_in_float_and_result_is_correct() {
118        let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsImV4cCI6MTY5Mzk4ODY1Ni41OTcsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.EswBSyD9976PVC6o4tWwsT5rGTf2RJcvL7hdpcTwXUo";
119
120        let jwt_claims = JWTClaimsParser::parse(token.to_string()).unwrap();
121        assert_eq!(jwt_claims.get_header("alg").unwrap(), "HS256");
122        assert_eq!(
123            jwt_claims.expiration().unwrap().to_string(),
124            "2023-09-06 08:24:16 UTC"
125        );
126    }
127
128    #[test]
129    fn parse_doesnt_fail_on_missing_signature() {
130        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.";
131
132        let jwt_claims = JWTClaimsParser::parse(token.to_string());
133
134        assert!(jwt_claims.is_ok());
135    }
136
137    #[test]
138    fn parse_fails_on_missing_signature_with_no_ending_point() {
139        let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30";
140
141        let jwt_claims = JWTClaimsParser::parse(token.to_string());
142
143        assert_error_kind(&jwt_claims, JWTErrorEnumKind::TokenParseFailed);
144    }
145
146    #[test]
147    fn headers_unparseable_token_is_rejected() {
148        let token = "WhatAmI?AnInvalidToken!";
149        let headers = JWTClaimsParser::parse_headers(token.to_string());
150
151        assert_error_kind(&headers, JWTErrorEnumKind::TokenParseFailed);
152    }
153
154    #[test]
155    fn headers_parse_fails_when_token_not_sent() {
156        let token = "";
157        let headers = JWTClaimsParser::parse_headers(token.to_string());
158
159        assert_error_kind(&headers, JWTErrorEnumKind::TokenParseFailed);
160    }
161
162    #[test]
163    fn headers_parse_successful() {
164        let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3JlZGFjdGVkLmFuei5jb20iLCJhdWQiOiJyZWRhY3RlZC5hbnouY29tL2VtYWlsQWRkcmVzcy1yZWRhY3RlZEBhbnouY29tIiwic3ViIjoicmVkYWN0ZWQuYW56LmNvbS9lbWFpbEFkZHJlc3MtcmVkYWN0ZWRAYW56LmNvbSIsImV4cCI6MTY5Mzk4ODY1Ni41OTcsInNjb3BlcyI6WyJyZWRhY3RlZDEiLCJyZWRhY3RlZCJdLCJhbXIiOlsicmVkYWN0ZWQiXSwiYWNyIjoicmVkYWN0ZWQifQ.EswBSyD9976PVC6o4tWwsT5rGTf2RJcvL7hdpcTwXUo";
165        let headers = JWTClaimsParser::parse_headers(token.to_string());
166
167        assert!(headers.is_ok());
168        assert_eq!(
169            headers.unwrap().get("alg").unwrap().as_str().unwrap(),
170            "HS256"
171        );
172    }
173}