Note: The content in this blog is also available in a video format:
Introduction and Background
Next-Auth is a complete open-source authentication solution for Next.js. For those who are unfamiliar with Next.js, it’s a React framework that enhances features such as routing, easy and fast development cycles, and serverless functions.
This article is mainly targeting developers who are interested in using Next-Auth within their Next.js projects to integrate with SailPoint Identity Security Cloud (ISC). It’s also suggested to have some basic knowledge of TypeScript, React, and Next.js. For the moment, Next-Auth is mainly used for the Next.js framework only. Currently, there is progress moving Next-Auth to Auth.js, which is framework independent. While Auth.js is still in beta, here I will use Next-Auth as a case study for ISC OAuth usage. In the future, this will be a similar setup when moving to Auth.js with another framework.
SailPoint ISC defines a set of authentication methods which can be used by 3rd party or custom applications. (Reference: Authentication) In this article, we will go through how to request an access_token with an authorization code and refresh token.
Before we start, here is the diagram which describes the authorization code grant flow:
- The user clicks the login link on a web app.
- The web app sends an authorization request to ISC in this form:
GET https://{tenant}.login.sailpoint.com/oauth/authorize?client_id={client-id}&response_type=code&redirect_uri={redirect-url}
- ISC redirects the user to a login prompt to authenticate to Identity Security Cloud.
- The user authenticates to ISC.
- Once authentication is successful, ISC issues an authorization code back to the web app.
- The web app submits an OAuth 2.0 token request to ISC in this form:
POST https://{tenant}.api.identitynow.com/oauth/token?grant_type=authorization_code&client_id={client-id}&code={code}&redirect_uri={redirect-url}
Pre-requistes
- ISC Tenant OAuth Information (can be retrieved under
https://{tenant}.api.identitynow.com/oauth/info
- Create OAuth Client with grant types:
AUTHORIZATION_TOKEN
andREFRESH_TOKEN
. Use the scopesp:scope:all
. We also needs to set the redirect URL tohttp://localhost:3000/api/auth/callback/identitySecureCloud
. The redirect URL is constructed by Next-Auth follow following the formathttp://{domain}/api/auth/callback/{providerId}
. The providerId will be configured later in the session. However, we simply pre-configure it first. Note down the clientId and clientSecret. - Initiate a React project. While there are many ways to initiate a React project, here I recommend using Next.js installation.
npx create-next-app@latest
- Install Next-Auth
npm install next-auth
Creating a Next-Auth OAuth Provider
Next-Auth does have lots of built-in OAuth providers such as Google, Apple, GitHub, …, even TikTok. For the moment, there is still no template for SailPoint Identity Secure Cloud. Therefore, in this article, we will use the Custom OAuth provider.
- Create folder(s)
/api/auth/[...nextauth]
. Note, in the Next.js framework, the /api folder defines the REST API endpoints. - Create the custom provider file:
/api/auth/sailpoint.ts
import { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
export interface IdentitySecureCloudProfile extends Record<string, any> {
tenant: string;
id: string;
uid: string;
email: string;
phone: string;
workPhone: string;
firstname: string;
lastname: string;
capabilities: string[];
displayName: string;
name: string;
}
export default function SailPoint(
config: OAuthUserConfig<IdentitySecureCloudProfile> & {
baseUrl: string,
apiUrl: string,
clientId: string,
clientSecret: string,
scope: string
}
): OAuthConfig<IdentitySecureCloudProfile> {
return {
id: "identitySecureCloud",
name: "Identity Secure Cloud",
type: "oauth",
clientId: config.clientId,
clientSecret: config.clientSecret,
authorization: {
url: `${config.baseUrl}/oauth/authorize`,
params: { scope: config.scope },
},
token: `${config.apiUrl}/oauth/token`,
userinfo: `${config.apiUrl}/oauth/userinfo`,
profile(profile: IdentitySecureCloudProfile) {
return {
tenant: profile.tenant,
id: profile.id,
uid: profile.uid,
email: profile.email,
phone: profile.phone,
workPhone: profile.workPhone,
firstname: profile.firstname,
lastname: profile.lastname,
capabilities: profile.capabilities,
displayName: profile.displayName,
name: profile.uid,
}
},
options: config,
}
}
- The interface
IdentitySecureCloudProfile
describes the user profile which is returned from the access_token. - The SailPoint function has the input arguments
config
, which requires information such as base_url (e.g., https://{tenant}.identitynow.com), api_url (e.g., https://{tenant}.api.identitynow.com), clientId, clientSecret, and scope. We then use that information to construct the custom OAuth provider.
Setup Refresh Token
We then create another file /api/auth/authOptions
:
import { NextAuthOptions } from "next-auth";
import axios from "axios";
import SailPoint from "./sailpoint";
export const authOptions: NextAuthOptions = {
providers: [
SailPoint({
baseUrl: process.env.ISC_BASE_URL!,
apiUrl: process.env.ISC_BASE_API_URL!,
clientId: process.env.ISC_CLIENT_ID!,
clientSecret: process.env.ISC_CLIENT_SECRET!,
scope: 'sp:scopes:all',
}),
],
callbacks: {
async jwt({ token, user, account, profile }) {
// First login
if (account && user && profile) {
token.accessToken = account.access_token;
token.id = profile.id;
token.tenant = profile.tenant;
token.displayName = profile.displayName;
token.name = profile.uid;
token.capabilities = profile.capabilities;
token.accessTokenExpires= account.expires_at!;
token.refreshToken= account.refresh_token;
return token;
}
// Return previous token if the access token has not expired yet
if (Date.now() < (token.accessTokenExpires as number) * 1000) {
return token
}
// Access token has expired, try to update it
return refreshAccessToken(process.env.ISC_BASE_API_URL!, token)
},
async session({ session, user, token }) {
session.accessToken = token.accessToken as string;
session.user.id = token.id as string;
session.user.capabilities = token.capabilities as string[];
session.user.tenant = token.tenant as string;
session.user.name = token.name as string;
session.user.uid = token.name as string;
session.user.displayName = token.displayName as string;
return session;
},
}
}
/**
* Takes a token, and returns a new token with updated
* `accessToken` and `accessTokenExpires`. If an error occurs,
* returns the old token and an error property
*/
async function refreshAccessToken(apiUrl: string, token: any) {
try {
const response = await axios.post(`${apiUrl}/oauth/token`, {
client_id: process.env.ISC_CLIENT_ID!,
client_secret: process.env.ISC_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
}, {
headers: {
'Content-Type': "application/x-www-form-urlencoded"
}
})
const { access_token, expires_in, token_type, refresh_token } = response.data;
return {
...token,
accessToken: access_token,
accessTokenExpires: Date.now() + expires_in,
refreshToken: refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.error('Error refreshing access token:', error);
throw error;
}
}
- The authOptions is a NextAuthOptions object, which contains an array of providers. We first define SailPoint (our custom OAuth provider) in the providers list.
- In order to implement the practice of refresh tokens, we need to further define the callback functions:
jwt()
andsession()
. The jwt() function is called once Next-Auth retrieves the access_token from ISC. On the first login, we store all required user information into the token object. We then check if the current token expiration timestamp has already been exceeded. If yes, then we call therefreshAccessToken()
function. - The refreshAccessToken() function uses axios to request a new access_token from ISC. Once successfully requested, it updates the
accessToken
,accessTokenExpires
, andrefreshToken
in the token object. - The session() function is used to store additional information in the session, and can also be used in server or client components within the React/Next.js project.
Note: Here we may find that session.user may not contain the additional attributes we want to store in the session. To resolve this TypeScript error, we shall create a folder and file under the root folder of the project: /types/next-auth.d.ts
import NextAuth from "next-auth/next";
declare module "next-auth" {
interface Session {
user: {
id: string;
name: string;
email: string;
tenant: string;
uid: string;
capabilities: string[];
displayName: string;
}
accessToken: string;
}
interface Profile {
tenant: string;
id: string;
uid: string;
email: string;
phone: string;
workPhone: string;
firstname: string;
lastname: string;
capabilities: string[];
displayName: string;
}
interface User {
id: string;
}
}
Create a middleware.ts to protect the routing pages
Under the Next.js framework, we simply create a file middleware.ts
under the root directory.
export { default } from "next-auth/middleware"
// Uncomment below to specify proectection on specific pages.
// export const config = { matcher: ["/dashboard/*"] }
In this example, simply exporting the default function will protect all web pages. We can also define a matcher regex to mark which pages require authentication.
Configure Environment Variables
As you might notice, under auhtOptions.ts, there are some variables with the process.env.* prefix. Those are referencing the environment variables. To configure the variables under the dev server, first create a file .env
. There we can put the corresponding variables in the file:
ISC_BASE_API_URL=https://{tenant}.api.identitynow.com
ISC_BASE_URL=https://{tenant}.identitynow.com
ISC_CLIENT_ID={clientId}
ISC_CLIENT_SECRET={clientSecret}
NEXTAUTH_SECRET={random long string}
NEXTAUTH_URL=http://localhost:3000
Note that NEXTAUTH_SECRET is required.
Get Server Session
Under the React component or any server-side pages, we can use getServerSession(authOptions)
to retrieve the session user information. In this practice, we will simply demo with the default home page of Next.js, which is /app/page.tsx
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/authOptions";
export default async function Home() {
const session = await getServerSession(authOptions);
return (
<>
<div className="flex flex-col">
<div className="py-2 container">
<span className="text-2xl">Hello {session?.user.displayName}</span>
</div>
</div>
</>
);
}
Testing
Run npm run dev
in the Terminal and access http://localhost:3000. We shall see the page redirect to http://localhost:3000/api/auth/signin?callbackUrl=%2F
Once select the Sing In option, it will then redriect to ISC login page
Once the Sign In option is selected, it will then redirect to the ISC login page.
Conclusion
So here we have successfully demonstrated how to use Next-Auth within React to conduct OAuth authentication integrated with ISC. There are many use cases that can be extended later, such as custom sign-in/sign-out pages. Nevertheless, this could be a good starting point for your first Next.js web application to authenticate with an Identity Provider such as ISC.
At the time of writing this blog, Next-Auth is at stable version 4, and Next-Auth will then be renamed to Auth.js in version 5. However, it’s still in beta version. I will keep this post updated on the future case for Auth.js.