Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c4c8227

Browse files
authored
Merge pull request #79 from willemvermeer/master
hmac encoded csrf token
2 parents 4611e3d + 29aed19 commit c4c8227

File tree

6 files changed

+72
-10
lines changed

6 files changed

+72
-10
lines changed

core/src/main/java/com/softwaremill/session/javadsl/CsrfDirectives.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ import com.softwaremill.session.CsrfCheckMode
1111
*/
1212
trait CsrfDirectives {
1313

14-
def randomTokenCsrfProtection[T](checkMode: CsrfCheckMode[T], inner: Supplier[Route]): Route = RouteAdapter {
15-
com.softwaremill.session.CsrfDirectives.randomTokenCsrfProtection(checkMode) {
14+
def hmacTokenCsrfProtection[T](checkMode: CsrfCheckMode[T], inner: Supplier[Route]): Route = RouteAdapter {
15+
com.softwaremill.session.CsrfDirectives.hmacTokenCsrfProtection(checkMode) {
1616
inner.get.asInstanceOf[RouteAdapter].delegate
1717
}
1818
}
1919

20+
/**
21+
* @deprecated as of release 0.6.1, replaced by {@link #hmacTokensCsrfProtection()}
22+
*/
23+
def randomTokenCsrfProtection[T](checkMode: CsrfCheckMode[T], inner: Supplier[Route]): Route =
24+
hmacTokenCsrfProtection(checkMode, inner)
25+
2026
def setNewCsrfToken[T](checkMode: CsrfCheckMode[T], inner: Supplier[Route]): Route = RouteAdapter {
2127
com.softwaremill.session.CsrfDirectives.setNewCsrfToken(checkMode) {
2228
inner.get.asInstanceOf[RouteAdapter].delegate

core/src/main/scala/com/softwaremill/session/CsrfDirectives.scala

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.softwaremill.session
22

33
import akka.http.scaladsl.server.Directives._
4-
import akka.http.scaladsl.server.{Directive0, Directive1}
4+
import akka.http.scaladsl.server.{ Directive0, Directive1 }
55
import akka.stream.Materializer
66

77
trait CsrfDirectives {
@@ -11,19 +11,23 @@ trait CsrfDirectives {
1111
* doesn't have the token set in the header. For all other requests, the value of the token from the CSRF cookie must
1212
* match the value in the custom header (or request body, if `checkFormBody` is `true`).
1313
*
14+
* The cookie value is the concatenation of a timestamp and its HMAC hash following the OWASP recommendation for
15+
* CSRF prevention:
16+
* @see <a href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#hmac-based-token-pattern">OWASP</a>
17+
*
1418
* Note that this scheme can be broken when not all subdomains are protected or not using HTTPS and secure cookies,
1519
* and the token is placed in the request body (not in the header).
1620
*
1721
* See the documentation for more details.
1822
*/
19-
def randomTokenCsrfProtection[T](checkMode: CsrfCheckMode[T]): Directive0 = {
23+
def hmacTokenCsrfProtection[T](checkMode: CsrfCheckMode[T]): Directive0 = {
2024
csrfTokenFromCookie(checkMode).flatMap {
2125
case Some(cookie) =>
2226
// if a cookie is already set, we let through all get requests (without setting a new token), or validate
2327
// that the token matches.
2428
get.recover { _ =>
2529
submittedCsrfToken(checkMode).flatMap { submitted =>
26-
if (submitted == cookie && !cookie.isEmpty) {
30+
if (submitted == cookie && !cookie.isEmpty && checkMode.csrfManager.validateToken(cookie)) {
2731
pass
2832
} else {
2933
reject(checkMode.csrfManager.tokenInvalidRejection).toDirective[Unit]
@@ -36,6 +40,9 @@ trait CsrfDirectives {
3640
}
3741
}
3842

43+
@deprecated("use hmacTokenCsrfProtection", "0.6.1")
44+
def randomTokenCsrfProtection[T](checkMode: CsrfCheckMode[T]): Directive0 = hmacTokenCsrfProtection(checkMode)
45+
3946
def submittedCsrfToken[T](checkMode: CsrfCheckMode[T]): Directive1[String] = {
4047
headerValueByName(checkMode.manager.config.csrfSubmittedName).recover { rejections =>
4148
checkMode match {

core/src/main/scala/com/softwaremill/session/SessionManager.scala

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.softwaremill.session
22

33
import java.util.concurrent.TimeUnit
4-
54
import akka.http.scaladsl.server.AuthorizationFailedRejection
65

76
import scala.concurrent.duration.Duration
87
import scala.concurrent.{ExecutionContext, Future}
98

109
import akka.http.scaladsl.model.headers.{RawHeader, HttpCookie}
1110

11+
import scala.util.Try
12+
1213
class SessionManager[T](val config: SessionConfig)(implicit sessionEncoder: SessionEncoder[T]) { manager =>
1314

1415
val clientSessionManager: ClientSessionManager[T] = new ClientSessionManager[T] {
@@ -19,6 +20,7 @@ class SessionManager[T](val config: SessionConfig)(implicit sessionEncoder: Sess
1920

2021
val csrfManager: CsrfManager[T] = new CsrfManager[T] {
2122
override def config = manager.config
23+
override def nowMillis = manager.nowMillis
2224
}
2325

2426
def createRefreshTokenManager(_storage: RefreshTokenStorage[T]): RefreshTokenManager[T] = new RefreshTokenManager[T] {
@@ -82,9 +84,27 @@ trait ClientSessionManager[T] {
8284

8385
trait CsrfManager[T] {
8486
def config: SessionConfig
87+
def nowMillis: Long
8588

8689
def tokenInvalidRejection = AuthorizationFailedRejection
87-
def createToken(): String = SessionUtil.randomString(64)
90+
91+
def createToken(): String = {
92+
val millis = nowMillis.toString
93+
val hmac = generateHmac(millis)
94+
encodeToken(millis, hmac)
95+
}
96+
def validateToken(token: String): Boolean =
97+
token.nonEmpty &&
98+
decodeToken(token).fold(
99+
_ => false,
100+
{ case (millis, hmac) => SessionUtil.constantTimeEquals(hmac, generateHmac(millis)) }
101+
)
102+
private def encodeToken(millis: String, hmac: String): String = s"$millis-$hmac"
103+
private def decodeToken(token: String): Try[(String, String)] = Try {
104+
val splitted = token.split("-", 2)
105+
(splitted(0), splitted(1))
106+
}
107+
private def generateHmac(t: String): String = Crypto.sign_HmacSHA256_base64_v0_5_2(t, config.serverSecret)
88108

89109
def createCookie() =
90110
HttpCookie(

core/src/test/scala/com/softwaremill/session/CsrfDirectivesTest.scala

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class CsrfDirectivesTest extends AnyFlatSpec with ScalatestRouteTest with Matche
1818
implicit val csrfCheckMode = checkHeader
1919

2020
def routes[T](implicit manager: SessionManager[T], checkMode: CsrfCheckMode[T]) =
21-
randomTokenCsrfProtection(checkMode) {
21+
hmacTokenCsrfProtection(checkMode) {
2222
get {
2323
path("site") {
2424
complete {
@@ -97,6 +97,35 @@ class CsrfDirectivesTest extends AnyFlatSpec with ScalatestRouteTest with Matche
9797
}
9898
}
9999

100+
it should "reject requests if the csrf cookie and the header contain illegal value" in {
101+
Get("/site") ~> routes ~> check {
102+
responseAs[String] should be("ok")
103+
104+
Post("/transfer_money") ~>
105+
addHeader(Cookie(cookieName, "x")) ~>
106+
addHeader(sessionConfig.csrfSubmittedName, "x") ~>
107+
routes ~>
108+
check {
109+
rejections should be(List(AuthorizationFailedRejection))
110+
}
111+
}
112+
}
113+
114+
it should "reject requests if the csrf cookie and the header contain structurally correct but incorrectly hashed value" in {
115+
Get("/site") ~> routes ~> check {
116+
responseAs[String] should be("ok")
117+
118+
val wrong = s"wrong${System.currentTimeMillis()}"
119+
Post("/transfer_money") ~>
120+
addHeader(Cookie(cookieName, wrong)) ~>
121+
addHeader(sessionConfig.csrfSubmittedName, wrong) ~>
122+
routes ~>
123+
check {
124+
rejections should be(List(AuthorizationFailedRejection))
125+
}
126+
}
127+
}
128+
100129
it should "accept requests if the csrf cookie matches the header value" in {
101130
Get("/site") ~> routes ~> check {
102131
responseAs[String] should be("ok")

example/src/main/scala/com/softwaremill/example/ScalaExample.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ object Example extends App with StrictLogging {
3737
path("") {
3838
redirect("/site/index.html", Found)
3939
} ~
40-
randomTokenCsrfProtection(checkHeader) {
40+
hmacTokenCsrfProtection(checkHeader) {
4141
pathPrefix("api") {
4242
path("do_login") {
4343
post {

example/src/main/scala/com/softwaremill/example/session/SetSessionScala.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ object SetSessionScala extends App with StrictLogging {
3232
val myInvalidateSession = invalidateSession(refreshable, usingCookies)
3333

3434
val routes =
35-
randomTokenCsrfProtection(checkHeader) {
35+
hmacTokenCsrfProtection(checkHeader) {
3636
pathPrefix("api") {
3737
path("do_login") {
3838
post {

0 commit comments

Comments
 (0)