pdk-jwt-lib 1.10.0

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

//! # HMAC Signature Validator
//!
//! This module provides signature validation for HMAC algorithms
//! (HS256, HS384, HS512) used in JWT tokens.

use jwt_compact::alg::{Hs256, Hs256Key, Hs384, Hs384Key, Hs512, Hs512Key};
use jwt_compact::AlgorithmExt;

use crate::model::claims::TryFromTrustedJWTCompactClaims;
use crate::{
    error::jwt_error::JWTError,
    model::{
        claims::JWTClaims, signing_algorithm::SigningKeyLength,
        untrusted_token::untrusted_jwt_compact::UntrustedJWT,
    },
};

use super::signature_validator::SignatureValidation;

// Signature validator for HMAC algorithm.
// Supports key lengths 256, 384 and 512.
pub struct HmacSignatureValidator {
    key_length: SigningKeyLength,
    secret: String,
}

impl HmacSignatureValidator {
    /// Create a new [`HmacSignatureValidator`] from the given key length and secret.
    pub fn new(key_length: SigningKeyLength, secret: String) -> Self {
        Self { key_length, secret }
    }
}

impl SignatureValidation for HmacSignatureValidator {
    fn validate(&self, token: String) -> Result<JWTClaims, JWTError> {
        UntrustedJWT::new(token.as_str())
            .and_then(|untrusted| {
                match self.key_length {
                    SigningKeyLength::Len256 => {
                        let key = Hs256Key::new(self.secret.clone());
                        Hs256.validator(&key).validate(&untrusted.into())
                    }
                    SigningKeyLength::Len384 => {
                        let key = Hs384Key::new(self.secret.clone());
                        Hs384.validator(&key).validate(&untrusted.into())
                    }
                    SigningKeyLength::Len512 => {
                        let key = Hs512Key::new(self.secret.clone());
                        Hs512.validator(&key).validate(&untrusted.into())
                    }
                }
                .map_err(JWTError::from)
            })
            .and_then(|claims| JWTClaims::try_from_trusted_jwt_compact_claims(claims, token))
    }
}

#[cfg(test)]
mod tests {
    use super::HmacSignatureValidator;

    const HMAC_SECRET_256: &str = "dURKmcK2ENvNnfO1p1DPDmb8UfTGImHP";
    const HMAC_SECRET_384: &str = "dURKmcK2ENvNnfO1p1DPDmb8UfTGImHPdURKmcK2ENvNnfO1";
    const HMAC_SECRET_512: &str =
        "dURKmcK2ENvNnfO1p1DPDmb8UfTGImHPdURKmcK2ENvNnfO1p1DPDmb8UfTGImHP";

    fn hs256_validator() -> HmacSignatureValidator {
        HmacSignatureValidator::new(
            crate::model::signing_algorithm::SigningKeyLength::Len256,
            String::from(HMAC_SECRET_256),
        )
    }

    fn hs384_validator() -> HmacSignatureValidator {
        HmacSignatureValidator::new(
            crate::model::signing_algorithm::SigningKeyLength::Len384,
            String::from(HMAC_SECRET_384),
        )
    }

    fn hs512_validator() -> HmacSignatureValidator {
        HmacSignatureValidator::new(
            crate::model::signing_algorithm::SigningKeyLength::Len512,
            String::from(HMAC_SECRET_512),
        )
    }

    mod hmac_validator_256 {
        use crate::error::jwt_error::JWTErrorEnumKind;
        use crate::validator::hmac_validator::SignatureValidation;
        use crate::validator::signature_validator::tests::assert_error_kind;
        use test_case::test_case;

        use super::hs256_validator;

        extern crate test_case;

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

            let validation: Result<_, crate::error::jwt_error::JWTError> =
                hs256_validator().validate(token.to_string());

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

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

            let validation = hs256_validator().validate(token.to_string());

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

        #[test]
        fn validation_fails_on_invalid_signature() {
            let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.bmI9mERm8_qnYPgBMh-IaQdA5KBdRPOSvazkGB-7ElI";

            let validation = hs256_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

        #[test]
        fn validation_fails_on_missing_signature() {
            let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.";

            let validation = hs256_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

        #[test]
        fn verify_valid_token_is_correctly_parsed() {
            let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjgyMDA1ODI2NH0.mu54d8dajyU_eMa4VCE4CcG5HzN7nNflxbWUw7JO0V0";

            let validation = hs256_validator().validate(token.to_string());

            let jwt_claims = validation.unwrap();
            assert_eq!(jwt_claims.get_header("alg").unwrap(), "HS256");
            assert_eq!(
                jwt_claims.expiration().unwrap().to_string(),
                "1995-12-27 09:57:44 UTC"
            );
        }

        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.5bW99CUMyOZjGkG1niOmlBINR8Vzp9x6tO_4rja9naw"; "verifyValidToken 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjgyMDA1ODI2NH0.mu54d8dajyU_eMa4VCE4CcG5HzN7nNflxbWUw7JO0V0"; "exceptionNotThrownOnExpiredTokenWithNoClaimValidation 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.e30.5bW99CUMyOZjGkG1niOmlBINR8Vzp9x6tO_4rja9naw"; "exceptionNotThrownOnMissingClaim 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjMyODAyMzMyMDl9.gmXcQZjAjUu5yDDF8Tz6PHgkjyaDezrT2-fRDQ0AGKA"; "verifyNotExpiredToken 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjgyMDA1ODMyMn0.QJoKD7HaF7nOiTZO6esguJ7tF7MCRiDAmf553OtAxBA"; "exceptionNotThrownOnExpiredToken 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjgyMDA3MjU2Mn0.7C6AhlGvT0KrmKcV4LnKPmMB_pq0aBfdG7r2TkbViLA"; "verifyTokenWithNbf 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjMyODAyOTA0MzR9.Joc61cnZDakVYIjaOARLWLlrM0qgG4O0OlaJp3Q58g4"; "exceptionNotThrownNotYetActiveToken 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmb28ifQ.fBfbNCCLjjrRf8dg3htXM-r0hh4m7jLCvLAzRsgqBCg"; "verifyTokenWithAud 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIiwiYmxhaCJdfQ.IRYninPbdgWb1i_9FhRSobjjVHPkCg4nH_QH1MqfhHo"; "verifyTokenWithAudArray 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIGJhciIsImJsYWgiXX0.2diRbOvDICKy14bWgeqnzVTpe_ig3bTTx6FSw9jwi6I"; "verifyTokenWithAudiencesWithSpaces 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiRm9vIiwiYmxhaCJdfQ.esjtb7g7x7p4dbEkF7dh7PRhR___dWW8llq5Hm6lGQE"; "verifyTokenWithAudiencesCaseSensitive 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOlsiZXRjIiwiYmxhaCJdfQ.mweyE1Ip21sSrcDkr_IndOOW26UygQ3eicSYXyXTcto"; "exceptionNotThrownWhenInvalidAudToken 256")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJmb28iLCJuYmYiOjgyMDA3Mjk0MSwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGVzIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjMyODAyOTE3Njd9.dSZOHQCS7RGt6nQKjMnfQg7V8UxLAPZ1QzwcZ1Aaz6s"; "checkAllValidations 256")]
        fn verify_valid_token_256(token: &str) {
            let validation = hs256_validator().validate(token.to_string());

            assert!(validation.is_ok());
        }
    }

    mod hmac_validator_384 {
        use crate::validator::hmac_validator::SignatureValidation;
        use crate::{
            error::jwt_error::JWTErrorEnumKind,
            validator::signature_validator::tests::assert_error_kind,
        };
        use test_case::test_case;

        use super::hs384_validator;

        extern crate test_case;

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

            let validation = hs384_validator().validate(token.to_string());

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

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

            let validation = hs384_validator().validate(token.to_string());

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

        #[test]
        fn validation_fails_on_invalid_signature() {
            let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.e30.XYsOC2_46dliJ2gL5h6KC_h1-LBSzJ1PyYL4cK5vON-SGSGHDEl8z6ikWaH9s4Ne";

            let validation = hs384_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

        #[test]
        fn validation_fails_on_missing_signature() {
            let token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.e30.";

            let validation = hs384_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.e30.ss6xb6Vbjrutq2OjcNI2uxwLhL1zkMzDb4ff0vY8sLoa2eOJioVwBm4EM1ALtye6"; "verifyValidToken 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJleHAiOjgyMDA5NTAyMn0.gcG76syx-Eq8p4Mq6c-LIoMJ6pA1371YVmAeADgKGAbLwON-rrTQVmqQpvrqABY3"; "exceptionNotThrownOnExpiredTokenWithNoClaimValidation 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.e30.ss6xb6Vbjrutq2OjcNI2uxwLhL1zkMzDb4ff0vY8sLoa2eOJioVwBm4EM1ALtye6"; "exceptionNotThrownOnMissingClaim 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJleHAiOjMyODAzODA5MjR9.bApP3Q27KFePahE4LTvZPQTW__qwbc99MlKklGE_XeXJQrq6SV6UwlDdoAaVIyoB"; "verifyNotExpiredToken 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJleHAiOjgyMDA5NTI1Nn0.ZEAPO_dzAjmx504Fa3TBKGJd9nJbSFdwUwu6al8YgeNb65ug6epHDa1Mm5cdC4tk"; "exceptionNotThrownOnExpiredToken 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJuYmYiOjgyMDA5NTI5Nn0.qTfbKz9-PJNDiZh5-RHkMFSEDiZf49zNawIhu-tXapMY9SROARtVyikQ-vYerN1p"; "verifyTokenWithNbf 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJuYmYiOjMyODAzODEyODd9.raA8Qj2pdQC-PNG_efkOdQIII4nW1Vcos0vLxvBhJD7631_9ztOBA6Q3Y_qQO0L5"; "exceptionNotThrownNotYetActiveToken 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOiJmb28ifQ.ZibYb11Fq-PgHQWDwmn9lcNU4Zl_AeDK7_KJ9h56CfULhnCCNju7wmnVM9N-oylT"; "verifyTokenWithAud 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOlsiZXRjIiwiZm9vIiwiYmxhaCJdfQ.WmLAwgP1NJMa_mbxF3N3ijCejFFbpQHXcDfEDa_oBYQyDOHrGCT2C9awEHkYefZ-"; "verifyTokenWithAudArray 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOlsiZXRjIiwiZm9vIGJhciIsImJsYWgiXX0.vKw-gGIIHTrM27j6AZ1964KuhzWZdX2c5hl21ElpGK8SATk0_JBV6Fo4f37H2W9b"; "verifyTokenWithAudiencesWithSpaces 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOlsiZXRjIiwiRm9vIiwiYmxhaCJdfQ.eO2q__MG-dJ4g9ms7dAD5RtnGIQ5_keoOhnn44jBVOErIhGFMOgDwoY2Two9ySKM"; "verifyTokenWithAudiencesCaseSensitive 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOlsiZXRjIiwiYmxhaCJdfQ.R9b-YQ__m-qCZwb2ibo6dpkg-pLBTs_p8UJW-4kp-ROsp4DHcbNISYQXZZyLOx_y"; "exceptionNotThrownWhenInvalidAudToken 384")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJhdWQiOiJmb28iLCJuYmYiOjgyMDA5NTQ1OSwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGVzIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjMyODAzODE4Mzd9.ba_JRLG82Pmj7u7ybfhnFHGwn75HemL4YuMRC5ug3llo82gkykooPZlZniAhaxl6"; "checkAllValidations 384")]
        fn verify_valid_token_384(token: &str) {
            let validation = hs384_validator().validate(token.to_string());

            assert!(validation.is_ok());
        }
    }

    mod hmac_validator_512 {
        use super::hs512_validator;
        use crate::validator::hmac_validator::SignatureValidation;
        use crate::{
            error::jwt_error::JWTErrorEnumKind,
            validator::signature_validator::tests::assert_error_kind,
        };
        use test_case::test_case;

        extern crate test_case;

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

            let validation = hs512_validator().validate(token.to_string());

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

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

            let validation = hs512_validator().validate(token.to_string());

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

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

            let validation = hs512_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

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

            let validation = hs512_validator().validate(token.to_string());

            assert_error_kind(&validation, JWTErrorEnumKind::ValidationFailed);
        }

        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.BW5kqJDEnSVJeUK0xxK2eu6ZmRUI-NjRF1i9D3p1Pq2GiKxCiLBhISXGJ081TH5yDgSAOvuXn4Ie0UeEqa6aEA"; "verifyValidToken 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjgyMDA5NTgzMX0.p8Tqf9PFUxLWfFgOw7HfJNgItHQa0qfyn-bAESAwZZR4Mr-8yRk72dc83iz1remfeVHLD65lWZRUPjAzcvV-iQ"; "exceptionNotThrownOnExpiredTokenWithNoClaimValidation 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.e30.BW5kqJDEnSVJeUK0xxK2eu6ZmRUI-NjRF1i9D3p1Pq2GiKxCiLBhISXGJ081TH5yDgSAOvuXn4Ie0UeEqa6aEA"; "exceptionNotThrownOnMissingClaim 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjMyODAzODMzMjd9.0BalxeKKvKSn4296jc-PZtuAUGMvPz567Mrz8FhPq__Tpgpvh_KtbVmZJv73-R6uWqElgU7TP--iLOiXhK6WpA"; "verifyNotExpiredToken 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjgyMDA5NTgzMX0.p8Tqf9PFUxLWfFgOw7HfJNgItHQa0qfyn-bAESAwZZR4Mr-8yRk72dc83iz1remfeVHLD65lWZRUPjAzcvV-iQ"; "exceptionNotThrownOnExpiredToken 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjgyMDA5NTgzMX0.UVZnwEmojvsdGYu1mUPMvke4pah7w0sVmFwPCQjgOziJT_OCf80XwjJe9Paja7cmI1pwNj29Jhm7bNGHEPF9nQ"; "verifyTokenWithNbf 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJuYmYiOjMyODAzODMzMjd9.kOPL23VA1adljaBIcxcFze3sETlKUT6Bxdwkkef6RJMi7bJJZs7WXXskDBem_Bb4HNAesJq_OEN0K0uOXpWCBw"; "exceptionNotThrownNotYetActiveToken 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOiJmb28ifQ.2KV55R0_PnHQt3Ua2BRg1xC8Tz_dz87jt7Jn-_ya4EmCQeJzVrGnhiQFn3pW9990loBt_sg64ssWF2LpLpmkfg"; "verifyTokenWithAud 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIiwiYmxhaCJdfQ.hejTNLlKpqka_dX7dDNsm2ht7ehEsFPnSi85jAJFuT7PBNjkyAOA6ydBfDCrWoKJvoCpxtvpf-g3H0c0zbBixg"; "verifyTokenWithAudArray 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOlsiZXRjIiwiZm9vIGJhciIsImJsYWgiXX0.kE5CaSAY7Xw_YKJ3hx288_pchLRqMb9x5-auybhmJK6IJ-abEOwxusxV7zgTX0UDx_Z8Xz99Rg29rSI4IikAQA"; "verifyTokenWithAudiencesWithSpaces 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOlsiZXRjIiwiRm9vIiwiYmxhaCJdfQ.wjqG6WEvzJlyNvsdX2pitffMC4C2fL5wuwPNAGbYITtIDveeaBB2RZr0az08gw8wQWLnj_4zDnAt0tvrL-9ObA"; "verifyTokenWithAudiencesCaseSensitive 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOlsiZXRjIiwiYmxhaCJdfQ.2Fkk6wdr0FPmG0dyJHC6hF81xf3R-VKBVPJgPfc_fcLXrRlafMo-ClZXtPV5FXmeArkEuQ8wt6p7rGoLiRC1-g"; "exceptionNotThrownWhenInvalidAudToken 512")]
        #[test_case("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJhdWQiOiJmb28iLCJuYmYiOjgyMDA5NTgzMSwibmFtZSI6IkpvaG4gRG9lIiwic2NvcGVzIjpbInJlYWQiLCJ3cml0ZSJdLCJleHAiOjMyODAzODMzMjd9.TFaQJ46jOGIP8wBD2EaMIsheC-nQgTlj0a5IwUZQtW6h_C6xVjxPI0lr_9H73gF1S2-Ksf8q3OC6BaTFag6SXg"; "checkAllValidations 512")]
        fn verify_valid_token_512(token: &str) {
            let validation = hs512_validator().validate(token.to_string());

            assert!(validation.is_ok());
        }
    }
}