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:
After parsing the JWT’s header, retrieve the key ID (kid).
Locate the key in your JWKS that matches the kid.
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.
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.
Note
Example: Validate SLAS JWT with JWKS
1@Slf4j
2@Component
3public class JwksValidatorExample {
45 @PostConstruct
6 public void init() throws Exception {
7 // This method runs when the application starts
8 log.info("Application started, initializing JWT Validator...");
910 // Example JWTs for testing purposes
11 String validJwt = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS1hYmJmLWY5N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lP6Fj3Wn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";
12 String invalidJwtSignature = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS1hYmJmLWY5N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lWn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";
13 String invalidJwtKid = "eyJ2ZXIiOiIxLjAiLCJqa3UiOiJzbGFzL2Rldi9iY2dsX3N0ZyIsImtpZCI6IjdlOWYyZWNhLTQyZDMtNDEzNS15N2NhMTk5MDFhNSIsInR5cCI6Imp3dCIsImNsdiI6IkoyLjMuNCIsImFsZyI6IkVTMjU2In0.eyJhdXQiOiJHVUlEIiwic2NwIjoic2ZjYy5zaG9wcGVyLWJhc2tldHMtb3JkZXJzIHNmY2Muc2hvcHBlci1iYXNrZXRzLW9yZGVycy5ydyBzZmNjLnNob3BwZXItY2F0ZWdvcmllcyBzZmNjLnNob3BwZXItY3VzdG9tZXJzLmxvZ2luIHNmY2Muc2hvcHBlci1jdXN0b21lcnMucmVnaXN0ZXIgc2ZjYy5zaG9wcGVyLWRpc2NvdmVyeS1zZWFyY2ggc2ZjYy5zaG9wcGVyLWdpZnQtY2VydGlmaWNhdGVzIHNmY2Muc2hvcHBlci1teWFjY291bnQgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5hZGRyZXNzZXMucncgc2ZjYy5zaG9wcGVyLW15YWNjb3VudC5iYXNrZXRzIHNmY2Muc2hvcHBlci1teWFjY291bnQub3JkZXJzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzIHNmY2Muc2hvcHBlci1teWFjY291bnQucGF5bWVudGluc3RydW1lbnRzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzIHNmY2Muc2hvcHBlci1teWFjY291bnQucHJvZHVjdGxpc3RzLnJ3IHNmY2Muc2hvcHBlci1teWFjY291bnQucncgc2ZjYy5zaG9wcGVyLXByb2R1Y3Qtc2VhcmNoIHNmY2Muc2hvcHBlci1wcm9kdWN0bGlzdHMgc2ZjYy5zaG9wcGVyLXByb2R1Y3RzIHNmY2Muc2hvcHBlci1wcm9tb3Rpb25zIHNmY2Muc2hvcHBlci1zdG9yZXMiLCJzdWIiOiJjYy1zbGFzOjpiY2dsX3N0Zzo6c2NpZDphNGYzODExYS04YTZjLTQwNzUtYTEyMy04MjQ5MDE5ZmI4NWM6OnVzaWQ6Y2I0NTk3OGQtY2M4MC00YTg3LWI2MGYtNDlkNjA2Y2E2YWUzIiwiY3R4Ijoic2xhcyIsImlzcyI6InNsYXMvZGV2L2JjZ2xfc3RnIiwiaXN0IjoxLCJkbnQiOiIwIiwiYXVkIjoiY29tbWVyY2VjbG91ZC9kZXYvYmNnbF9zdGciLCJuYmYiOjE3Mjc3MzE0NzAsInN0eSI6IlVzZXIiLCJpc2IiOiJ1aWRvOnNsYXM6OnVwbjpHdWVzdDo6dWlkbjpHdWVzdCBVc2VyOjpnY2lkOmFidzBnWWxySTFtZW9SdzBrMmthWVl3ckUxOjpjaGlkOlJlZkFyY2giLCJleHAiOjE3Mjc3MzMzMDAsImlhdCI6MTcyNzczMTUwMCwianRpIjoiQzJDODY5NDcyMDc1MC0xMDA4Mjk4MDUyMzM4MzgyOTA5ODIxODA4OCJ9.TtzVB8NQt4vKv41lP6Fj3Wn83wa3_jMQceDBcYZEDni5_4foNMvTXPi7yaJ6N8qGV-Jx6op6DPdbZM_e7YlRZg";
1415 // Validate the JWT and log the result
16 String validationResult = validateJwt(validJwt);
17 log.info("*** JWT Validation Result: {}", validationResult);
18 }
1920 // Fetch public EC keys from the JWKS endpoint
21 public List getPublicKeysFromJwksEndpoint(String jwksUrl) throws Exception {
22 // Fetch the JWKS JSON data from the specified URL, using Spring's RestTemplate
23 RestTemplate restTemplate = new RestTemplate();
24 String jwkSetJson = restTemplate.getForObject(jwksUrl, String.class);
2526 // Parse the JSON response into a JWKSet
27 JWKSet jwkSet = JWKSet.parse(JSONObjectUtils.parse(jwkSetJson));
2829 // Extract EC keys from the JWKS
30 List ecKeys = jwkSet.getKeys().stream()
31 .filter(key -> key instanceof ECKey) // Filter only EC keys
32 .map(key -> (ECKey) key)
33 .collect(Collectors.toList());
3435 // Log the Key IDs (kid) for demo purposes
36 for (ECKey ecKey : ecKeys) {
37 log.info("Public Key ID from JWKS: " + (ecKey.getKeyID().substring(0, 5)) + "...");
38 }
3940 return ecKeys;
41 }
4243 // Find the corresponding ECKey for a given JWT's 'kid'
44 public Optional findPublicKeyForJwt(List publicKeys, String jwtToken) throws ParseException {
45 // Parse the JWT and extract its header
46 SignedJWT signedJwt = SignedJWT.parse(jwtToken);
47 JWSHeader header = signedJwt.getHeader();
4849 // Extract the Key ID (kid) from the JWT header
50 String jwtKid = header.getKeyID();
5152 // Match the JWT 'kid' with the appropriate ECKey 'kid'
53 return publicKeys.stream()
54 .filter(ecKey -> ecKey.getKeyID().equals(jwtKid))
55 .findFirst();
56 }
5758 // Verify the JWT using the matched EC public key
59 public boolean verifyJwtSignature(String jwtToken, ECKey publicKey) throws ParseException, JOSEException {
60 // Parse the JWT
61 SignedJWT signedJwt = SignedJWT.parse(jwtToken);
6263 // Convert the ECKey to a usable public key
64 ECPublicKey ecPublicKey = publicKey.toECPublicKey();
6566 // Verify the JWT using the public key and algorithm specified in the JWT header
67 return signedJwt.verify(new com.nimbusds.jose.crypto.ECDSAVerifier(ecPublicKey));
68 }
6970 // Main method to validate the JWT
71 public String validateJwt(String jwtToken) throws Exception {
72 // The JWKS endpoint URL
73 String jwksUrl = "https://{shortCode}.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/{organizationId}/oauth2/jwks";
7475 // Step 1: Retrieve the EC keys from the JWKS endpoint
76 // IMPORTANT: You should cache the public keys and retrieve them from the cache
77 // instead of calling getPublicKeysFromJwksEndpoint each time, to improve performance.
78 List publicKeys = getPublicKeysFromJwksEndpoint(jwksUrl);
7980 // If no public keys are found, return an error message
81 if (publicKeys.isEmpty()) {
82 log.info("No public keys found at JWKS endpoint.");
83 return "No public keys found in JWKS, unable to validate JWT.";
84 }
8586 // Step 2: Find the public key that matches the JWT's 'kid'
87 Optional matchingKey = findPublicKeyForJwt(publicKeys, jwtToken);
88 if (matchingKey.isEmpty()) {
89 return "No matching Key ID found in JWT, unable to validate JWT.";
90 }
9192 // Step 3: Verify the JWT signature using the matching public key
93 log.info("JWT Key ID: " + (matchingKey.get().getKeyID().substring(0, 5)) + "...");
94 boolean isSignatureValid = verifyJwtSignature(jwtToken, matchingKey.get());
9596 // Return the result of the verification
97 return isSignatureValid ? "JWT signature is valid." : "Invalid JWT signature.";
98 }
99}
Example: JWT Validation Output
Validation Demo Run
1JwksValidatorExample Application started, initializing JWT Validator...2JwksValidatorExample Public Key ID from JWKS: 7e9f2...3JwksValidatorExample Public Key ID from JWKS: a5fdc...4JwksValidatorExample Public Key ID from JWKS: 4f3df...5JwksValidatorExample JWT Key ID: 7e9f2...6JwksValidatorExample *** 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.
1JwksValidatorExample Application started, initializing JWT Validator...2JwksValidatorExample Public Key ID from JWKS: a5fdc...3JwksValidatorExample Public Key ID from JWKS: a4d21...4JwksValidatorExample Public Key ID from JWKS: 7e9f2...5JwksValidatorExample JWT Key ID: 7e9f2...6JwksValidatorExample *** 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.
1JwksValidatorExample Application started, initializing JWT Validator...2JwksValidatorExample Public Key ID from JWKS: a5fdc...3JwksValidatorExample Public Key ID from JWKS: a4d21...4JwksValidatorExample Public Key ID from JWKS: 7e9f2...5JwksValidatorExample JWT Key ID: a5fdc...6JwksValidatorExample *** 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.
1/**2 * Validates the SLAS token via the JWKS3 * @param{string} jwt - the JWT token to validate4 * @param{Array<object>} keys - array of keys5 * @return{boolean} true if the token is valid, false otherwise6 */7const validateSLASToken = (jwt, keys)=>{8 const CertificateUtils = require("dw/crypto/CertificateUtils");9 const Encoding = require("dw/crypto/Encoding");10 const JWS = require("dw/crypto/JWS");11 const JWSHeader = require("dw/crypto/JWSHeader");12 const Logger = require("dw/system/Logger");1314 try{15 const jwtParts = jwt.split(".");16 if(jwtParts.length !== 3){17 Logger.error("Invalid JWT format - must have 3 parts");18 return false;19}2021 const encodedHeader = jwtParts[0];22 const encodedPayload = jwtParts[1];2324 const jwsHeader = JWSHeader.parseEncoded(encodedHeader);25 const kid = jwsHeader.toMap().get("kid");2627 if(!kid){28 Logger.error("JWT header missing kid");29 return false;30}3132 // Find the matching key by kid; SLAS rotates keys and provides a key set33 const jwk = keys.find((key)=> key.kid === kid);34 if(!jwk){35 Logger.error("No matching key found in JWKS for kid: " + kid);36 return false;37}3839 // Parse the public key from JWK (needs string form)40 const publicKey = CertificateUtils.parsePublicKeyFromJWK(JSON.stringify(jwk));4142 const jws = JWS.parse(jwt);4344 const isValid = jws.verify(publicKey);4546 if(isValid){47 const base64urlDecode = (str)=>{48 let base64 = str.replace(/-/g, "+").replace(/_/g, "/");49 while(base64.length % 4){50 base64 += "=";51}52 return Encoding.fromBase64(base64);53};5455 const payloadJson = base64urlDecode(encodedPayload);56 const payload = JSON.parse(payloadJson);5758 // Check expiration59 const currentTime = Math.floor(Date.now() / 1000);60 if(payload.exp && payload.exp<currentTime){61 Logger.error("JWT has expired");62 return false;63}6465 return true;66}67 Logger.error("JWT signature verification failed");68 return false;69}catch(error){70 Logger.error("Error validating JWT: " + error.toString());71 return false;72}73};7475module.exports = {76 validateSLASToken,77};