11package com .softwaremill .session
22
3- import sttp .model .Method . _
3+ import sttp .model .Method
44import sttp .model .headers .{CookieValueWithMeta , CookieWithMeta }
5- import sttp .model .{Method , StatusCode }
6- import sttp .monad .FutureMonad
7- import sttp .tapir .server .PartialServerEndpointWithSecurityOutput
85import sttp .tapir ._
6+ import sttp .tapir .server .PartialServerEndpointWithSecurityOutput
97
108import scala .concurrent .{ExecutionContext , Future }
119
12- private [session] trait CsrfEndpoints [T ] extends CsrfCheck {
13-
14- import com .softwaremill .session .AkkaToTapirImplicits ._
15-
16- def manager : SessionManager [T ]
17-
18- implicit def ec : ExecutionContext
19-
20- def csrfCookie : EndpointIO .Header [Option [CookieValueWithMeta ]] =
21- setCookieOpt(manager.config.csrfCookieConfig.name).description(" set csrf token as cookie" )
22-
23- def submittedCsrfCookie : EndpointInput .Cookie [Option [String ]] =
24- cookie(manager.config.csrfCookieConfig.name)
25-
26- def submittedCsrfHeader : EndpointIO .Header [Option [String ]] = header[Option [String ]](
27- manager.config.csrfSubmittedName
28- ).description(" read csrf token as header" )
29-
30- def setNewCsrfToken (): CookieWithMeta = manager.csrfManager.createCookie()
31-
32- def hmacTokenCsrfProtectionLogic (
33- method : Method ,
34- csrfTokenFromCookie : Option [String ],
35- submittedCsrfToken : Option [String ]
36- ): Either [Unit , (Option [CookieValueWithMeta ], Unit )] = {
37- csrfTokenFromCookie match {
38- case Some (cookie) =>
39- val token = cookie
40- submittedCsrfToken match {
41- case Some (submitted) =>
42- if (submitted == token && token.nonEmpty && manager.csrfManager.validateToken(token)) {
43- Right ((None , ()))
44- } else {
45- Left (())
46- }
47- case _ =>
48- Left (())
49- }
50- // if a cookie is not set, generating a new one for **get** requests, rejecting other
51- case _ =>
52- if (method.is(GET )) {
53- Right ((Some (manager.csrfManager.createCookie().valueWithMeta), ()))
54- } else {
55- Left (())
56- }
57- }
58- }
59-
60- def hmacTokenCsrfProtection [
61- SECURITY_INPUT ,
62- PRINCIPAL ,
63- SECURITY_OUTPUT
64- ](
10+ trait CsrfEndpoints {
11+
12+ /** Protects against CSRF attacks using a double-submit cookie. The cookie will be set on any
13+ * `GET` request which doesn't have the token set in the header. For all other requests, the
14+ * value of the token from the CSRF cookie must match the value in the custom header (or request
15+ * body, if `checkFormBody` is `true`).
16+ *
17+ * The cookie value is the concatenation of a timestamp and its HMAC hash following the OWASP
18+ * recommendation for CSRF prevention:
19+ * @see
20+ * <a
21+ * href="https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#hmac-based-token-pattern">OWASP</a>
22+ *
23+ * Note that this scheme can be broken when not all subdomains are protected or not using HTTPS
24+ * and secure cookies, and the token is placed in the request body (not in the header).
25+ *
26+ * See the documentation for more details.
27+ */
28+ def hmacTokenCsrfProtection [T , SECURITY_INPUT , PRINCIPAL , SECURITY_OUTPUT ](
29+ checkMode : TapirCsrfCheckMode [T ]
30+ )(
6531 body : => PartialServerEndpointWithSecurityOutput [
6632 SECURITY_INPUT ,
6733 PRINCIPAL ,
@@ -81,86 +47,48 @@ private[session] trait CsrfEndpoints[T] extends CsrfCheck {
8147 Unit ,
8248 Any ,
8349 Future
84- ] = {
85- val partial =
86- // extract csrf token from cookie
87- csrfTokenFromCookie()
88- // extract request method
89- .securityIn(extractFromRequest(req => req.method))
90- // extract submitted csrf token from header
91- .securityIn(submittedCsrfHeader)
92- // extract submitted csrf token from form
93- .securityIn(formBody[Map [String , String ]])
94- .out(csrfCookie)
95- .errorOut(statusCode(StatusCode .Unauthorized ))
96- .serverSecurityLogicWithOutput {
97- case (
98- csrfTokenFromCookie,
99- method,
100- submittedCsrfTokenFromHeader,
101- submittedCsrfTokenFromForm
102- ) =>
103- Future .successful(
104- hmacTokenCsrfProtectionLogic(
105- method,
106- csrfTokenFromCookie,
107- if (checkHeaderAndForm)
108- submittedCsrfTokenFromHeader.fold(
109- submittedCsrfTokenFromForm.get(manager.config.csrfSubmittedName)
110- )(Option (_))
111- else
112- submittedCsrfTokenFromHeader
113- )
114- )
115- }
116- partial.endpoint
117- .prependSecurityIn(body.securityInput)
118- .out(body.securityOutput)
119- .out(partial.securityOutput)
120- .serverSecurityLogicWithOutput {
121- case (
122- securityInput,
123- csrfTokenFromCookie,
124- method,
125- submittedCsrfTokenFromHeader,
126- submittedCsrfTokenFromForm
127- ) =>
128- partial
129- .securityLogic(new FutureMonad ())(
130- (
131- csrfTokenFromCookie,
132- method,
133- submittedCsrfTokenFromHeader,
134- submittedCsrfTokenFromForm
135- )
136- )
137- .flatMap {
138- case Left (l) => Future .successful(Left (l))
139- case Right (r) =>
140- body.securityLogic(new FutureMonad ())(securityInput).map {
141- case Left (l2) => Left (l2)
142- case Right (r2) =>
143- Right (((r2._1, r._1), r2._2))
144- }
145- }
146- }
147- }
50+ ] =
51+ checkMode.hmacTokenCsrfProtection {
52+ body
53+ }
14854
149- def csrfTokenFromCookie () : Endpoint [ Option [ String ], Unit , Unit , Unit , Any ] =
150- endpoint
151- // extract csrf token from cookie
152- .securityIn(submittedCsrfCookie)
55+ def csrfCookie [ T ](
56+ checkMode : TapirCsrfCheckMode [ T ]
57+ ) : EndpointIO . Header [ Option [ CookieValueWithMeta ]] =
58+ checkMode.csrfCookie
15359
154- }
60+ def csrfTokenFromCookie [T ](
61+ checkMode : TapirCsrfCheckMode [T ]
62+ ): Endpoint [Option [String ], Unit , Unit , Unit , Any ] =
63+ checkMode.csrfTokenFromCookie()
15564
156- sealed trait CsrfCheck {
157- def checkHeaderAndForm : Boolean
65+ def setNewCsrfToken [T ](
66+ checkMode : TapirCsrfCheckMode [T ]
67+ ): CookieWithMeta =
68+ checkMode.setNewCsrfToken()
15869}
15970
160- trait CsrfCheckHeader extends CsrfCheck {
161- val checkHeaderAndForm : Boolean = false
71+ object CsrfEndpoints extends CsrfEndpoints
72+
73+ object TapirCsrfOptions {
74+ def checkHeader [T ](implicit manager : SessionManager [T ], ec : ExecutionContext ) =
75+ new TapirCsrfCheckHeader [T ]()
76+ def checkHeaderAndForm [T ](implicit manager : SessionManager [T ], ec : ExecutionContext ) =
77+ new TapirCsrfCheckHeaderAndForm [T ]()
16278}
16379
164- trait CsrfCheckHeaderAndForm extends CsrfCheck {
165- val checkHeaderAndForm : Boolean = true
80+ sealed trait TapirCsrfCheckMode [T ] extends TapirCsrf [T ] {
81+ def manager : SessionManager [T ]
82+ def ec : ExecutionContext
83+ def csrfManager : CsrfManager [T ] = manager.csrfManager
16684}
85+
86+ class TapirCsrfCheckHeader [T ](implicit val manager : SessionManager [T ], val ec : ExecutionContext )
87+ extends TapirCsrfCheckMode [T ]
88+ with CsrfCheckHeader
89+
90+ class TapirCsrfCheckHeaderAndForm [T ](implicit
91+ val manager : SessionManager [T ],
92+ val ec : ExecutionContext
93+ ) extends TapirCsrfCheckMode [T ]
94+ with CsrfCheckHeaderAndForm
0 commit comments