クイックスタート
パブリッククライアントのユースケース
プライベートクライアントのユースケース
SLAS JWT の検証
信頼できるエージェントの認可
信頼できる代理システムの認可
パスワードのリセット
カスタムパラメーターとフック
買い物客プロフィールのマージ
買い物客の追跡環境設定の管理
プロモーションのタイプと要件
Agentic MCP Shopper Tools クイックスタート (パイロット)
URL マッピング
Ask the Community
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 を検証するには、次のようにします。
kid) を取得します。kid と一致するキーを JWKS で見つけます。新しく作成されたテナントの場合、SLAS JWKS エンドポイントは通常 2 つのキーを返します。キーの最初の正常なローテーションが完了した後、それ以降、エンドポイントはテナントごとに 3 つのキーを返します。これにより、SLAS が署名キーをローテーションする場合でも JWT を検証できます。
パフォーマンスを向上させ、レート制限を回避するには、署名キーのキャッシュを検討してください。トークンのデコードに失敗した場合は、キャッシュを無効にし、新しい署名キーを取得して、検証を 1 回だけ 再試行します。
Note
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}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.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.注: この関数に渡す 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};