Next.js Authentication Strategies
Asked Answered
S

3

27

I've been trying to implement a reliable authentication flow for a Next.js project but I'm completely lost now. I've already seen the examples repo of Next.js. But I have a lot of questions for a complete solution.

I have a express.js API and a separate Next.js frontend project. All the data and the authentication is handled by the API. Frontend just renders the pages with SSR. If I would just create a monolith project, where rendering the pages and all the data is handled by a single server (with a custom server option for Next.js I mean), I would just use express-session and csurf. It would be a traditional way to manage sessions and create security against CSRF.

Express.js API is not a requirement. It is just an example. It could be a Django API, or a .Net Core API. The main point is, it is a separate server and a separate project.

How can I have a simple, yet reliable structure? I've examined some of my favorite websites (netlify, zeit.co, heroku, spectrum.chat etc). Some of them use localstorage to store access and refresh tokens (XSS vulnerable). Some of them use cookies and they are not even HTTPOnly (both XSS and CSRF vulnerable). And examples like spectrum.chat use the way I mentioned above (cookie-session + preventing csrf).

I know there is the giant hype around the JWT tokens. But I find them too complex. Most of the tutorials just skips all the expiration, token refreshing, token revocation, blacklisting, whitelisting etc.

And many of the session cookie examples for Next.js almost never mention CSRF. Honestly, authentication is always a big problem for me. One day I read that HTTPOnly cookies should be used, next day I see a giant popular site not even using them. Or they say "never store your tokens to localStorage", and boom some giant project just uses this method.

Can anyone show me some direction for this situation?

Spectacled answered 22/3, 2020 at 13:12 Comment(2)
JWT is fairly simple actually. If you want I can send my example?Opposite
That might be helpful. JWT can be used for a lot of things but I just don't get when people just create a token, expire it in x hours and don't even show how they refresh it etc.Saraann
S
4

I've finally found a solution!

Now I'm using csrf npm package, not csurf. csurf is just turns csrf into an express middleware.

So, I create a csrfSecret in the getInitialProps of _app. It creates the secret, sets it as a httpOnly cookie. Later, it creates a csrfToken and returns it with pageProps. So, I can access it with window.NEXT_DATA.props.csrfToken. If user refreshes the page, csrfSecret remains the same, but csrfToken gets renewed.

When I make a request to the proxied "/api/graphql API route, it first gets the csrf token from x-xsrf-token header and verifies it with the csrfSecret cookie value. After that, it extracts the value of authToken cookie and passes it to the actual GraphQL API.

API is all token based. It only needs a non-expiring access token. (BTW, It doesn't need to be JWT. Any cryptographically strong, random token can be used. Which means a reference/opaque token.)

CSRF check is not needed for the actual API, because it doesn't rely on cookies for authentication. It only checks authorization header. Both authToken and csrfSecret is httpOnly cookies. And I never even store them in client-side memory.

I think this is as secure as I could get. Now I'm happy with this solution.

Spectacled answered 18/4, 2020 at 23:20 Comment(1)
thank you for the detailed answer, helped understand this moreHervey
B
11

Disclaimer: I am a maintainer of the free open source package below, but I think it's appropriate here as it's a common question there isn't a great answer for, as many of the popular solutions have the specific security flaws raised in the question (such as not using CSRF where appropriate and exposing Session Tokens or web tokens to client side JavaScript).

The package NextAuth.js attempts to address the issues raised above, with free open source software.

  • It uses httpOnly cookies with secure.
  • It has CSRF protection (double submit cookie method, with signed cookies).
  • Cookies are prefixed as appropriate (e.g. __HOST- or __Secure).
  • It supports email/passwordless signin and OAuth providers (with many included).
  • It supports both JSON Web Tokens (signed + encrypted) and Session Databases.
  • You can use it without a database (e.g. any ANSI SQL, MongoDB).
  • Has a live demo (view source).
  • It is 100% FOSS, it is not commercial software or a SaaS solution (is not selling anything).

Example API Route

e.g. page/api/auth/[...nextauth.js]

import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'

const options = {
  providers: [
    // OAuth authentication providers
    Providers.Apple({
      clientId: process.env.APPLE_ID,
      clientSecret: process.env.APPLE_SECRET
    }),
    Providers.Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET
    }),
    // Sign in with email (passwordless)
    Providers.Email({
      server: process.env.MAIL_SERVER,
      from: '<[email protected]>'
    }),
  ],
  // MySQL, Postgres or MongoDB database (or leave empty)
  database: process.env.DATABASE_URL
}

export default (req, res) => NextAuth(req, res, options)

Example React Component

e.g. pages/index.js

import React from 'react'
import { 
  useSession, 
  signin, 
  signout 
} from 'next-auth/client'

export default () => {
  const [ session, loading ] = useSession()

  return <p>
    {!session && <>
      Not signed in <br/>
      <button onClick={signin}>Sign in</button>
    </>}
    {session && <>
      Signed in as {session.user.email} <br/>
      <button onClick={signout}>Sign out</button>
    </>}
  </p>
}

Even if you don't choose to use it, you may find the code useful as a reference (e.g. how JSON Web Tokens are handled and how they are rotated in sessions.

Beestings answered 12/6, 2020 at 12:31 Comment(4)
Do you have any guide for me, I am using Django Rest Framework with NextJs at the frontend.Pinsk
I've been checking next-auth recently. Is there a way to use it for registering users and storing them to mongodb and also fetching the user data from mongodb again to login?Bayles
This could've been much better answer if you mentioned how nextjs-auth handles external API authentication (or how we should be?). Most projects are nextjs + some external rest API. That was the main point of the question and you just missed it.Nada
@ShinebayarG That would have been a much better comment if you had tried to answer it yourself instead.Beestings
L
7

I've had to think about this as well for my current project. I use the same technologies: an ExpressJS API and a NextJS server-side-rendered front-end.

What I chose to do is use passport.js in the ExpressJS API. TheNetNinja on YouTube has a really good playlist of this with 21 episodes. He shows you how to implement Google OAuth 2.0 in your API, but this logic transfers to any other strategy (JWT, Email + Password, Facebook authentication etc.).

In the front-end, I would literally redirect the user to a url in the Express API. This url would show the user the Google OAuth screen, the user clicks on "Allow", the API does some more stuff, makes a cookie for the specific user and then redirects back to a url in the front end. Now, the user is authenticated.

About HTTPOnly cookies: I chose to turn off this feature, because I was storing information in the cookie that I needed in the front-end. If you have this feature enabled, then the front-end (javascript) doesn't have access to those cookies, because they are HTTPOnly.

Here's the link to the playlist I was talking about: https://www.youtube.com/watch?v=sakQbeRjgwg&list=PL4cUxeGkcC9jdm7QX143aMLAqyM-jTZ2x

Hope I've given you a direction you can take.

EDIT: I haven't answered your question about CSURF, but that's because I'm not familiar with it.

Legator answered 23/3, 2020 at 10:31 Comment(7)
Thanks for you advice. I will definitely look for that series! I mean the whole point of setting cookies and not storing the authentication state in localStorage is preventing XSS. I don't get how people prevent it without HTTPOnly. But like I said, zeit.co uses something like this too. Do you use a reverse proxy or some subdomains to use the cookie that you set from the API in the frontend?Saraann
BTW, I nearly have the same structure as you described. Frontend redirects to backend. It handles the authentication by Google and sets some cookies. My pain point is CSRF basically. Another way would be redirecting to Google auth screen from the frontend. Getting the auth code when redirected from Google to frontend. Passing the code to your own API, validating it by sending it to Google from your API and then starting the session. This was something I've read in Google Dev Docs. I think this is nearly the same thing what passport does.Saraann
The only subdomain I use is the one for my api. That's where all the authentication happens. I've thought about taking a front-end approach as well. Getting a code on the front end and then validating it in the back end, but chose not to take that route, because I didn't want to complicate the front end too much. At some point in time, I would like to to move all of my authentication to a dedicated subdomain and maybe even a dedicated server. I'm sorry I cant help you with CSRF, I would have to read up on it, but I manage to stay secure by not storing sensitive info in the cookie.Legator
Thanks! I may dive deep into this authentication stuff. But it just keeps getting complicated and just locks every othery feature that I want to implement. Will try to find a simple yet secure (at least to some level) path. Thanks again.Saraann
@JonathanvandeGroep Huh. I've been struggling with this issue as well. I'm using nextjs for a personal blog but needed to figure out auth for admin endpoints. So what you are saying is I could just authenticate with my external API (because all my endpoints requiring auth are also on that API)? In other words, if my external API handles resource creation, and I need to authenticate to POST to it, I can just auth directly with the API because both endpoints share that origin (as opposed to being handled by the nextjs server).Foretop
@goldmund Yes, exactly. I do my Google OAuth authentication by literally redirecting the front-end user to api.myapp.com/auth/google. The passportjs google strategy automatically handles this and redirects the user to the oauth consent screen from google. After the user has chosen a google account and consent is given, it redirects back to the api, which handles the consent and then redirects the user back to the front-end, this time with a session cookie that was made by passportjs. Now the user is authenticated.Legator
@goldmund For email + password authentication this works a bit different, because you need to POST the email and password, of course, so a redirect won't work, but the passport-local strategy will help you with this.Legator
S
4

I've finally found a solution!

Now I'm using csrf npm package, not csurf. csurf is just turns csrf into an express middleware.

So, I create a csrfSecret in the getInitialProps of _app. It creates the secret, sets it as a httpOnly cookie. Later, it creates a csrfToken and returns it with pageProps. So, I can access it with window.NEXT_DATA.props.csrfToken. If user refreshes the page, csrfSecret remains the same, but csrfToken gets renewed.

When I make a request to the proxied "/api/graphql API route, it first gets the csrf token from x-xsrf-token header and verifies it with the csrfSecret cookie value. After that, it extracts the value of authToken cookie and passes it to the actual GraphQL API.

API is all token based. It only needs a non-expiring access token. (BTW, It doesn't need to be JWT. Any cryptographically strong, random token can be used. Which means a reference/opaque token.)

CSRF check is not needed for the actual API, because it doesn't rely on cookies for authentication. It only checks authorization header. Both authToken and csrfSecret is httpOnly cookies. And I never even store them in client-side memory.

I think this is as secure as I could get. Now I'm happy with this solution.

Spectacled answered 18/4, 2020 at 23:20 Comment(1)
thank you for the detailed answer, helped understand this moreHervey

© 2022 - 2024 — McMap. All rights reserved.