Fast Serverless Authentication with Next.js and PropelAuth

Fast Serverless Authentication with Next.js and PropelAuth

Next.js is a React framework that provides a lot of useful features out of the box. One of these powerful features is API routes, which allows you to make an API/backend entirely within your Next.js application.

// pages/api/hello.js
//   requests to /api/hello return {"message":"Hello, World!"}
export default function handler(req, res) {
  res.status(200).json({ message: 'Hello, World' })
}

Services like Vercel and Netlify manage your API routes in a serverless environment, meaning you get a very scalable API and your code can be deployed globally.

In this post, we'll create an authenticated API route that looks like this:

// pages/api/whoami.js
export default async function handler(req, res) {
    // check if the request contains a valid token
    await requireUser(req, res)

    // req.user is automatically set by requireUser 
    res.status(200).json({user_id: req.user.userId}) 
}

and make a React component that sends authenticated requests to this API route.

Making an unauthenticated request

Before we dive into authenticated requests, let's first make an endpoint that we request on the frontend. If you don't already have a Next.js project, you can create one with:

$ npx create-next-app@latest

We will use the library SWR to fetch data, but you can also use fetch directly or a different library like axios.

$ yarn add swr # or npm i --save swr

Making an API 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"}

Making a component that fetches

We have the backend, now we just need the frontend. Make a new file components/WhoAmI.js:

import useSWR from 'swr'

// Used by SWR to fetch data
const fetcher = (url) => fetch(url).then(res => res.json())

const WhoAmI = () => {
    const { data, error } = useSWR('/api/whoami', fetcher)

    if (error) return <div>failed to load</div>
    if (!data) return <div>loading...</div>
    return <pre>{JSON.stringify(data)}</pre>
}

export default WhoAmI

SWR makes everything pretty simple. All we need to do is add this component to a page. Any files in pages are automatically treated as frontend routes (excluding pages/api which are API routes), so we can make a new file pages/whoami.js which is automatically rendered when we visit localhost:3000/whoami.

import WhoAmI from "../components/WhoAmI";
import Head from "next/head";

export default function WhoAmIPage() {
    return <div>
        <Head><title>Who Am I?</title></Head>
        <WhoAmI/>
    </div>
}

If you go to localhost:3000/whoami, you will now see a brief flash of loading... followed by {"user_id":"Not sure yet"}.

Adding Authentication

We wrote an introduction to user authentication to provide more context, but as a quick summary - we need some way to register new users and some way for our API routes to know which user (if any) created a request.

Setting up PropelAuth

We will use PropelAuth for both of these. PropelAuth provides a hosted, configurable UI which manages all aspects of authentication for us, from login/signup to account pages and transactional emails. For B2B/SaaS use cases, we also get support for creating organizations and allowing our users to invite other users to their orgs.

After signing up, we can configure the look and feel of all our auth pages/emails:

A user configuring hosted auth pages

We can also optionally enable features like social logins or conversion tracking.

Integrating our Frontend

Since PropelAuth handles logging users in, how do we know that a user is logged in our Next.js app? We need to use @propelauth/react. The full documentation is available here. First, we install the library:

$ yarn add @propelauth/react
# OR
$ npm install --save @propelauth/react

Then, in pages/_app.js, we wrap our application with an AuthProvider. The AuthProvider reaches out to our PropelAuth instance and fetches our current user's metadata, if they are logged in. You'll need your authUrl which you can find in your dashboard under Frontend Integration.

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

function MyApp({Component, pageProps}) {
    return <AuthProvider authUrl="REPLACE_ME">
        <Component {...pageProps} />
    </AuthProvider>
}

Now, we are ready to update our component. Let's look at the changed code first and then break it down:

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

// 1. fetcher now takes in an accessToken and passes it in an Authorization header
const fetcher = (url, accessToken) => fetch(url, {
    method: "GET",
    headers: {"Authorization": `Bearer ${accessToken}`}
}).then(res => res.json())

// 2. function is wrapped with withAuthInfo
const WhoAmI = withAuthInfo((props) => {
    // 3. props.accessToken comes from withAuthInfo
    const { data, error } = useSWR(['/api/whoami', props.accessToken], fetcher)
// ... nothing changed after this

We made three changes. The first change is that we pass in an accessToken to our fetcher, and our fetcher passes it along in the header of the request. What is an accessToken? You can read more about it here, but at a high level, PropelAuth creates accessTokens for your users. It then provides metadata that your backend can use to verify these accessTokens. Passing it along in the Authorization header is a standard convention, and our backend will expect this exact format (Bearer TOKEN).

The second change is we called withAuthInfo with our React component. withAuthInfo automatically injects useful props into our Component, which is where props.accessToken comes from. We could also use other properties like props.isLoggedIn or props.user.email.

The third change is we need to pass our access token into useSWR for our fetcher to use.

Integrating our Backend

Our frontend is done, now we just need to update our backend to verify the accessTokens that are passed in. To do this, we'll use @propelauth/express, since Next.js API routes support Express/Connect middleware.

$ yarn add @propelauth/express
# OR
$ npm install --save @propelauth/express

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

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

const propelauth = initAuth({
    authUrl: "REPLACE_ME",
    apiKey: "REPLACE_ME",
    manualTokenVerificationMetadata: {
        verifierKey: "REPLACE_ME",
        issuer: "REPLACE_ME"
    }
})

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 })
}

Note that requireUser does NOT need to make any external requests to validate the token. This allows our authentication step to be fast regardless of where the function is currently running.

Testing

We now have everything we need. If we are not logged in, and we visit localhost:3000/whoami, we will get a 401 Unauthorized error. If we sign up and visit localhost:3000/whoami, we will see:

{"user_id":"5395219c-7d05-4085-a05c-5f5e98ede166"}

Speaking of logging in and out, if we want to make that easier from within our app, we can use an example from the docs to add Signup/Login buttons if we aren't logged in, and a Logout button if we are.

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

function AuthenticationButtons(props) {
    const logoutFn = useLogoutFunction()
    const {redirectToSignupPage, redirectToLoginPage} = useRedirectFunctions()

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

export default withAuthInfo(AuthenticationButtons);

In this case, we are using the React hooks useLogoutFunction and useRedirectFunctions to log the user out or navigate them to the signup/login pages. We are also using withAuthInfo, but this time instead of getting the accessToken, we just need to check if the user isLoggedIn or not.

Summary

In this post, we were able to build a decently complex application very quickly. With PropelAuth, our users can register, login, manage their account information, etc. Afterwards, we used Next.js on the frontend make an authenticated request to our backend. We also conditionally rendered different buttons depending on if the user was logged in or not.

Our backend is also powered by Next.js and can scale easily with API routes. These serverless functions can be hosted anywhere, but will always be able to quickly determine which user made an authenticated request.

Bonus: B2B/SaaS

While we mostly covered B2C authentication, where each user is independent. You can also use PropelAuth for B2B authentication, where each user can be a member of an organization. PropelAuth provides you with UIs for your end-users to manage their own organizations and invite new users. PropelAuth also provides a role-based system where users within an organization can be Owners, Admins, or Members.

On the frontend, we can still use withAuthInfo, but now we can use an orgHelper to help us manage a user's organizations. For example, here's a React component that displays a list of organizations that the current user is a member of:

const Orgs = withAuthInfo((props) => {
    // get all orgs that the current user is a member of
    const orgs = props.orgHelper.getOrgs();

    // Display their names in a list
    return <ul>
        {orgs.map((org, i) => 
            <li key={i}>{org.orgName}</li>
        )}
    </ul>
})

We can also use functions like orgHelper.selectOrg(orgId) and orgHelper.getSelectedOrg() to select/get a single organization so our frontend can operate only on one organization at a time.

On the backend, we can use the middleware propelauth.requireOrgMember similarly to how we used propelauth.requireUser, except this will verify that the user is also a member of an organization.

If you have any questions, please reach out at