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.
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.
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.
BoxyHQ's SAML SSO - Primary single sign-on resource
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:
-
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 😉
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
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.
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!
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)
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?
Hey Anthony, I'm tagging Sama from BoxyHQ to help you out.
@caloique
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.
Thanks Sama, I just tagged you in #general, I'm @atonyman
Super informative for such a technical subject!
Thank you @kalpeshbhalekar
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.
Thank you @srbhr! I really appreciate the support.
Question, do you need to sign-up for BoxyHQ's SaaS product to use this repo?
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
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 🤙
Thanks for picking this up Sama!
love to see Boxy here more often! :))
Thanks, @jobenjada, for the support.
We are huge fans of Formbricks as well so it's mutual.
How does one make this work with SAML IdP initiated login?
Hi @geduldig, let me tag @caloique who can get you the correct answer.
Thanks @nathan_tarbert
You're welcome @deepakprab
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.
Really detailed and great explanations Nathan 🔥
And yes
Saml Jackson 🙏🙏
Thanks @thevinitgupta!
I really appreciate your feedback :)
Second time reading this article. Well detailed
Thanks, Nathan.
Wow, thanks so much for the amazing feedback @emmacode.
I hope it's clear and concise!
This is both clear and thorough, thank you for taking the time to write it!
That's really great to hear @danidoesthings, thank you for the feedback!