From 15c8edd5fe32c239d11e30d9fd5a87154907437e Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Thu, 23 Jun 2022 14:37:49 -0700 Subject: [PATCH 01/16] events_webhook module with signature verification feature and test --- .../adobe/aio/cache/CaffeinCacheUtils.java | 40 ++++ .../com/adobe/aio/retrofit/RetrofitUtils.java | 55 +++++ events_webhook/pom.xml | 0 .../event/webhook/service/EventVerifier.java | 202 ++++++++++++++++++ .../event/webhook/service/PubKeyService.java | 23 ++ .../webhook/service/EventVerifierTest.java | 4 + 6 files changed, 324 insertions(+) create mode 100644 core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java create mode 100644 core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java create mode 100644 events_webhook/pom.xml create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java create mode 100644 events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java diff --git a/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java b/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java new file mode 100644 index 00000000..42719592 --- /dev/null +++ b/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CaffeinCacheUtility { + + private static final Logger logger = LoggerFactory.getLogger(CaffeinCacheUtility.class); + + private CaffeinCacheUtility() { + throw new IllegalStateException("This class is not meant to be instantiated."); + } + + public static Cache buildCacheWithExpiryAfterWrite(String cacheName, + long expiryInMinutes, long maxEntryCount) { + + logger.info("Initializing cache: {} with expiry-after-write: {} minutes, maxEntryCount: {}", + cacheName, expiryInMinutes, maxEntryCount); + + return Caffeine.newBuilder() + .expireAfterWrite(expiryInMinutes, TimeUnit.MINUTES) + .maximumSize(maxEntryCount) + .build(); + } +} + diff --git a/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java b/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java new file mode 100644 index 00000000..99048fdc --- /dev/null +++ b/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.retrofit; + +import com.adobe.aio.util.JacksonUtil; +import java.util.concurrent.TimeUnit; +import okhttp3.OkHttpClient; +import retrofit2.Converter.Factory; +import retrofit2.Retrofit; +import retrofit2.Retrofit.Builder; +import retrofit2.converter.jackson.JacksonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + + +public class RetrofitUtility { + + /** + * Scalars converter supports converting strings and both primitives and their boxed types to + * text/plain bodies. + */ + private static Builder getRetrofitBuilderWithScalarsConverter(String url, + int readTimeoutInSeconds) { + Builder builder = new Builder(); + OkHttpClient okHttpClient = new OkHttpClient().newBuilder(). + readTimeout(readTimeoutInSeconds, TimeUnit.SECONDS).build(); + builder.baseUrl(url); + builder.addConverterFactory(ScalarsConverterFactory.create()); + builder.client(okHttpClient); + return builder; + } + + private static Builder getRetrofitBuilder(String url, int readTimeoutInSeconds, + Factory converterFactory) { + return getRetrofitBuilderWithScalarsConverter(url, readTimeoutInSeconds) + .addConverterFactory(converterFactory); + } + + /** + * @return Retrofit with a jackson converter + */ + public static Retrofit getRetrofitWithJacksonConverterFactory(String url, + int readTimeoutInSeconds) { + return getRetrofitBuilder(url, readTimeoutInSeconds, + JacksonConverterFactory.create(JacksonUtil.DEFAULT_OBJECT_MAPPER)).build(); + } +} diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml new file mode 100644 index 00000000..e69de29b diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java new file mode 100644 index 00000000..bc4a662b --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -0,0 +1,202 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.service; + +import static com.adobe.aio.cache.CaffeinCacheUtils.buildCacheWithExpiryAfterWrite; +import static com.adobe.aio.retrofit.RetrofitUtils.getRetrofitWithJacksonConverterFactory; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.adobe.aio.exception.AIOException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Map; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.validator.routines.UrlValidator; +import org.h2.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import retrofit2.Call; +import retrofit2.Response; + + +public class EventVerifier { + + private static Logger logger = LoggerFactory.getLogger(EventVerifier.class); + + private static final String ADOBE_IOEVENTS_SECURITY_DOMAIN = "https://static.adobeioevents.com"; + private static final Long CACHE_EXPIRY_IN_MINUTES = 1440L; // expiry of 24 hrs + private static final Long CACHE_MAX_ENTRY_COUNT = 100L; + public static final String ADOBE_IOEVENTS_DIGI_SIGN_1 = "x-adobe-digital-signature-1"; + public static final String ADOBE_IOEVENTS_DIGI_SIGN_2 = "x-adobe-digital-signature-2"; + public static final String ADOBE_IOEVENTS_PUB_KEY_1_PATH = "x-adobe-public-key1-path"; + public static final String ADOBE_IOEVENTS_PUB_KEY_2_PATH = "x-adobe-public-key2-path"; + + private final Cache pubKeyCache; + private PubKeyService pubKeyService; + + public EventVerifier() { + this.pubKeyCache = buildCacheWithExpiryAfterWrite("publicKeyCache", + CACHE_EXPIRY_IN_MINUTES, CACHE_MAX_ENTRY_COUNT); + this.pubKeyService = getRetrofitWithJacksonConverterFactory(ADOBE_IOEVENTS_SECURITY_DOMAIN, 60) + .create(PubKeyService.class); + } + + /** + * Authenticate the event by checking the target recipient + * and verifying the signatures + * @param message - the event payload + * @param clientId - recipient client id in the payload + * @param headers - webhook request headers + * @return boolean - TRUE if valid event else FALSE + * @throws Exception + */ + public boolean authenticateEvent(String message, String clientId, + Map headers) throws Exception { + if(!isValidTargetRecipient(message, clientId)) { + logger.error("target recipient {} is not valid for message {}", clientId, message); + return false; + } + if (!verifyEventSignatures(message, headers)) { + logger.error("signatures are not valid for message {}", message); + return false; + } + return true; + } + + private boolean verifyEventSignatures(String message, + Map headers) { + String[] digitalSignatures = {headers.get(ADOBE_IOEVENTS_DIGI_SIGN_1), + headers.get(ADOBE_IOEVENTS_DIGI_SIGN_2)}; + String[] pubKeyPaths = {headers.get(ADOBE_IOEVENTS_PUB_KEY_1_PATH), + headers.get(ADOBE_IOEVENTS_PUB_KEY_2_PATH)}; + String publicKey1Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + headers.get(ADOBE_IOEVENTS_PUB_KEY_1_PATH); + String publicKey2Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + headers.get(ADOBE_IOEVENTS_PUB_KEY_2_PATH); + + try { + if (isValidUrl(publicKey1Url) && isValidUrl(publicKey2Url)) { + return verifySignature(message, pubKeyPaths, digitalSignatures); + } + } catch (Exception e) { + throw new AIOException("Error verifying signature for public keys " + publicKey1Url + + " & " + publicKey2Url + ". Reason -> " + e.getMessage()); + } + return false; + } + + private boolean verifySignature(String message, String[] publicKeyPaths, String[] signatures) + throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { + byte[] data = message.getBytes(UTF_8); + + for (int i = 0; i < signatures.length; i++) { + // signature generated at I/O Events side is Base64 encoded, so it must be decoded + byte[] sign = Base64.decodeBase64(signatures[i]); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(getPublic(fetchPemEncodedPublicKey(publicKeyPaths[i]))); + sig.update(data); + boolean result = sig.verify(sign); + if (result) { + return true; + } + } + return false; + } + + private boolean isValidTargetRecipient(String message, String clientId) { + ObjectMapper mapper = new ObjectMapper(); + try { + JsonNode jsonPayload = mapper.readTree(message); + JsonNode recipientClientIdNode = jsonPayload.get("recipient_client_id"); + if (recipientClientIdNode != null) { + return recipientClientIdNode.textValue().equals(clientId); + } + } catch (JsonProcessingException e) { + throw new AIOException("error parsing the event payload during target recipient check.."); + } + } + + private PublicKey getPublic(String pubKey) + throws NoSuchAlgorithmException, InvalidKeySpecException { + String publicKeyPEM = pubKey + .replace("-----BEGIN PUBLIC KEY-----", "") + .replaceAll(System.lineSeparator(), "") + .replace("-----END PUBLIC KEY-----", ""); + + byte[] encoded = Base64.decodeBase64(publicKeyPEM); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded); + return keyFactory.generatePublic(keySpec); + } + + private String fetchPemEncodedPublicKey(String publicKeyPath) { + return fetchKeyFromCacheOrApi(publicKeyPath); + } + + private String fetchKeyFromCacheOrApi(String pubKeyPath) { + String pubKeyFileName = getPublicKeyFileName(pubKeyPath); + String pubKey = getKeyFromCache(pubKeyFileName); + if (StringUtils.isNullOrEmpty(pubKey)) { + pubKey = fetchKeyFromApiAndPutInCache(pubKeyPath, pubKeyFileName); + } + return pubKey; + } + + private String fetchKeyFromApiAndPutInCache(String pubKeyPath, String pubKeyFileName) { + try { + logger.warn("public key {} not present in cache, fetching directly from the cdn url {}", + pubKeyFileName, ADOBE_IOEVENTS_SECURITY_DOMAIN + pubKeyPath); + String pubKey = ""; + Call pubKeyFetchCall = pubKeyService.getPubKeyFromCDN(pubKeyPath); + Response response = pubKeyFetchCall.execute(); + if (response.isSuccessful()) { + pubKey = response.body(); + pubKeyCache.put(pubKeyFileName, pubKey); + } + return pubKey; + } catch (Exception e) { + throw new AIOException("error fetching public key from CDN url" + + ADOBE_IOEVENTS_SECURITY_DOMAIN + pubKeyPath + " due to " + e.getMessage()); + } + } + + private String getKeyFromCache(String pubKeyFileNameAsKey) { + String pubKey = pubKeyCache.getIfPresent(pubKeyFileNameAsKey); + if (pubKey != null) { + logger.debug("fetched key successfully for pub key path {} from cache", pubKeyFileNameAsKey); + } + return pubKey; + } + + /** + * Parses the pub key file name from the relative path + * + * @param pubKeyPath - relative path in the format /prod/keys/pub-key-voy5XEbWmT.pem + * @return public key file name + */ + String getPublicKeyFileName(String pubKeyPath) { + return pubKeyPath.substring(pubKeyPath.lastIndexOf('/') + 1); + } + + private boolean isValidUrl(String url) { + return new UrlValidator().isValid(url); + } +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java new file mode 100644 index 00000000..6eee0e11 --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java @@ -0,0 +1,23 @@ +/************************************************************************* + * ADOBE CONFIDENTIAL ___________________ + *

+ * Copyright 2017 Adobe Systems Incorporated All Rights Reserved. + *

+ * NOTICE: All information contained herein is, and remains the property of Adobe Systems + * Incorporated and its suppliers, if any. The intellectual and technical concepts contained herein + * are proprietary to Adobe Systems Incorporated and its suppliers and are protected by all + * applicable intellectual property laws, including trade secret and copyright laws. Dissemination + * of this information or reproduction of this material is strictly forbidden unless prior written + * permission is obtained from Adobe Systems Incorporated. + **************************************************************************/ + +package com.adobe.egqa.service; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; + +public interface PubKeyService { + @GET("{pubKeyPath}") + Call getPubKeyFromCDN(@Path(value = "pubKeyPath", encoded = true) String pubKeyPath); +} diff --git a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java new file mode 100644 index 00000000..87b55323 --- /dev/null +++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java @@ -0,0 +1,4 @@ +package PACKAGE_NAME;/** + * Created by abhupadh on 16 June, 2022 + */public class EventVerifierTest { +} From e9aaf182598dc711c359720055e66f353facd5ae Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Thu, 23 Jun 2022 14:39:40 -0700 Subject: [PATCH 02/16] modified core, webhook and main pom, events_webhook module classes --- core/pom.xml | 23 +++- .../adobe/aio/cache/CaffeinCacheUtils.java | 6 +- .../com/adobe/aio/retrofit/RetrofitUtils.java | 2 +- events_webhook/pom.xml | 89 ++++++++++++++ .../event/webhook/service/PubKeyService.java | 26 ++--- .../webhook/service/EventVerifierTest.java | 109 +++++++++++++++++- pom.xml | 78 ++++++++++++- 7 files changed, 310 insertions(+), 23 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 65486baf..789db277 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -42,14 +42,17 @@ io.github.openfeign feign-core + io.github.openfeign feign-jackson + io.github.openfeign feign-slf4j + io.github.openfeign.form feign-form @@ -65,6 +68,24 @@ slf4j-simple - + + com.github.ben-manes.caffeine + caffeine + + + com.squareup.retrofit2 + retrofit + + + + com.squareup.retrofit2 + converter-jackson + + + + com.squareup.retrofit2 + converter-scalars + + diff --git a/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java b/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java index 42719592..2aae5aa9 100644 --- a/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java +++ b/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java @@ -17,11 +17,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class CaffeinCacheUtility { +public class CaffeinCacheUtils { - private static final Logger logger = LoggerFactory.getLogger(CaffeinCacheUtility.class); + private static final Logger logger = LoggerFactory.getLogger(CaffeinCacheUtils.class); - private CaffeinCacheUtility() { + private CaffeinCacheUtils() { throw new IllegalStateException("This class is not meant to be instantiated."); } diff --git a/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java b/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java index 99048fdc..164b052b 100644 --- a/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java +++ b/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java @@ -21,7 +21,7 @@ import retrofit2.converter.scalars.ScalarsConverterFactory; -public class RetrofitUtility { +public class RetrofitUtils { /** * Scalars converter supports converting strings and both primitives and their boxed types to diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml index e69de29b..be2d9f44 100644 --- a/events_webhook/pom.xml +++ b/events_webhook/pom.xml @@ -0,0 +1,89 @@ + + + + + com.adobe.aio + aio-lib-java + 0.0.45-SNAPSHOT + ../pom.xml + + 4.0.0 + + Adobe I/O - Events Webhook Library + Adobe I/O - Java SDK - Events Webhook Library + aio-lib-java-events-webhook + + + + com.adobe.aio + aio-lib-java-core + ${project.version} + + + + org.slf4j + slf4j-api + + + + com.squareup.retrofit2 + retrofit + + + + com.h2database + h2 + + + + commons-validator + commons-validator + + + + com.github.ben-manes.caffeine + caffeine + + + + commons-codec + commons-codec + + + + com.google.guava + guava + + + + + + org.mockito + mockito-core + + + + org.springframework.boot + spring-boot-test + + + + junit + junit + + + \ No newline at end of file diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java index 6eee0e11..63357a06 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java @@ -1,17 +1,15 @@ -/************************************************************************* - * ADOBE CONFIDENTIAL ___________________ - *

- * Copyright 2017 Adobe Systems Incorporated All Rights Reserved. - *

- * NOTICE: All information contained herein is, and remains the property of Adobe Systems - * Incorporated and its suppliers, if any. The intellectual and technical concepts contained herein - * are proprietary to Adobe Systems Incorporated and its suppliers and are protected by all - * applicable intellectual property laws, including trade secret and copyright laws. Dissemination - * of this information or reproduction of this material is strictly forbidden unless prior written - * permission is obtained from Adobe Systems Incorporated. - **************************************************************************/ - -package com.adobe.egqa.service; +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.service; import retrofit2.Call; import retrofit2.http.GET; diff --git a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java index 87b55323..652fb57a 100644 --- a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java +++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java @@ -1,4 +1,107 @@ -package PACKAGE_NAME;/** - * Created by abhupadh on 16 June, 2022 - */public class EventVerifierTest { +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.service; + +import static com.adobe.aio.event.webhook.service.EventVerifier.ADOBE_IOEVENTS_DIGI_SIGN_1; +import static com.adobe.aio.event.webhook.service.EventVerifier.ADOBE_IOEVENTS_DIGI_SIGN_2; +import static com.adobe.aio.event.webhook.service.EventVerifier.ADOBE_IOEVENTS_PUB_KEY_1_PATH; +import static com.adobe.aio.event.webhook.service.EventVerifier.ADOBE_IOEVENTS_PUB_KEY_2_PATH; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class EventVerifierTest { + + private static final String TEST_CLIENT_ID = "client_id1"; + private static final String INVALID_TEST_CLIENT_ID = "invalid_client_id"; + private static final String TEST_DIGI_SIGN_1 = "mvEj6rEiJK9QFtX5QNl1bc9AJW7sOFAHgUk25ytroi8yqrRAPtijCu19vS6o96eANnjHQzS76p+sA1Y4giQ+DnQOZz35AJwvnDmK2FVkbaiupinCjRK4pnwd0aY88HI6o8Fmj6zomAKcF46D8IHglirCjiUHT3gENFHnuro55rgnJ3eWrT4ldcruKHSKAopapAhqQBr1BhrHVqtoz8Zg+ZWGsSgY+tAje3m59mTjWPSC1/KfjgpjhADDKHz+I1eT/z5k0667hlLSgYvHGhmhh8aAcLKgM5tzcXWpYQz4xCZgSF/MqAIUkmnVVWHhs18rr1WSaJ4j2ZTO6vaj8XoHng=="; + private static final String TEST_DIGI_SIGN_2 = "GpMONiPMHY51vpHF3R9SSs9ogRn8i2or/bvV3R+PYXGgDzAxDhRdl9dIUp/qQ3vsxDGEv045IV4GQ2f4QbsFvWLJsBNyCqLs6KL8LsRoGfEC4Top6c1VVjrEEQ1MOoFcoq/6riXzg4h09lRTfARllVv+icgzAiuv/JW2HNg5yQ4bqenFELD6ipCStuaI/OGS0A9s0Hc6o3aoHz3r5d5DecwE6pUdpG8ODhKBM+34CvcvMDNdrj8STYWHsEUqGdR9klpaqaC1QRYFIO7WgbgdwsuGULz6Sjm+q5s5Wh++fz5E+gXkizFviD389gDIUylFTig/1h7WTLRDuSz69Q+C5w=="; + private static final String TEST_PUB_KEY1_PATH = "/qe/keys/pub-key-voy5XEbWmT.pem"; + private static final String TEST_PUB_KEY2_PATH = "/qe/keys/pub-key-maAv3Tg6ZH.pem"; + + private EventVerifier underTest; + + @Before + public void setup() throws Exception { + underTest = new EventVerifier(); + } + + @Test + public void testVerifyValidSignature() throws Exception { + String message = getTestMessage(); + Map headers = getTestHeadersWithValidSignature(); + boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); + Assert.assertEquals(true, result); + } + + @Test + public void testVerifyInvalidSignature() throws Exception { + String message = getTestMessage(); + Map headers = getTestHeadersWithInvalidSignature(); + boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testVerifyInvalidPublicKey() throws Exception { + String message = getTestMessage(); + Map headers = getTestHeadersWithInvalidPubKey(); + boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); + Assert.assertEquals(Boolean.FALSE, result); + } + + @Test + public void testVerifyInvalidRecipientClient() throws Exception { + String message = getTestMessage(); + Map headers = getTestHeadersWithInvalidPubKey(); + boolean result = underTest.authenticateEvent(message, INVALID_TEST_CLIENT_ID, headers); + Assert.assertEquals(Boolean.FALSE, result); + } + + // ============================ PRIVATE HELPER METHODS ================================ + private String getTestMessage() { + return "{\"event_id\":\"eventId1\",\"event\":{\"hello\":\"world\"},\"recipient_client_id\":\"client_id1\"}"; + } + + private Map getTestSignatureHeaders() { + Map testSignatureHeaders = new HashMap<>(); + testSignatureHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_1, TEST_DIGI_SIGN_1); + testSignatureHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_2, TEST_DIGI_SIGN_2); + testSignatureHeaders.put(ADOBE_IOEVENTS_PUB_KEY_1_PATH, TEST_PUB_KEY1_PATH); + testSignatureHeaders.put(ADOBE_IOEVENTS_PUB_KEY_2_PATH, TEST_PUB_KEY2_PATH); + return testSignatureHeaders; + } + + private Map getTestHeadersWithValidSignature() { + return getTestSignatureHeaders(); + } + + private Map getTestHeadersWithInvalidSignature() { + Map signHeaders = getTestSignatureHeaders(); + signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_1, TEST_DIGI_SIGN_2); + signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_2, TEST_DIGI_SIGN_1); + return signHeaders; + } + + private Map getTestHeadersWithInvalidPubKey() { + Map signHeaders = getTestSignatureHeaders(); + signHeaders.put(ADOBE_IOEVENTS_PUB_KEY_1_PATH, TEST_PUB_KEY2_PATH); + signHeaders.put(ADOBE_IOEVENTS_PUB_KEY_2_PATH, TEST_PUB_KEY1_PATH); + return signHeaders; + } + } diff --git a/pom.xml b/pom.xml index 19bfe5b8..d083636d 100644 --- a/pom.xml +++ b/pom.xml @@ -28,7 +28,7 @@ events_mgmt events_ingress events_journal - + events_webhook aem @@ -104,6 +104,16 @@ [1.7.21,1.7.25] 2.12.3 0.11.2 + 2.7.2 + 2.5.0 + 2.7.2 + 2.7.2 + 2.6.2 + 29.0-jre + 1.4.200 + 1.6 + 1.15 + 2.5.6 11.2 3.8.0 @@ -122,6 +132,31 @@ provided + + com.squareup.retrofit2 + converter-scalars + ${converter-scalars.version} + + + + com.squareup.retrofit2 + converter-jackson + ${converter-jackson.version} + + + + com.squareup.retrofit2 + retrofit + ${retrofit.version} + + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + compile + + org.apache.commons commons-text @@ -135,6 +170,25 @@ provided + + commons-codec + commons-codec + ${commons-codec.version} + + + + com.google.guava + guava + ${guava.version} + compile + + + + commons-validator + commons-validator + ${commons-validator.version} + + org.slf4j slf4j-api @@ -142,6 +196,12 @@ provided + + com.h2database + h2 + ${h2.version} + + io.jsonwebtoken jjwt-api @@ -198,6 +258,20 @@ test + + junit + junit + ${junit.version} + test + + + + org.springframework.boot + spring-boot-test + ${spring-boot-test.version} + test + + com.github.tomakehurst wiremock-jre8 @@ -226,6 +300,8 @@ test + + From 75a567843427940f632d7618ac7dc67ae3f04631 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Thu, 23 Jun 2022 14:48:50 -0700 Subject: [PATCH 03/16] missing return statement --- .../java/com/adobe/aio/event/webhook/service/EventVerifier.java | 1 + 1 file changed, 1 insertion(+) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java index bc4a662b..3c54ef18 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -131,6 +131,7 @@ private boolean isValidTargetRecipient(String message, String clientId) { } catch (JsonProcessingException e) { throw new AIOException("error parsing the event payload during target recipient check.."); } + return false; } private PublicKey getPublic(String pubKey) From f0d83a194351c7dc470755c0df99c3d9444321e8 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Thu, 23 Jun 2022 15:46:40 -0700 Subject: [PATCH 04/16] adding EventVerifierBuilder --- .../aio/event/webhook/service/EventVerifier.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java index 3c54ef18..1501ddbc 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -193,11 +193,20 @@ private String getKeyFromCache(String pubKeyFileNameAsKey) { * @param pubKeyPath - relative path in the format /prod/keys/pub-key-voy5XEbWmT.pem * @return public key file name */ - String getPublicKeyFileName(String pubKeyPath) { + private String getPublicKeyFileName(String pubKeyPath) { return pubKeyPath.substring(pubKeyPath.lastIndexOf('/') + 1); } private boolean isValidUrl(String url) { return new UrlValidator().isValid(url); } + + // ----------------------- Instance Builder --------------------------- + + public static class EventVerifierBuilder { + public EventVerifier build() { + EventVerifier eventVerifier = new EventVerifier(); + return eventVerifier; + } + } } From 914e0e7293293a0f09e8fd90cd0e029bb5f58b47 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Tue, 23 Aug 2022 17:23:51 -0700 Subject: [PATCH 05/16] refactoring event verifier, using map cache, open fiegn for http call --- .../event/webhook/service/EventVerifier.java | 57 +++++++------------ .../event/webhook/service/PubKeyService.java | 21 ------- 2 files changed, 21 insertions(+), 57 deletions(-) delete mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java index 1501ddbc..872031eb 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -11,15 +11,15 @@ */ package com.adobe.aio.event.webhook.service; -import static com.adobe.aio.cache.CaffeinCacheUtils.buildCacheWithExpiryAfterWrite; -import static com.adobe.aio.retrofit.RetrofitUtils.getRetrofitWithJacksonConverterFactory; +import static com.adobe.aio.event.webhook.cache.CacheServiceImpl.cacheBuilder; import static java.nio.charset.StandardCharsets.UTF_8; +import com.adobe.aio.event.webhook.cache.CacheServiceImpl; +import com.adobe.aio.event.webhook.feign.FeignPubKeyService; import com.adobe.aio.exception.AIOException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.benmanes.caffeine.cache.Cache; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; @@ -34,30 +34,26 @@ import org.h2.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import retrofit2.Call; -import retrofit2.Response; - +import org.springframework.stereotype.Service; +@Service public class EventVerifier { private static Logger logger = LoggerFactory.getLogger(EventVerifier.class); - private static final String ADOBE_IOEVENTS_SECURITY_DOMAIN = "https://static.adobeioevents.com"; - private static final Long CACHE_EXPIRY_IN_MINUTES = 1440L; // expiry of 24 hrs - private static final Long CACHE_MAX_ENTRY_COUNT = 100L; + public static final String ADOBE_IOEVENTS_SECURITY_DOMAIN = "https://static.adobeioevents.com/"; public static final String ADOBE_IOEVENTS_DIGI_SIGN_1 = "x-adobe-digital-signature-1"; public static final String ADOBE_IOEVENTS_DIGI_SIGN_2 = "x-adobe-digital-signature-2"; public static final String ADOBE_IOEVENTS_PUB_KEY_1_PATH = "x-adobe-public-key1-path"; public static final String ADOBE_IOEVENTS_PUB_KEY_2_PATH = "x-adobe-public-key2-path"; + private static final int CACHE_EXPIRY_IN_MINUTES = 1440; // expiry of 24 hrs - private final Cache pubKeyCache; - private PubKeyService pubKeyService; + private final CacheServiceImpl pubKeyCache; + private FeignPubKeyService pubKeyService; public EventVerifier() { - this.pubKeyCache = buildCacheWithExpiryAfterWrite("publicKeyCache", - CACHE_EXPIRY_IN_MINUTES, CACHE_MAX_ENTRY_COUNT); - this.pubKeyService = getRetrofitWithJacksonConverterFactory(ADOBE_IOEVENTS_SECURITY_DOMAIN, 60) - .create(PubKeyService.class); + this.pubKeyCache = cacheBuilder().buildWithExpiry(CACHE_EXPIRY_IN_MINUTES); + this.pubKeyService = new FeignPubKeyService(ADOBE_IOEVENTS_SECURITY_DOMAIN); } /** @@ -70,7 +66,7 @@ public EventVerifier() { * @throws Exception */ public boolean authenticateEvent(String message, String clientId, - Map headers) throws Exception { + Map headers) { if(!isValidTargetRecipient(message, clientId)) { logger.error("target recipient {} is not valid for message {}", clientId, message); return false; @@ -148,7 +144,7 @@ private PublicKey getPublic(String pubKey) return keyFactory.generatePublic(keySpec); } - private String fetchPemEncodedPublicKey(String publicKeyPath) { + String fetchPemEncodedPublicKey(String publicKeyPath) { return fetchKeyFromCacheOrApi(publicKeyPath); } @@ -165,26 +161,24 @@ private String fetchKeyFromApiAndPutInCache(String pubKeyPath, String pubKeyFile try { logger.warn("public key {} not present in cache, fetching directly from the cdn url {}", pubKeyFileName, ADOBE_IOEVENTS_SECURITY_DOMAIN + pubKeyPath); - String pubKey = ""; - Call pubKeyFetchCall = pubKeyService.getPubKeyFromCDN(pubKeyPath); - Response response = pubKeyFetchCall.execute(); - if (response.isSuccessful()) { - pubKey = response.body(); - pubKeyCache.put(pubKeyFileName, pubKey); + String pubKeyFetchResponse = pubKeyService.getPubKeyFromCDN(pubKeyPath); + if (!StringUtils.isNullOrEmpty(pubKeyFetchResponse)) { + pubKeyCache.put(pubKeyFileName, pubKeyFetchResponse); } - return pubKey; + return pubKeyFetchResponse; } catch (Exception e) { - throw new AIOException("error fetching public key from CDN url" + throw new AIOException("error fetching public key from CDN url -> " + ADOBE_IOEVENTS_SECURITY_DOMAIN + pubKeyPath + " due to " + e.getMessage()); } } private String getKeyFromCache(String pubKeyFileNameAsKey) { - String pubKey = pubKeyCache.getIfPresent(pubKeyFileNameAsKey); + Object pubKey = pubKeyCache.get(pubKeyFileNameAsKey); if (pubKey != null) { logger.debug("fetched key successfully for pub key path {} from cache", pubKeyFileNameAsKey); + return String.valueOf(pubKey); } - return pubKey; + return null; } /** @@ -200,13 +194,4 @@ private String getPublicKeyFileName(String pubKeyPath) { private boolean isValidUrl(String url) { return new UrlValidator().isValid(url); } - - // ----------------------- Instance Builder --------------------------- - - public static class EventVerifierBuilder { - public EventVerifier build() { - EventVerifier eventVerifier = new EventVerifier(); - return eventVerifier; - } - } } diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java deleted file mode 100644 index 63357a06..00000000 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2017 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -package com.adobe.aio.event.webhook.service; - -import retrofit2.Call; -import retrofit2.http.GET; -import retrofit2.http.Path; - -public interface PubKeyService { - @GET("{pubKeyPath}") - Call getPubKeyFromCDN(@Path(value = "pubKeyPath", encoded = true) String pubKeyPath); -} From cd9498074d9f77cf7770a0329e7fb9cee08dc152 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Tue, 23 Aug 2022 17:25:30 -0700 Subject: [PATCH 06/16] adding api, feign service, cache, model --- .../event/webhook/api/PublicKeyCdnApi.java | 20 +++++ .../aio/event/webhook/cache/CacheService.java | 27 ++++++ .../event/webhook/cache/CacheServiceImpl.java | 89 +++++++++++++++++++ .../webhook/feign/FeignPubKeyService.java | 32 +++++++ .../event/webhook/model/CacheableObject.java | 66 ++++++++++++++ .../event/webhook/service/PubKeyService.java | 16 ++++ 6 files changed, 250 insertions(+) create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/api/PublicKeyCdnApi.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/feign/FeignPubKeyService.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java create mode 100644 events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/api/PublicKeyCdnApi.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/api/PublicKeyCdnApi.java new file mode 100644 index 00000000..6e9bcba7 --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/api/PublicKeyCdnApi.java @@ -0,0 +1,20 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.api; + +import feign.Param; +import feign.RequestLine; + +public interface PublicKeyCdnApi { + @RequestLine(value = "GET {pubKeyPath}") + String getPubKeyFromCDN(@Param("pubKeyPath") String pubKeyPath); +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java new file mode 100644 index 00000000..5d158c27 --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java @@ -0,0 +1,27 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.cache; + +import com.adobe.aio.event.webhook.model.CacheableObject; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public interface CacheService { + + @Nullable + Object get(@Nonnull String key); + + void put(@Nonnull String key, @Nonnull Object value); + + boolean isExpired(CacheableObject cacheable); + +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java new file mode 100644 index 00000000..31ea52be --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java @@ -0,0 +1,89 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.cache; + +import com.adobe.aio.event.webhook.model.CacheableObject; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class CacheServiceImpl implements CacheService { + + private static final Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class); + + Map cacheMap; + Date cacheExpirationDate; + + @Nullable + @Override + public String get(@Nonnull String key) { + CacheableObject obj = (CacheableObject) cacheMap.get(key); + if (obj != null) { + if (isExpired(obj)) { + logger.debug("cache is expired..invalidating entry for key {}", key); + cacheMap.remove(key); + return null; + } + else { + return obj.getValue(); + } + } + return null; + } + + @Override + public void put(@Nonnull String key, @Nonnull Object value) { + CacheableObject cacheableObject = new CacheableObject(key, (String) value, 1440); + cacheMap.put(key, cacheableObject); + } + + @Override + public boolean isExpired(CacheableObject cacheableObject) { + return getExpirationDate(cacheableObject.getExpiryInMinutes()).after(this.cacheExpirationDate); + } + + private CacheServiceImpl buildWithExpiry(int ttl) { + this.cacheExpirationDate = getExpirationDate(ttl); + return this; + } + + private CacheServiceImpl initialiseCacheMap() { + this.cacheMap = new HashMap<>(); + return this; + } + + public static CacheBuilder cacheBuilder() { + return new CacheBuilder(); + } + + public static class CacheBuilder { + public CacheServiceImpl buildWithExpiry(int expiryInMinutes) { + return new CacheServiceImpl() + .initialiseCacheMap() + .buildWithExpiry(expiryInMinutes); + } + } + + private Date getExpirationDate(int minutesToLive) { + Date expirationDate = new java.util.Date(); + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.setTime(expirationDate); + cal.add(cal.MINUTE, minutesToLive); + expirationDate = cal.getTime(); + return expirationDate; + } +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/feign/FeignPubKeyService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/feign/FeignPubKeyService.java new file mode 100644 index 00000000..705db916 --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/feign/FeignPubKeyService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.feign; + +import com.adobe.aio.event.webhook.api.PublicKeyCdnApi; +import com.adobe.aio.event.webhook.service.PubKeyService; +import com.adobe.aio.util.feign.FeignUtil; + +public class FeignPubKeyService implements PubKeyService { + + private final PublicKeyCdnApi publicKeyCdnApi; + + public FeignPubKeyService(final String pubKeyCdnBaseUrl) { + this.publicKeyCdnApi = FeignUtil.getBaseBuilder() + .target(PublicKeyCdnApi.class, pubKeyCdnBaseUrl); + } + + @Override + public String getPubKeyFromCDN(String pubKeyPath) { + String pubKey = publicKeyCdnApi.getPubKeyFromCDN(pubKeyPath); + return pubKey; + } +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java new file mode 100644 index 00000000..26f13f5d --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.model; + +import java.util.Objects; + +public class CacheableObject { + + private String key; + private String value; + private int expiryInMinutes; + + public CacheableObject(String key, String value, int expiryInMinutes) { + this.key = key; + this.value = value; + this.expiryInMinutes = expiryInMinutes; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public int getExpiryInMinutes() { + return expiryInMinutes; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CacheableObject that = (CacheableObject) o; + return expiryInMinutes == that.expiryInMinutes && Objects.equals(key, that.key) + && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value, expiryInMinutes); + } + + @Override + public String toString() { + return "CacheableObject{" + + "key='" + key + '\'' + + ", value='" + value + '\'' + + ", expiryInMinutes=" + expiryInMinutes + + '}'; + } +} diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java new file mode 100644 index 00000000..74026ef0 --- /dev/null +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/PubKeyService.java @@ -0,0 +1,16 @@ +/* + * Copyright 2017 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package com.adobe.aio.event.webhook.service; + +public interface PubKeyService { + String getPubKeyFromCDN(String pubKeyPath); +} From 1f6450774434374446809b76e4b60353263defee Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Tue, 23 Aug 2022 17:26:03 -0700 Subject: [PATCH 07/16] updating test --- .../webhook/service/EventVerifierTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java index 652fb57a..ceb88fb7 100644 --- a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java +++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java @@ -21,27 +21,28 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.springframework.boot.test.context.SpringBootTest; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; -@SpringBootTest +@RunWith(MockitoJUnitRunner.class) public class EventVerifierTest { private static final String TEST_CLIENT_ID = "client_id1"; private static final String INVALID_TEST_CLIENT_ID = "invalid_client_id"; private static final String TEST_DIGI_SIGN_1 = "mvEj6rEiJK9QFtX5QNl1bc9AJW7sOFAHgUk25ytroi8yqrRAPtijCu19vS6o96eANnjHQzS76p+sA1Y4giQ+DnQOZz35AJwvnDmK2FVkbaiupinCjRK4pnwd0aY88HI6o8Fmj6zomAKcF46D8IHglirCjiUHT3gENFHnuro55rgnJ3eWrT4ldcruKHSKAopapAhqQBr1BhrHVqtoz8Zg+ZWGsSgY+tAje3m59mTjWPSC1/KfjgpjhADDKHz+I1eT/z5k0667hlLSgYvHGhmhh8aAcLKgM5tzcXWpYQz4xCZgSF/MqAIUkmnVVWHhs18rr1WSaJ4j2ZTO6vaj8XoHng=="; private static final String TEST_DIGI_SIGN_2 = "GpMONiPMHY51vpHF3R9SSs9ogRn8i2or/bvV3R+PYXGgDzAxDhRdl9dIUp/qQ3vsxDGEv045IV4GQ2f4QbsFvWLJsBNyCqLs6KL8LsRoGfEC4Top6c1VVjrEEQ1MOoFcoq/6riXzg4h09lRTfARllVv+icgzAiuv/JW2HNg5yQ4bqenFELD6ipCStuaI/OGS0A9s0Hc6o3aoHz3r5d5DecwE6pUdpG8ODhKBM+34CvcvMDNdrj8STYWHsEUqGdR9klpaqaC1QRYFIO7WgbgdwsuGULz6Sjm+q5s5Wh++fz5E+gXkizFviD389gDIUylFTig/1h7WTLRDuSz69Q+C5w=="; - private static final String TEST_PUB_KEY1_PATH = "/qe/keys/pub-key-voy5XEbWmT.pem"; - private static final String TEST_PUB_KEY2_PATH = "/qe/keys/pub-key-maAv3Tg6ZH.pem"; + private static final String TEST_PUB_KEY1_PATH = "qe/keys/pub-key-voy5XEbWmT.pem"; + private static final String TEST_PUB_KEY2_PATH = "qe/keys/pub-key-maAv3Tg6ZH.pem"; private EventVerifier underTest; @Before - public void setup() throws Exception { + public void setup() { underTest = new EventVerifier(); } @Test - public void testVerifyValidSignature() throws Exception { + public void testVerifyValidSignature() { String message = getTestMessage(); Map headers = getTestHeadersWithValidSignature(); boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); @@ -49,7 +50,7 @@ public void testVerifyValidSignature() throws Exception { } @Test - public void testVerifyInvalidSignature() throws Exception { + public void testVerifyInvalidSignature() { String message = getTestMessage(); Map headers = getTestHeadersWithInvalidSignature(); boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); @@ -57,7 +58,7 @@ public void testVerifyInvalidSignature() throws Exception { } @Test - public void testVerifyInvalidPublicKey() throws Exception { + public void testVerifyInvalidPublicKey() { String message = getTestMessage(); Map headers = getTestHeadersWithInvalidPubKey(); boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); @@ -65,7 +66,7 @@ public void testVerifyInvalidPublicKey() throws Exception { } @Test - public void testVerifyInvalidRecipientClient() throws Exception { + public void testVerifyInvalidRecipientClient() { String message = getTestMessage(); Map headers = getTestHeadersWithInvalidPubKey(); boolean result = underTest.authenticateEvent(message, INVALID_TEST_CLIENT_ID, headers); @@ -103,5 +104,4 @@ private Map getTestHeadersWithInvalidPubKey() { signHeaders.put(ADOBE_IOEVENTS_PUB_KEY_2_PATH, TEST_PUB_KEY1_PATH); return signHeaders; } - } From 01f11d500b14cb759a80d45f955e6678726e3369 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Tue, 23 Aug 2022 17:41:10 -0700 Subject: [PATCH 08/16] updating poms --- events_webhook/pom.xml | 10 ++++++++++ pom.xml | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml index be2d9f44..798d0567 100644 --- a/events_webhook/pom.xml +++ b/events_webhook/pom.xml @@ -34,6 +34,12 @@ ${project.version} + + com.adobe.aio + aio-lib-java-ims + ${project.version} + + org.slf4j slf4j-api @@ -69,6 +75,10 @@ guava + + org.springframework + spring-context + diff --git a/pom.xml b/pom.xml index d083636d..6d597cdf 100644 --- a/pom.xml +++ b/pom.xml @@ -114,6 +114,7 @@ 1.6 1.15 2.5.6 + 5.3.20 11.2 3.8.0 @@ -189,6 +190,13 @@ ${commons-validator.version} + + org.springframework + spring-context + ${spring-context.version} + compile + + org.slf4j slf4j-api From 2af9561e0f5b8c733a6e593162adb9389eb2bd0b Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Tue, 23 Aug 2022 17:45:17 -0700 Subject: [PATCH 09/16] minor refactoring --- .../java/com/adobe/aio/event/webhook/cache/CacheService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java index 5d158c27..4750a12c 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheService.java @@ -15,7 +15,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -public interface CacheService { +public interface CacheService { @Nullable Object get(@Nonnull String key); From 8fa9f04d9c0e2b9e25c9af69e025755eb4de0ef6 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Fri, 2 Dec 2022 00:19:30 -0800 Subject: [PATCH 10/16] removing caffeine, retrofit, other unwanted dependencies --- core/pom.xml | 19 ------- .../adobe/aio/cache/CaffeinCacheUtils.java | 40 -------------- .../com/adobe/aio/retrofit/RetrofitUtils.java | 55 ------------------- events_webhook/pom.xml | 54 ++++-------------- 4 files changed, 11 insertions(+), 157 deletions(-) delete mode 100644 core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java delete mode 100644 core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java diff --git a/core/pom.xml b/core/pom.xml index 9dd78d87..f65f31de 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -75,24 +75,5 @@ slf4j-simple - - com.github.ben-manes.caffeine - caffeine - - - - com.squareup.retrofit2 - retrofit - - - - com.squareup.retrofit2 - converter-jackson - - - - com.squareup.retrofit2 - converter-scalars - diff --git a/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java b/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java deleted file mode 100644 index 2aae5aa9..00000000 --- a/core/src/main/java/com/adobe/aio/cache/CaffeinCacheUtils.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -package com.adobe.aio.cache; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; -import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CaffeinCacheUtils { - - private static final Logger logger = LoggerFactory.getLogger(CaffeinCacheUtils.class); - - private CaffeinCacheUtils() { - throw new IllegalStateException("This class is not meant to be instantiated."); - } - - public static Cache buildCacheWithExpiryAfterWrite(String cacheName, - long expiryInMinutes, long maxEntryCount) { - - logger.info("Initializing cache: {} with expiry-after-write: {} minutes, maxEntryCount: {}", - cacheName, expiryInMinutes, maxEntryCount); - - return Caffeine.newBuilder() - .expireAfterWrite(expiryInMinutes, TimeUnit.MINUTES) - .maximumSize(maxEntryCount) - .build(); - } -} - diff --git a/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java b/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java deleted file mode 100644 index 164b052b..00000000 --- a/core/src/main/java/com/adobe/aio/retrofit/RetrofitUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2017 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ -package com.adobe.aio.retrofit; - -import com.adobe.aio.util.JacksonUtil; -import java.util.concurrent.TimeUnit; -import okhttp3.OkHttpClient; -import retrofit2.Converter.Factory; -import retrofit2.Retrofit; -import retrofit2.Retrofit.Builder; -import retrofit2.converter.jackson.JacksonConverterFactory; -import retrofit2.converter.scalars.ScalarsConverterFactory; - - -public class RetrofitUtils { - - /** - * Scalars converter supports converting strings and both primitives and their boxed types to - * text/plain bodies. - */ - private static Builder getRetrofitBuilderWithScalarsConverter(String url, - int readTimeoutInSeconds) { - Builder builder = new Builder(); - OkHttpClient okHttpClient = new OkHttpClient().newBuilder(). - readTimeout(readTimeoutInSeconds, TimeUnit.SECONDS).build(); - builder.baseUrl(url); - builder.addConverterFactory(ScalarsConverterFactory.create()); - builder.client(okHttpClient); - return builder; - } - - private static Builder getRetrofitBuilder(String url, int readTimeoutInSeconds, - Factory converterFactory) { - return getRetrofitBuilderWithScalarsConverter(url, readTimeoutInSeconds) - .addConverterFactory(converterFactory); - } - - /** - * @return Retrofit with a jackson converter - */ - public static Retrofit getRetrofitWithJacksonConverterFactory(String url, - int readTimeoutInSeconds) { - return getRetrofitBuilder(url, readTimeoutInSeconds, - JacksonConverterFactory.create(JacksonUtil.DEFAULT_OBJECT_MAPPER)).build(); - } -} diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml index 798d0567..a25b6a25 100644 --- a/events_webhook/pom.xml +++ b/events_webhook/pom.xml @@ -18,13 +18,17 @@ com.adobe.aio aio-lib-java - 0.0.45-SNAPSHOT + 1.0.1-SNAPSHOT ../pom.xml 4.0.0 Adobe I/O - Events Webhook Library Adobe I/O - Java SDK - Events Webhook Library + + 11 + 11 + aio-lib-java-events-webhook @@ -40,60 +44,24 @@ ${project.version} - - org.slf4j - slf4j-api - - - - com.squareup.retrofit2 - retrofit - - - - com.h2database - h2 - - - - commons-validator - commons-validator - - - - com.github.ben-manes.caffeine - caffeine - - - - commons-codec - commons-codec - - - - com.google.guava - guava - - org.springframework spring-context - - - org.mockito - mockito-core + com.google.code.findbugs + jsr305 + - org.springframework.boot - spring-boot-test + org.mockito + mockito-core - junit junit + \ No newline at end of file From 6d5b8bb4f5b8394c45418545bc4c03eb7e613698 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Fri, 2 Dec 2022 00:20:03 -0800 Subject: [PATCH 11/16] refactoring hashmap used cache service --- .../event/webhook/cache/CacheServiceImpl.java | 27 +++++++++---------- .../event/webhook/model/CacheableObject.java | 19 ++++++------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java index 31ea52be..161eddf3 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java @@ -24,9 +24,10 @@ public class CacheServiceImpl implements CacheService { private static final Logger logger = LoggerFactory.getLogger(CacheServiceImpl.class); + private static final int DEFAULT_TTL_IN_MINUTES = 1440; + private static final Date CURRENT_SYSTEM_DATE = new Date(); Map cacheMap; - Date cacheExpirationDate; @Nullable @Override @@ -34,11 +35,10 @@ public String get(@Nonnull String key) { CacheableObject obj = (CacheableObject) cacheMap.get(key); if (obj != null) { if (isExpired(obj)) { - logger.debug("cache is expired..invalidating entry for key {}", key); + logger.debug("public key object in cache is expired..invalidating entry for key {}", key); cacheMap.remove(key); return null; - } - else { + } else { return obj.getValue(); } } @@ -47,18 +47,18 @@ public String get(@Nonnull String key) { @Override public void put(@Nonnull String key, @Nonnull Object value) { - CacheableObject cacheableObject = new CacheableObject(key, (String) value, 1440); + putWithExpiry(key, value, DEFAULT_TTL_IN_MINUTES); + } + + public void putWithExpiry(@Nonnull String key, @Nonnull Object value, int ttlInMinutes) { + CacheableObject cacheableObject = new CacheableObject(key, (String) value, + getExpirationDate(ttlInMinutes)); cacheMap.put(key, cacheableObject); } @Override public boolean isExpired(CacheableObject cacheableObject) { - return getExpirationDate(cacheableObject.getExpiryInMinutes()).after(this.cacheExpirationDate); - } - - private CacheServiceImpl buildWithExpiry(int ttl) { - this.cacheExpirationDate = getExpirationDate(ttl); - return this; + return CURRENT_SYSTEM_DATE.after(cacheableObject.getPubKeyExpiryDate()); } private CacheServiceImpl initialiseCacheMap() { @@ -71,10 +71,9 @@ public static CacheBuilder cacheBuilder() { } public static class CacheBuilder { - public CacheServiceImpl buildWithExpiry(int expiryInMinutes) { + public CacheServiceImpl buildCache() { return new CacheServiceImpl() - .initialiseCacheMap() - .buildWithExpiry(expiryInMinutes); + .initialiseCacheMap(); } } diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java index 26f13f5d..839cfd9e 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java @@ -11,18 +11,19 @@ */ package com.adobe.aio.event.webhook.model; +import java.util.Date; import java.util.Objects; public class CacheableObject { private String key; private String value; - private int expiryInMinutes; + private Date pubKeyExpiryDate; - public CacheableObject(String key, String value, int expiryInMinutes) { + public CacheableObject(String key, String value, Date expiryInMinutes) { this.key = key; this.value = value; - this.expiryInMinutes = expiryInMinutes; + this.pubKeyExpiryDate = expiryInMinutes; } public String getKey() { @@ -33,8 +34,8 @@ public String getValue() { return value; } - public int getExpiryInMinutes() { - return expiryInMinutes; + public Date getPubKeyExpiryDate() { + return pubKeyExpiryDate; } @Override @@ -46,13 +47,13 @@ public boolean equals(Object o) { return false; } CacheableObject that = (CacheableObject) o; - return expiryInMinutes == that.expiryInMinutes && Objects.equals(key, that.key) - && Objects.equals(value, that.value); + return Objects.equals(key, that.key) && Objects.equals(value, that.value) + && Objects.equals(pubKeyExpiryDate, that.pubKeyExpiryDate); } @Override public int hashCode() { - return Objects.hash(key, value, expiryInMinutes); + return Objects.hash(key, value, pubKeyExpiryDate); } @Override @@ -60,7 +61,7 @@ public String toString() { return "CacheableObject{" + "key='" + key + '\'' + ", value='" + value + '\'' + - ", expiryInMinutes=" + expiryInMinutes + + ", pubKeyExpiryDate=" + pubKeyExpiryDate + '}'; } } From a2f77f606da464ccf10dbc1eebdf5de44ba8abd4 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Fri, 2 Dec 2022 00:20:43 -0800 Subject: [PATCH 12/16] refactoring event verifier and its test --- .../event/webhook/service/EventVerifier.java | 91 ++++++++----------- .../webhook/service/EventVerifierTest.java | 17 +++- 2 files changed, 51 insertions(+), 57 deletions(-) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java index 872031eb..60f56ae1 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -20,18 +20,16 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import java.security.InvalidKeyException; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; import java.security.Signature; -import java.security.SignatureException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.Map; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.validator.routines.UrlValidator; -import org.h2.util.StringUtils; +import java.util.Base64; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -41,19 +39,24 @@ public class EventVerifier { private static Logger logger = LoggerFactory.getLogger(EventVerifier.class); - public static final String ADOBE_IOEVENTS_SECURITY_DOMAIN = "https://static.adobeioevents.com/"; + public static final String ADOBE_IOEVENTS_SECURITY_DOMAIN = "https://static.adobeioevents.com"; public static final String ADOBE_IOEVENTS_DIGI_SIGN_1 = "x-adobe-digital-signature-1"; public static final String ADOBE_IOEVENTS_DIGI_SIGN_2 = "x-adobe-digital-signature-2"; public static final String ADOBE_IOEVENTS_PUB_KEY_1_PATH = "x-adobe-public-key1-path"; public static final String ADOBE_IOEVENTS_PUB_KEY_2_PATH = "x-adobe-public-key2-path"; - private static final int CACHE_EXPIRY_IN_MINUTES = 1440; // expiry of 24 hrs + //private static final int CACHE_EXPIRY_IN_MINUTES = 1440; // expiry of 24 hrs + private final FeignPubKeyService pubKeyService; - private final CacheServiceImpl pubKeyCache; - private FeignPubKeyService pubKeyService; + private CacheServiceImpl pubKeyCache; + + EventVerifier(String url) { + this.pubKeyService = new FeignPubKeyService(url); + + } public EventVerifier() { - this.pubKeyCache = cacheBuilder().buildWithExpiry(CACHE_EXPIRY_IN_MINUTES); - this.pubKeyService = new FeignPubKeyService(ADOBE_IOEVENTS_SECURITY_DOMAIN); + this(ADOBE_IOEVENTS_SECURITY_DOMAIN); + this.pubKeyCache = cacheBuilder().buildCache(); } /** @@ -65,9 +68,8 @@ public EventVerifier() { * @return boolean - TRUE if valid event else FALSE * @throws Exception */ - public boolean authenticateEvent(String message, String clientId, - Map headers) { - if(!isValidTargetRecipient(message, clientId)) { + public boolean authenticateEvent(String message, String clientId, Map headers) { + if (!isValidTargetRecipient(message, clientId)) { logger.error("target recipient {} is not valid for message {}", clientId, message); return false; } @@ -78,56 +80,46 @@ public boolean authenticateEvent(String message, String clientId, return true; } - private boolean verifyEventSignatures(String message, - Map headers) { + private boolean verifyEventSignatures(String message, Map headers) { String[] digitalSignatures = {headers.get(ADOBE_IOEVENTS_DIGI_SIGN_1), headers.get(ADOBE_IOEVENTS_DIGI_SIGN_2)}; String[] pubKeyPaths = {headers.get(ADOBE_IOEVENTS_PUB_KEY_1_PATH), headers.get(ADOBE_IOEVENTS_PUB_KEY_2_PATH)}; - String publicKey1Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + headers.get(ADOBE_IOEVENTS_PUB_KEY_1_PATH); - String publicKey2Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + headers.get(ADOBE_IOEVENTS_PUB_KEY_2_PATH); - - try { - if (isValidUrl(publicKey1Url) && isValidUrl(publicKey2Url)) { - return verifySignature(message, pubKeyPaths, digitalSignatures); - } - } catch (Exception e) { - throw new AIOException("Error verifying signature for public keys " + publicKey1Url + - " & " + publicKey2Url + ". Reason -> " + e.getMessage()); - } - return false; + return verifySignature(message, pubKeyPaths, digitalSignatures); } - private boolean verifySignature(String message, String[] publicKeyPaths, String[] signatures) - throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException { + private boolean verifySignature(String message, String[] publicKeyPaths, String[] signatures) { byte[] data = message.getBytes(UTF_8); for (int i = 0; i < signatures.length; i++) { - // signature generated at I/O Events side is Base64 encoded, so it must be decoded - byte[] sign = Base64.decodeBase64(signatures[i]); - Signature sig = Signature.getInstance("SHA256withRSA"); - sig.initVerify(getPublic(fetchPemEncodedPublicKey(publicKeyPaths[i]))); - sig.update(data); - boolean result = sig.verify(sign); - if (result) { - return true; + try { + // signature generated at I/O Events side is Base64 encoded, so it must be decoded + byte[] sign = Base64.getDecoder().decode(signatures[i]); + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(getPublic(fetchPemEncodedPublicKey(publicKeyPaths[i]))); + sig.update(data); + boolean isSignValid = sig.verify(sign); + if (isSignValid) { + return true; + } + } catch (GeneralSecurityException e) { + throw new AIOException("Error verifying signature for public key " + publicKeyPaths[i] + +". Reason -> " + e.getMessage(), e); } } return false; } private boolean isValidTargetRecipient(String message, String clientId) { - ObjectMapper mapper = new ObjectMapper(); try { + ObjectMapper mapper = new ObjectMapper(); JsonNode jsonPayload = mapper.readTree(message); JsonNode recipientClientIdNode = jsonPayload.get("recipient_client_id"); - if (recipientClientIdNode != null) { - return recipientClientIdNode.textValue().equals(clientId); - } + return (recipientClientIdNode != null && recipientClientIdNode.textValue() !=null + && recipientClientIdNode.textValue().equals(clientId)); } catch (JsonProcessingException e) { throw new AIOException("error parsing the event payload during target recipient check.."); } - return false; } private PublicKey getPublic(String pubKey) @@ -137,10 +129,9 @@ private PublicKey getPublic(String pubKey) .replaceAll(System.lineSeparator(), "") .replace("-----END PUBLIC KEY-----", ""); - byte[] encoded = Base64.decodeBase64(publicKeyPEM); - + byte[] keyBytes = Base64.getDecoder().decode(publicKeyPEM); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); return keyFactory.generatePublic(keySpec); } @@ -151,7 +142,7 @@ String fetchPemEncodedPublicKey(String publicKeyPath) { private String fetchKeyFromCacheOrApi(String pubKeyPath) { String pubKeyFileName = getPublicKeyFileName(pubKeyPath); String pubKey = getKeyFromCache(pubKeyFileName); - if (StringUtils.isNullOrEmpty(pubKey)) { + if (StringUtils.isEmpty(pubKey)) { pubKey = fetchKeyFromApiAndPutInCache(pubKeyPath, pubKeyFileName); } return pubKey; @@ -162,7 +153,7 @@ private String fetchKeyFromApiAndPutInCache(String pubKeyPath, String pubKeyFile logger.warn("public key {} not present in cache, fetching directly from the cdn url {}", pubKeyFileName, ADOBE_IOEVENTS_SECURITY_DOMAIN + pubKeyPath); String pubKeyFetchResponse = pubKeyService.getPubKeyFromCDN(pubKeyPath); - if (!StringUtils.isNullOrEmpty(pubKeyFetchResponse)) { + if (!StringUtils.isEmpty(pubKeyFetchResponse)) { pubKeyCache.put(pubKeyFileName, pubKeyFetchResponse); } return pubKeyFetchResponse; @@ -190,8 +181,4 @@ private String getKeyFromCache(String pubKeyFileNameAsKey) { private String getPublicKeyFileName(String pubKeyPath) { return pubKeyPath.substring(pubKeyPath.lastIndexOf('/') + 1); } - - private boolean isValidUrl(String url) { - return new UrlValidator().isValid(url); - } } diff --git a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java index ceb88fb7..a474dcf8 100644 --- a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java +++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java @@ -9,6 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + package com.adobe.aio.event.webhook.service; import static com.adobe.aio.event.webhook.service.EventVerifier.ADOBE_IOEVENTS_DIGI_SIGN_1; @@ -29,10 +30,12 @@ public class EventVerifierTest { private static final String TEST_CLIENT_ID = "client_id1"; private static final String INVALID_TEST_CLIENT_ID = "invalid_client_id"; - private static final String TEST_DIGI_SIGN_1 = "mvEj6rEiJK9QFtX5QNl1bc9AJW7sOFAHgUk25ytroi8yqrRAPtijCu19vS6o96eANnjHQzS76p+sA1Y4giQ+DnQOZz35AJwvnDmK2FVkbaiupinCjRK4pnwd0aY88HI6o8Fmj6zomAKcF46D8IHglirCjiUHT3gENFHnuro55rgnJ3eWrT4ldcruKHSKAopapAhqQBr1BhrHVqtoz8Zg+ZWGsSgY+tAje3m59mTjWPSC1/KfjgpjhADDKHz+I1eT/z5k0667hlLSgYvHGhmhh8aAcLKgM5tzcXWpYQz4xCZgSF/MqAIUkmnVVWHhs18rr1WSaJ4j2ZTO6vaj8XoHng=="; + private static final String TEST_DIGI_SIGN_1 = "pZY22OGm8/6H6bJXSi+/4VztsPN+fPZtHgHrrASuTw7LTUZVpbAZNaXVTzQsFd47PvaI8aQxbl874GFmH0QfAVQaRT93x5O/kQdM1ymG03303QaFY/mjm/Iot3VEwq5xOtM8f5a2mKUce9bgEv28iN7z9H/MbBOSmukPSJh/vMLkFAmMZQwdP4SRK3ckxQg6wWTbeMRxjw8/FLckznCGPZri4c0O7WPr8wnrWcvArlhBpIPJPeifJOyDj/woFQzoeemdrVoBFOieE/j3RoMWzcQeLENaSrqk00MPL2svNQcTLMkmWuICOjYSbnlv/EPFCQS8bQsnVHxGFD1yDeFa7Q=="; private static final String TEST_DIGI_SIGN_2 = "GpMONiPMHY51vpHF3R9SSs9ogRn8i2or/bvV3R+PYXGgDzAxDhRdl9dIUp/qQ3vsxDGEv045IV4GQ2f4QbsFvWLJsBNyCqLs6KL8LsRoGfEC4Top6c1VVjrEEQ1MOoFcoq/6riXzg4h09lRTfARllVv+icgzAiuv/JW2HNg5yQ4bqenFELD6ipCStuaI/OGS0A9s0Hc6o3aoHz3r5d5DecwE6pUdpG8ODhKBM+34CvcvMDNdrj8STYWHsEUqGdR9klpaqaC1QRYFIO7WgbgdwsuGULz6Sjm+q5s5Wh++fz5E+gXkizFviD389gDIUylFTig/1h7WTLRDuSz69Q+C5w=="; - private static final String TEST_PUB_KEY1_PATH = "qe/keys/pub-key-voy5XEbWmT.pem"; - private static final String TEST_PUB_KEY2_PATH = "qe/keys/pub-key-maAv3Tg6ZH.pem"; + + private static final String TEST_INVALID_DIGI_SIGN_1 = "abc22OGm8/6H6bJXSi+/4VztsPN+fPZtHgHrrASuTw7LTUZVpbAZNaXVTzQsFd47PvaI8aQxbl874GFmH0QfAVQaRT93x5O/kQdM1ymG03303QaFY/mjm/Iot3VEwq5xOtM8f5a2mKUce9bgEv28iN7z9H/MbBOSmukPSJh/vMLkFAmMZQwdP4SRK3ckxQg6wWTbeMRxjw8/FLckznCGPZri4c0O7WPr8wnrWcvArlhBpIPJPeifJOyDj/woFQzoeemdrVoBFOieE/j3RoMWzcQeLENaSrqk00MPL2svNQcTLMkmWuICOjYSbnlv/EPFCQS8bQsnVHxGFD1yDeFa7Q=="; + private static final String TEST_PUB_KEY1_PATH = "/stage/keys/pub-key-3fVj0Lv0QB.pem"; + private static final String TEST_PUB_KEY2_PATH = "/stage/keys/pub-key-sKNHrkauqi.pem"; private EventVerifier underTest; @@ -45,6 +48,10 @@ public void setup() { public void testVerifyValidSignature() { String message = getTestMessage(); Map headers = getTestHeadersWithValidSignature(); + + //String validPubKey1Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + TEST_PUB_KEY1_PATH; + + //when(pubKeyService.getPubKeyFromCDN(any())).thenReturn("test-public-key"); boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); Assert.assertEquals(true, result); } @@ -93,8 +100,8 @@ private Map getTestHeadersWithValidSignature() { private Map getTestHeadersWithInvalidSignature() { Map signHeaders = getTestSignatureHeaders(); - signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_1, TEST_DIGI_SIGN_2); - signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_2, TEST_DIGI_SIGN_1); + signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_1, TEST_INVALID_DIGI_SIGN_1); + signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_2, TEST_INVALID_DIGI_SIGN_1); return signHeaders; } From 5d1859dcb3908511ee6abb2e4f00f1b453b5b53c Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Fri, 2 Dec 2022 00:21:23 -0800 Subject: [PATCH 13/16] updating main pom with required dependencies, removing caffiene, retrofit and unwanted dependecies --- pom.xml | 54 +++++++++--------------------------------------------- 1 file changed, 9 insertions(+), 45 deletions(-) diff --git a/pom.xml b/pom.xml index ccac483b..d77586f7 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ 2.12.3 0.11.2 1.2.0 + 5.3.22 1.0.9 @@ -119,31 +120,6 @@ - - com.squareup.retrofit2 - converter-scalars - ${converter-scalars.version} - - - - com.squareup.retrofit2 - converter-jackson - ${converter-jackson.version} - - - - com.squareup.retrofit2 - retrofit - ${retrofit.version} - - - - com.github.ben-manes.caffeine - caffeine - ${caffeine.version} - compile - - org.apache.commons commons-text @@ -182,12 +158,6 @@ provided - - com.h2database - h2 - ${h2.version} - - io.jsonwebtoken jjwt-api @@ -242,12 +212,15 @@ feign-form ${feign-form.version} - - junit - junit - ${junit.version} - test + org.springframework + spring-context + ${spring-context.version} + + + com.google.code.findbugs + jsr305 + 3.0.2 @@ -257,13 +230,6 @@ test - - org.springframework.boot - spring-boot-test - ${spring-boot-test.version} - test - - com.github.tomakehurst wiremock-jre8 @@ -292,8 +258,6 @@ test - - From 250cc7f241702a820b6d144ec78303f12983b46d Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Fri, 2 Dec 2022 00:42:59 -0800 Subject: [PATCH 14/16] removing commented sentences --- .../adobe/aio/event/webhook/service/EventVerifierTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java index a474dcf8..615d2548 100644 --- a/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java +++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java @@ -32,7 +32,6 @@ public class EventVerifierTest { private static final String INVALID_TEST_CLIENT_ID = "invalid_client_id"; private static final String TEST_DIGI_SIGN_1 = "pZY22OGm8/6H6bJXSi+/4VztsPN+fPZtHgHrrASuTw7LTUZVpbAZNaXVTzQsFd47PvaI8aQxbl874GFmH0QfAVQaRT93x5O/kQdM1ymG03303QaFY/mjm/Iot3VEwq5xOtM8f5a2mKUce9bgEv28iN7z9H/MbBOSmukPSJh/vMLkFAmMZQwdP4SRK3ckxQg6wWTbeMRxjw8/FLckznCGPZri4c0O7WPr8wnrWcvArlhBpIPJPeifJOyDj/woFQzoeemdrVoBFOieE/j3RoMWzcQeLENaSrqk00MPL2svNQcTLMkmWuICOjYSbnlv/EPFCQS8bQsnVHxGFD1yDeFa7Q=="; private static final String TEST_DIGI_SIGN_2 = "GpMONiPMHY51vpHF3R9SSs9ogRn8i2or/bvV3R+PYXGgDzAxDhRdl9dIUp/qQ3vsxDGEv045IV4GQ2f4QbsFvWLJsBNyCqLs6KL8LsRoGfEC4Top6c1VVjrEEQ1MOoFcoq/6riXzg4h09lRTfARllVv+icgzAiuv/JW2HNg5yQ4bqenFELD6ipCStuaI/OGS0A9s0Hc6o3aoHz3r5d5DecwE6pUdpG8ODhKBM+34CvcvMDNdrj8STYWHsEUqGdR9klpaqaC1QRYFIO7WgbgdwsuGULz6Sjm+q5s5Wh++fz5E+gXkizFviD389gDIUylFTig/1h7WTLRDuSz69Q+C5w=="; - private static final String TEST_INVALID_DIGI_SIGN_1 = "abc22OGm8/6H6bJXSi+/4VztsPN+fPZtHgHrrASuTw7LTUZVpbAZNaXVTzQsFd47PvaI8aQxbl874GFmH0QfAVQaRT93x5O/kQdM1ymG03303QaFY/mjm/Iot3VEwq5xOtM8f5a2mKUce9bgEv28iN7z9H/MbBOSmukPSJh/vMLkFAmMZQwdP4SRK3ckxQg6wWTbeMRxjw8/FLckznCGPZri4c0O7WPr8wnrWcvArlhBpIPJPeifJOyDj/woFQzoeemdrVoBFOieE/j3RoMWzcQeLENaSrqk00MPL2svNQcTLMkmWuICOjYSbnlv/EPFCQS8bQsnVHxGFD1yDeFa7Q=="; private static final String TEST_PUB_KEY1_PATH = "/stage/keys/pub-key-3fVj0Lv0QB.pem"; private static final String TEST_PUB_KEY2_PATH = "/stage/keys/pub-key-sKNHrkauqi.pem"; @@ -48,10 +47,6 @@ public void setup() { public void testVerifyValidSignature() { String message = getTestMessage(); Map headers = getTestHeadersWithValidSignature(); - - //String validPubKey1Url = ADOBE_IOEVENTS_SECURITY_DOMAIN + TEST_PUB_KEY1_PATH; - - //when(pubKeyService.getPubKeyFromCDN(any())).thenReturn("test-public-key"); boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers); Assert.assertEquals(true, result); } From df90b84d055737e96e2663e16cb547d760010f45 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Mon, 5 Dec 2022 15:00:06 -0800 Subject: [PATCH 15/16] removing spring context dependency --- events_webhook/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml index a25b6a25..6e74ab80 100644 --- a/events_webhook/pom.xml +++ b/events_webhook/pom.xml @@ -44,10 +44,6 @@ ${project.version} - - org.springframework - spring-context - com.google.code.findbugs jsr305 From 2bdc23efda3b1f5c7b3b78b37dbc1582cee519b4 Mon Sep 17 00:00:00 2001 From: abhupadh <> Date: Mon, 5 Dec 2022 15:00:46 -0800 Subject: [PATCH 16/16] removing Service annotation, minor refactoring --- .../com/adobe/aio/event/webhook/service/EventVerifier.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java index 60f56ae1..67fb2993 100644 --- a/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java +++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java @@ -27,14 +27,12 @@ import java.security.Signature; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; -import java.util.Map; import java.util.Base64; +import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -@Service public class EventVerifier { private static Logger logger = LoggerFactory.getLogger(EventVerifier.class); @@ -44,7 +42,7 @@ public class EventVerifier { public static final String ADOBE_IOEVENTS_DIGI_SIGN_2 = "x-adobe-digital-signature-2"; public static final String ADOBE_IOEVENTS_PUB_KEY_1_PATH = "x-adobe-public-key1-path"; public static final String ADOBE_IOEVENTS_PUB_KEY_2_PATH = "x-adobe-public-key2-path"; - //private static final int CACHE_EXPIRY_IN_MINUTES = 1440; // expiry of 24 hrs + private final FeignPubKeyService pubKeyService; private CacheServiceImpl pubKeyCache; @@ -66,7 +64,6 @@ public EventVerifier() { * @param clientId - recipient client id in the payload * @param headers - webhook request headers * @return boolean - TRUE if valid event else FALSE - * @throws Exception */ public boolean authenticateEvent(String message, String clientId, Map headers) { if (!isValidTargetRecipient(message, clientId)) {