Skip to main content

Add SAML SSO to Next.js App

This guide assumes that you have a Next.js app and want to enable SAML Single Sign-On authentication for your enterprise customers. By the end of this guide, you'll have an app that allows you to authenticate the users using SAML Single Sign-On.

Visit the GitHub repository to view BoxyHQ's SAML SSO.

Visit the GitHub repository to view the source code for the Next.js SAML SSO integration.

Integrating SAML SSO into an app involves the following steps.

  • Configure SAML Single Sign-On
  • Authenticate with SAML Single Sign-On

Configure SAML Single Sign-On

This step allows your tenants to configure SAML connections for their users. Read the following guides to understand more about this.

Authenticate with SAML Single Sign-On

Once you add a SAML connection, the app can use this SAML connection to initiate the SSO authentication flow using SAML Jackson. The following sections focus more on the SSO authentication side.

Install SAML Jackson

To get started with SAML Jackson, use the Node Package Manager to add the package to your project's dependencies.

npm i --save @boxyhq/saml-jackson

Setup SAML Jackson

Setup the SAML Jackson to work with Next.js app.

.env
NEXTAUTH_URL=https://your-app.com
NEXTAUTH_SECRET= #A random string is used to hash tokens, sign/encrypt cookies and generate cryptographic keys.
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,
};
}

samlPath is where the identity provider POST the SAML response after authenticating the user.

We'll use NextAuth.js for the authentication. NextAuth is a complete open-source authentication solution for Next.js applications.

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.

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

Make Authentication Request

Let's add a route to begin the authenticate flow; this route initiates the SAML SSO flow by redirecting the users to their configured Identity Provider.

pages/api/oauth/authorize.ts
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);
}

Receives SAML Response

After successful authentication, Identity Provider POST the SAML response to the Assertion Consumer Service (ACS) URL.

Let's add a route to handle the SAML response. Ensure the route matches the value of the samlPath you configured while initializing the SAML Jackson library and should be able to receives POST request.

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

Request Access Token

Let's add another route for receiving the callback after the authentication.

The NextAuth requests an access_token by passing the authorization code along with authentication details, including the grant_type, redirect_uri, and code_verifier.

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

Fetch 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.

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 entire response will look something like this:

{
"id":"<id from the Identity Provider>",
"email": "[email protected]",
"firstName": "SAML",
"lastName": "Jackson",
"requested": {
"tenant": "<tenant>",
"product": "<product>",
"client_id": "<client_id>",
"state": "<state>"
},
"raw": {
...
}
}

Authenticate User

Once the user has been retrieved from the Identity Provider, you may determine if the user exists in your application and authenticate the user. If the user does not exist in your application, you will typically create a new record in your database to represent the user.

Starts OAuth sign-in flow

You can use NextAuth's signIn method to initiate the authentication with 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.

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;