This page provides a step-by-step guide on how to integrate the TypeScript SDK in a Next.js (>= 13.4) project with server-side rendering (SSR) using the app router, server components, and middleware.
To illustrate our example, we will create a demo app which will allow you to retrieve your user's name and favorite ice cream flavor after they log in with sgID.
If you have not already obtained your client credentials via registration, please register your client before proceeding.
For this example, you should add:
1. [openid, myinfo.name] as the scopes and
2. http://localhost:5001/success as a redirect URL
To run the example locally, clone from our by running:
Update your .env file with your client credentials.
Visit to check that your app is running.
If you click on 'Login with Singpass' and authenticate with your Singpass mobile app, you should see your user info on the success screen.
In this section, we'll break down the different steps that our example app goes through.
In this step, we will create an instance of our SgidClient class which will help us to interface with the sgID server.
In the .env file created from the previous step, fill out your sgID credentials.
Next, Initialize the SDK by calling the constructor and providing your client credentials.
To maintain session data across requests, we will need to initialize an in-memory store for our server.
The purpose of this middleware is to generate a session ID and set it in the browser's cookies when the user visits the site. This session ID is important for the server to identify the browser and track the session data through the login flow.
Now, we need to create a button to redirect the browser to the /login page. Additionally, we will be creating an ice cream flavour selector in order to demonstrate the OAuth 2.0 protocol's ability to carry over state.
In the following examples, we will not include any styling in order to keep the code snippet short. If you would like to view and interact with an example with styling, feel free to refer to the .
As we will be using useState, this component will be a .
Next, add the log in button to the home page as such.
/login page which redirects to the authorization URLWhen the user clicks on the 'Login with Singpass' button, it should direct them to the /login page which will generate the authorization URL to redirect the browser to.
The server function handleLogin will do the following.
Retrieve the session ID from the browser cookies (that was set by the middleware)
Generate a PKCE pair (consisting of code challenge and code verifier)
Generate an authorization URL
Store the code verifier in the in-memory store with the session ID as the key
/success pageOnce the end-user has successfully authorized your application with their Singpass mobile app, the sgID server will redirect the browser to the provided redirect URL.
When the browser visits this URL, the following will happen.
The authorization code is retrieved from the URL search params
The code is exchanged for an access token
The user info is requested with the access token
The user info is stored in-memory for subsequent requests
Now if you run your Next.js app, click on Login with Singpass and complete the authorization flow with your Singpass mobile app, you should be brought to this success page where you can view your personal details as well as your ice cream flavour selected on the login page.
The purpose of this page is to demonstrate how to retrieve the user data stored in-memory for subsequent requests. We will simply be creating a dummy page that displays the user info just like in the previous step.
If you complete the log in flow once again but click on the 'View user info' button, you will be brought to this /login page where you can view user data. To ensure that the user info has been properly stored in the in-memory store, try refreshing the page - you should see that the user info can still be fetched by the page.
You have reached the end of the Next.js (SSR) step-by-step guide.
The for this example can be found here which includes an /logout page (and middleware) for logging out, styling with Tailwind CSS, and a README on how to run the example locally.
If you want to find out more about how sgID works, click here to .
If you have more questions about sgID, check out our for answers to common questions.
Redirect the browser to the authorization URL
The user info is used to generate the HTML page on the server

git clone https://github.com/opengovsg/sgid-client.git
cd sgid-client/examples/nextjs-ssr
cat .env.example > .env # Copy the `.env.example` file
npm installSGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key># In the /sgid-client/examples/nextjs-ssr directory
npm run devSGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>// Server-side code
import { SgidClient } from '@opengovsg/sgid-client'
const sgidClient = new SgidClient({
clientId: String(process.env.SGID_CLIENT_ID),
clientSecret: String(process.env.SGID_CLIENT_SECRET),
privateKey: String(process.env.SGID_PRIVATE_KEY),
redirectUri: 'http://localhost:5001/success',
})
export { sgidClient }/**
* We place the store in the global object to prevent it from being cleared whenever compilation happens
*/
declare global {
var store: Map<string, Session> | undefined
}
type Session = {
state?: string;
nonce?: string;
codeVerifier?: string;
accessToken?: string;
userInfo?: Record<string, string>;
sub?: string;
};
let store: Map<string, Session>
if (process.env.NODE_ENV === 'production') {
store = new Map<string, Session>()
} else {
// If the store does not exist, initialize it
if (!global.store) {
global.store = new Map<string, Session>()
}
store = global.store
}
export { store }
import { NextRequest, NextResponse } from "next/server";
import { v4 as uuidv4 } from "uuid";
export async function middleware(req: NextRequest) {
switch (req.nextUrl.pathname) {
case "/":
// Generate new session ID
const sessionId = req.cookies.get("sessionId")?.value || uuidv4();
// Set session ID in cookie
const res = NextResponse.next();
res.cookies.set({
name: "sessionId",
value: sessionId,
httpOnly: true,
});
return res;
}
}// Client-side code
"use client";
import { useState } from "react";
import Link from "next/link";
const flavours = ["Vanilla", "Chocolate", "Strawberry"] as const;
type IceCream = (typeof flavours)[number];
const LoginWithIceCream = () => {
const [state, setState] = useState<IceCream>("Vanilla");
return (
<div>
<h2>Favourite ice cream flavour</h2>
<div>
{flavours.map((flavour) => (
<div
key={flavour}
onClick={() => setState(flavour)}
>
<input
type="radio"
checked={state === flavour}
value={flavour}
onChange={(e) => setState(e.target.value as IceCream)}
title="flavour"
/>
{flavour}
</div>
))}
</div>
<Link
prefetch={false}
href={`/login?state=${state}`}
>
<button>
Login with Singpass app
</button>
</Link>
</div>
);
};
export default LoginWithIceCream;// Server-side code
import LoginWithIceCream from "@/components/LoginWithIceCream";
export default function Home() {
return (
<main>
<LoginWithIceCream />
</main>
);
}// Server-side code
import { redirect } from "next/navigation";
import { sgidClient } from "@/lib/sgidClient";
import { NextSSRPage } from "@/types";
import { cookies } from "next/headers";
import { store } from "@/lib/store";
import { generatePkcePair } from "@opengovsg/sgid-client";
const handleLogin = async (state: string) => {
const sessionId = cookies().get("sessionId")?.value || "";
if (!sessionId) {
throw new Error("Session ID not found in browser's cookies");
}
// Generate PKCE pair
const { codeChallenge, codeVerifier } = generatePkcePair();
// Generate authorization url
const { url, nonce } = sgidClient.authorizationUrl({
state,
codeChallenge,
});
// Store code verifier, state, and nonce in memory
store.set(sessionId, { state, codeVerifier, nonce });
redirect(url);
};
export default async function Login(
{ searchParams }: { searchParams: { [key: string]: string | undefined }}
) {
await handleLogin(searchParams?.state || "");
return <></>;
}// Server-side code
import { store } from '@/lib/store'
import { sgidClient } from '@/lib/sgidClient'
import { cookies } from 'next/headers'
import Link from 'next/link'
const getAndStoreUserInfo = async (code: string, sessionId: string) => {
const session = store.get(sessionId)
if (!session) {
throw new Error('Session not found')
}
const { nonce, codeVerifier } = session
if (!codeVerifier) {
throw new Error('Code verifier not found')
}
// Exchange auth code for access token
const { accessToken, sub } = await sgidClient.callback({
code,
nonce,
codeVerifier,
})
// Request user info with access token
const { data } = await sgidClient.userinfo({
accessToken,
sub,
})
// Store userInfo and sgID in memory
const updatedSession = {
...session,
userInfo: data,
sub, // `sub` is a unique sgID identifier for your end-user
}
store.set(sessionId, updatedSession)
return updatedSession
}
export default async function Redirect({
searchParams,
}: {
searchParams: { [key: string]: string | undefined }
}) {
// Auth code is retrieved from the URL search params
const code = searchParams?.code
const sessionId = cookies().get('sessionId')?.value
if (!code) {
throw new Error(
'Authorization code is not present in the url search params',
)
} else if (!sessionId) {
throw new Error("Session ID not found in browser's cookies")
}
const { state, userInfo, sub } = await getAndStoreUserInfo(code, sessionId)
// HTML page is generated in the server
return (
<div>
<div>
Logged in successfully!
</div>
{sub ? (
<div>{`sgID: ${sub}`}</div>
) : null}
{Object.entries(userInfo ?? {}).map(([field, value]) => (
<div>{`${field}: ${value}`}</div>
))}
{state ? (
<div>
{`Favourite ice cream flavour: ${state}`}
</div>
) : null}
<Link href="/user-info">
View user info
</Link>
</div>
)
}// Server-side code
import { store } from "@/lib/store";
import { cookies } from "next/headers";
const getUserInfo = async (sessionId: string) => {
// Retrieve session from memory
const session = store.get(sessionId);
if (!session) {
throw new Error("No session found");
}
return session;
};
export default async function LoggedIn() {
const sessionId = cookies().get("sessionId")?.value;
if (!sessionId) {
throw new Error("Session ID not found in browser's cookies");
}
const { sub, userInfo } = await getUserInfo(sessionId);
if (!sub || !userInfo) {
throw new Error("User has not authenticated");
}
return (
<div>
<div>User Info</div>
{sub ? (
<div>{`sgID: ${sub}`}</div>
) : null}
{Object.entries(userInfo ?? {}).map(([field, value]) => (
<div>{`${field}: ${value}`}</div>
))}
</div>
);
}