feat: implement GitHub webhook HMAC-SHA256 signature verification
Verify X-Hub-Signature-256 header using CryptoUtil.hmacSha256 from onixbyte crypto-toolbox. Signature check is skipped when no secret is configured. Uses MessageDigest.isEqual for constant-time comparison.
This commit is contained in:
@@ -36,6 +36,7 @@ dependencies {
|
|||||||
implementation(libs.onixbyte.identityGenerator)
|
implementation(libs.onixbyte.identityGenerator)
|
||||||
implementation(libs.onixbyte.captcha)
|
implementation(libs.onixbyte.captcha)
|
||||||
implementation(libs.onixbyte.regions)
|
implementation(libs.onixbyte.regions)
|
||||||
|
implementation(libs.onixbyte.cryptoToolbox)
|
||||||
implementation(libs.jwt.core)
|
implementation(libs.jwt.core)
|
||||||
implementation(libs.spring.boot.configurationProcessor)
|
implementation(libs.spring.boot.configurationProcessor)
|
||||||
implementation(libs.spring.boot.actuator)
|
implementation(libs.spring.boot.actuator)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ jackson-jsr310 = { group = "com.fasterxml.jackson.datatype", name = "jackson-dat
|
|||||||
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
onixbyte-versionCatalogue = { group = "com.onixbyte", name = "version-catalogue", version.ref = "onixbyteVersion" }
|
||||||
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
onixbyte-tuple = { group = "com.onixbyte", name = "tuple" }
|
||||||
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
|
onixbyte-commonToolbox = { group = "com.onixbyte", name = "common-toolbox" }
|
||||||
|
onixbyte-cryptoToolbox = { group = "com.onixbyte", name = "crypto-toolbox", version.ref = "onixbyteVersion" }
|
||||||
onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
|
onixbyte-mathToolbox = { group = "com.onixbyte", name = "math-toolbox" }
|
||||||
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
onixbyte-identityGenerator = { group = "com.onixbyte", name = "identity-generator" }
|
||||||
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
onixbyte-captcha = { group = "com.onixbyte", name = "captcha", version.ref = "onixbyteCaptcha" }
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.onixbyte.deltaforceguide.interceptor;
|
||||||
|
|
||||||
|
import com.onixbyte.crypto.util.CryptoUtil;
|
||||||
|
import com.onixbyte.deltaforceguide.exeption.BizException;
|
||||||
|
import com.onixbyte.deltaforceguide.manager.WebhookManager;
|
||||||
|
import com.onixbyte.deltaforceguide.shared.GitHubWebhookHeader;
|
||||||
|
import com.onixbyte.deltaforceguide.wrapper.RepeatedlyReadRequestWrapper;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.jspecify.annotations.NonNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies GitHub webhook requests by validating the {@code X-Hub-Signature-256}
|
||||||
|
* header against the configured secret using HMAC-SHA256.
|
||||||
|
*
|
||||||
|
* <p>Verification is skipped when no secret is configured. The signature format is
|
||||||
|
* {@code sha256=<hex-digest>} as documented by GitHub.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class GitHubWebhookInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GitHubWebhookInterceptor.class);
|
||||||
|
|
||||||
|
private final WebhookManager webhookManager;
|
||||||
|
|
||||||
|
public GitHubWebhookInterceptor(WebhookManager webhookManager) {
|
||||||
|
this.webhookManager = webhookManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean preHandle(
|
||||||
|
@NonNull HttpServletRequest request,
|
||||||
|
@NonNull HttpServletResponse response,
|
||||||
|
@NonNull Object handler
|
||||||
|
) {
|
||||||
|
if (!(request instanceof RepeatedlyReadRequestWrapper req)) {
|
||||||
|
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"Request body is not readable");
|
||||||
|
}
|
||||||
|
|
||||||
|
var secret = webhookManager.github().secret();
|
||||||
|
if (secret == null || secret.isBlank()) {
|
||||||
|
log.debug("No GitHub webhook secret configured, skipping signature verification");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var signatureHeader = req.getHeader(GitHubWebhookHeader.SIGNATURE_256);
|
||||||
|
if (signatureHeader == null || signatureHeader.isBlank()) {
|
||||||
|
log.warn("Missing {} header from ip={}",
|
||||||
|
GitHubWebhookHeader.SIGNATURE_256, request.getRemoteAddr());
|
||||||
|
throw new BizException(HttpStatus.UNAUTHORIZED,
|
||||||
|
"Missing webhook signature header");
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = req.getBodyString();
|
||||||
|
try {
|
||||||
|
var computed = "sha256=" + CryptoUtil.hmacSha256(secret, body);
|
||||||
|
|
||||||
|
if (!MessageDigest.isEqual(
|
||||||
|
computed.getBytes(StandardCharsets.UTF_8),
|
||||||
|
signatureHeader.getBytes(StandardCharsets.UTF_8))) {
|
||||||
|
log.warn("Invalid webhook signature from ip={}", request.getRemoteAddr());
|
||||||
|
throw new BizException(HttpStatus.UNAUTHORIZED, "Invalid webhook signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||||
|
log.error("Failed to compute HMAC-SHA256", e);
|
||||||
|
throw new BizException(HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to verify webhook signature");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user