diff --git a/core/pom.xml b/core/pom.xml
index 13401e2d..f65f31de 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
@@ -73,5 +76,4 @@
-
diff --git a/events_webhook/pom.xml b/events_webhook/pom.xml
new file mode 100644
index 00000000..6e74ab80
--- /dev/null
+++ b/events_webhook/pom.xml
@@ -0,0 +1,63 @@
+
+
+
+
+ com.adobe.aio
+ aio-lib-java
+ 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
+
+
+
+ com.adobe.aio
+ aio-lib-java-core
+ ${project.version}
+
+
+
+ com.adobe.aio
+ aio-lib-java-ims
+ ${project.version}
+
+
+
+ com.google.code.findbugs
+ jsr305
+
+
+
+
+ org.mockito
+ mockito-core
+
+
+ junit
+ junit
+
+
+
+
\ No newline at end of file
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..4750a12c
--- /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..161eddf3
--- /dev/null
+++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/cache/CacheServiceImpl.java
@@ -0,0 +1,88 @@
+/*
+ * 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);
+ private static final int DEFAULT_TTL_IN_MINUTES = 1440;
+ private static final Date CURRENT_SYSTEM_DATE = new Date();
+
+ Map cacheMap;
+
+ @Nullable
+ @Override
+ public String get(@Nonnull String key) {
+ CacheableObject obj = (CacheableObject) cacheMap.get(key);
+ if (obj != null) {
+ if (isExpired(obj)) {
+ logger.debug("public key object in 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) {
+ 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 CURRENT_SYSTEM_DATE.after(cacheableObject.getPubKeyExpiryDate());
+ }
+
+ private CacheServiceImpl initialiseCacheMap() {
+ this.cacheMap = new HashMap<>();
+ return this;
+ }
+
+ public static CacheBuilder cacheBuilder() {
+ return new CacheBuilder();
+ }
+
+ public static class CacheBuilder {
+ public CacheServiceImpl buildCache() {
+ return new CacheServiceImpl()
+ .initialiseCacheMap();
+ }
+ }
+
+ 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..839cfd9e
--- /dev/null
+++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/model/CacheableObject.java
@@ -0,0 +1,67 @@
+/*
+ * 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.Date;
+import java.util.Objects;
+
+public class CacheableObject {
+
+ private String key;
+ private String value;
+ private Date pubKeyExpiryDate;
+
+ public CacheableObject(String key, String value, Date expiryInMinutes) {
+ this.key = key;
+ this.value = value;
+ this.pubKeyExpiryDate = expiryInMinutes;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public Date getPubKeyExpiryDate() {
+ return pubKeyExpiryDate;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CacheableObject that = (CacheableObject) o;
+ 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, pubKeyExpiryDate);
+ }
+
+ @Override
+ public String toString() {
+ return "CacheableObject{" +
+ "key='" + key + '\'' +
+ ", value='" + value + '\'' +
+ ", pubKeyExpiryDate=" + pubKeyExpiryDate +
+ '}';
+ }
+}
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..67fb2993
--- /dev/null
+++ b/events_webhook/src/main/java/com/adobe/aio/event/webhook/service/EventVerifier.java
@@ -0,0 +1,181 @@
+/*
+ * 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.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 java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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_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 FeignPubKeyService pubKeyService;
+
+ private CacheServiceImpl pubKeyCache;
+
+ EventVerifier(String url) {
+ this.pubKeyService = new FeignPubKeyService(url);
+
+ }
+
+ public EventVerifier() {
+ this(ADOBE_IOEVENTS_SECURITY_DOMAIN);
+ this.pubKeyCache = cacheBuilder().buildCache();
+ }
+
+ /**
+ * 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
+ */
+ 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;
+ }
+ 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)};
+ return verifySignature(message, pubKeyPaths, digitalSignatures);
+ }
+
+ private boolean verifySignature(String message, String[] publicKeyPaths, String[] signatures) {
+ byte[] data = message.getBytes(UTF_8);
+
+ for (int i = 0; i < signatures.length; i++) {
+ 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) {
+ try {
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode jsonPayload = mapper.readTree(message);
+ JsonNode recipientClientIdNode = jsonPayload.get("recipient_client_id");
+ 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..");
+ }
+ }
+
+ private PublicKey getPublic(String pubKey)
+ throws NoSuchAlgorithmException, InvalidKeySpecException {
+ String publicKeyPEM = pubKey
+ .replace("-----BEGIN PUBLIC KEY-----", "")
+ .replaceAll(System.lineSeparator(), "")
+ .replace("-----END PUBLIC KEY-----", "");
+
+ byte[] keyBytes = Base64.getDecoder().decode(publicKeyPEM);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
+ return keyFactory.generatePublic(keySpec);
+ }
+
+ String fetchPemEncodedPublicKey(String publicKeyPath) {
+ return fetchKeyFromCacheOrApi(publicKeyPath);
+ }
+
+ private String fetchKeyFromCacheOrApi(String pubKeyPath) {
+ String pubKeyFileName = getPublicKeyFileName(pubKeyPath);
+ String pubKey = getKeyFromCache(pubKeyFileName);
+ if (StringUtils.isEmpty(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 pubKeyFetchResponse = pubKeyService.getPubKeyFromCDN(pubKeyPath);
+ if (!StringUtils.isEmpty(pubKeyFetchResponse)) {
+ pubKeyCache.put(pubKeyFileName, pubKeyFetchResponse);
+ }
+ return pubKeyFetchResponse;
+ } 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) {
+ 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 null;
+ }
+
+ /**
+ * 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
+ */
+ private String getPublicKeyFileName(String pubKeyPath) {
+ return pubKeyPath.substring(pubKeyPath.lastIndexOf('/') + 1);
+ }
+}
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);
+}
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..615d2548
--- /dev/null
+++ b/events_webhook/src/test/java/com/adobe/aio/event/webhook/service/EventVerifierTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@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 = "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";
+
+ private EventVerifier underTest;
+
+ @Before
+ public void setup() {
+ underTest = new EventVerifier();
+ }
+
+ @Test
+ public void testVerifyValidSignature() {
+ String message = getTestMessage();
+ Map headers = getTestHeadersWithValidSignature();
+ boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers);
+ Assert.assertEquals(true, result);
+ }
+
+ @Test
+ public void testVerifyInvalidSignature() {
+ String message = getTestMessage();
+ Map headers = getTestHeadersWithInvalidSignature();
+ boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers);
+ Assert.assertEquals(Boolean.FALSE, result);
+ }
+
+ @Test
+ public void testVerifyInvalidPublicKey() {
+ String message = getTestMessage();
+ Map headers = getTestHeadersWithInvalidPubKey();
+ boolean result = underTest.authenticateEvent(message, TEST_CLIENT_ID, headers);
+ Assert.assertEquals(Boolean.FALSE, result);
+ }
+
+ @Test
+ public void testVerifyInvalidRecipientClient() {
+ 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_INVALID_DIGI_SIGN_1);
+ signHeaders.put(ADOBE_IOEVENTS_DIGI_SIGN_2, TEST_INVALID_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 01bda0f2..d77586f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,10 +28,9 @@
events_mgmt
events_ingress
events_journal
-
+ events_webhook
events_test
events_xdm
-
aem
@@ -108,6 +107,7 @@
2.12.3
0.11.2
1.2.0
+ 5.3.22
1.0.9
@@ -212,6 +212,16 @@
feign-form
${feign-form.version}
+
+ org.springframework
+ spring-context
+ ${spring-context.version}
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
junit