hasura / graphql-engine

Blazing fast, instant realtime GraphQL APIs on your DB with fine grained access control, also trigger webhooks on database events.
https://hasura.io
Apache License 2.0
31.07k stars 2.76k forks source link

CSRF mitigation with JWT and cookie #2205

Open campie opened 5 years ago

campie commented 5 years ago

Hi,

Referencing #2183, it would be great to have --jwt-from-cookie .

This will solve the problem of having to write an auth-webhook every time we want to store the jwt client-side in a http-only cookie (secure=true http-only=true same-site=strict; more on this below).

IMHO, we should NEVER STORE JWT in LocalStorage or in anything else JS has access to. For browser clients, using http-only cookie is currently the only solution to this problem (that I'm aware of).

However, the Hasura implementation should add some protection against csrf attacks for old browsers that do not support same-site flag on cookies (more on this below).

I'm already using "jwt cookies" with Hasura and this solution will allow me to ditch the auth-webhook altogether.

I really would like to help design this solution so I wrote this.

Proposal for Hasura in JWT mode supporting both Authorization: Bearer < JWT > header and jwt inside cookies (backward compatible)

Additional Hasura Graphql Engine flags:

--jwt-from-cookie (no default value, disabled by default) --jwt-csrf-token (default value: csrf-token) --csrf-token-header (default value: equals to --jwt-csrf-token) --csrf-token-disabled (dafault value: false)

To authorize an incoming request:

1 - Hasura looks for an Authorization: Bearer < JWT > header. If it finds it, Hasura skips to step 7 (jwt verification).

(Servers and mobile clients can still request the Hasura api by sending the jwt in the Authorization header. For browser clients, an http-only cookie containing the jwt is preferable)

2 - If no Authorization header is found, then Hasura checks if _--jwt-from-cookie: _ is set. If it's not, then the request is not authorized (skip all next steps).

3 If the new flag --crsf-token-disabled is set to true, skip to step 7. This can be vulnerable to csrf attacks.

(The csrf-token is necessary to avoid csrf attacks because not all browsers support same-site cookies (more on this below). The Hasura implementation might give the option to disable csrf-tokens with the flag --csrf-token-disabled=true which is set to false by default. This could be useful, for example, in an Electron app that doesn't require csrf-token protection. Another option could be disabling the need for a csrf-token when the --jwt-csrf-token flag is not set, but this makes the default configuration insecure. It would be better having a default value for --jwt-csrf-token (something like csrf-token) and forcing users to drop csrf-token usage with --disable-csrf-token=true)

4 - Hasura looks for the csrf-token header specified by --csrf-token-header or by --jwt-csrf-token if the former is not set or it uses the default name of csrf-token if none are set. If the csrf-token is not found, the authorization does NOT pass and all the next steps are skipped.

5 - If the csrf-token header is found, Hasura then checks the existence of the cookie containing the jwt (from --jwt-from-cookie: ). If the cookie is not found, the authorization does not pass and all the next steps are skipped.

6 - Hasura reads the jwt from the cookie and decodes it. Then it checks whether the csrf-token value inside the jwt payload matches the value from the csrf-token header. If not, the authorization does not pass and the next step is skipped. This step protects against csrf attacks.

7 Hasura verifies the jwt and gets the request metadata if verification succeeds.

Auth_flowchart

Example Scenario

Hasura-Cookie

Cookie containing the jwt

The jwt is stored in a secure=true http-only=true same-site=strict cookie. The max-age cookie flag can be used to keep the cookie in the client browser for a specific time.

1 - Secure cookie

This flag makes sure the cookie is only sent to the server through https.

2 - Http-only cookie

With the http-only flag set to true, JS never has access to the token.

In case of a xss attack, the jwt can't be stolen and the attacker has to do ALL his evil job at www.app.foo through the user's browser. It mitigates a lot the trouble a xss attack could cause.

3 - Same-site cookie

The same-site flag has two options: strict or lax.

If the flag is omitted (or set to none in the future), the cookies are sent with every request

Sending cookies with every request was the only option available before the same-site flag was introduced.

This flag distinguishes same-site requests from cross-site requests, NOT cross-origin requests.

Attention: a cross-site request is different from a cross-origin request. (Please, correct me if I'm wrong)

www.app.foo and api.app.foo are two different origins but the same site (both have app.foo as their public suffix). So in the example above, the spa is loaded from a cdn at www.app.foo and then the user's browser requests the hasura api at api.app.foo: this api call is a same-site request and at the same time a cross-origin request.

If the same-site flag is set to strict, the cookies are NEVER sent with any cross-site request, only same-site requests (it doesn't matter if it's cross-origin or not). This is the "strictest/safest" mode and it works in the example above. This prevents all csrf attacks (for browsers that support this flag, of course).

In lax mode, cookies are sent for all same-site requests (just like in strict mode) plus cross-site requests if it's a GET request and the URL in the address bar changes because of the request. Cross-site requests from iframes, images or XMLHttpRequests never send same-site=lax cookies because they don't change the URL in the address bar. If a user clicks on a link from a different site and then gets to your site, thus changing the URL in the address bar, lax mode will send cookies. This can be useful for server-side rendered apps and websites.

Browsers that do not support the same-site flag will send cookies with every request, be it same-site, cross-site or cross-origin. This is why we need the csrf-token!!!.

The example above is hosted on a CDN (static app) and uses same-site=strict. When the user calls the login mutation at api.app.foo, the response set-cookie header for the jwt cookie doesn't have a domain flag, so the final domain gets to be api.app.foo and this cookie is never sent to the CDN at www.app.foo. This is fine and desirable; the CDN doesn't need the jwt.

I'm imagining the case where the server may want the jwt. For example, a server-side rendered app or website where we want to know if the user is logged in when he/she clicks an external link and arrives at our website. We can set the jwt cookie to secure=true http-only-true same-site=lax and domain=app.foo. When a user clicks on a link and gets to www.app.foo, the server running at www.app.foo (this is not hasura) will get the jwt token because same-site=lax (or the user's browsers do not support same-site cookies) AND ALSO domain is set to app.foo. This is a GET request to our app server where there's no csrf-token header. Instead of just assuming the user is logged-out, the server can then decide to render a logged-in UI with even some non-sensitive data by calling the hasura api at api.foo.com with a _Authorization: Bearer < JWT > header. (please, someone tell me if this is a crazy idea).

In both scenarios (static app hosted on a CDN or server-side rendered app or website), the hasura api ALWAYS need to receive both the csrf-token header and the jwt cookie containing a csrf-token in its payload in order to authenticate a request where the jwt is inside a cookie. If the jwt is in the Authorization header, the csrf-token header is not needed (nor the jwt cookie).

Cookie containing the payload

This is a secure=true same-site=strict domain=app.foo cookie. Its usage is optional.

The domain flag has to be set to app.foo by the api so that client-side js at www.app.foo can read the cookie. If domain is not set, only client-side js at api.app.foo can read the cookie (not useful since the app is served at www.app.foo).

This cookie improves the UX because, at start-up, if the payload cookie is present, we know that the http-only jwt token is also present and the user is logged-in (remember client js cannot read the http-only jwt cookie). Besides, the payload contains user info to update the UI accordingly. If an evil force changes the payload content, all we get is a broken UI.

An alternative to the payload cookie could be an api call at start-up to check login status, but this adds overhead to the process.

Payload duplication

My app is a little bit different from the example above. My http-only jwt cookie only contains the jwt header and signature. The payload cookie is exactly the same. Server side, my hasura auth webhook gets the csrf-token from the csrf-token header, reassembles the jwt from this two cookies, decodes the jwt, checks if the payload csrf-token is the same as the csrf-token header and the verifies the jwt. This saves some bytes in every client request and it's basically the same. The example above is easier to explain, though, and may be a better way to implement it into hasura for the community.

Set-Cookie Headers from remote schema

I'm still struggling with this issue #1654, so my login mutation is currently not in a hasura remote schema.

nolandg commented 5 years ago

I am currently building a framework with Hasura and I'm finding it impossible to follow many security best practices (eg. OWASP cheat sheets) without using the web hook. Correct me if I'm wrong but simply a JWT in a header doesn't leave many options for CSFR protection and allows anything with access to client javascript to authenticate. The Hasura examples of using the JWT cannot be made very secure in a real app...unless I missed something?

I like @campie's suggestion. I'm implementing rotating HMAC tokens for CSRF and http-only cookies to help with XSS in my webhook. It would improve performance and reduce app complexity if Hasura could do this. It would also improve the security of apps developed with Hasura.

Or are we over-complicating this?

coco98 commented 5 years ago

@nolandg I think this is a super important.

For now, working with a webhook, using Hasura CORS settings etc. is the best way forward, but it would be perfect to roll this up as configuration within Hasura itself over time. Also, we want to make sure that Hasura is definitely checks through the entire OWASP list so it would definitely be ideal for Hasura to absorb some of the complexity.

cc: @ecthiender @dsandip

ecthiender commented 5 years ago

@coco98 #2183 covers reading JWT from cookie and is a WIP. We are evaluating what @campie said about CSRF concerns, and will update here.

ecthiender commented 5 years ago

We will be having configuration as @campie mentioned to check CSRF tokens from header and compare it with the CSRF token in the JWT claims. Also to option to disable the CSRF check (defaults to false).

However, instead of flags, the entire configuration will be in the --jwt-secret flag as JSON. Will update with the exact spec/details in the PR #2327 when we take it up (very soon).

nolandg commented 5 years ago

This might be easy to add now while this is open and I think it's a nice-to-have for improved security: dual auth cookies to allow client-side/offline signout.

One drawback of stateless JWT authentication is you can't really signout or invalidate a token, anyone can use that token to sign in until it expires. The user could clear the auth token cookie from their client but best practice for an auth token cookie is of course http-only and hence the client can't clear it. The user can trigger a sign out action in the client which makes a request to the server and the server then responds with a cookie clearing header. This is the only way I know how to do stateless sign out with a single cookie.

This is sub-optimal though because it relies on a connection to the server to sign out. If the wifi drops or the 3G runs out, the user can't sign out on that device until they get a connection again. Offline-tolerant apps are getting more popular (think Google docs etc, allowing you to work offline for periods of time) so this might become more important.

The dual cookie method uses two unique JWT tokens (same user ID claim but at least one other unique claim), one http-only and one client script accessible. The server checks for the presence of both cookies to authenticate a request. The user can clear the non-http-only cookie without a network connection and thus sign out even in offline mode. Attackers through XSS who have the non-http-only cookie cannot authenticate because they lack the 2nd cookie and they can't forge it because it makes a unique claim that's never made in the other cookie.

ecthiender commented 5 years ago

Is this a well-known/standard practice? Can you point to any document/articles where this is talked about in details?

On Sat, Jun 15, 2019 at 1:03 AM Noland notifications@github.com wrote:

This might be easy to add now while this is open and I think it's a nice-to-have for improved security: dual auth cookies to allow client-side/offline signout.

One drawback of stateless JWT authentication is you can't really signout or invalidate a token, anyone can use that token to sign in until it expires. The user could clear the auth token cookie from their client but best practice for an auth token cookie is of course http-only and hence the client can't clear it. The user can trigger a sign out action in the client which makes a request to the server and the server then responds with a cookie clearing header. This is the only way I know how to do stateless sign out with a single cookie.

This is sub-optimal though because it relies on a connection to the server to sign out. If the wifi drops or the 3G runs out, the user can't sign out on that device until they get a connection again. Offline-tolerant apps are getting more popular (think Google docs etc, allowing you to work offline for periods of time) so this might become more important.

The dual cookie method uses two unique JWT tokens (same user ID claim but at least one other unique claim), one http-only and one client script accessible. The server checks for the presence of both cookies to authenticate a request. The user can clear the non-http-only cookie without a network connection and thus sign out even in offline mode. Attackers through XSS who have the non-http-only cookie cannot authenticate because they lack the 2nd cookie and they can't forge it because it makes a unique claim that's never made in the other cookie.

— You are receiving this because you were assigned. Reply to this email directly, view it on GitHub https://github.com/hasura/graphql-engine/issues/2205?email_source=notifications&email_token=AAEWPQ5DEXXE755K62G5QNDP2PW6XA5CNFSM4HNOH462YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXXYHZI#issuecomment-502236133, or mute the thread https://github.com/notifications/unsubscribe-auth/AAEWPQZKXK2KQQJMTSQM463P2PW6XANCNFSM4HNOH46Q .

nolandg commented 5 years ago

I don't think this is a problem many people have cared enough to solve. I can't find anyone else solving it.

My use case was management software on mobile devices for industrial welding. Very RF noisy environment, workers would have to leave the station but couldn't log out of the time tracking software because wifi dropped momentarily, next worker would then access the previous guy's account. Expiry doesn't work so well because times are just too short.

The best XSS mitigation is non-client-accessible session token. The only way to logout offline is client-accessible session token. Thus you need both if you care about offline logout.

Totally understand if Hasura doesn't want to support this, just suggesting it to widen the use cases it can handle without resorting to webhook.

mradke commented 5 years ago

I really like the proposal of @campie (n. b. props for the awesome issue and detailed workflow description 👍 ) and would love to see it rolled out!

Albeit I also think that the usage will be relatively hard to explain in the docs, since this most certainly requires a pretty involved session management service.

What are the plans regarding this? Especially since you endorsed Auth0 for many of the tutorials that are online and I have honestly no idea if they would/do provide such a service.

darrenbutcher commented 4 years ago

There was an awesome video I saw awhile back about JWT authorization, understanding the place of Session IDs, JWT Tokens and a "better" approach 100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière.

My auth experience is limited and still trying to warp my head around its complexity, however the idea presented in the video seemed to make sense, using session id/cookie from the client and JWTs among internal resources. Hoping there will be some way also to try out this approach.

srghma commented 3 years ago

Summary of 100% Stateless with JWT (JSON Web Token) by Hubert Sablonnière

The author proposes to store csrf to jwt token

2020-10-22-16:47:46-screenshot

The author also argues that it's better to use (jwt-)session-id like approach on web and ordinary jwt for mobile and microservices (but use https://ionicframework.com/docs/native/secure-storage , not localStorage)

2020-10-22-16:48:00-screenshot

BONUS 1: Web vs mobile UX flows

on the web we may want:

on mobile:

BONUS 2: access token (AT) / refresh token (RT)

where they came from?

AT/RT approach came from OAuth

AT is a cached , short-lived (e.g. valid for 15min) result of expensive validating RT (long lived, e.g. 7d).

By this I mean:

on Web, it's better to use session-id, unless:

All this comes with a downside:

tsujp commented 3 years ago

Would PRs be accepted for this?

matmut7 commented 2 years ago

Hi, any updates on this or the CSRF mitigation topic in general?