web
Next.js
In this quickstart you are going to build an application with Next.js and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.
The docker compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-javascript-nextjs-web.
Prerequisites
- Node LTS
- Docker: The quickest way to stand up FusionAuth. (There are other ways).
- git: Not required but recommended if you want to track your changes.
General Architecture
While this sample application doesn’t have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.
In that case, the system might look like this before FusionAuth is introduced.
Request flow during login before FusionAuth
The login flow will look like this after FusionAuth is introduced.
Request flow during login after FusionAuth
In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.
Getting Started
In this section, you’ll get FusionAuth up and running, and configured with the ChangeBank application.
Clone The Code
First off, grab the code from the repository and change into that directory.
git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-nextjs-web.git
cd fusionauth-quickstart-javascript-nextjs-web
Run FusionAuth Via Docker
In the root directory of the repo you’ll find a Docker compose file (docker-compose.yml) and an environment variables configuration file (.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:
docker compose up -d
This will start three containers, once each for FusionAuth, Postgres, and Elastic.
Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth comes up for the first time, it will look at the kickstart/kickstart.json
file and configure FusionAuth to a certain initial state.
If you ever want to reset the FusionAuth system, delete the volumes created by docker compose by executing docker compose down -v
, then re-run docker compose up -d
.
FusionAuth will be configured with these settings
- Your client Id is
e9fdb985-9173-4e01-9d73-ac2d60d1dc8e
. - Your client secret is
super-secret-secret-that-should-be-regenerated-for-production
. - Your example username is
richard@example.com
and the password ispassword
. - Your admin username is
admin@example.com
and the password ispassword
. - The base URL of FusionAuth
http://localhost:9011/
.
You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.
Create Next.js Application
In this section, you’ll set up a basic Next.js application with two pages.
- Homepage
- Account - protected
Create a new application using the npx
.
npx create-next-app@latest changebank --ts --eslint --no-tailwind --src-dir --app --import-alias "@/*"
Make sure you are in your new directory changebank
.
cd changebank
Install NextAuth.js, which simplifies integrating with FusionAuth and creating a secure web application.
npm install next-auth
Copy environment variables from our complete application example.
cp ../complete-application/.env.example .env.local
Also copy an image file into a new directory within public
called img
.
mkdir ./public/img && cp ../complete-application/public/img/money.jpg ./public/img/money.jpg
As you will be recreating all of the files in our app directory, please delete all files within /src/app
.
rm -rf ./src/app && mkdir ./src/app
Authentication
Next.js 13.2 introduced Route Handlers, which are the preferred way to handle REST-like requests. In the Changebank
application you can configure NextAuth.js FusionAuth’s provider in a new route handler by creating a file within src/app/api/auth/[...nextauth]/route.ts
.
On first load of Next.js this file will make sure that you have all of the correct environment variables. The variables are then exported in an object called authOptions
which can be imported on the server when you need to get our session using getServerSession
.
The FusionAuthProvider
is then provided to NextAuth
as a provider for any GET
or POST
commands that are sent to the /api/auth/*
route.
Create a new file named src/app/api/auth/[...nextauth]/route.ts
and copy the following code for the ChangeBank application.
import NextAuth from "next-auth"
import FusionAuthProvider from "next-auth/providers/fusionauth"
const fusionAuthIssuer = process.env.FUSIONAUTH_ISSUER;
const fusionAuthClientId = process.env.FUSIONAUTH_CLIENT_ID;
const fusionAuthClientSecret = process.env.FUSIONAUTH_CLIENT_SECRET;
const fusionAuthUrl = process.env.FUSIONAUTH_URL;
const fusionAuthTenantId = process.env.FUSIONAUTH_TENANT_ID;
const missingError = 'missing in environment variables.';
if (!fusionAuthIssuer) {
throw Error('FUSIONAUTH_ISSUER' + missingError)
}
if (!fusionAuthClientId) {
throw Error('FUSIONAUTH_CLIENT_ID' + missingError)
}
if (!fusionAuthClientSecret) {
throw Error('FUSIONAUTH_CLIENT_SECRET' + missingError)
}
if (!fusionAuthUrl) {
throw Error('FUSIONAUTH_URL' + missingError)
}
if (!fusionAuthTenantId) {
throw Error('FUSIONAUTH_TENANT_ID' + missingError)
}
export const authOptions =
{
providers: [
FusionAuthProvider({
issuer: fusionAuthIssuer,
clientId: fusionAuthClientId,
clientSecret: fusionAuthClientSecret,
wellKnown: `${fusionAuthUrl}/.well-known/openid-configuration/${fusionAuthTenantId}`,
tenantId: fusionAuthTenantId, // Only required if you're using multi-tenancy
}),
],
}
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
App Customization
Styles
Create a new file named src/app/globals.css
and copy the below CSS for the ChangeBank application.
h1 {
color: #096324;
}
h3 {
color: #096324;
margin-top: 20px;
margin-bottom: 40px;
}
a {
color: #096324;
}
p {
font-size: 18px;
}
.header-email {
color: #096324;
margin-right: 20px;
}
.fine-print {
font-size: 16px;
}
body {
font-family: sans-serif;
padding: 0px;
margin: 0px;
}
.h-row {
display: flex;
align-items: center;
}
#page-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
#page-header {
flex: 0;
display: flex;
flex-direction: column;
}
#logo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
}
.menu-bar {
display: flex;
flex-direction: row-reverse;
align-items: center;
height: 35px;
padding: 15px 50px 15px 30px;
background-color: #096324;
font-size: 20px;
}
.menu-link {
font-weight: 600;
color: #ffffff;
margin-left: 40px;
}
.menu-link {
font-weight: 600;
color: #ffffff;
margin-left: 40px;
}
.inactive {
text-decoration-line: none;
}
.button-lg {
width: 150px;
height: 30px;
background-color: #096324;
color: #ffffff;
font-size: 16px;
font-weight: 700;
border-radius: 10px;
text-align: center;
text-decoration-line: none;
cursor: pointer;
}
.column-container {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 60px 20px 20px 40px;
}
.balance {
font-size: 50px;
font-weight: 800;
}
.change-label {
font-size: 20px;
margin-right: 5px;
}
.change-input {
font-size: 20px;
height: 40px;
text-align: end;
padding-right: 10px;
}
.change-submit {
font-size: 15px;
height: 40px;
margin-left: 15px;
border-radius: 5px;
}
.change-message {
font-size: 20px;
margin-bottom: 15px;
}
.error-message {
font-size: 20px;
color: #ff0000;
margin-bottom: 15px;
}
.app-container {
flex: 0;
min-width: 440px;
display: flex;
flex-direction: column;
margin-top: 40px;
margin-left: 80px;
}
.change-container {
flex: 1;
}
Login Button
Create a new file in src/components/LoginButton.tsx
that will be used for a button component. Our login button will only be used on the client side so make sure to add use client
at the top of this file. For this button you can use the signIn
and signOut
functions from next-auth/react
. By passing in the session from our pages you can determine if the Log in
or Log out
should be shown.
Copy the below code for the ChangeBank application into src/components/LoginButton.tsx
.
'use client';
import { signIn, signOut } from 'next-auth/react';
export default function LoginButton({ session }: { session: any }) {
if (session) {
return (
<>
Status: Logged in as {session?.user?.email} <br />
<button className="button-lg" onClick={() => signOut()}>
Log out
</button>
</>
);
}
return (
<>
<button className="button-lg" onClick={() => signIn()}>
Log in
</button>
</>
);
}
Login Link
Create a new file in src/components/LoginLink.tsx
that will be used for a link component. Our login link will only be used on the client side so make sure to add use client
at the top of this file. For this link you can use the signIn
function from next-auth/react
.
Copy the below code for the ChangeBank application into src/components/LoginLink.tsx
.
'use client';
import { signIn } from 'next-auth/react';
export default function LoginButton({ session }: { session: any }) {
return (
<>
<p>
To get started,{' '}
<a
onClick={() => signIn()}
style={{ textDecoration: 'underline', cursor: 'pointer' }}
>
log in or create a new account.
</a>
</p>
</>
);
}
Layout
If this is your first time using the Next.js App Router, you should read through Routing Fundamentals.
Below you will find the full code for the Root Layout.
This has the overall structure of our application. The other pages will be added where {children}
is located.
Create a new file named src/app/layout.tsx
and copy the below code to create the layout for the ChangeBank application.
import './globals.css';
import type { Metadata } from 'next';
import Image from 'next/image';
import LoginButton from '../components/LoginButton';
import { getServerSession } from 'next-auth/next';
import { authOptions } from './api/auth/[...nextauth]/route';
export const metadata: Metadata = {
title: 'FusionAuth Next.js with NextAuth.js',
description: 'Generated by create next app',
};
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await getServerSession(authOptions);
return (
<html lang="en">
<body>
<div id="page-container">
<div id="page-header">
<div id="logo-header">
<Image
src="https://fusionauth.io/assets/img/samplethemes/changebank/changebank.svg"
alt="change bank logo"
width="257"
height="55"
/>
<LoginButton session={session} />
</div>
<div id="menu-bar" className="menu-bar">
{session ? (
<>
<a
href="/makechange"
className="menu-link"
style={{ textDecorationLine: 'underline' }}
>
Make Change
</a>
<a
href="/account"
className="menu-link"
style={{ textDecorationLine: 'underline' }}
>
Account
</a>
</>
) : (
<>
<a className="menu-link">About</a>
<a className="menu-link">Services</a>
<a className="menu-link">Products</a>
<a
className="menu-link"
style={{ textDecorationLine: 'underline' }}
>
Home
</a>
</>
)}
</div>
</div>
{children}
</div>
</body>
</html>
);
}
Home Page
Create a new file src/app/page.tsx
which will have the Homepage details. Not much here just an image and another Login button.
import { getServerSession } from 'next-auth';
import Image from 'next/image';
import { redirect } from 'next/navigation';
import { authOptions } from './api/auth/[...nextauth]/route';
import LoginLink from '../components/LoginLink';
export default async function Home() {
const session = await getServerSession(authOptions);
if (session) {
redirect('/account');
}
return (
<main>
<div style={{ flex: '1' }}>
<div className="column-container">
<div className="content-container">
<div style={{ marginBottom: '100px' }}>
<h1>Welcome to Changebank</h1>
<LoginLink session={session} />
</div>
</div>
<div style={{ width: '100%', maxWidth: 800 }}>
<Image
src="/img/money.jpg"
alt="money"
width={1512}
height={2016}
style={{
objectFit: 'contain',
width: '100%',
position: 'relative',
height: 'unset',
}}
/>
</div>
</div>
</div>
</main>
);
}
Account Page
Create a new file src/app/account/page.tsx
which will have the Account details.
One special note here is that there is a check to see if the session is missing. If it is, you redirect back to the homepage which protects this page on the server for unauthorized access. (You can find the same when a user is logged in on the homepage, it will redirect to /account
)
Here’s the contents of src/app/account/page.tsx
.
import { getServerSession } from 'next-auth';
import Image from 'next/image';
import { authOptions } from '../api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';
export default async function Account() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/');
}
return (
<section>
<div style={{ flex: '1' }}>
<div className="column-container">
<div className="app-container">
<h3>Your balance</h3>
<div className="balance">$0.00</div>
</div>
</div>
</div>
</section>
);
}
Make Change Page
Finally, we’ll add some business logic for logged in users to make change with the following code in src/app/makechange/page.tsx
:
import { getServerSession } from 'next-auth';
import { authOptions } from '../api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';
import MakeChangeForm from '../../components/MakeChangeForm';
export default async function MakeChange() {
const session = await getServerSession(authOptions);
if (!session) {
redirect('/');
}
return (
<>
<MakeChangeForm />
</>
);
}
If the user session is not present the user is redirect back the the homepage at the base route. If the user is present then the MakeChangeForm
is presented. Create a new file located at /src/components/MakeChangeForm.tsx
with the below code. This component has all of the business logic needed for taking in a dollar amount of money and returning the correct amount of each coin.
'use client';
import { useEffect, useState } from 'react';
var coins = {
quarters: 0.25,
dimes: 0.1,
nickels: 0.05,
pennies: 0.01,
};
export default function MakeChangeForm() {
const [message, setMessage] = useState('');
const [amount, setAmount] = useState(0);
useEffect(() => {
setMessage('');
setAmount(0);
}, []);
const onMakeChange = (event: any) => {
event.preventDefault();
try {
setMessage('We can make change for');
let remainingAmount = amount;
for (const [name, nominal] of Object.entries(coins)) {
let count = Math.floor(remainingAmount / nominal);
remainingAmount =
Math.ceil((remainingAmount - count * nominal) * 100) / 100;
setMessage((m) => `${m} ${count} ${name}`);
}
setMessage((m) => `${m}!`);
} catch (ex: any) {
setMessage(
`There was a problem converting the amount submitted. ${ex.message}`
);
}
};
return (
<section>
<div style={{ flex: '1' }}>
<div className="column-container">
<div className="app-container change-container">
<h3>We Make Change</h3>
<div className="change-message">{message}</div>
<form onSubmit={onMakeChange}>
<div className="h-row">
<div className="change-label">Amount in USD: $</div>
<input
className="change-input"
type="number"
step={0.01}
name="amount"
value={amount}
onChange={(e) => setAmount(+e.target.value)}
/>
<input
className="change-submit"
type="submit"
value="Make Change"
/>
</div>
</form>
</div>
</div>
</div>
</section>
);
}
Run the Application
You can now open up an incognito window and visit the NextJS app at http://localhost:3000/. Log in with the user account you created when setting up FusionAuth, and you’ll see the email of the user next to a logout button.
npm run dev
Try clicking the Login
button at the top or center of the screen.
This will take you through the NextAuth.js
authentication flow. First prompting you to select Sign in with FusionAuth
.
You can then login to FusionAuth with Email: richard@example.com
Password: password
(as you might expect not ideal for production.)
This will then take you back to the application in the API route it will check for a session and appropriately redirect you to the /account
route when your session has been established.
Next Steps
This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you’re going to want to do.
FusionAuth Customization
FusionAuth gives you the ability to customize just about everything with the user’s experience and your application’s integration. This includes
- Hosted pages such as login, registration, email verification, and many more
- Email templates
- User data and custom claims in access token JWTs
Security
- You may want to customize the token expiration times and policies in FusionAuth
- You may also want to check the next-auth.js FusionAuth Docs for any of the FusionAuth Provider options that you might require.
- Choose password rules and a hashing algorithm that meet your security needs
Tenant and Application Management
- Model your application topology using Applications, Roles, Groups, Entities, and more
- Set up MFA, Social login, and/or SAML integrations
- Integrate with external systems using Webhooks, SCIM, and Lambdas