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:
Here are the options I see
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.
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?
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)?
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
tokenThe 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
tokenThe 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
tokenThe 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="...">
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With