csrf: cookie settings + builder + `createResponseCookie` usage #2228
Conversation
I don't know CSRF very well, but I stumbled across this discussion. Does that discussion ring true to people who know what they're taking about? |
@rossabaker that's correct. I was actually wrong about this. I traditionally only had it set up to send the csrf token in certain locations, but this is correct. I think better yet would be making this field optional. The csrf token being accessible from js is sort of a necessity anyway @ some point. |
@jmcardon @rossabaker Updated the PR by adding |
I like this setup. Was looking to add |
I like the approach. See comment below. |
@@ -244,6 +247,24 @@ final class CSRF[F[_], G[_]] private[middleware] ( | |||
|
|||
object CSRF { | |||
|
|||
final case class CSRFCookieSettings( |
rossabaker
Nov 1, 2018
Member
Are we fairly certain that nothing will be added to this in the future? I'm trying to move away from case classes for config for binary compatibility reasons. My altnernative has been the server and client builders, which are painful to write, but can support additions in the future without breaking code.
Are we fairly certain that nothing will be added to this in the future? I'm trying to move away from case classes for config for binary compatibility reasons. My altnernative has been the server and client builders, which are painful to write, but can support additions in the future without breaking code.
jarreds
Nov 1, 2018
+1 on builders. Started on a (much less thorough) builder-like addition here #2235 until i discovered this PR.
+1 on builders. Started on a (much less thorough) builder-like addition here #2235 until i discovered this PR.
ahjohannessen
Nov 1, 2018
Author
Contributor
I don’t think much more is needed wrt cookie settings. Are you thinking about a builder for the middleware itself @rossabaker ?
I don’t think much more is needed wrt cookie settings. Are you thinking about a builder for the middleware itself @rossabaker ?
rossabaker
Nov 2, 2018
Member
I was thinking a builder for the config, but I suppose it could be a builder for the middleware. We merged configuration and builder in the server and client builders. I haven't fully thought through the ergonomics of it for middleware. 🤔
CSRF(http)
: defaults
CSRF.withCookieName("x-csrf-token").withSecure(true)(http)
So CSRF
is a class that is Http
=> Http
. Object (or value) CSRF
is CSRF
with default values. And you can configure things until applying the http
.
This pattern could apply to a few middlewares, and people are antsy for 0.20. Maybe we roll with it as is, and introduce this refactoring before 1.0.
I was thinking a builder for the config, but I suppose it could be a builder for the middleware. We merged configuration and builder in the server and client builders. I haven't fully thought through the ergonomics of it for middleware.
CSRF(http)
: defaultsCSRF.withCookieName("x-csrf-token").withSecure(true)(http)
So CSRF
is a class that is Http
=> Http
. Object (or value) CSRF
is CSRF
with default values. And you can configure things until applying the http
.
This pattern could apply to a few middlewares, and people are antsy for 0.20. Maybe we roll with it as is, and introduce this refactoring before 1.0.
152aac5
to
93e35d7
According to this site, apparently:
@rossabaker wrt nothing being added in the future, these two are missing: expires: Option[HttpDate] = None,
maxAge: Option[Long] = None, I do not see how those are relevant. |
@rossabaker Would it be useful to add this as a middleware: /** [[Middleware]] for lifting application/x-www-form-urlencoded values
* into the request header values. This middleware is particular useful
* for scenarios where you use some middleware, e.g. `CSRF`, and need
* to check for a header value that is only available as a form value.
* */
object UrlFormToHeader {
type FieldName = String
type HeaderName = String
/*
* Form fields listed in `fieldNames` are attempted to be lifted into headers.
* It does not try to replace headers that already exist with same name.
* */
def apply[F[_]: Sync, G[_]: Sync](nt: G ~> F)(
fieldNames: NonEmptyList[String],
fieldToHeader: FieldName ⇒ HeaderName = identity,
checkRequest: Request[F] ⇒ Boolean = !_.method.isSafe,
strictDecode: Boolean = false
): Http[F, G] ⇒ Http[F, G] =
http ⇒
Kleisli { req ⇒
def fieldsToHeaders(form: UrlForm): F[Response[G]] = {
val relevantFormValues = form.values.filterKeys(fieldNames.contains_).toList
val lifted = relevantFormValues.flatMap {
case (k, vs) ⇒ vs.headOption.map(v ⇒ Header(fieldToHeader(k), v))
}
val originalHeaders = req.headers.toList
val filteredHeaders = lifted.filterNot(l ⇒ originalHeaders.exists(_.name === l.name))
val newRequest = req.putHeaders(filteredHeaders: _*)
http(newRequest)
}
req.headers.get(headers.`Content-Type`) match {
case Some(headers.`Content-Type`(MediaType.application.`x-www-form-urlencoded`, _)) if checkRequest(req) ⇒
for {
decoded ← nt(UrlForm.entityDecoder[G].decode(req, strictDecode).value)
resp ← decoded.fold(mf ⇒ nt(mf.toHttpResponse[G](req.httpVersion)), fieldsToHeaders)
} yield resp
case _ ⇒ http(req)
}
}
} This means that one could to this: def csrf(routes: HttpRoutes[F]) = {
val ufthM = UrlFormToHeader(NonEmptyList.one("X-Csrf-Token"))
val csrfM = ???
utfhM(csrfM.validate()(routes))
}
val wrapped = csrf(routes1 <+> routes2) |
I think that's true. I'd like to futureproof it eventually, but I'm comfortable doing that for 1.0.
The idea of "a header value that is only available as a form value" is strange to me. I've seen more people conflate query strings and url forms than headers and url forms. But it seems you've got a real-world use case, and I don't object to adding it if it simplifies things. |
To be honest @rossabaker I only have a specific need wrt. CSRF. It can be solved similar to this: def filter2(predicate: Request[G] ⇒ Boolean = _.method.isSafe,
r: Request[G],
http: Http[F, G],
f: G ~> F): F[Response[G]] =
if (predicate(r)) {
validate(r, http)
} else {
checkCSRFDefaultV2(r, http, f)
}
def validate2(predicate: Request[G] ⇒ Boolean = _.method.isSafe,
f: G ~> F): Middleware[F, Request[G], Response[G], Request[G], Response[G]] = { http ⇒
Kleisli { r: Request[G] ⇒
filter2(predicate, r, http, f)
}
}
def checkCSRFDefaultV2(r: Request[G], http: Http[F, G], fk: G ~> F): F[Response[G]] = {
for {
ht ← F.pure(r.headers.get(headerName).map(_.value))
ft ← if (ht.isDefined) F.pure(ht) else {
fk(r.as[UrlForm].map(_.values.get("X-TSec-Csrf").flatMap(_.headOption)))
}
r ← ft match {
case Some(t) ⇒ checkCSRFToken(r, http, t)
case None ⇒ F.pure(onFailure)
}
} yield r
} in user code. |
@rossabaker Perhaps adding something like def checkAlsoForm(fieldName: String, nt: G ~> F): CSRF[F, G] Would be a good compromise and helpful for people using twirl? |
Yeah, that could be good. |
93e35d7
to
0f4b15c
@rossabaker I took at stab at using a builder approach, WDYT? |
b1c69d5
to
500fbc0
- fix `checkCSRFToken` to use `createResponseCookie`. - make it possible to configure various relevant settings for csrf cookie. - add builder for `CSRF`. - add ability to read token from form in `CSRF`.
500fbc0
to
5c9c2ba
} | ||
|
||
for { | ||
fst <- F.pure(csrf.getHeaderToken(r)) |
ChristopherDavenport
Nov 9, 2018
Member
Why is the pure
necessary here, seems this should just be on the next line
Why is the pure
necessary here, seems this should just be on the next line
ahjohannessen
Nov 9, 2018
•
Author
Contributor
Not sure why I did that, perhaps because a fold resulted in v => F.pure(Some(v))
- If you have a more elegant approach please share:) on the phone now
Not sure why I did that, perhaps because a fold resulted in v => F.pure(Some(v))
- If you have a more elegant approach please share:) on the phone now
ChristopherDavenport
Nov 10, 2018
Member
You're right, all of them I know of would involve at least one thunk. Here's hoping that someone else has a clean idea.
You're right, all of them I know of would involve at least one thunk. Here's hoping that someone else has a clean idea.
rossabaker
Nov 13, 2018
Member
A pure generator that begins a for-comprehension can move outside:
val fst = F.pure(csrf.getHeaderToken(r))
for {
snd <- if(fst.isDefined) F.pure(fst) else getFormToken
I don't think this is a dealbreaker to getting this merged and fixed.
A pure generator that begins a for-comprehension can move outside:
val fst = F.pure(csrf.getHeaderToken(r))
for {
snd <- if(fst.isDefined) F.pure(fst) else getFormToken
I don't think this is a dealbreaker to getting this merged and fixed.
} | ||
|
||
for { | ||
fst <- F.pure(csrf.getHeaderToken(r)) |
ChristopherDavenport
Nov 10, 2018
Member
You're right, all of them I know of would involve at least one thunk. Here's hoping that someone else has a clean idea.
You're right, all of them I know of would involve at least one thunk. Here's hoping that someone else has a clean idea.
} | ||
|
||
for { | ||
fst <- F.pure(csrf.getHeaderToken(r)) |
rossabaker
Nov 13, 2018
Member
A pure generator that begins a for-comprehension can move outside:
val fst = F.pure(csrf.getHeaderToken(r))
for {
snd <- if(fst.isDefined) F.pure(fst) else getFormToken
I don't think this is a dealbreaker to getting this merged and fixed.
A pure generator that begins a for-comprehension can move outside:
val fst = F.pure(csrf.getHeaderToken(r))
for {
snd <- if(fst.isDefined) F.pure(fst) else getFormToken
I don't think this is a dealbreaker to getting this merged and fixed.
|
checkCSRFToken
to usecreateResponseCookie
.for csrf cookie.
CSRF