Next.js + PropelAuth Authentication Guide

Next.js + PropelAuth Authentication Guide

In this guide, we’ll build an example application in Next.js where users can sign up, login, and manage their accounts. We'll include social logins (Login with Google), passwordless logins, and allow our users to upload their own profile pictures.

We're going to use the following technologies for this blog post:

  • Next.js - for our frontend and backend (with API routes)
  • PropelAuth - for user login and management

The code is available on GitHub here.

Setting up Authentication

PropelAuth fully manages your signup, login, and account management flows. Features like social login (Login with Google), passwordless/magic links, and 2FA for our end users can be enabled in one click. You can sign up here.

The first thing to do after you sign up is create your project:

Afterwards, a test environment is created for you.

Your default login page

It includes login, signup, account pages, and optional organization management pages. There's a default logo, color scheme, and passwordless and password-based authentication. At this point, you can fully test what your end users will experience, transactional emails included, but you might want to configure its style first. You can do this in you dashboard under the Hosted Auth Pages section.

From here, you can configure other aspects of your end-users auth experience, including:

  • Adding "Login in with Google" or other SSO providers
  • Collecting additional metadata on signup - like username or first name/last name
  • Allowing your users to upload their own profile picture
  • Letting your end-users create organizations and invite their coworkers (called B2B support)

After configuring your project, it's now time to integrate it with your frontend and backend.

Authentication in Next.js

One of the benefits of having PropelAuth manage your users is you can use whatever frontend or backend you want - or even migrate between them.

Creating a new Next.js project

If you don't have a project already, you can create one with:

$ npx create-next-app@latest

Installation

The @propelauth/react package provides an easy interface to access your users information. It will manage auth tokens for you and has nice features like refreshing auth information when the user reconnects or switches back to your tab.

$ yarn add @propelauth/react

Setup

At the top of our application, in pages/_app.js, we will add our AuthProvider

import {AuthProvider} from "@propelauth/react";

function MyApp({Component, pageProps}) {
    return <AuthProvider authUrl={process.env.NEXT_PUBLIC_PROPELAUTH_AUTH_URL}>
        <Component {...pageProps} />
    </AuthProvider>
}

The authUrl is available on the Frontend Integration section of your PropelAuth project.

When a user logs in to our hosted pages, a secure, HTTP-only cookie is set. The AuthProvider is responsible for reaching out to PropelAuth and checking if the current user is logged in, and if they are, fetching auth tokens and user information. In production, we require you use a custom domain to avoid third-party cookie issues for your users.

Usage

withAuthInfo is a function that injects user information into your React components. Let's look at a simple example:

import {withAuthInfo} from '@propelauth/react';

// user is automatically injected from withAuthInfo
function AuthInfoOnFrontend({user}) {
    return <span>
        <h2>User Info</h2>
        {user && user.pictureUrl && <img src={user.pictureUrl} />}
        <pre>user: {JSON.stringify(user, null, 2)}</pre>
    </span>
}

export default withAuthInfo(AuthInfoOnFrontend);

If we add this component to our App and visit our site before we login, we'll see:

Null user because we aren't logged in

After logging in, we'll see:

Full user information, including a profile picture

In addition to user, withAuthInfo also injects useful values like isLoggedIn, accessToken (which we'll use shortly), and orgHelper (if you are using B2B support).

Creating Login/Logout Buttons

@propelauth/react also provides React hooks for redirecting your users to the login/signup/account pages, or logging your users out. Here's a common example of what we can do with that:

import {withAuthInfo, useLogoutFunction, useRedirectFunctions} from '@propelauth/react';

// isLoggedIn is automatically injected from withAuthInfo
function AuthenticationButtons({isLoggedIn}) {
    const logoutFn = useLogoutFunction()
    const {redirectToSignupPage, redirectToLoginPage, redirectToAccountPage} = useRedirectFunctions()

    if (isLoggedIn) {
        return <div>
            <button onClick={redirectToAccountPage}>Account</button>
            <button onClick={() => logoutFn()}>Logout</button>
        </div>
    } else {
        return <div>
            <button onClick={redirectToSignupPage}>Signup</button>
            <button onClick={redirectToLoginPage}>Login</button>
        </div>
    }
}

export default withAuthInfo(AuthenticationButtons);

Making authenticated requests

To make an authenticated request on behalf of your user, you’ll need to provide an access token. Just like isLoggedIn and user, the access token is available from withAuthInfo. You provide it in the request in the Authorization header, like so:

Authorization: Bearer ACCESS_TOKEN

With fetch, this looks like:

function fetchWhoAmI(accessToken) {
    return fetch("/api/whoami", {
        headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${accessToken}`
        }
    }).then(response => {
        if (response.ok) {
            return response.json()
        } else {
            return {status: response.status}
        }
    })
}

We can add this to a component using React's useEffect hook.

import {withAuthInfo} from "@propelauth/react";
import {useEffect, useState} from "react";

// accessToken is automatically injected from withAuthInfo
function AuthenticatedRequestToBackend({accessToken}) {
    const [response, setResponse] = useState(null)

    useEffect(() => {
        fetchWhoAmI(accessToken).then(setResponse)
    }, [accessToken])

    return <span>
        <h2>Server Response</h2>
        <pre>{response ? JSON.stringify(response, null, 2) : "Loading..."}</pre>
    </span>
}

export default withAuthInfo(AuthenticatedRequestToBackend);

Great, we can now make authenticated requests to any backend we want. Luckily, Next.js has support for API routes so we will use that - but we do have guides for other backends.

Authentication with Next.js API Routes

Creating an unprotected route

Any files in pages/api are treated as a route. Let’s make a new file pages/api/whoami.js:

export default function handler(req, res) {
  res.status(200).json({ user_id: 'Not sure yet' })
}

And that’s all, we can test this route with curl

$ curl localhost:3000/api/whoami
{"user_id":"Not sure yet"}

Protecting our route

At this point, our frontend is passing in an access token, but our server isn’t doing anything with it yet. We’ll use PropelAuth’s Express library @propelauth/express to get the user, since Next.js API routes support Express/Connect middleware.

$ yarn add @propelauth/express

Then we'll create a new file lib/propelauth.js

import {initAuth} from "@propelauth/express";

const propelauth = initAuth({
    authUrl: process.env.NEXT_PUBLIC_PROPELAUTH_AUTH_URL,
    apiKey: process.env.PROPELAUTH_API_KEY,
    manualTokenVerificationMetadata: {
        verifierKey: process.env.PROPELAUTH_VERIFIER_KEY,
        issuer:  process.env.PROPELAUTH_VERIFIER_ISSUER
    
})

export default propelauth

Your specific values can be found in the Backend Integration section of your PropelAuth project. This exports a set of functions like propelauth.requireUser, which will make sure a valid accessToken was provided and automatically set req.user with the user’s information. The full reference is available here.

The Next.js docs also provide a runMiddleware function, which we need both for our auth middleware and any other middleware (like CORS middleware). We can place this in lib/middleware.js:

// From the Next.js docs about running middleware:
//   Helper method to wait for a middleware to execute before continuing
//   And to throw an error when an error happens in a middleware
export default function runMiddleware(req, res, fn) {
    return new Promise((resolve, reject) => {
        fn(req, res, (result) => {
            if (result instanceof Error) {
                return reject(result)
            }

            return resolve(result)
        })
    })
}

And now we have everything we need to update our pages/api/whoami.js route:

import propelauth from "../../lib/propelauth"
import runMiddleware from "../../lib/middleware"

// Calls our runMiddleware function with PropelAuth's requireUser function
const requireUser = (req, res) =>
    runMiddleware(req, res, propelauth.requireUser)

export default async function handler(req, res) {
    // Verifies that a valid accessToken is provided
    await requireUser(req, res);
    // req.user comes from requireUser
    res.status(200).json({ user_id: req.user.userId })
}
  • requireUser is an Express middleware that validates the access token, and sets req.user if a valid token was provided. If a valid token wasn’t provided, it will return a 401 Unauthorized error. If you want the request to continue even without a valid access token, use optionalUser instead.
  • Access tokens (which are JWTs) are validated quickly without needing to make any external requests.

Wrapping up

Without a lot of code, we made a pretty advanced application. By using PropelAuth for authentication, we were able to skip building any auth UIs, transactional emails, 2FA enrollment, profile picture management, and more.

Our frontend made a request to our backend, and our backend was able to identify the user that made the request. You can use this for things like saving information in a database per user_id.

If you have any questions, please reach out at support@propelauth.com.