After my last post Custom Authentication UI for Amplify and Next.js website with React Hook Form, Next.js, Tailwind CSS I had wanted to try NextAuth.js
To begin, I removed all uses of the AWS Amplify Auth class. I had intended to do a custom UI, however, it seems currently you can only use the hosted UI when using NextAuth.js (unless you are doing a custom provider https://next-auth.js.org/providers/credentials#example-ui). Which makes sense as it's supposed to be drop-in authentication for your app/website. However, please correct me if I'm wrong.
Amazon Cognito
The authentication flow will need an Amazon Cognito user pool. Follow the steps below to add one if you don't have one already set up:
Log into http://console.aws.amazon.com/ and click Manage User Pools
Click 'Create a user pool', mine is in the top right as I already have user pools set up.
Enter the pool name and select Review Defaults -feel free to step through but for this case, we just need to change a few defaults
Important: select Add app client - this will contain the details required to hook the client up to the authentication service
And click 'Add an app client'
Ensure you enter a App client name and Generate client secret is checked
Select Return to pool details
Click 'Create pool'
Now we need to setup a domain, select 'Domain name' from the left hand menu.
Enter a domain name -
Check availability
Save the domain name as we'll need it later
Click 'Save changes'
Almost there 😓, select App client settings
- Check Cognito User Pool
- Add a Callback URL, for local development that would be http://localhost:3000/api/auth/callback/cognito
- Check Authorization code grant
- And the following Allowed oAuth Scopes: Email, openid and profile
- And Save changes
That's all the setup needed, one final step is to get the relevant .env variables required for NextAuth.js. Go back to App clients on the left menu, make a note of the App client id. Select Show Details and make a more of the App client secret.
NextAuth.js
Now for the fun part. In an existing or new project install the NextAuth.js dependency:
yarn add next-auth
// or
npm install next-auth
.env
Before adding any js lets get the environment variables setup. Add a .env.local
file in the root of the project
COGNITO_CLIENT_ID=*App client id*
COGNITO_CLIENT_SECRET=*App client secret*
COGNITO_DOMAIN=*Domain name*
Replace with the id, secret and domain we set up previously
These details can be found by logging into and going to Cognito > Manage user pools
API Route
As per the documentation add a file called [...nextauth].js
in pages/api/auth
.
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
export default NextAuth({
providers: [
Providers.Cognito({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
domain: process.env.COGNITO_DOMAIN,
})
],
debug: process.env.NODE_ENV === 'development' ? true : false
})
We supply NextAuth with a host of providers, which I've added the cognito details. Adding debug for development mode enables various logs in the terminal. Useful if there are any issues and seeing the key-value pairs for the signed-in user.
Now any requests to api/auth/*
will be handle by NextAuth, for example sign in, sign out etc).
Use NextAuth hook
Inside pages/index.js
replace all code with the following:
import Head from 'next/head'
import Link from 'next/link'
import { signIn, signOut, useSession } from 'next-auth/client'
export default function Home() {
const [session, loading] = useSession()
return (
<div>
<Head>
<title>Authentication with NextAuth and AWS Cognito</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 flex items-center flex-col">
<a>
<div className="flex-shrink-0 flex items-center bg-orange-500 h-20 w-20 border-radius p-2 font-bold text-4xl">
dlw
</div>
</a>
{!session &&
<>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Example NextAuth Sign In
</h2>
<button
type="submit"
className="inline-flex items-center justify-center w-1/2 mt-12 rounded-md border border-transparent px-5 py-3 bg-gray-900 text-base font-medium text-white shadow hover:bg-black focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-rose-500 sm:px-10 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
onClick={() => signIn('cognito', {
callbackUrl: `${window.location.origin}/protected`
})}
>
Sign In
</button>
</>
}
{session &&
<>
<h1 className="my-6 text-center text-3xl font-extrabold text-gray-900">
Welcome, {session.user.name ?? session.user.email}
</h1>
<nav>
<Link href="/protected">
<a className="text-orange-500 hover:bg-black hover:text-white">Protected Page</a>
</Link>
</nav>
<button
type="submit"
className="inline-flex items-center justify-center w-1/2 mt-12 rounded-md border border-transparent px-5 py-3 bg-gray-900 text-base font-medium text-white shadow hover:bg-black focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-rose-500 sm:px-10 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={loading}
onClick={() => signOut({
callbackUrl: `${window.location.origin}`
})}
>
Sign Out
</button>
</>
}
</div>
</div>
</main>
</div>
)
}
The useSession hook is the simplest way to check the logged-in state. if the user isn't logged in show a Sign in button with a click handler:
onClick={() => signIn('cognito', {
callbackUrl: `${window.location.origin}/client-protected`
})}
I've added cognito as the first argument which ensures the user is shown the Cognito hosted UI when clicking the sign-in button. The callbackUrl will redirect to a protected page
At this point, we can test the sign-up or sign in. Run the development server yarn run dev
and go to http://localhost:3000/. if you select Sign in you should be able to log in with an existing account or sign up for a new account.
Session Provider
In a multi-page application, we'll want to add the session provider. This ensure session state can be shared between pages:
which improves performance, reduces network traffic and avoids component state changes while rendering -
Update pages/_app.js
:
import { Provider } from 'next-auth/client'
import "tailwindcss/tailwind.css";
function MyApp({ Component, pageProps }) {
return (
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
)
}
export default MyApp
Adding the Provider import import { Provider } from 'next-auth/client'
and wrapping the main app component in the Provider component:
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
Once logged in you'll be re
Protected Page
For the protected page, add a new page called protected.js
inside /pages directory (the actual page name isn't important):
import Head from 'next/head'
import Link from 'next/link'
import { getSession } from 'next-auth/client'
function Protected() {
return (
<div>
<Head>
<title>Authentication with NextAuth and AWS Cognito</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 flex items-center flex-col">
<a>
<div className="flex-shrink-0 flex items-center bg-orange-500 h-20 w-20 border-radius p-2 font-bold text-4xl">
dlw
</div>
</a>
<h1 className="my-6 text-center text-3xl font-extrabold text-gray-900">Client Protected</h1>
<nav>
<Link href="/">
<a className="text-orange-500 hover:bg-black hover:text-white">Home</a>
</Link>
</nav>
<div className="prose">
<p>Faucibus commodo massa rhoncus, volutpat. <strong>Dignissim</strong> sed <strong>eget risus enim</strong>. Mattis mauris semper sed amet vitae sed turpis id. Id dolor praesent donec est. Odio penatibus risus viverra tellus varius sit neque erat velit. Faucibus commodo massa rhoncus, volutpat. Dignissim sed eget risus enim. <a href="#">Mattis mauris semper</a> sed amet vitae sed turpis id.</p>
</div>
</div>
</div>
</main>
</div>
)
}
export default Protected
export async function getServerSideProps(context) {
const { res } = context;
const session = await getSession(context)
if (!session) {
res.writeHead(302, {
Location: "/",
});
return res.end();
}
return {
props: { session }
}
}
The important part is inside the getServerSideProps which checks for a session and if isn't available redirects to the index page with the sign-in button.
Repository: https://github.com/dwhiteGUK/dlw-next-auth-cognito
Deploying to production
Before deploying to production we need to add a NEXTAUTH_URL
to our environment variables. Plus the previous environment variables will need adding to production environment
On Vercel were I host most of my Next.js websites they are added in the settings for the project.