Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add and Handle Hmac Sha256 with timestamp #617

Merged
merged 1 commit into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions framework/entity/ServiceEntities.xml
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ along with this software (see the LICENSE.md file). If not, see
<moqui.basic.Enumeration enumId="SmatNone" description="No Auth" enumTypeId="SystemMessageAuthType"/>
<moqui.basic.Enumeration enumId="SmatLogin" description="User Login (basic, api_key, etc)" enumTypeId="SystemMessageAuthType"/>
<moqui.basic.Enumeration enumId="SmatHmacSha256" description="HMAC SHA-256" enumTypeId="SystemMessageAuthType"/>
<moqui.basic.Enumeration enumId="SmatHmacSha256Timestamp" description="HMAC SHA-256 with Timestamp" enumTypeId="SystemMessageAuthType"/>
</seed-data>
</entity>
<entity entity-name="SystemMessageEnumMap" package="moqui.service.message" use="configuration">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand Down
Loading