DEV Community

Cover image for SAML SSO: The Missing Piece in Your Next.js App's Authentication Puzzle 🧩
Nathan Tarbert
Nathan Tarbert

Posted on • Edited on

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

Find out how to complete the picture for a seamless login

Table of Contents

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.

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.

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 have some resources to help us along the way.

To better understand the implementation there are two GitHub repositories you should check out.

  1. BoxyHQ's SAML SSO - Primary single sign-on resource

  2. Source code - for the 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:

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 😉

SAML Jackson with a drink

By the way 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


Enter fullscreen mode Exit fullscreen mode

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.


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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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": {
...
}
}
Enter fullscreen mode Exit fullscreen mode




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;

Enter fullscreen mode Exit fullscreen mode




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.

Top comments (25)

Collapse
 
anthonymanzo profile image
Anthony Manzo

I've managed to create an SSO connection now (as a service with a self hosted instance of boxyhq on vercel one-click :) and I'm digging into some new challenges with using NextJS App router and auth v5. I'm trying to port this code and am having some challenges. Before I ask a specific question, is this possible and has anyone here done it yet?

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Hey Anthony, I'm tagging Sama from BoxyHQ to help you out.
@caloique

Collapse
 
caloique profile image
Sama

Thanks Nathan!

Hey Anthony, it is great to hear that you got BoxyHQ set up and that the Vercel 1-Click worked for you. We would be happy to help you trouble shoot the challenges you are running into, but it would be easiest to do so through our BoxyHQ Discord community. You can join us there using the following URL: discord.boxyhq.com - Once you have joined feel free to tag me and we can take it from there.

Thread Thread
 
anthonymanzo profile image
Anthony Manzo

Thanks Sama, I just tagged you in #general, I'm @atonyman

Collapse
 
kalpeshbhalekar profile image
Kalpesh Bhalekar

Super informative for such a technical subject!

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Thank you @kalpeshbhalekar

Collapse
 
srbhr profile image
Saurabh Rai

Really cool article Nathan! I'm glad people are also discussing and diving deep into SSO and SAML. We really need more of these 🔥 articles.

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Thank you @srbhr! I really appreciate the support.

Collapse
 
anthonymanzo profile image
Anthony Manzo

Question, do you need to sign-up for BoxyHQ's SaaS product to use this repo?

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Hi Anthony, BoxyHQ is free and open source.
I'm not an employee so I tagged Sama from BoxyHQ to fully answer that question.

ATTN: @caloique

Collapse
 
caloique profile image
Sama

Thanks, Nathan!

Hey Anthony, you don't have to sign up for BoxyHQ's SaaS to access our products. While you can do so, you also have the option to visit our GitHub and self-host for free or opt for our premium self-hosted service. Happy to help you with the integration if needed 🤙

Thread Thread
 
nathan_tarbert profile image
Nathan Tarbert

Thanks for picking this up Sama!

Collapse
 
jobenjada profile image
Johannes

love to see Boxy here more often! :))

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Thanks, @jobenjada, for the support.

We are huge fans of Formbricks as well so it's mutual.

Collapse
 
geduldig profile image
geduldig

How does one make this work with SAML IdP initiated login?

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Hi @geduldig, let me tag @caloique who can get you the correct answer.

Collapse
 
deepakprab profile image
Deepak Prabhakara
Thread Thread
 
nathan_tarbert profile image
Nathan Tarbert

You're welcome @deepakprab

Collapse
 
deepakprab profile image
Deepak Prabhakara

This example is a good starting place - github.com/boxyhq/jackson-examples..., specifically
github.com/boxyhq/jackson-examples... which outlines how the IdP initiated login can be supported.

Collapse
 
thevinitgupta profile image
Vinit Gupta

Really detailed and great explanations Nathan 🔥
And yes

Saml Jackson 🙏🙏

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Thanks @thevinitgupta!
I really appreciate your feedback :)

Collapse
 
emmacode profile image
Emmanuel

Second time reading this article. Well detailed
Thanks, Nathan.

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Wow, thanks so much for the amazing feedback @emmacode.
I hope it's clear and concise!

Collapse
 
danidoesthings profile image
Dani K

This is both clear and thorough, thank you for taking the time to write it!

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

That's really great to hear @danidoesthings, thank you for the feedback!