Next.js Example

This page provides a step-by-step guide of how to integrate the Node.js SDK in a Next.js (>= 13.4) project. The guide is split into two sections to focus on the API route handlers and the front end pages.

  1. Front end pages

    1. Create a button to redirect to /api/auth-url

    2. Create a /logged-in page to fetch and render user info

If you would like to skip the step-by-step guide and view the code in its entirety, jump to the source code.

Step 1.1: Setup the base Next app

In the directory you want to create your server, run

npx create-next-app@latest

then follow the prompts to customize your app.

To start the Next app, run

npm run dev

To confirm that you have correctly setup your Next app, visithttp://localhost:3000

During integration with sgID, please start the server by running

npm run build
npm run start

This is because the in-memory store gets reset while loading a page/route for the first time - thus invalidating the browser's session. (This is due to hot module reloading (HMR) in the development environment)

Step 1.2: Initialize the SDK

After setting up the base Next app, you are ready to start integrating with sgID.

Firstly, create a .env file, copy the following into the file, and fill out your sgID credentials.

// Replace the values below with your own client credentials
SGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>

Initialize the SDK by calling the constructor and providing your client credentials.

If you have not already obtained your client credentials via registration, please register your client before proceeding.

src/services/sgid.ts
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:3000/api/callback',
})

Step 1.3: Create the /api/auth-url route handler

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.

Clicking 'Login with Singpass app' redirects the browser to the /api/auth-url 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)

  • Store the code verifier in an in-memory cache with the session ID as the key

  • Generate an authorization URL

  • Set the session ID in the browser's cookies

  • Redirect the browser to the authorization URL

src/app/api/auth-url/routes.ts
import NodeCache from "node-cache";

const nodeCache = new NodeCache();

app.get("/api/auth-url", (req, res) => {
    // Generate a session ID
    const sessionId = uuidv4();

    // Generate a PKCE pair
    const { codeVerifier, codeChallenge } = generatePkcePair();
    
    // Store the code verifier in memory
    const memoryObject = { codeVerifier };
    nodeCache.set(sessionId, memoryObject);
    
    // Generate an authorization URL
    const { url } = sgidClient.authorizationUrl({
        codeChallenge: codeChallenge
    });
    
    // Set the session ID in the browser's cookies
    res.cookie("sessionId", sessionId);
    
    // Redirect to the authorization URL (i.e. QR code page)
    res.redirect(url);
});

Step 1.4: Create the /api/callback route handler

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:3000/api/callback?
    code=someAuthCode
    &state=somestate

The /api/callback endpoint should do the following

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

  • Retrieve the code verifier from memory using the session ID

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

  • Store the access token in memory

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

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

app.get("/api/callback", async (req, res) => {
    // Retrieve the authorization code from the query params
    const { code } = req.query;
    
    // Retrieve the session ID from the browser's cookies
    const { sessionId } = req.cookies;
    
    // Retrieve the code verifier from memory using the session ID
    const memoryObject = nodeCache.take(sessionId);
    if (!memoryObject) {
        res.status(400).send("No authorization request was made before");
    }
    const { codeVerifier } = memoryObject;
    
    // Exchange the auth code for the access token
    const { accessToken, sub } = await sgidClient.callback({
        code,
        codeVerifier
    });
    
    // Store the access token in memory
    const newMemoryObject = { accessToken };
    nodeCache.set(sessionId, newMemoryObject);
    
    // Redirect the browser to a logged in page instead 
    // - if your Express server is a back-end for your front-end
    res.redirect("https://yourSPAFrontEnd.com/logged-in");
    // - OR if your Express server is a web server serving the HTML pages
    res.redirect("http://localhost:3000/logged-in");
});

Step 1.5: Create the /api/userinfo route handler

Once the browser has been redirected to a logged in page, your are able to use the access token stored in memory to request user info from the sgID server through this endpoint.

The /api/userinfo endpoint should do the following

  • Retrieve the session ID from browser cookies

  • Retrieve the access token from memory using the session ID

  • Request user info using the access token

  • Return the user info

As part of sgID's privacy-preserving approach, the data returned from requesting user info is encrypted by a symmetric key which itself is encrypted by your client's public key.

However, the SDK handles all the decryption for you in the userinfo function, making it a lot simpler to request user info.

app.get("/api/userinfo", async (req, res) => {
    // Retrieve the session ID from the browser's cookies
    const { sessionId } = req.cookies;
    
    // Retrieve the access token from memory using the session ID
   const memoryObject = nodeCache.take(sessionId);
    if (!memoryObject) {
        res.status(400).send("No access token found with the associated session ID");
    }
    const { accessToken } = memoryObject;
    
    // Request the userinfo using the access token
    const data = await sgidClient.userinfo({ accessToken });
    
    // Return the user info
    res.json(data);
});

Congratulations 🎉! Your application is now capable of using sgID as an authentication and authorization service. To test out your local Express server as a back-end for our SPA front-end, please refer to the development environment page.

Source code

JavaScript source code
index.js
// SDK imports
import { SgidClient, generatePkcePair } from "@opengovsg/sgid-client"

// Other imports
import express from "express";
import NodeCache from "node-cache";
import cookieParser from "cookie-parser";
import { v4 as uuidv4 } from 'uuid';

// Replace the values below with your own client credentials 
const sgidClient = new SgidClient({
  clientId: 'CLIENT-ID',  
  clientSecret: 'cLiEnTsEcReT',
  privateKey: '-----BEGIN PRIVATE KEY-----MII ... XXX-----END PRIVATE KEY-----',
  redirectUri: 'http://localhost:3000/callback',
})

const nodeCache = new NodeCache();

const app = express();

app.use(cookieParser());

app.get("/", (req, res) => {
  res.send("Hello from sgID");
});

app.get("/api/auth-url", (req, res) => {
    // Generate a session ID
    const sessionId = uuidv4();

    // Generate a PKCE pair
    const { codeVerifier, codeChallenge } = generatePkcePair();
    
    // Store the code verifier in memory
    const memoryObject = { codeVerifier };
    nodeCache.set(sessionId, memoryObject);
    
    // Generate an authorization URL
    const { url } = sgidClient.authorizationUrl({
        codeChallenge: codeChallenge
    });
    
    // Set the session ID in the browser's cookies
    res.cookie("sessionId", sessionId);
    
    // Redirect to the authorization URL (i.e. QR code page)
    res.redirect(url);
});

app.get("/api/callback", async (req, res) => {
    // Retrieve the authorization code from the query params
    const { code } = req.query;
    
    // Retrieve the session ID from the browser's cookies
    const { sessionId } = req.cookies;
    
    // Retrieve the code verifier from memory using the session ID
    const memoryObject = nodeCache.take(sessionId);
    if (!memoryObject) {
        res.status(400).send("No authorization request was made before");
    }
    const { codeVerifier } = memoryObject;
    
    // Exchange the auth code for the access token
    const { accessToken,  } = await sgidClient.callback({
        code,
        codeVerifier
    });
    
    // Store the access token in memory
    const newMemoryObject = { accessToken };
    nodeCache.set(sessionId, newMemoryObject);
    
    // Return sub, which is a unique identifer for the user
    res.json({ sub });
});

app.get("/api/userinfo", async (req, res) => {
    // Retrieve the session ID from the browser's cookies
    const { sessionId } = req.cookies;
    
    // Retrieve the access token from memory using the session ID
    const memoryObject = nodeCache.take(sessionId);
    if (!memoryObject) {
        res.status(400).send("No access token found with the associated session ID");
    }
    const { accessToken } = memoryObject;
    
    // Request the userinfo using the access token
    const data = await sgidClient.userinfo({ accessToken });
    
    // Return the user data
    res.json(data);
});

app.listen(3000, () => {
  console.log(`⚡️[server]: Server is running at http://localhost:3000`);
});

TypeScript source code
index.ts
// SDK imports
import { SgidClient, generatePkcePair } from "@opengovsg/sgid-client";

// Other imports
import express, { Express } from "express";
import NodeCache from "node-cache";
import cookieParser from "cookie-parser";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";

// Replace the values below with your own client credentials
const sgidClient = new SgidClient({
  clientId: "CLIENT-ID",
  clientSecret: "cLiEnTsEcReT",
  privateKey: "-----BEGIN PRIVATE KEY-----MII ... XXX-----END PRIVATE KEY-----",
  redirectUri: "http://localhost:3000/callback",
});

const nodeCache = new NodeCache();

const app: Express = express();

app.use(cookieParser());

app.get("/", (req, res) => {
  res.send("Hello from sgID");
});

app.get("/api/auth-url", (req, res) => {
  // Generate a session ID
  const sessionId = uuidv4();

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

  // Store the code verifier in memory
  const memoryObject = { codeVerifier };
  nodeCache.set(sessionId, memoryObject);

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

  // Set the session ID in the browser's cookies
  res.cookie("sessionId", sessionId);

  // Redirect to the authorization URL (i.e. QR code page)
  res.redirect(url);
});

app.get("/api/callback", async (req, res) => {
  // Retrieve the authorization code from the query params
  let { code } = req.query;
  code = z.string().parse(code);

  // Retrieve the session ID from the browser's cookies
  const { sessionId } = req.cookies;

  // Retrieve the code verifier from memory using the session ID
  const memoryObject = nodeCache.take(sessionId);
  const parsedMemoryObject = z
    .object({ codeVerifier: z.string() })
    .parse(memoryObject);
  const { codeVerifier } = parsedMemoryObject;

  // Exchange the auth code for the access token
  const { accessToken, sub } = await sgidClient.callback({
    code,
    codeVerifier,
  });

  // Store the access token in memory
  const newMemoryObject = { accessToken };
  nodeCache.set(sessionId, newMemoryObject);

  // Return sub, which is a unique identifer for the user
  res.json({ sub });
});

app.get("/api/userinfo", async (req, res) => {
  // Retrieve the session ID from the browser's cookies
  const { sessionId } = req.cookies;

  // Retrieve the access token from memory using the session ID
  const memoryObject = nodeCache.take(sessionId);
  const parsedMemoryObject = z
    .object({ accessToken: z.string() })
    .parse(memoryObject);
  const { accessToken } = parsedMemoryObject;

  // Request the userinfo using the access token
  const data = await sgidClient.userinfo({ accessToken });

  // Return the user data
  res.json(data);
});

app.listen(3000, () => {
  console.log(`⚡️[server]: Server is running at http://localhost:3000`);
});

Last updated