Provide API to validate HmacSHA256 signatures on incoming payloads
See original GitHub issueDiscussed in https://github.com/orgs/playframework/discussions/11489
<div type='discussions-op-text'>Originally posted by phelps-sg October 16, 2022
I have also discussed this issue on StackOverflow (https://stackoverflow.com/questions/73926931/in-scala-play-framework-is-there-a-simple-way-to-verify-signatures-on-form-requ), and the only answer so far suggests that there is currently no straightforward way to validate Hmac signatures when processing form data using the Play Framework.
I am trying to write a Scala Play Framework action that will verify a HmacSHA256 signature on an incoming POST request containing form-url-encoded data.
This is not straightforward in the Play framework because: i) actions builders only have access to headers, but do not have access to the request body, and ii) in order to calculate the signature we have to treat the request body as Array[ByteString]
, but when we come to process the form data we have to treat it as Map[String, Seq[String]]
, the problem being that Play forces us to choose a single type for our request, and we cannot easily “cast” the request body to a different type.
The only solution I have been able to come up with is to use an ActionRefiner
that returns a WrappedRequest
that embeds a callback to validate the signature. The callback in turn reparses the data using FormUrlEncodedParser.parse(new String(request.body.toArray))
. This approach is illustrated in the code below. However, this is overly convoluted. It would be a lot nicer if the ability to validate hmac signatures on incoming payloads was facilitated directly by the Play Framework API.
package actions
import akka.util.ByteString
import com.google.inject.Inject
import play.api.Logging
import play.api.mvc.Results.Unauthorized
import play.api.mvc._
import play.core.parsers.FormUrlEncodedParser
import services.SlackSignatureVerifierService
import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
class SlackRequest[A](
val validateSignature: String => Try[String],
request: Request[A]
) extends WrappedRequest[A](request)
object SlackSignatureVerifyAction {
implicit class SlackRequestByteStringValidator(
slackRequest: SlackRequest[ByteString]
) {
def validateSignatureAgainstBody(): Try[Map[String, Seq[String]]] = {
val raw = slackRequest.body.utf8String
slackRequest.validateSignature(raw) map { _ =>
FormUrlEncodedParser.parse(new String(slackRequest.body.toArray))
}
}
}
val HEADERS_TIMESTAMP: String = "X-Slack-Request-Timestamp"
val HEADERS_SIGNATURE: String = "X-Slack-Signature"
}
class SlackSignatureVerifyAction @Inject() (
val parser: BodyParsers.Default,
slackSignatureVerifierService: SlackSignatureVerifierService
)(implicit ec: ExecutionContext)
extends ActionBuilder[SlackRequest, AnyContent]
with ActionRefiner[Request, SlackRequest]
with Logging {
override protected def executionContext: ExecutionContext = ec
override protected def refine[A](
request: Request[A]
): Future[Either[Result, SlackRequest[A]]] = {
val timestamp =
request.headers.get(SlackSignatureVerifyAction.HEADERS_TIMESTAMP)
val signature =
request.headers.get(SlackSignatureVerifyAction.HEADERS_SIGNATURE)
(timestamp, signature) match {
case (Some(timestamp), Some(signature)) =>
Future.successful {
val validate = (body: String) =>
slackSignatureVerifierService.validate(timestamp, body, signature)
Right(new SlackRequest[A](validate, request))
}
case _ =>
Future { Left(Unauthorized("Invalid signature headers")) }
}
}
}
The validation on the controller side looks like this:
def slashCommand: Action[ByteString] = {
slackSignatureVerifyAction.async(parse.byteString) { request =>
logger.debug("received slash command")
request
.validateSignatureAgainstBody()
.map(processForm) match {
case Success(result) => result
case Failure(ex) =>
ex.printStackTrace()
Future { Unauthorized(ex.getMessage) }
}
}
}
Issue Analytics
- State:
- Created a year ago
- Reactions:1
- Comments:5 (5 by maintainers)
I have started documenting outstanding issues here: https://github.com/phelps-sg/play-hmac-signatures/issues. If anyone wants to take any of these on give me a shout.
I have something that may at least cut down on the signature building for this https://github.com/tersesystems/securitybuilder#macs-signatures-passwords