SAML SSO:  The Missing Piece in Your Next.js App's Authentication Puzzle 🧩

SAML SSO: The Missing Piece in Your Next.js App's Authentication Puzzle 🧩

Find out how to complete the picture for a seamless login

TL;DR:

By the end of this tutorial, you'll have a fully functional SAML SSO integration for your Next.js app. 🚀

This will be the process:

  • Walk through the process step by step, complete with code examples.

GIF

cat with sunglasses

Before you make it too far, a good place to start is to find out your use case.

If you're looking to enhance the security and user experience of your Next.js app by implementing SAML Single Sign-On (SSO) authentication for your customers, you’ve landed in the right place. Let’s get started!

A Brief Description of SSO

Single Sign-On (SSO) simplifies user authentication by allowing them to log in once using one set of credentials to access multiple internal applications or services.

GIF

I have a question

Authentication vs Authorization

I wanted to quickly distinguish the difference between Single Sign-On (SSO) authentication and authorization because they serve distinct yet interconnected purposes in the realm of user access control. SSO authentication primarily focuses on verifying the identity of a user, ensuring that they are who they claim to be, and granting them access to an internal system, application, or services.

Authorization is the process of defining and granting specific permissions and access rights to authenticated users, specifying what actions or resources they are allowed or denied within a system or application.

Now, you might be wondering how to bring this seamless SSO experience to your Next.js app for your startup/SMB/Enterprise users. That is what we are about to build and we should have some references to help us along the way.

Make sure you check out two GitHub repositories.

  1. BoxyHQ's SAML SSO

  2. Next.js SAML SSO integration

Integrating SAML SSO into Your App

The integration of SAML Single Sign-On (SSO) into your app involves the following key steps:

  • Configure SAML Single Sign-On: This step enables your tenants to configure SAML connections for their users. Be sure to review the following guides for a deeper understanding of this process:

  • Authenticate with SAML Single Sign-On: After adding a SAML connection, your app can utilize this SAML connection to initiate the SSO authentication flow using SAML Jackson. The following sections will focus more on the SSO authentication side.

…Yes that’s correct you heard it right - SAML Jackson 😉

GIF

GIF

SAML Jackson with a drink

BTW, when you head to the ⬆️ SAML Jackson repo ⬆️,
would you please give me a star? ⭐

Let’s get back to our regularly scheduled program 🙂 and get coding.

Install SAML Jackson

To get started with SAML Jackson, add it to your project's dependencies using Node Package Manager (NPM):

npm i --save @boxyhq/saml-jackson

Setup SAML Jackson

Next, you'll need to configure SAML Jackson to work seamlessly with your Next.js app. This involves modifying your environment variables (.env) and creating a Jackson configuration file.

NEXTAUTH_URL=https://your-app.com
NEXTAUTH_SECRET= #A random string is used to hash tokens, sign/encrypt cookies, and generate cryptographic keys.

Before we can go any further we need to create a random string for our NEXTAUTH_SECRET shown above in our .env file. This can easily be done by downloading OpenSSL and then typing this command in the terminal openssl rand -base64 24 which will generate a random 32-character key.

Next, let’s create a new file lib/jackson.ts


import jackson, {
  type IOAuthController,
  type JacksonOption,
} from "@boxyhq/saml-jackson";

const samlAudience = "https://saml.boxyhq.com";
const samlPath = "/api/oauth/saml";

const opts: JacksonOption = {
  externalUrl: `${process.env.NEXTAUTH_URL}`,
  samlAudience,
  samlPath,
  db: {
    engine: "sql",
    type: "postgres",
    url: "postgres://postgres:postgres@localhost:5432/postgres",
  },
};

let oauthController: IOAuthController;

const g = global as any;

export default async function init() {
  if (!g.oauthController) {
    const ret = await jackson(opts);

    oauthController = ret.oauthController;
    g.oauthController = oauthController;
  } else {
    oauthController = g.oauthController;
  }

  return {
    oauthController,
  };
}

One quick note: The samlPath is where the identity provider POSTs the SAML response after authenticating the user.

In brief, let's break down what our code is doing.
We first ensure that only a single Jackson controller instance is created and used throughout the application. Whenever we need to access the Jackson OAuth controller, you can import the jackson instance into the file where it’s needed.

NextAuth.js Integration

For authentication, we'll use NextAuth.js, a comprehensive open-source authentication solution designed for Next.js applications. Let’s go ahead and install it.
(Note): I will add the link to NextAuth’s docs covering SAML Jackson as a reference.

npm i --save next-auth

NextAuth ships with BoxyHQ SAML boxyhq-saml as a built-in SAML authentication provider. We'll use this provider to authenticate the users.

We'll now create a new file pages/api/auth/[...nextauth].ts

import NextAuth, { type NextAuthOptions } from 'next-auth';
import BoxyHQSAMLProvider from 'next-auth/providers/boxyhq-saml';

export const authOptions: NextAuthOptions = {
  providers: [
    BoxyHQSAMLProvider({
      authorization: { params: { scope: '' } },
      issuer: `${process.env.NEXTAUTH_URL}`,
      clientId: 'dummy',
      clientSecret: 'dummy',
      httpOptions: {
        timeout: 30000,
      },
    }),
  ],
  session: {
    strategy: 'jwt',
  },
};

export default NextAuth(authOptions);

Let’s take a look at what’s going on here.
This code is essentially providing a set of instructions to your app on how to handle user logins securely. The authOptions object defines the provider and any other relevant settings, such as the session strategy. Then exports the NextAuth instance, passing in these authentication options, allowing the Next.js application to leverage the specified authentication provider and strategy for user authentication and session management.

Making the Authentication Request

Now, let's add a route that initiates the authentication flow for SAML SSO by redirecting users to their configured Identity Provider.

Let's call this file pages/api/oauth/authorize.t

import type { NextApiRequest, NextApiResponse } from "next";
import type { OAuthReq } from "@boxyhq/saml-jackson";

import jackson from "../../../../lib/jackson";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { oauthController } = await jackson();

  const { redirect_url } = await oauthController.authorize(
    req.query as unknown as OAuthReq
  );

  return res.redirect(302, redirect_url as string);
}

Receiving the SAML Response

After successful authentication, the Identity Provider (IdP) POSTs the SAML response to the Assertion Consumer Service (ACS) URL. We need to create a route to handle this response.

New file pages/api/oauth/saml.ts

import type { NextApiRequest, NextApiResponse } from "next";

import jackson from "../../../../lib/jackson";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { oauthController } = await jackson();

  const { RelayState, SAMLResponse } = req.body;

  const { redirect_url } = await oauthController.samlResponse({
    RelayState,
    SAMLResponse,
  });

  return res.redirect(302, redirect_url as string);
}

Requesting the Access Token

Next, we need a route to receive the callback after authentication. NextAuth requests an access token by passing the authorization code, along with authentication details including grant_type, redirect_uri, and code_verifier.

We want to now create a file pages/api/oauth/token.ts

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { oauthController } = await jackson();

  const response = await oauthController.token(req.body);

  return res.json(response);
}

Fetching the User Profile

Once the access_token has been fetched, NextAuth can use it to retrieve the user profile from the Identity Provider. The userInfo method returns a response containing the user profile if the authorization is valid.

We'll need a new file pages/api/oauth/userinfo.ts

import type { NextApiRequest, NextApiResponse } from 'next';

import jackson from '../../../../lib/jackson';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { oauthController } = await jackson();

  const authHeader = req.headers['authorization'];

  if (!authHeader) {
    throw new Error('Unauthorized');
  }

  const token = authHeader.split(' ')[1];

  const user = await oauthController.userInfo(token);

  return res.json(user);
}

The response will contain user information, including their ID, email, first name, last name, and more.

{
  "id":"<id from the Identity Provider>",
  "email": "jackson@coolstartup.com",
  "firstName": "SAML",
  "lastName": "Jackson",
  "requested": {
    "tenant": "<tenant>",
    "product": "<product>",
    "client_id": "<client_id>",
    "state": "<state>"
  },
  "raw": {
    ...
  }
}

Authenticating the User

Finally, once you've retrieved the user's information from the Identity Provider, you can determine if the user exists in your application and authenticate them accordingly. If the user doesn't exist, you can create a new record in your database and add them.

Starts OAuth sign-in flow

To initiate our application's OAuth sign-in flow, we will use NextAuth's signIn method and authenticate with the boxyhq-saml provider.

You can pass the tenant and product as additional parameters to the /api/oauth/authorize endpoint through the third argument of signIn().

:::info
Make sure you add a valid SAML connection for the tenant and product combination. Otherwise, the authentication will fail. Read about creating SAML connections here
:::

For this example app to work, you need to add a SAML connection for the tenant boxyhq.com and product saml-demo.boxyhq.com before you can authenticate the users.

Let's do that by creating pages/login.tsx

import type { NextPage } from 'next';
import { useSession, signIn } from 'next-auth/react';

const Login: NextPage = () => {
  const { data: session, status } = useSession();

  if (status === 'loading') {
    return <>Loading...</>;
  }

  if (status === 'authenticated') {
    return <>Authenticated</>;
  }

  // Starts OAuth sign-in flow
  signIn('boxyhq-saml', undefined, {
    tenant: 'boxyhq.com',
    product: 'saml-demo.boxyhq.com',
  });

  return <>Unauthenticated</>;
};

export default Login;

Congratulations!

High fives

Let’s Take a Moment and Review What We've Learned 🥇

🎬 We started by exploring the world of Single Sign-On (SSO) and its transformative power to authenticate for your Next.js app.

🔐 SSO simplifies user authentication, allowing the user to access multiple internal applications with a single login which is crucial for both security and user experience.

💡 Our focus has been on implementing SAML-based SSO, a robust and widely-used protocol.

🗺️ We then walked through the process step-by-step, from configuring SAML Single Sign-On to authenticating users in your app.

📚 We learned how to set up SAML Jackson SSO, integrate it with NextAuth.js, and make the SSO magic happen with carefully crafted code snippets. Each section has brought you closer to creating a seamless SSO experience for your customers.

🚀 Let’s say goodbye to password fatigue and embrace the future of authentication in your Next.js app. With SAML SSO, you'll simplify login, enhance security, and elevate user satisfaction. We can now say we’ve successfully unlocked the doors to effortless authentication and a brighter future for your app!

Community

If you have any questions along the way or get stuck while building your app join our BoxyHQ Discord Developer Community.