Front-End Web & Mobile

How to Build Next.js Apps with Authentication using AWS Amplify and Auth0

In this post, we’ll show how to authenticate and authorize Next.js applications using an OpenID Connect Identity Provider (OIDC) with AWS Amplify. AWS Amplify is everything frontend developers need to develop and deploy cloud-powered fullstack applications. To enhance the capabilities of AWS Amplify applications, we’ll be leveraging the identity management platform offered by Auth0. Auth0 by Okta is an identity management platform that supports both authentication and authorization.

We’ll demonstrate how to integrate Auth0 with AWS Amplify using a Next.js app, showcase authentication, and deploy it using Amplify Hosting.

Solution Overview

The web application allows users to sign up and login using their email/password or social account like Google. Then, authenticated users will be able to create and delete to do list items. For the backend, the application leverages Amplify to handle authentication with Amazon Cognito, and data management with AWS AppSync and Amazon DynamoDB, providing a seamless and secure experience for users.

The following architecture diagram shows the AWS services used in the solution:

Archtiecture diagram on solution

The following Amazon Cognito concepts are covered in this blog:

  • User pool is a user directory for web and mobile app authentication and authorization
  • Identity pools can be used for federating identities across different authentication providers including Cognito user pools and SAML. It also grants temporary access to other AWS services via OIDC providers.

Here is a summary of how the authentication will flow:

  1. The user initiates a log in from the web application, which redirects them to the Auth0 portal for authentication.
  2. The user can sign in using either a new profile or a social login like Google.
  3. After successful authentication, Auth0 redirects back to Amazon Cognito with an authorization code.
  4. Cognito then exchanges the authorization code for an access token and ID token from Auth0.
  5. Cognito uses the information in the ID token to create a user profile in the Cognito user pool.
  6. Finally, Cognito returns both the access token and ID token to the application, which can be used to access protected resources.

Prerequisites

Creating Auth0 application

Let’s start with setting up an Auth0 Application which represents the application clients that have access to the services configured in Auth0.

  1. Sign up for an Auth0 account (If you have an existing Account with Auth0, log into the account)
  2. Click on the Applications tab in the Auth0 dashboard in the left navigation barAuth0 Dashboard Page
  3. Create a new Auth0 application by clicking the Create Application button
  4. Provide a Name for the application (Here, we use Amplify-Auth0-Demo)
  5. Select the Regular Web Application option as we are using a Next.js app.

Once the application is created, you will be redirected to the dashboard page, showing configuration information like Client ID, Client Secret, and Domain in the Settings tab. Leave the page open as we’ll use the values in the next steps.

Auth0-Application-Dashboard

Cloning Amplify Gen2 To-do application

Next, we’ll want to create a new repository using an existing template from the AWS samples GitHub repository to have the Next.js app ready to go.

  1. Clone the repository locally and open the project in your IDE:
   git clone https://github.com/aws-samples/amplify-next-template.git
  1. Install all dependencies locally in the project, including the necessary packages for defining your backend and client libraries
npm install dotenv react react-intl 

Modifying the auth resource

Now let’s look at the repo’s contents. You will see the frontend under app/ folder and Amplify Gen 2’s backend in the amplify/ folder.

In the amplify/auth/resource.ts, override the existing code with the following:

import {defineAuth, secret} from "@aws-amplify/backend";
import 'dotenv/config';

export const auth = defineAuth({
  loginWith: {
    email: {
      verificationEmailSubject: 'Welcome to the Blog post app 👋 Verify your email!'
    },
    externalProviders: {
      oidc: [
        {
          name: 'Auth0',
          clientId: secret('AUTH0_CLIENT_ID'),
          clientSecret: secret('AUTH0_CLIENT_SECRET'),
          issuerUrl: process.env.ISSUER_URL || '',
          scopes: ['openid', 'profile', 'email']
        },
      ],
      callbackUrls: [process.env.CALLBACK_URL || ''],
      logoutUrls: [process.env.LOGOUT_URL || ''],
    },
  },
});

This code configures an authentication backend that supports email login and integrates with Auth0 as the OIDC provider . It sets up a Cognito user pool on the backend with federated login capabilities.

To summarize the configuration:

  • issuerUrl: is the URL of the identity provider which provides the OpenID tokens
  • callbackUrls defines the URLS to which users are redirected after authenticating with OIDC provider
  • logoutUrls define the URLs to which users are redirected after logging out of an application
  • scopes are a way to specify the level of access that a client application is requesting from an identity provider

Setting up secrets and env variables locally

Amplify provides the secret function to set and retrieve secrets from AWS Systems Manager Parameter Store. We will use this to set the credentials from Auth0.

To set the secrets to Amplify, run the following command in your terminal:

npx ampx sandbox secret set AUTH0_CLIENT_ID

You’ll be prompted to enter the AUTH0_CLIENT_ID. Copy the Client ID from your Auth0 application and enter it as the secret value. Once completed, you should see a confirmation message similar to: Successfully created version 1 of secret AUTH0_CLIENT_ID.

Repeat the previous step for the AUTH0_CLIENT_SECRET:

npx ampx sandbox secret set AUTH0_CLIENT_SECRET

Once complete, you will see a confirmation message Successfully created version 1 of secret AUTH0_CLIENT_SECRET

Next, you will need to setup environment variables for LOGOUT_URL, CALLBACK_URL, and ISSUER_URL. At the root directory of the project, create a .env file. Add the following environment variables, each on separate lines:

ISSUER_URL=<AUTH0_DOMAIN_URL>
CALLBACK_URL=http://localhost:3000
LOGOUT_URL=http://localhost:3000

Replace the AUTH0_DOMAIN_URL with the Auth0 Domain URL from your Auth0 application, including the https:// prefix. This is shown in the Creating Auth0 application section, screenshot 4.

Configuring DynamoDB data source with AppSync

In the amplify/data/resource.ts, replace the existing code with the following:

import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  Todo: a
    .model({
      content: a.string(),
    })
    .authorization((allow) => [allow.authenticated()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: 'userPool'
  },
});

The defineData function defines a data model for a ToDo table in DynamoDB that is exposed through AWS AppSync API. The API authorization rule is set to only allow authenticated users from the Cognito user pool to create and delete ToDo items.

Next JS Client Side Code

Next, we’ll take a look at the Next.js frontend code. In the app directory, create a new file called translations.js and paste the following code:

// translations.js
export const messages = {
  en: {
    welcome: "Welcome, {username}",
    myTodos: "My todos",
    newTodo: "+ new",
    signOut: "Sign Out",
    signInWithAuth0: "Sign in with Auth0",
    appHosted: "App successfully hosted. Try creating a new todo.",
  },
  // Add other languages as needed
  es: {
    welcome: "Bienvenido, {username}",
    myTodos: "Mis tareas",
    newTodo: "+ nueva",
    signOut: "Cerrar sesión",
    signInWithAuth0: "Iniciar sesión con Auth0",
    appHosted: "Aplicación alojada con éxito. Intenta crear una nueva tarea.",
  },
};

To enhance user experience, the app employs react-intl to manage localized messages. These messages, defined in the translations.js file, ensure that users interact with the app in their preferred language. For example, the “Sign in with Auth0” button and other key interface elements are automatically translated based on the user’s locale, improving usability and making the app more engaging across different language settings.

In the app/page.tsx file, replace the existing code with the code provided below:

"use client";
import {useState, useEffect} from "react";
import { getCurrentUser, signInWithRedirect, signOut, fetchUserAttributes } from 'aws-amplify/auth';
import {generateClient} from "aws-amplify/data";
import type {Schema} from "@/amplify/data/resource";
import "./../app/app.css";
import {Amplify} from "aws-amplify";
import outputs from "@/amplify_outputs.json";
import "@aws-amplify/ui-react/styles.css";
import {messages} from './translations';
import { IntlProvider, FormattedMessage, useIntl } from "react-intl"; 

Amplify.configure(outputs);
const client = generateClient<Schema>();
function App() {
    const [todos, setTodos] = useState<Array<Schema["Todo"]["type"]>>([]);
    const [username, setUsername] = useState<string | null>(null);
    const [locale, setLocale] = useState('en'); // Default to English
    const intl = useIntl();

    function listTodos() {
        client.models.Todo.observeQuery().subscribe({
            next: (data) => setTodos([...data.items]),
        });
    }

    function deleteTodo(id: string) {
        client.models.Todo.delete({id})
    }

    async function checkUserAuthentication() {
        try {
            const currentUser = await getCurrentUser();
            if (currentUser) {
                const attributes = await fetchUserAttributes();        
                const displayName = attributes.email || currentUser.username || currentUser.userId;
                setUsername(displayName);
                return true;
            }
        } catch (error){
            console.error("Error getting current user:", error);
            setUsername(null);
            return false;
        }
    }

    useEffect(() => {
        const fetchTodos = async () => {
            const isAuthenticated = await checkUserAuthentication();
            if (isAuthenticated) {
                listTodos();
            }
        }
        fetchTodos();
    }, []);

    function createTodo() {
        client.models.Todo.create({
            content: window.prompt("Todo content"),
        });
    }

    const handleSignOut = async () => {
        await signOut();
        setUsername(null);
    }

    return (

        <main>
            {username ?
                <div>
                    <h1><FormattedMessage id="welcome" values={{ username }} /></h1>
                    <h1><FormattedMessage id="myTodos" /></h1>
                    <button onClick={createTodo}>+ new</button>
                    <ul>
                        {todos.map((todo) => (
                            <li key={todo.id} onClick={() => deleteTodo(todo.id)}>
                                {todo.content}
                            </li>
                        ))}
                    </ul>
                    <div>
                        <FormattedMessage id="appHosted" />
                        <br/>
                        <button onClick={handleSignOut}>
                        <FormattedMessage id="signOut" />
                        </button>
                    </div>
                </div> :
                <button onClick={() => signInWithRedirect({
                    provider: {custom: 'Auth0'}
                })}>
                    <FormattedMessage id="signInWithAuth0" />
                </button>
            }
        </main>
    );
}

export default function IntlApp() {
    return (
        <IntlProvider messages={messages['en']} locale="en" defaultLocale="en">
            <App />
        </IntlProvider>
    );
}

The app automatically checks the user’s authentication status on load, and if authenticated, it fetches and displays the user’s to-do list. Otherwise, it displays a “Sign in with Auth0” button. When clicked, the button triggers the signInWithRedirect function, which redirects the user to the Auth0 portal for authentication or account creation. Upon successful authentication, the user is redirected back to the Next.js app.

Once authenticated, the app retrieves the user’s information using getCurrentUser, followed by fetchUserAttributes to obtain the user’s email address. This allows the app to display a personalized dashboard featuring a to-do list and a form for creating new to-dos. The interface also includes a sign-out button, enabling users to securely log out when finished.

Creating Cloud sandbox environment

Before you can start deploying resources in the cloud sandbox environment, Amplify must complete a one-time bootstrap setup for the AWS account and Region. To begin, run the following command in your current project terminal:

npx ampx sandbox

During the first-time setup, npx ampx sandbox will ask you to sign in to the AWS Management Console. You must sign in as the account root user or as a user that has AdministratorAccess. Once signed in, you will be redirected to the Amplify console. On the Create new app page, choose Initialize setup now. The bootstrapping process may take a few minutes to complete.

Once the setup is finished, you’re ready to test your backend. Run the sandbox command again to start deploying resources.

The initial deployment may take 5-10 minutes as new AWS resources are provisioned. After deployment, an amplify_outputs.json file is generated in your project, containing all the backend resources ready for use.

Setting Cognito User Pool Domain in Auth0

In order for the authentication process to work, we need to configure the Auth0 application to return the token to Cognito when the user successfully authenticates.

In the amplify_outputs.json, look for the Cognito Domain output and copy the value. Alternatively, you can retrieve this value directly from the AWS Management Console:

  1. Navigate to Amazon Cognito console and choose the user pool deployed by the sandbox.
  2. Navigate to the App integration section and under the domain, copy the Cognito Domain.

Cognito-Domain

Navigate to your Auth0 application (Amplify-app) and open the Settings tab. Under Allowed Callback URLs,  enter https://<COGNITO_DOMAIN>/oauth2/idpresponse , replacing <COGNITO_DOMAIN> placeholder with your Cognito Domain. Choose Save changes for it to take effect.

Auth0-allowed-callback

Running the application

You have everything needed to begin testing locally! Open a new terminal session in your IDE and run the command below to start a localhost development server:

npm run dev

Open the localhost in a web browser of your choice e.g http://localhost:3000

auth0-login-portal

Auth0-login-demo

Deploy the App to Amplify Hosting

With the cloud sandbox environment successfully configured, the next step is to deploy the application using Amplify Hosting. Amplify Hosting offers easy-to-use CI/CD for static and dynamic web applications with simple Git-based workflows. In this section, we will use GitHub as our source repository to store the modified Amplify app.

  1. Log into a GitHub account and create a GitHub repository.
  2. Follow instructions in the repository to commit the changes you have made and push your application to your remote repository.
  3. On the AWS Amplify console, choose Create new app at the middle of the page. If you already have an Amplify application then you will see Create new app at the top right corner of the page.
  4. Under the Deploy your app section, choose your GitHub then choose Next.

Amplify-Hosting-Src-Provider-Page

Note: If you are deploying your first app in the current Region, by default you will start from the AWS Amplify service page.

Amplify uses the GitHub Apps feature to authorize access to GitHub repository. For more information about installing and authorizing the GitHub App, see Setting up Amplify access to GitHub repositories.

5. On the Add repository and branch page, complete the following steps:

    • Select the name of the repository to connect.
    • Select the name of the repository branch to connect.
    • Choose Next

6. Under the App Settings page

    • Enter the Application name. By default, Amplify uses the repository name.
    • In the Build settings section, verify that the Frontend build command and Build output directory are correct. For this Next.js example app, the Build output directory is set to .next.
    • For Service role, select the Create and use a new service role then click Next


Amplify-Hosting-App-Settings

7. Review your configuration and click Save and deploy

Adding environment variables and secrets to Amplify Hosting

  1. Select View app and on the left pane, and expand the Hosting section.
  2. Go to the Environment variables and add CALLBACK_URL, ISSUER_URL, and LOGOUT_URL variables
  3. Enter the Amplify App domain as the value for both CALLBACK_URL and LOGOUT_URL. It will be under the Overview section and look like the following: https://<GITHUB_BRANCH>.<APP_ID>.amplifyapp.com
  4. For the ISSUER_URL, refer back to the Auth0 Domain value
  5. Save the environment variables

amplify-hosting-env-variables

Under the hosting section, select Secrets and open the Manage Secrets. Add both the AUTH0_CLIENT_ID and AUTH0_CLIENT_SECRET keys with their corresponding values from Auth0 application.

amplify-hosting-secrets-management

Note: By default, Amplify auto-deploys upon creation of a new application. However, we need to configure the application with environment variables and secrets otherwise the deployment will fail. If you complete the steps below before it fails, then you can proceed to the next section. If the deployment fails, you can redeploy after adding the environment variables and secret values.

Once deployed, the final step is to configure the Cognito domain in the Auth0 application callback URLs. You can find the Cognito domain by downloading the amplify_outputs.json in the Deployments console and retrieving the value.

Follow the steps in Setting Cognito User Pool Domain in Auth0 to add the Domain to your Auth0 application.

Test the solution

You can use the domain URL or Visit deployed Url to open the web app.

Amplify app dashboard

Clean Up Resources

To avoid incurring future charges, delete the resources used in this solution:

Amplify Hosting and Sandbox:

  • On the Amplify console, select View app and on the left pane expand the App settings section
  • Select General settings  and choose the Delete app button
  • Delete the Amplify Sandbox by stopping the process or running npx ampx sandbox delete

Auth0 application:

  • Navigate the Auth0 dashboard page, select Amplify App
  • Under the Settings tab, choose Delete this application
  • Enter the name of the Auth0 application and choose Delete

Conclusion

Congratulations! In this post, you have learned how to successfully integrate Auth0 as a Federated OIDC provider with AWS Amplify Gen 2. You can follow a similar approach with other external identity providers like Microsoft Entra ID, Clerk, etc. To get started with Amplify Gen 2, try out our Quickstart tutorial, explore the AWS Amplify Gen 2 documentation, and join our Community Discord to leave any feedback or feature requests.

photo-of-taf

Tafadzwa Chimbindi

Tafadzwa Chimbindi is a Solutions Architect for AWS with a focus on Observability, Data Analytics, and Serverless technology. He is a skilled builder in full-stack technology and loves developing solutions that drive cloud adoption with organizations. Outside of work, he loves soccer, traveling, and watching movies.

photo-of-getnet

Getnet Mekuriyaw

Getnet Mekuriyaw is a Solutions Architect at AWS, with primary focus on Frontend Web, Mobile, and Operational Analytics domains. He is passionate about learning AWS technologies and guiding EdTech customers/partner system integrators through their cloud journey challenges. Outside work, Getnet enjoys playing and watching soccer.