diff --git a/framework/entity/ServiceEntities.xml b/framework/entity/ServiceEntities.xml index 81696e90f..7b51a3ab3 100644 --- a/framework/entity/ServiceEntities.xml +++ b/framework/entity/ServiceEntities.xml @@ -383,6 +383,7 @@ along with this software (see the LICENSE.md file). If not, see + diff --git a/framework/src/main/groovy/org/moqui/impl/context/WebFacadeImpl.groovy b/framework/src/main/groovy/org/moqui/impl/context/WebFacadeImpl.groovy index 8fdf52158..04f065734 100644 --- a/framework/src/main/groovy/org/moqui/impl/context/WebFacadeImpl.groovy +++ b/framework/src/main/groovy/org/moqui/impl/context/WebFacadeImpl.groovy @@ -49,6 +49,8 @@ import javax.servlet.ServletContext import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpSession +import java.nio.charset.StandardCharsets +import java.sql.Timestamp /** This class is a facade to easily get information from and about the web context. */ @CompileStatic @@ -1210,6 +1212,66 @@ class WebFacadeImpl implements WebFacade { return } + // login anonymous if not logged in + eci.userFacade.loginAnonymousIfNoUser() + } else if ("SmatHmacSha256Timestamp".equals(messageAuthEnumId)) { + // validate HMAC value from authHeaderName HTTP header using sharedSecret and messageText + String authHeaderName = (String) systemMessageRemote.authHeaderName + String sharedSecret = (String) systemMessageRemote.sharedSecret + + String headerValue = request.getHeader(authHeaderName) + if (!headerValue) { + logger.warn("System message receive HMAC verify no header ${authHeaderName} value found, for remote ${systemMessageRemoteId}") + response.sendError(HttpServletResponse.SC_FORBIDDEN, "No HMAC header ${authHeaderName} found for remote system ${systemMessageRemoteId}") + return + } + + // This assumes a header format like + // Example-Signature-Header: + //t=1492774577, + //v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd + // We’ve added newlines for clarity, but a realExample-Signature-Header is on a single line. + String timestamp = null; + String incomingSignature = null; + String[] headerValueList = headerValue.split(",") // split on comma + for (String headerValueItem : headerValueList) { + String key = headerValueItem.split("=")[0].trim() + if ("t".equals(key)) + timestamp = headerValueItem.split("=")[1].trim() + else if ("v1".equals(key)) + incomingSignature = headerValueItem.split("=")[1].trim() + } + + // This also assumes that the signature is generated from the following concatenated strings: + // Timestamp in the header + // The character . + // The text body of the request + String signatureTextToVerify = timestamp + "." + messageText + + Mac hmac = Mac.getInstance("HmacSHA256") + hmac.init(new SecretKeySpec(sharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")) + // NOTE: if this fails try with "ISO-8859-1" + byte[] hash = hmac.doFinal(signatureTextToVerify.getBytes(StandardCharsets.UTF_8)); + String signature = "" + for (byte b : hash) { + // Came from https://github.com/stripe/stripe-java/blob/3686feb8f2067878b7bb4619f931580a3d31bf4f/src/main/java/com/stripe/net/Webhook.java#L187 + signature += Integer.toString((b & 0xff) + 0x100, 16).substring(1); + } + + if (incomingSignature != signature) { + logger.warn("System message receive HMAC verify header value ${incomingSignature} calculated ${signature} did not match for remote ${systemMessageRemoteId}") + response.sendError(HttpServletResponse.SC_FORBIDDEN, "HMAC verify failed for remote system ${systemMessageRemoteId}") + return + } + + Timestamp timestampTimestamp = new Timestamp(Long.parseLong(timestamp) * 1000) + // If timestamp was not sent in past 5 minutes, reject message (5 minutes = 300000 milliseconds = 5*60*1000) + if (!timestampTimestamp.before(eci.user.nowTimestamp) || !timestampTimestamp.after(new Timestamp(eci.user.nowTimestamp.getTime() - 300000))) { + logger.warn("System message receive HMAC invalid timestamp ${timestamp}") + response.sendError(HttpServletResponse.SC_FORBIDDEN, "HMAC timestamp verification failed") + return + } + // login anonymous if not logged in eci.userFacade.loginAnonymousIfNoUser() } else if (!"SmatNone".equals(messageAuthEnumId)) {