Skip to content

Commit b4a3f4d

Browse files
make it generic
1 parent 79db643 commit b4a3f4d

2 files changed

Lines changed: 81 additions & 28 deletions

File tree

readme.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ An [Otoroshi](https://github.com/MAIF/otoroshi) plugin that validates [YouSign](
44

55
## How it works
66

7-
Every webhook sent by YouSign includes an `X-Yousign-Signature-256` header whose value is an HMAC-SHA256 hash of the raw request body, prefixed with `sha256=`.
7+
The plugin is provider-agnostic: the signature header, HMAC algorithm and prefix are all configurable. Out of the box it is pre-configured for YouSign, whose webhooks include an `X-Yousign-Signature-256` header containing an HMAC-SHA256 hash of the raw request body prefixed with `sha256=`.
88

99
The plugin:
1010

1111
1. Reads the raw request body.
12-
2. Computes `HMAC-SHA256(secret, rawBody)` using the secret configured in the plugin.
13-
3. Compares the result (constant-time, to prevent timing attacks) against the `X-Yousign-Signature-256` header.
14-
4. Forwards the request to your backend unchanged when the signature is valid.
15-
5. Returns **401 Unauthorized** when the signature is missing or invalid.
12+
2. Computes `HMAC-<algorithm>(secret, rawBody)` using the configured secret and algorithm.
13+
3. Prepends the configured prefix to the hex-encoded hash to form the expected signature.
14+
4. Compares the result (constant-time, to prevent timing attacks) against the configured signature header.
15+
5. Forwards the request to your backend unchanged when the signature is valid.
16+
6. Returns **401 Unauthorized** when the signature is missing or invalid.
1617

1718
## Create a route to receive YouSign webhooks
1819

@@ -37,7 +38,10 @@ $ curl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \
3738
"enabled": true,
3839
"plugin": "cp:otoroshi_plugins.com.cloud.apim.otoroshi.plugins.yousign.YouSignWebhookValidator",
3940
"config": {
40-
"secret": "your-yousign-webhook-secret"
41+
"secret": "your-yousign-webhook-secret",
42+
"signature_header": "X-Yousign-Signature-256",
43+
"algorithm": "HmacSHA256",
44+
"prefix": "sha256="
4145
}
4246
}
4347
]
@@ -46,16 +50,31 @@ $ curl -X POST 'http://otoroshi-api.oto.tools:8080/api/routes' \
4650

4751
## Plugin configuration
4852

49-
| Field | Type | Required | Description |
50-
|----------|----------|----------|---------------------------------------------------------------------------------------------|
51-
| `secret` | `string` | yes | The webhook secret copied from your YouSign App → Webhook subscription settings page. |
53+
| Field | Type | Required | Default | Description |
54+
|--------------------|----------|----------|----------------------------|--------------------------------------------------------------------------------------|
55+
| `secret` | `string` | yes || The HMAC secret shared with the webhook provider (e.g. YouSign webhook secret). |
56+
| `signature_header` | `string` | no | `X-Yousign-Signature-256` | Name of the HTTP header that carries the signature. |
57+
| `algorithm` | `string` | no | `HmacSHA256` | Java HMAC algorithm name. Supported values: `HmacSHA256`, `HmacSHA512`, `HmacSHA384`, `HmacSHA1`. |
58+
| `prefix` | `string` | no | derived from `algorithm` | String prepended to the hex hash before comparison (e.g. `sha256=`). Defaults are derived automatically from the chosen algorithm. |
5259

5360
```json
5461
{
55-
"secret": "your-yousign-webhook-secret"
62+
"secret": "your-webhook-secret",
63+
"signature_header": "X-Yousign-Signature-256",
64+
"algorithm": "HmacSHA256",
65+
"prefix": "sha256="
5666
}
5767
```
5868

69+
### Algorithm / prefix defaults
70+
71+
| `algorithm` | Default `prefix` |
72+
|--------------|-----------------|
73+
| `HmacSHA256` | `sha256=` |
74+
| `HmacSHA512` | `sha512=` |
75+
| `HmacSHA384` | `sha384=` |
76+
| `HmacSHA1` | `sha1=` |
77+
5978
## Responses
6079

6180
| Status | Body | Meaning |

src/main/scala/com/cloud/apim/otoroshi/plugins/yousign/validator.scala

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import scala.concurrent.{ExecutionContext, Future}
1818
import scala.util.{Failure, Success, Try}
1919

2020
case class YouSignWebhookValidatorConfig(
21-
secret: String = "",
21+
secret: String = "",
22+
signatureHeader: String = "X-Yousign-Signature-256",
23+
algorithm: String = "HmacSHA256",
24+
prefix: String = "sha256=",
2225
) extends NgPluginConfig {
2326
def json: JsValue = YouSignWebhookValidatorConfig.format.writes(this)
2427
}
@@ -27,20 +30,51 @@ object YouSignWebhookValidatorConfig {
2730
val default: YouSignWebhookValidatorConfig = YouSignWebhookValidatorConfig()
2831
val format: Format[YouSignWebhookValidatorConfig] = new Format[YouSignWebhookValidatorConfig] {
2932
override def writes(o: YouSignWebhookValidatorConfig): JsValue = Json.obj(
30-
"secret" -> o.secret,
33+
"secret" -> o.secret,
34+
"signature_header" -> o.signatureHeader,
35+
"algorithm" -> o.algorithm,
36+
"prefix" -> o.prefix,
3137
)
3238
override def reads(json: JsValue): JsResult[YouSignWebhookValidatorConfig] = Try {
39+
val algo = json.select("algorithm").asOpt[String].getOrElse("HmacSHA256")
3340
YouSignWebhookValidatorConfig(
34-
secret = json.select("secret").asOpt[String].getOrElse(""),
41+
secret = json.select("secret").asOpt[String].getOrElse(""),
42+
signatureHeader = json.select("signature_header").asOpt[String].getOrElse("X-Yousign-Signature-256"),
43+
algorithm = algo,
44+
prefix = json.select("prefix").asOpt[String].getOrElse(YouSignWebhookValidatorConfig.defaultPrefix(algo)),
3545
)
3646
} match {
3747
case Failure(e) => JsError(e.getMessage)
3848
case Success(e) => JsSuccess(e)
3949
}
4050
}
41-
val configFlow: Seq[String] = Seq("secret")
51+
52+
def defaultPrefix(algorithm: String): String = algorithm.toLowerCase match {
53+
case a if a.contains("sha512") => "sha512="
54+
case a if a.contains("sha384") => "sha384="
55+
case a if a.contains("sha256") => "sha256="
56+
case a if a.contains("sha1") => "sha1="
57+
case a if a.contains("md5") => "md5="
58+
case _ => "sha256="
59+
}
60+
61+
val configFlow: Seq[String] = Seq("secret", "signature_header", "algorithm", "prefix")
4262
val configSchema: Option[JsObject] = Some(Json.obj(
43-
"secret" -> Json.obj("type" -> "password", "label" -> "Webhook Secret"),
63+
"secret" -> Json.obj("type" -> "password", "label" -> "Webhook Secret"),
64+
"signature_header" -> Json.obj("type" -> "string", "label" -> "Signature Header"),
65+
"algorithm" -> Json.obj(
66+
"type" -> "select",
67+
"label" -> "HMAC Algorithm",
68+
"props" -> Json.obj(
69+
"options" -> Json.arr(
70+
Json.obj("label" -> "HMAC-SHA256", "value" -> "HmacSHA256"),
71+
Json.obj("label" -> "HMAC-SHA512", "value" -> "HmacSHA512"),
72+
Json.obj("label" -> "HMAC-SHA384", "value" -> "HmacSHA384"),
73+
Json.obj("label" -> "HMAC-SHA1", "value" -> "HmacSHA1"),
74+
),
75+
),
76+
),
77+
"prefix" -> Json.obj("type" -> "string", "label" -> "Signature Prefix"),
4478
))
4579
}
4680

@@ -54,7 +88,7 @@ class YouSignWebhookValidator extends NgRequestTransformer {
5488
override def multiInstance: Boolean = true
5589
override def core: Boolean = false
5690
override def name: String = "Cloud APIM - YouSign Webhook Validator"
57-
override def description: Option[String] = Some("This plugin validates YouSign webhook payloads by verifying the HMAC SHA-256 signature present in the X-Yousign-Signature-256 header.")
91+
override def description: Option[String] = Some("This plugin validates webhook payloads by verifying an HMAC signature. The header name, algorithm and prefix are all configurable (defaults to YouSign's X-Yousign-Signature-256 / HmacSHA256 / sha256=).")
5892
override def defaultConfigObject: Option[NgPluginConfig] = Some(YouSignWebhookValidatorConfig.default)
5993
override def noJsForm: Boolean = false
6094
override def configFlow: Seq[String] = YouSignWebhookValidatorConfig.configFlow
@@ -71,9 +105,9 @@ class YouSignWebhookValidator extends NgRequestTransformer {
71105
().vfuture
72106
}
73107

74-
private def computeHmacSha256(secret: String, body: ByteString): String = {
75-
val mac = Mac.getInstance("HmacSHA256")
76-
val keySpec = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256")
108+
private def computeHmac(algorithm: String, secret: String, body: ByteString): String = {
109+
val mac = Mac.getInstance(algorithm)
110+
val keySpec = new SecretKeySpec(secret.getBytes("UTF-8"), algorithm)
77111
mac.init(keySpec)
78112
mac.doFinal(body.toArray).map(b => f"${b & 0xff}%02x").mkString
79113
}
@@ -82,22 +116,22 @@ class YouSignWebhookValidator extends NgRequestTransformer {
82116
val config = ctx.cachedConfig(internalName)(YouSignWebhookValidatorConfig.format).getOrElse(YouSignWebhookValidatorConfig.default)
83117

84118
if (config.secret.isEmpty) {
85-
logger.warn("[YouSign Webhook Validator] no secret configured, rejecting request")
119+
logger.warn("[Webhook Validator] no secret configured, rejecting request")
86120
Left(Results.Unauthorized(Json.obj("error" -> "webhook secret not configured"))).vfuture
87121
} else {
88-
ctx.request.headers.get("X-Yousign-Signature-256") match {
122+
ctx.request.headers.get(config.signatureHeader) match {
89123
case None =>
90-
if (logger.isDebugEnabled) logger.debug("[YouSign Webhook Validator] missing X-Yousign-Signature-256 header")
91-
Left(Results.Unauthorized(Json.obj("error" -> "missing X-Yousign-Signature-256 header"))).vfuture
124+
if (logger.isDebugEnabled) logger.debug(s"[Webhook Validator] missing ${config.signatureHeader} header")
125+
Left(Results.Unauthorized(Json.obj("error" -> s"missing ${config.signatureHeader} header"))).vfuture
92126

93127
case Some(receivedSignature) =>
94128
ctx.otoroshiRequest.body.runFold(ByteString.empty)(_ ++ _).map { bodyBytes =>
95-
val computedHash = computeHmacSha256(config.secret, bodyBytes)
96-
val expectedSignature = s"sha256=$computedHash"
129+
val computedHash = computeHmac(config.algorithm, config.secret, bodyBytes)
130+
val expectedSignature = s"${config.prefix}$computedHash"
97131

98132
if (logger.isDebugEnabled) {
99-
logger.debug(s"[YouSign Webhook Validator] expected : $expectedSignature")
100-
logger.debug(s"[YouSign Webhook Validator] received : $receivedSignature")
133+
logger.debug(s"[Webhook Validator] expected : $expectedSignature")
134+
logger.debug(s"[Webhook Validator] received : $receivedSignature")
101135
}
102136

103137
// Constant-time comparison to prevent timing-attack side channels
@@ -108,7 +142,7 @@ class YouSignWebhookValidator extends NgRequestTransformer {
108142
// Re-emit the already-consumed body so downstream plugins / the backend still see it
109143
Right(ctx.otoroshiRequest.copy(body = Source.single(bodyBytes)))
110144
} else {
111-
logger.warn("[YouSign Webhook Validator] invalid webhook signature")
145+
logger.warn(s"[Webhook Validator] invalid signature for header ${config.signatureHeader}")
112146
Left(Results.Unauthorized(Json.obj("error" -> "invalid signature")))
113147
}
114148
}

0 commit comments

Comments
 (0)