11
votes

I'm trying to implement auth between a Next.js frontend and a Rails API backend, and I'm having difficulty understanding the how to properly do this securely.

I'm using the jwt-sessions gem on the rails side, which upon login returns an access_token, a refresh_token and a csrf token. The csrf can only be sent via header, and is required for all non-GET or HEAD requests. The access and refresh can be given via header or by cookies.

I can implement the backend to either return all the tokens in a JSON response, or set cookies, or a mix of both.

The problem is that, client-side I see no real way to store these tokens that is:

  1. Secure
  2. Available during SSR as well as in the browser

Here are the options I see

Option 1: Server-Set httpOnly Cookies

This is what the jwt-sessions gem recommends. Keep CSRF in localStorage, but the other tokens in httpOnly cookies set by the server.

This seems the most secure, but then SSR via getInitialProps won't work at all, because it can't send the cookies via fetch. I can't even manually send them because I can't see them in JS.

Option 2: non httpOnly Cookies

This is what the official next example seems to do (albeit with a single, probably stateless token)

Either have the server or the browser set the cookie without httpOnly.

I can access it via SSR, but doesn't this open me up to XSS?

Option 3: Use Browser Storage

Just put all the tokens in localStorage and/or sessionStorage. This seems to be the worst option, as it won't work in SSR, and to my knowledge is not secure.

????

Am I missing something? Is non-httpOnly, like in the official example, OK? Is there a better approach? Or will I have to ignore SSR (and thus one of Next.js's killer features)?

1
Is the next.js server-side app served by the same domain as the jwt-sessions gem? If not, the jwt-sessions gem can't set cookies that the next.js server side rendering can see.Cirdec
The csrf can only be sent via header, and is required for all non-GET or HEAD requests to the rail backend, so it needs to be passed to the next.js server side any time the server side might make a non-GET or HEAD request to the backend?Cirdec

1 Answers

3
votes

Lets examine each of the tokens and where it will be used. Cookies can only be presented to the server that set them, so any cookie will need to be set by the server that uses it.

refresh token

The refresh token will be presented to the jwt-sessions authentication server to get a new access and csrf token if the access token expires. Since the refresh token both comes from the authentication server, and is used on the authentication server, it can be set as an HttpOnly cookie by the authentication server

Set-Cookie: refresh-token=...; domain=rails-backend.example.com; secure; HttpOnly

access token

The access token will be used in two ways, it will be presented to the rails backend by the next.js client side, and it will be used by the next.js server side to make calls to the rails backend to server-side render pages via getInitialProps.

Presenting the access token to the rails backend from the client side is easy. The jwt-sessions authentication server runs on the same server as the rails backend, so the authentication server can set an HttpOnly cookie.

Set-Cookie: access-token=...; domain=rails-backend.example.com; secure; HttpOnly

Presenting it from the next.js server-side is more difficult, to get it there the client needs to send the access token to the next.js server-side. A header would be the appropriate way to present it initially. And to send it to the next.js server side, the client side needs the access token included in the json response from the authentication server.

{
    'access': "..."
}
Host: next-js-server-side.example.com
X-Rails-Backend-Access-Token: ...

Once the next.js server-side has the access token, it can set an HttpOnly cookie to have the access token available for future requests.

Set-Cookie: rails-backend-access-token=...; domain=next-js-server-side.example.com; secure; HttpOnly

csrf token

The csrf token is used to protect requests to the rails backend from cross-site forged requests. It needs to be presented by anything that will make a request to the rails backend, both the client-side and server-side.

The client will get the csrf from the authentication server as part of the json payload, because that's the only safe way to get it to the client.

{
    'csrf': "..."
}

The client can use it right now to make requests to the rails backend, but it won't be stored durably.

To get the csrf token to the next.js server side, the client will send it as a header

Host: next-js-server-side.example.com
X-Rails-Backend-CSRF-Token: ...

The next.js server side has the luxury of turning around and setting an HttpOnly cookie to have the csrf token available for future requests.

Set-Cookie: rails-backend-csrf-token=...; domain=next-js-server-side.example.com; secure; HttpOnly

This cookie is useless for cross-site forging requests to the rails backend, because it's only presented to the next-js server-side, and isn't presented to the rails backend at all.

The next.js server side can give this token back to the client, as long as it does it in a way that it can't be hijacked by a site mounting a cross-site forgery attack.

<input type="hidden" id="rails-backend-csrf-token" value="...">

Cross-site request forgery for next.js

The next.js server side will need to provide it's own cross-site forgery protection. It can't rely on the HttpOnly cookie it set above, because that will be presented in cross-site requests.