Next.js (client-side rendering)

This page provides a step-by-step guide on how to integrate the TypeScript SDK in a Next.js project using the pages router, api routes and client-side rendering (CSR).

To illustrate our example, we have prepared 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/api/redirect as a redirect URL

Running the example locally

Step 1: Clone the repo

To run the example locally, clone from our source code by running:

git clone https://github.com/opengovsg/sgid-client.git
cd sgid-client/examples/nextjs-csr
cat .env.example > .env # Copy the `.env.example` file
npm install

Step 2: Update your environment variables

Update your .env file with your client credentials.

examples/nextjs-csr/.env
SGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>

Step 3: Run the example

# In the /sgid-client/examples/nextjs-csr directory
npm run dev

Visit http://localhost:5001 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.

Breaking the example down

In this section, we'll break down the different steps that our example app goes through.

Step 1: Initialize the SDK

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.

examples/nextjs-csr/.env
SGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>

The main idea here is to load your sgID credentials in a secure way using environment variables instead of hard-coding them into your app.

Next, initialize the SDK by calling the constructor and providing your client credentials.

src/lib/sgidClient.ts
// 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/api/redirect',
})

export { sgidClient }

Step 2: Initialize an in-memory store for sessions

To maintain session data across requests, we will need to initialize an in-memory store for our server.

In a production environment, your session data should be stored in a database but for our example, we will be using an in-memory store to reduce complexity.

src/lib/store.ts
/**
 * 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 }

Step 3: Create the /api/auth-url endpoint

When an end user clicks on the sign in button on your application (e.g. 'Login with Singpass app'), it should redirect the browser to this endpoint.

The /api/auth-url endpoint should do the following

  • Generate a session ID

  • 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

  • Set the session ID in the browser's cookies

  • Redirect the browser to the authorization URL

pages/api/auth-url.ts
// Server-side code
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 as uuidv4 } from "uuid";
import { store } from "../../lib/store";
import { sgidClient } from "../../lib/sgidClient";
import { setCookie } from "cookies-next";
import { generatePkcePair } from "@opengovsg/sgid-client";

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  let { state } = req.query;
  state = String(state);

  // Generate a session ID
  const sessionId = uuidv4();

  // Generate a PKCE pair
  const { codeChallenge, codeVerifier } = generatePkcePair();

  // Generate an authorization URL
  const { url, nonce } = sgidClient.authorizationUrl({
    state,
    codeChallenge,
  });

  // Store the code verifier, state, and nonce
  store.set(sessionId, { state, nonce, codeVerifier });

  // Set the sessionID in the browser's cookies
  setCookie("sessionId", sessionId, { req, res });

  // Redirect the browser to the authorization URL
  res.redirect(url);
}

Step 4: Create the /api/redirect endpoint

After the user scans the QR code with their Singpass mobile app and authorizes your application to access the specified scopes, the sgID server will redirect the user's browser to the redirect_uri you specified earlier (either when initializing the SDK or when passed as a parameter to the authorizationUrl function).

The redirect will include the authorization code and the state (if provided earlier) in the form of query parameters. An example URL would look something like this

http://localhost:5001/api/redirect?
    code=someAuthCode
    &state=someState

The /api/redirect endpoint should do the following

  • Retrieve the authorization code from query params, and the session ID from browser cookies

  • Retrieve the code verifier from the in-memory store

  • Exchange the authorization code and code verifier for the access token

  • Store the access token and sub in the in-memory store

  • Redirect the browser to a logged in page (or any page of your choice)

pages/api/redirect.ts
// Server-side code
import type { NextApiRequest, NextApiResponse } from 'next'
import { store } from '../../lib/store'
import { sgidClient } from '../../lib/sgidClient'
import { getCookie, setCookie } from 'cookies-next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  // Retrieve the authorization code from query params
  let { code, state } = req.query
  
  // Retrieve the session ID from browser cookies
  const sessionId = getCookie('sessionId', { req, res })

  // Validating that the sessionID, contents in session, and auth code is present
  if (typeof sessionId !== 'string') {
    return res.status(401).send('Session ID not found in browser cookies')
  } else if (!code) {
    return res.status(400).send('Authorization code not found in query params')
  }
  code = String(code)

  const session = store.get(sessionId)

  if (!session) {
    return res.status(401).send('Session not found')
  } else if (state && state !== session.state) {
    return res.status(400).send('State does not match')
  }

  const { nonce, codeVerifier } = session

  if (!codeVerifier || typeof codeVerifier !== 'string') {
    return res.status(400).send('Code verifier not found')
  }

  // Exchange the auth code for the access token
  // At the end of this function, users are considered logged in by the sgID server
  const { accessToken, sub } = await sgidClient.callback({
    code,
    nonce,
    codeVerifier,
  })

  // Store the access token and sub
  const updatedSession = {
    ...session,
    accessToken,
    sub,
  }
  store.set(sessionId, updatedSession)

  res.redirect('/logged-in')
}

The sub is an end-user's unique sgID identifier.

If your application only needs to verify that a user is a real person with a Singpass account without needing to access any government-verified data, then you can stop here (after Step 4) and utilize the sub value to identify the user.

Step 5: Create the /api/userinfo endpoint

Once the browser has been redirected to a logged in/success page, your app can make a GET request to this endpoint which will use the access token stored in session to request user info from the sgID server.

The /api/userinfo endpoint should do the following:

  • Retrieve the session ID from browser cookies

  • Retrieve the access token from the in-memory store using the session ID

  • Request user info using the access token

  • Store user info in the in-memory store

  • Return the user info

pages/api/userinfo.ts
// Server-side code
import type { NextApiRequest, NextApiResponse } from 'next'
import { store } from '../../lib/store'
import { sgidClient } from '../../lib/sgidClient'
import { getCookie } from 'cookies-next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  // Retrieve the session ID from browser cookies
  const sessionId = getCookie('sessionId', { req, res })

  if (typeof sessionId !== 'string') {
    return res.status(401).send('Session ID not found in browser cookies')
  }

  // Retrieve the access token from memory using the session ID
  const session = store.get(sessionId)

  if (!session) {
    return res.status(401).send('Session not found')
  }
  const { accessToken, sub } = session

  if (!accessToken || typeof accessToken !== 'string') {
    return res.status(400).send('Access token not in session')
  } else if (!sub || typeof sub !== 'string') {
    return res.status(400).send('Sub not in session')
  }

  // Request user info using the access token
  const { data } = await sgidClient.userinfo({ accessToken, sub })

  // Store user info in the in-memory store
  const updatedSession = {
    ...session,
    userInfo: data,
  }
  store.set(sessionId, updatedSession)

  const { accessToken: _, nonce: __, ...dataToReturn } = updatedSession

  // Return the user info
  res.json(dataToReturn)
}

With this step, the API endpoints are completed. In the next 2 steps, we will complete the Next.js application by creating the user interface to interact with these endpoints.

Step 6: Create a button to redirect to /api/auth-url

Now, we need to create a button to redirect the browser to the /api/auth-url endpoint.

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 source code.

pages/index.tsx
// Client-side code
import type { NextPage } from "next";
import { useState } from "react";
import Link from "next/link";

const flavours = ["Vanilla", "Chocolate", "Strawberry"] as const;
type IceCream = (typeof flavours)[number];

const Home: NextPage = () => {
  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={`/api/auth-url?state=${state}`}
        >
          <button>
            Login with Singpass app
          </button>
        </Link>
      </div>
  );
};

export default Home;

Now when you run your Next.js app and visit http://localhost:5001, you should see a Login with Singpass button which when clicked should bring you to the sgID approval page with the QR code.

Step 7: Create a /logged-in page to fetch and render user info

After the /api/redirect endpoint completes the request and the user info is stored in the in-memory store, the browser will be redirected to the /logged-in page.

The redirection to this page also marks that the user has successfully logged in and your application should be able to freely retrieve user info from the in-memory store.

As such, we will make a GET request to the /api/userinfo endpoint on this page and render the user info. As with above, we will omit styling from the following example.

pages/logged-in.tsx
// Client-side code
import { useEffect, useState } from "react";

type UserInfoRes = {
  sub?: string;
  userInfo?: Record<string, string>;
  state?: string;
};

const LoggedIn = () => {
  const [data, setData] = useState<UserInfoRes | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState("");

  useEffect(() => {
    const getUserInfo = async () => {
      try {
        setIsLoading(true);
        const res = await fetch("/api/userinfo", { credentials: "include" });
        const data = (await res.json()) as UserInfoRes;
        setData(data);
      } catch (error) {
        setError(error instanceof Error ? error.message : String(error));
      } finally {
        setIsLoading(false);
      }
    };
    getUserInfo();
  }, []);
  
  if (isLoading) {
    return <div>Loading...</div>;
  } else if (error) {
    return <div>{`Error: ${error}`}</div>;
  }

  return (
    <div>
      <div>User Info</div>
      
      {data?.sub ? (
        <div>{`sgID: ${data.sub}`}</div>
      ) : null}
      {Object.entries(data?.userInfo ?? {}).map(([field, value]) => (
        <div>{`${field}: ${value}`}</div>
      ))}
      {data?.state ? (
        <div>
          {`Favourite ice cream flavour: ${data.state}`}
        </div>
      ) : null}
    </div>
  );
};

export default LoggedIn;

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.

Congratulations! 🎉

You have reached the end of the Next.js (CSR) step-by-step guide.

The source code for this example can be found here which includes a /api/logout API endpoint 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 learn about the sgID protocol.

If you have more questions about sgID, check out our FAQ for answers to common questions.

Last updated