DEV Community

Cover image for The Complete Guide to Next.js Authentication
Nader Dabit
Nader Dabit

Posted on • Updated on

The Complete Guide to Next.js Authentication

Cover image by Kai Pilger

In this guide you will learn how to implement authentication in a Next.js app. I will cover client authentication, authenticated server-rendered pages, authenticated API routes, protected routes, and redirects.

The authentication service will be implemented with AWS Amplify, but the ideas and strategies covered here will work for any authentication service like Auth0 / Okta or even a custom back end implementation as long as it provides a way to manage sessions across the client and server.

The code for this project is located here. Video walkthrough is here.


Next.js Overview

Next.js combines client-side rendering with pre-rendered HTML in the form of static and server-rendered pages. The framework also makes it really easy to create APIs with API routes.

When running a build, the framework will determine whether a page should be generated statically or if it should be a server-rendered. By default all pages are statically generated unless the page is using the getServerSideProps function to pass props into the page. Also, all API routes will by default be server rendered.

Next.js Authentication Concepts

When working within a Next.js app you typically want to take advantage of all of these features and have your APIs work seamlessly across the framework (client and server). The problem that it is often not easy to securely access the user session on both the client and the server.

In this guide, I'll show you how to enable user authentication and authorization to implement the following:

  1. Client authentication
  2. Accessing the user session on the client
  3. Protected client routes
  4. Client-side redirects
  5. Accessing the user session in a server-side route (getServerSideProps)
  6. Protected server routes (getServerSideProps)
  7. Server-side redirects (getServerSideProps)
  8. Accessing the user session in an API route
  9. Social sign-in (OAuth)
  10. Deploying the app using the Next.js Serverless Component

Getting started

To get started, first create a new Next.js app:

npx create-next-app next-authentication
Enter fullscreen mode Exit fullscreen mode

Next, change into the new directory and install the dependencies:

cd next-authentication
npm install aws-amplify @aws-amplify/ui-react emotion
Enter fullscreen mode Exit fullscreen mode

Next, initialize a new Amplify project:

amplify init

> Choose defaults when prompted
Enter fullscreen mode Exit fullscreen mode

If you do not yet have the Amplify CLI installed and configured, see this video for a full walkthrough.

Next, add the authentication service:

amplify add auth

? Do you want to use the default authentication and security configuration? Default configuration
? How do you want users to be able to sign in? Username
? Do you want to configure advanced settings? No, I am done.
Enter fullscreen mode Exit fullscreen mode

Next, deploy the authentication service:

amplify push --y
Enter fullscreen mode Exit fullscreen mode

Enabling Amplify SSR

Next, to enable Amplify SSR support, open pages/_app.js and add the following at the top of the file:

import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({
  ...config,
  ssr: true
})
Enter fullscreen mode Exit fullscreen mode

🔥 Setting ssr to true is all you need to do to make your Amplify app SSR aware.

Creating the auth / profile route

Next, create a new file in the pages directory called profile.js.

Here, we will enable authentication by using the withAuthenticator component. This component will create a user authentication flow, enabling a user to sign up with MFA and sign in.

In this file, add the following code:

// pages/profile.js
import { useState, useEffect } from 'react'
import { Auth } from 'aws-amplify'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'

function Profile() {
  const [user, setUser] = useState(null)
  useEffect(() => {
    // Access the user session on the client
    Auth.currentAuthenticatedUser()
      .then(user => {
        console.log("User: ", user)
        setUser(user)
      })
      .catch(err => setUser(null))
  }, [])
  return (
    <div>
      { user && <h1>Welcome, {user.username}</h1> }
      <AmplifySignOut />
    </div>
  )
}

export default withAuthenticator(Profile)
Enter fullscreen mode Exit fullscreen mode

Finally, update pages/_app.js to add some navigation to link between pages:

import '../styles/globals.css'
import Link from 'next/link'
import { css } from 'emotion'

import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({
  ...config,
  ssr: true
})

export default function MyApp({ Component, pageProps }) {
  return (
    <div>
      <nav className={navStyle}>
        <Link href="/">
          <span className={linkStyle}>Home</span>
        </Link>
        <Link href="/profile">
          <span className={linkStyle}>Profile</span>
        </Link>
      </nav>
      <Component {...pageProps} />
    </div>
  )
}

const linkStyle = css`
  margin-right: 20px;
  cursor: pointer;
`

const navStyle = css`
  display: flex;
`
Enter fullscreen mode Exit fullscreen mode

Optional - Styling the component

You can configure styling for the authentication component. For example, to try and match the blue color scheme that the Next.js starter ships with, you can add the following to the bottom of styles/globals.css:

:root {
  --amplify-primary-color: #0083e8;
  --amplify-primary-tint: #006ec2;
  --amplify-primary-shade: #006ec2;
}
Enter fullscreen mode Exit fullscreen mode

Creating an account and signing in

Now that the Profile route has been created, let's test it out by creating a new account and signing in.

npm run dev
Enter fullscreen mode Exit fullscreen mode

Click here for more ways to customize the withAuthenticator component.

You should be able to navigate to the /profile route to create an account and sign in.

Using the Auth class directly

If you want to build your own custom authentication flow, you can also leverage the Auth class which has over 30 methods for managing user authentication state, including methods like signUp, confirmSignUp, signIn, and forgotPassword.

Accessing user session in an SSR route

Now that users can sign in, let's create a new route to test out SSR.

Create a new route called /protected.js in the pages directory.

Here, we want to have a route that authenticates the user on the server and returns either a success or error message based on the user's authentication state.

// pages/protected.js

import { withSSRContext } from 'aws-amplify'

function Protected({ authenticated, username }) {
  if (!authenticated) {
    return <h1>Not authenticated</h1>
  }
  return <h1>Hello {username} from SSR route!</h1>
}

export async function getServerSideProps(context) {
  const { Auth } = withSSRContext(context)
  try {
    const user = await Auth.currentAuthenticatedUser()
    console.log('user: ', user)
    return {
      props: {
        authenticated: true, username: user.username
      }
    }
  } catch (err) {
    return {
      props: {
        authenticated: false
      }
    }
  }
}

export default Protected
Enter fullscreen mode Exit fullscreen mode

Then update the nav in pages/_app.js with a link to the new route:

<Link href="/protected">
  <span className={linkStyle}>Protected route</span>
</Link>
Enter fullscreen mode Exit fullscreen mode

Now, when you are signed in you will notice that you will be able to access the authenticated user in the getServerSideProps method. You should also see the user object logged out to the terminal.

This is done using the withSSRContext function to destructure Auth from aws-amplify and making a call to Auth.currentAuthenticatedUser(). When gaining access to the Auth class in this way, Amplify automatically will read the request object and give you access to the signed in user's session on both API routes as well as SSR routes.

Accessing user session in an API route

In this API route, we want to access the user and return either null for a user who is not authenticated or the username for a user who is authenticated.

To do so, create a new file in pages/api called check-user.js:

// pages/api/check-user.js
import Amplify, { withSSRContext } from 'aws-amplify'
import config from "../../src/aws-exports.js"

// Amplify SSR configuration needs to be enabled within each API route
Amplify.configure({ ...config, ssr: true })

export default async (req, res) => {
  const { Auth } = withSSRContext({ req })
  try {
    const user = await Auth.currentAuthenticatedUser()
    res.json({ user: user.username })
  } catch (err) {
    res.statusCode = 200
    res.json({ user: null })
  }
}
Enter fullscreen mode Exit fullscreen mode

When you navigate or try to access /api/check-user you will notice that the user object is available when you are authenticated and not available when you are not authenticated.

Client-side redirect

Often you will want to detect whether a user is signed in and either allow access or redirect them based on whether they are authenticated or based on their credentials.

To do this you can use the withRouter hook from Next.js to programmatically route based on user state. Let's try this out.

Create a new file in the pages directory called protected-client-route.js.

Here, add the following code:

import { useState, useEffect } from 'react'
import { Auth } from 'aws-amplify'
import { useRouter } from 'next/router'

function ProtectedClient() {
  const [user, setUser] = useState(null)
  const router = useRouter()
  useEffect(() => {
    Auth.currentAuthenticatedUser()
      .then(user => setUser(user))
      // if there is no authenticated user, redirect to profile page
      .catch(() => router.push('/profile'))
  }, [])
  if (!user) return null
  return <h1>Hello {user.username} from client route!</h1>
}

export default ProtectedClient
Enter fullscreen mode Exit fullscreen mode

Next, add a link to this route in pages/_app.js:

<Link href="/protected-client-route">
  <span className={linkStyle}>Protected client route</span>
</Link>
Enter fullscreen mode Exit fullscreen mode

If you try to access the protected client route you will be automatically redirected to the profile route if you are not authenticated, and allowed to view the page if you are authenticated.

Server-side redirects

One of the benefits of SSR is the ability to implement server-side redirects. Using a server-side redirect is more secure in that you have the option to not render any html at all, instead redirecting the user to another page.

Open pages/protected.js and update with the following code:

// pages/protected.js
import { withSSRContext } from 'aws-amplify'

function Protected({ username }) {
  return <h1>Hello {username} from SSR route!</h1>
}

export async function getServerSideProps({ req, res }) {
  const { Auth } = withSSRContext({ req })
  try {
    const user = await Auth.currentAuthenticatedUser()
    return {
      props: {
        authenticated: true,
        username: user.username
      }
    }
  } catch (err) {
    res.writeHead(302, { Location: '/profile' })
    res.end()
  }
  return {props: {}}
}

export default Protected
Enter fullscreen mode Exit fullscreen mode

When you attempt to access this route, you will be redirected to the profile route if you are not signed in.

Social sign-in (OAuth)

To add social sign in, run amplify update auth and choose Apply default configuration with Social Provider.

From here you can add social sign in with Google, Facebook, or Amazon.

Once social sign in has been enabled, you can then sign users in from your app using the following code:

// username / password + all OAuth providers
Auth.federatedSignIn()

// specifying an OAuth provider
<button onClick={() => Auth.federatedSignIn({provider: 'Facebook'})}>Open Facebook</button>
<button onClick={() => Auth.federatedSignIn({provider: 'Google'})}>Open Google</button>
<button onClick={() => Auth.federatedSignIn({provider: 'Amazon'})}>Open Amazon</button>
Enter fullscreen mode Exit fullscreen mode

Deploying the Next.js app to AWS with the Serverless Framework

To deploy the app to AWS using the Serverless Framework and the Serverless Next Component, first create a file called serverless.yml at the root of your application.

Next, add the following two lines of configuration (feel free to change myNextApp to whatever name you'd like to use):

myNextApp:
    component: "@sls-next/serverless-component@1.17.0" 
Enter fullscreen mode Exit fullscreen mode

Next, deploy using npx:

npx serverless
Enter fullscreen mode Exit fullscreen mode

If you've never used an AWS CLI, you may have to configure your AWS credentials. See basic instructions here

Video Walkthrough

Conclusion

The final code for this project is located here

Big shout out to Eric Clemmons of the Amplify team who spearheaded this project and built this functionality into Amplify.

For part 2, we will be learning how to combine Auth and Data to accomplish the following:

  1. Fetching data in getStaticPaths for hydrating during SSG
  2. Making authenticated API calls in API routes
  3. Making an authenticated API request in getServerSideProps

Top comments (29)

Collapse
 
justincy profile image
Justin • Edited

UPDATE: support for redirects in getServerSideProps was released in v10. See the docs for more information about redirects: nextjs.org/docs/basic-features/dat...

OLD: FYI, redirects in getServerSideProps aren't 100% supported and a side-effect of that is they currently cause a full page reload. There's an RFC for redirects to be fully supported.

Collapse
 
wataruoguchi profile image
Wataru Oguchi

For a workaround, I found this comment.

I made the catch to return the following snippet, and I removed res.writeHead(302, { Location: '/profile' }) and res.end().

return {
  redirect: {
    destination: "/profile",
    statusCode: 302,
  },
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
paragbaxi profile image
Parag Baxi

I can't even get it to work with the code above.

Collapse
 
jamescarr profile image
James Carr

This was really great and easy to follow, thanks for the tutorial!

One small bit of feedback would be to change the check-user.js api route to use a non-200 statusCode for an unauthenticated user.

export default async (req, res) => {
  const { Auth } = withSSRContext({ req })
  try {
    const user = await Auth.currentAuthenticatedUser()
    res.json({ user: user.username })
  } catch (err) {
    res.statusCode = 401
    res.json({ user: null })
  }
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
oahmaro profile image
Osama Ahmaro

Do i have to deploy the app with serverless framework? can i just use amplify to deploy nextjs app, and what about deploying API routes?

Collapse
 
michael_webdev profile image
Michael B.

I'm currently working on the same stack and have the exact same question!

Collapse
 
dabit3 profile image
Nader Dabit

Hi Osama & Michael,

Amplify is releasing SSR support sometime soon, but for now if you need SSR or API routes on AWS you need to use the Serverless Framework. If you are running a completely static Next.js build, you can use the Amplify hosting service.

docs.amplify.aws/guides/hosting/ne...

Thread Thread
 
michael_webdev profile image
Michael B.

Really glad to know it will be available on Amplify! I have my frontend on Vercel right now but I will be happy to move everything on Amplify in the foreseeable future.

Collapse
 
gregfullard profile image
Greg Fullard

Great article and thanks a lot. I've been running into some issues with the protected server side rendered page.

When trying to access the page the whole app crashes with the following in the console:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
Enter fullscreen mode Exit fullscreen mode

I tested from many angles, and even when cloning your source code I ran into the issue.

There are some discussions on the Next.js GitHub (github.com/vercel/next.js/discussi...) on the same topic. Not sure if it's a Next.js version thing

For now, I'll continue to dig for a better understanding of what's going on.

Collapse
 
sotomaque profile image
sotomaque

same. any luck with this bug?

Collapse
 
ahmadawais profile image
Ahmad Awais ⚡️ • Edited

Excellent intro, Nader. I've just switched to npm@7 CLI and I'm having issues installing the dependencies in the second step.

This works fine on npm v6.14.8

error

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! Found: react@16.14.0
npm ERR! node_modules/react
npm ERR!   react@"16.14.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"16.13.1" from react-native@0.63.3
npm ERR! node_modules/react-native
npm ERR!   peer react-native@">=0.56" from react-native-get-random-values@1.5.0
npm ERR!   node_modules/@aws-sdk/middleware-retry/node_modules/react-native-get-random-values
npm ERR!     react-native-get-random-values@"^1.4.0" from @aws-sdk/middleware-retry@1.0.0-gamma.7
npm ERR!     node_modules/@aws-sdk/middleware-retry
npm ERR!       @aws-sdk/middleware-retry@"1.0.0-gamma.7" from @aws-sdk/client-cognito-identity@1.0.0-gamma.8
npm ERR!       node_modules/@aws-sdk/client-cognito-identity
npm ERR!       2 more (@aws-sdk/client-lex-runtime-service, @aws-sdk/client-s3)
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
Enter fullscreen mode Exit fullscreen mode
Collapse
 
geraudi profile image
geraudi

Hi Nader, Thanks for this great tutorial.
For now, we need have to add ssr configuration in all pages/* as mentioned in Amplify documentation :

Once vercel/next.js#16977 is resolved, you can hoist Amplify.configure into pages/_app.js. Until then, be sure that all pages/* run Amplify.configure({ ...awsExports, ssr: true })

Collapse
 
ibrahimcesar profile image
Ibrahim Cesar

Great content as usual! Waiting for part 2!

Collapse
 
dabit3 profile image
Nader Dabit

Thanks Ibrahim!

Collapse
 
francisco profile image
Francisco M. Delgado • Edited

@dabit currently trying to deploy via the cli and I get this error

pages with `getServerSideProps` can not be exported

. Does anyone have a workaround? The app works as expected when developing locally.

Collapse
 
eusholli profile image
Geoff Hollingworth

Hi I have tried to add Facebook as a federated identity provider and I also want to carry on using the default withAuthenticator and associated UI. The first strange thing I notice is that there is a new button a the top of my login panel but it says "Sign in with AWS". When I click I get taken to a new login screen with Facebook at the top. When I try to login in though there is a URL error in the debug on the following API call:
cognito-identity.us-east-1.amazona...
Response error: 400
{"__type":"ValidationException","message":"1 validation error detected: Value '{cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXX=}' at 'logins' failed to satisfy constraint: Map value must satisfy constraint: [Member must have length less than or equal to 50000, Member must have length greater than or equal to 1]"}

I appreciate any directions on how to start debugging. I have tried googling but cannot find where to start. I can see the user has been added to the AWS Cognito user pool in AWS Console. I upload some screen captures of the initial flow.
dev-to-uploads.s3.amazonaws.com/up...
dev-to-uploads.s3.amazonaws.com/up...

Collapse
 
uuykay profile image
William Kuang

I have this exact same problem!

Collapse
 
eherms profile image
eherms

I'm getting an error:

TypeError: Object(...) is not a function

pointing to the code below.

export async function getServerSideProps(context) {

372 | const { Auth } = withSSRContext(context)
| ^
373 | try {
374 | const user = await Auth.currentAuthenticatedUser()
375 | console.log('user: ', user)

Any suggestions?

Collapse
 
jerocosio profile image
Jerónimo Cosío • Edited

Amazing guidee, thanks for posting this! Any chance that for the second part you could add some examples around handling data based on roles?

Collapse
 
marcostreng profile image
Marco Streng

Great post!

I am still struggling with a problem: If a protect a route on server-side the request to Cognito (Auth.currentAuthenticatedUser()) takes about 400-500ms. Doing the same on client-side the request takes only < 15ms. Both ways are implemented like in this example.

Maybe someone has got an idea or made a similar experience?

Collapse
 
vfulco profile image
Vincent Fulco (It / It's)

Really strange, trying to hit the 'protected' page reroutes me back to 'profile' even though all my authenticated info pops up in the console and I can reach the 'protected-client-route'. Tried this a few times and looking at the code.

Collapse
 
szymonkochanski profile image
Szymon Kochanski

A small suggestion for importing emotion:

npm i @emotion/css
Enter fullscreen mode Exit fullscreen mode

Then, to use it:

import { css } from '@emotion/css'
Enter fullscreen mode Exit fullscreen mode
Collapse
 
vfulco profile image
Vincent Fulco (It / It's)

Thanks for this mention. For some reason, I couldn't get it to work but did so using "import { css } from '@emotion/react'; "

Collapse
 
keefdrive profile image
Keerthi

Thanks..I have just started to learn about next.js, I was looking for something like this