Validate SLAS JWTs

A JSON Web Key Set (JWKS) is a set of cryptographic public keys in a JSON-based format, commonly used to validate the authenticity of JSON Web Tokens (JWTs). Each JWKS key is represented as a JSON Web Key (JWK), which includes the key type (kty), algorithm (alg), and key ID (kid). This format allows servers to securely share their public keys through a JWKS endpoint, which clients can access to validate JWTs without needing the private key used to sign the tokens. This public-private key pair ensures the integrity and trustworthiness of the transmitted JWTs.

JWKS plays a crucial role in distributed systems that rely on token-based authentication, especially for OAuth 2.0 and OpenID Connect protocols. By providing a central, trusted repository for public keys, the use of JWKS simplifies key management and enhances security. Systems using JWKS can dynamically retrieve keys and handle key rotation without service interruptions, making it a preferred method for validating signatures and safeguarding data in modern authentication systems. For additional information, see:

To validate a SLAS JWT using JWKS:

  1. Fetch the JWKS from the SLAS JWKS endpoint.
  2. After parsing the JWT's header, retrieve the key ID (kid).
  3. Locate the key in your JWKS that matches the kid.
  4. Verify the JWT signature using the matching public key.

For a newly created tenant, the SLAS JWKS endpoint typically returns 2 keys. After the first successful rotation of the keys is completed, and from then on, the endpoint returns 3 keys per tenant. This makes it possible to validate JWTs even if SLAS rotates its signing keys.

note: To enhance performance and avoid rate limits, consider caching your signing keys. If a token fails to decode, invalidate the cache, retrieve new signing keys, and try the verification again only one time.

Example: Validate SLAS JWT with JWKS

@Slf4j
@Component
public class JwksValidatorExample {

    @PostConstruct
    public void init() throws Exception {
        // This method runs when the application starts
        log.info("Application started, initializing JWT Validator...");

        // Example JWTs for testing purposes
        String validJwt = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS1hYmJmLWY5N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lP6Fj3Wn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";
        String invalidJwtSignature = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS1hYmJmLWY5N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lWn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";
        String invalidJwtKid = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS15N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lP6Fj3Wn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";

        // Validate the JWT and log the result
        String validationResult = validateJwt(validJwt);
        log.info("*** JWT Validation Result: {}", validationResult);
    }

    // Fetch public EC keys from the JWKS endpoint
    public List<ECKey> getPublicKeysFromJwksEndpoint(String jwksUrl) throws Exception {
        // Fetch the JWKS JSON data from the specified URL, using Spring's RestTemplate
        RestTemplate restTemplate = new RestTemplate();
        String jwkSetJson = restTemplate.getForObject(jwksUrl, String.class);

        // Parse the JSON response into a JWKSet
        JWKSet jwkSet = JWKSet.parse(JSONObjectUtils.parse(jwkSetJson));

        // Extract EC keys from the JWKS
        List<ECKey> ecKeys = jwkSet.getKeys().stream()
                .filter(key -> key instanceof ECKey)  // Filter only EC keys
                .map(key -> (ECKey) key)
                .collect(Collectors.toList());

        // Log the Key IDs (kid) for demo purposes
        for (ECKey ecKey : ecKeys) {
            log.info("Public Key ID from JWKS: " + (ecKey.getKeyID().substring(0, 5)) + "...");
        }

        return ecKeys;
    }

    // Find the corresponding ECKey for a given JWT's 'kid'
    public Optional<ECKey> findPublicKeyForJwt(List<ECKey> publicKeys, String jwtToken) throws ParseException {
        // Parse the JWT and extract its header
        SignedJWT signedJwt = SignedJWT.parse(jwtToken);
        JWSHeader header = signedJwt.getHeader();

        // Extract the Key ID (kid) from the JWT header
        String jwtKid = header.getKeyID();

        // Match the JWT 'kid' with the appropriate ECKey 'kid'
        return publicKeys.stream()
                .filter(ecKey -> ecKey.getKeyID().equals(jwtKid))
                .findFirst();
    }

    // Verify the JWT using the matched EC public key
    public boolean verifyJwtSignature(String jwtToken, ECKey publicKey) throws ParseException, JOSEException {
        // Parse the JWT
        SignedJWT signedJwt = SignedJWT.parse(jwtToken);

        // Convert the ECKey to a usable public key
        ECPublicKey ecPublicKey = publicKey.toECPublicKey();

        // Verify the JWT using the public key and algorithm specified in the JWT header
        return signedJwt.verify(new com.nimbusds.jose.crypto.ECDSAVerifier(ecPublicKey));
    }

    // Main method to validate the JWT
    public String validateJwt(String jwtToken) throws Exception {
        // The JWKS endpoint URL
        String jwksUrl = "https://{shortCode}.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/{organizationId}/oauth2/jwks";

        // Step 1: Retrieve the EC keys from the JWKS endpoint
        // IMPORTANT: You should cache the public keys and retrieve them from the cache
        // instead of calling getPublicKeysFromJwksEndpoint each time, to improve performance.
        List<ECKey> publicKeys = getPublicKeysFromJwksEndpoint(jwksUrl);

        // If no public keys are found, return an error message
        if (publicKeys.isEmpty()) {
            log.info("No public keys found at JWKS endpoint.");
            return "No public keys found in JWKS, unable to validate JWT.";
        }

        // Step 2: Find the public key that matches the JWT's 'kid'
        Optional<ECKey> matchingKey = findPublicKeyForJwt(publicKeys, jwtToken);
        if (matchingKey.isEmpty()) {
            return "No matching Key ID found in JWT, unable to validate JWT.";
        }

        // Step 3: Verify the JWT signature using the matching public key
        log.info("JWT Key ID: " + (matchingKey.get().getKeyID().substring(0, 5)) + "...");
        boolean isSignatureValid = verifyJwtSignature(jwtToken, matchingKey.get());

        // Return the result of the verification
        return isSignatureValid ? "JWT signature is valid." : "Invalid JWT signature.";
    }
}

Example: JWT Validation Output

Validation Demo Run

JwksValidatorExample Application started, initializing JWT Validator...
JwksValidatorExample Public Key ID from JWKS: 7e9f2...
JwksValidatorExample Public Key ID from JWKS: a5fdc...
JwksValidatorExample Public Key ID from JWKS: 4f3df...
JwksValidatorExample JWT Key ID: 7e9f2...
JwksValidatorExample *** JWT Validation Result: JWT signature is valid.

After a Key Rotation

Note that kid “4f3df” is no longer in the JWKS output. The JWT still validates.

JwksValidatorExample Application started, initializing JWT Validator...
JwksValidatorExample Public Key ID from JWKS: a5fdc...
JwksValidatorExample Public Key ID from JWKS: a4d21...
JwksValidatorExample Public Key ID from JWKS: 7e9f2...
JwksValidatorExample JWT Key ID: 7e9f2...
JwksValidatorExample *** JWT Validation Result: JWT signature is valid.

JWT Minted with the Latest Key

Note that kid “a5fdc” is found in the JWKS output and the signature validates.

JwksValidatorExample Application started, initializing JWT Validator...
JwksValidatorExample Public Key ID from JWKS: a5fdc...
JwksValidatorExample Public Key ID from JWKS: a4d21...
JwksValidatorExample Public Key ID from JWKS: 7e9f2...
JwksValidatorExample JWT Key ID: a5fdc...
JwksValidatorExample *** JWT Validation Result: JWT signature is valid.

Example: Validate SLAS JWT with B2C Commerce Script APIs

Note: When retrieving the JWKS to be passed into this function, cache it using dw.system.Cache to avoid rate limits and improve performance.

/**
 * Validates the SLAS token via the JWKS
 * @param {string} jwt - the JWT token to validate
 * @param {Array<object>} keys - array of keys
 * @return {boolean} true if the token is valid, false otherwise
 */
const validateSLASToken = (jwt, keys) => {
  const CertificateUtils = require("dw/crypto/CertificateUtils");
  const Encoding = require("dw/crypto/Encoding");
  const JWS = require("dw/crypto/JWS");
  const JWSHeader = require("dw/crypto/JWSHeader");
  const Logger = require("dw/system/Logger");

  try {
    const jwtParts = jwt.split(".");
    if (jwtParts.length !== 3) {
      Logger.error("Invalid JWT format - must have 3 parts");
      return false;
    }

    const encodedHeader = jwtParts[0];
    const encodedPayload = jwtParts[1];

    const jwsHeader = JWSHeader.parseEncoded(encodedHeader);
    const kid = jwsHeader.toMap().get("kid");

    if (!kid) {
      Logger.error("JWT header missing kid");
      return false;
    }

    // Find the matching key by kid; SLAS rotates keys and provides a key set
    const jwk = keys.find((key) => key.kid === kid);
    if (!jwk) {
      Logger.error("No matching key found in JWKS for kid: " + kid);
      return false;
    }

    // Parse the public key from JWK (needs string form)
    const publicKey = CertificateUtils.parsePublicKeyFromJWK(JSON.stringify(jwk));

    const jws = JWS.parse(jwt);

    const isValid = jws.verify(publicKey);

    if (isValid) {
      const base64urlDecode = (str) => {
        let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
        while (base64.length % 4) {
          base64 += "=";
        }
        return Encoding.fromBase64(base64);
      };

      const payloadJson = base64urlDecode(encodedPayload);
      const payload = JSON.parse(payloadJson);

      // Check expiration
      const currentTime = Math.floor(Date.now() / 1000);
      if (payload.exp && payload.exp < currentTime) {
        Logger.error("JWT has expired");
        return false;
      }

      return true;
    }
    Logger.error("JWT signature verification failed");
    return false;
  } catch (error) {
    Logger.error("Error validating JWT: " + error.toString());
    return false;
  }
};

module.exports = {
  validateSLASToken,
};