SLAS JWT の検証

JSON Web Key Set (JWKS) は、JSON ベース形式の暗号化公開鍵のセットであり、JSON Web Token (JWT) の信頼性を検証するために一般的に使用されます。各 JWKS キーは、キーの種類 (kty)、アルゴリズム (alg)、およびキー ID (kid) を含む JSON Web Key (JWK) として表されます。この形式により、サーバーは JWKS エンドポイントを介して公開鍵を安全に共有でき、クライアントはトークンの署名に使用される秘密鍵を必要とせずにアクセスして JWT を検証できます。この公開鍵と秘密鍵のペアにより、送信される JWT の整合性と信頼性が確保されます。

JWKS は、トークンベースの認証に依存する分散システム、特に OAuth 2.0 および OpenID Connect プロトコルで重要な役割を果たします。JWKS を使用すると、公開鍵の信頼できる中央リポジトリが提供されるため、キーの管理が簡素化され、セキュリティが強化されます。JWKS を使用するシステムは、サービスを中断することなく動的にキーを取得し、キーのローテーションを処理できるため、最新の認証システムで署名を検証し、データを保護するための好ましい方法となっています。追加情報については、次を参照してください:

JWKS を使用して SLAS JWT を検証するには、次のようにします。

  1. SLAS JWKS エンドポイントから JWKS を取得します。
  2. JWT のヘッダーを解析した後、キー ID (kid) を取得します。
  3. kid と一致するキーを JWKS で見つけます。
  4. 一致する公開鍵を使用して JWT 署名を検証します。

新しく作成されたテナントの場合、SLAS JWKS エンドポイントは通常 2 つのキーを返します。キーの最初の正常なローテーションが完了した後、それ以降、エンドポイントはテナントごとに 3 つのキーを返します。これにより、SLAS が署名キーをローテーションする場合でも JWT を検証できます。

パフォーマンスを向上させ、レート制限を回避するには、署名キーのキャッシュを検討してください。トークンのデコードに失敗した場合は、キャッシュを無効にし、新しい署名キーを取得して、検証を 1 回だけ 再試行します。

Note

例: JWKS を使用した SLAS JWT の検証 

1@Slf4j
2@Component
3public class JwksValidatorExample {
4
5    @PostConstruct
6    public void init() throws Exception {
7        // This method runs when the application starts
8        log.info("Application started, initializing JWT Validator...");
9
10        // 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";
14
15        // Validate the JWT and log the result
16        String validationResult = validateJwt(validJwt);
17        log.info("*** JWT Validation Result: {}", validationResult);
18    }
19
20    // Fetch public EC keys from the JWKS endpoint
21    public List<ECKey> 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);
25
26        // Parse the JSON response into a JWKSet
27        JWKSet jwkSet = JWKSet.parse(JSONObjectUtils.parse(jwkSetJson));
28
29        // Extract EC keys from the JWKS
30        List<ECKey> ecKeys = jwkSet.getKeys().stream()
31                .filter(key -> key instanceof ECKey)  // Filter only EC keys
32                .map(key -> (ECKey) key)
33                .collect(Collectors.toList());
34
35        // 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        }
39
40        return ecKeys;
41    }
42
43    // Find the corresponding ECKey for a given JWT's 'kid'
44    public Optional<ECKey> findPublicKeyForJwt(List<ECKey> publicKeys, String jwtToken) throws ParseException {
45        // Parse the JWT and extract its header
46        SignedJWT signedJwt = SignedJWT.parse(jwtToken);
47        JWSHeader header = signedJwt.getHeader();
48
49        // Extract the Key ID (kid) from the JWT header
50        String jwtKid = header.getKeyID();
51
52        // Match the JWT 'kid' with the appropriate ECKey 'kid'
53        return publicKeys.stream()
54                .filter(ecKey -> ecKey.getKeyID().equals(jwtKid))
55                .findFirst();
56    }
57
58    // 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);
62
63        // Convert the ECKey to a usable public key
64        ECPublicKey ecPublicKey = publicKey.toECPublicKey();
65
66        // 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    }
69
70    // 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";
74
75        // 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<ECKey> publicKeys = getPublicKeysFromJwksEndpoint(jwksUrl);
79
80        // 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        }
85
86        // Step 2: Find the public key that matches the JWT's 'kid'
87        Optional<ECKey> matchingKey = findPublicKeyForJwt(publicKeys, jwtToken);
88        if (matchingKey.isEmpty()) {
89            return "No matching Key ID found in JWT, unable to validate JWT.";
90        }
91
92        // 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());
95
96        // Return the result of the verification
97        return isSignatureValid ? "JWT signature is valid." : "Invalid JWT signature.";
98    }
99}

例: JWT 検証出力 

検証デモの実行 

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.

キーローテーション後 

kid “4f3df” は JWKS 出力に含まれていないことに注意してください。JWT は引き続き検証されます。

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 

kid “a5fdc” が JWKS 出力で見つかり、署名が検証されることに注意してください。

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.

例:B2C Commerce Script API を使用した SLAS JWT の検証 

注: この関数に渡す JWKS を取得する際は、dw.system.Cache を使用してキャッシュできます。これにより、レート制限を回避し、パフォーマンスを向上させることができます。

1/**
2 * Validates the SLAS token via the JWKS
3 * @param {string} jwt - the JWT token to validate
4 * @param {Array<object>} keys - array of keys
5 * @return {boolean} true if the token is valid, false otherwise
6 */
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");
13
14  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    }
20
21    const encodedHeader = jwtParts[0];
22    const encodedPayload = jwtParts[1];
23
24    const jwsHeader = JWSHeader.parseEncoded(encodedHeader);
25    const kid = jwsHeader.toMap().get("kid");
26
27    if (!kid) {
28      Logger.error("JWT header missing kid");
29      return false;
30    }
31
32    // Find the matching key by kid; SLAS rotates keys and provides a key set
33    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    }
38
39    // Parse the public key from JWK (needs string form)
40    const publicKey = CertificateUtils.parsePublicKeyFromJWK(JSON.stringify(jwk));
41
42    const jws = JWS.parse(jwt);
43
44    const isValid = jws.verify(publicKey);
45
46    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      };
54
55      const payloadJson = base64urlDecode(encodedPayload);
56      const payload = JSON.parse(payloadJson);
57
58      // Check expiration
59      const currentTime = Math.floor(Date.now() / 1000);
60      if (payload.exp && payload.exp < currentTime) {
61        Logger.error("JWT has expired");
62        return false;
63      }
64
65      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};
74
75module.exports = {
76  validateSLASToken,
77};