Express Example

This page provides a step-by-step guide of how to integrate the Node.js SDK in a simple Express server. This Express app can be used as a

The steps to integrate the Node.js SDK are as follows

  1. Integrate the frontend and backend

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: Setup the base Express app

In the directory you want to create your server, run

npm init -y

Follow the relevant steps based on whether you are creating a TypeScript or JavaScript project.

A new package.json file should have been created. Open the file and

  • Add a new script "dev": "nodemon index.ts" under "scripts" - this will be the script we use to start our development server

Next, install the relevant dependencies, including the sgID Node.js SDK.

npm i @opengovsg/sgid-client express uuid cookie-parser node-cache ts-node dotenv
npm i -D nodemon typescript @types/node @types/express @types/cookie-parser @types/uuid

Then, create a new index.ts file and setup the base Express app as follows

index.ts
import express, { Express } from "express";
import cookieParser from "cookier-parser";
import dotenv from "dotenv";

dotenv.config();

const app: Express = express();

app.use(cookieParser()); // used to interact with the browser's cookies 

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

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

To start the Express server, run

npm run dev

To confirm that you have correctly setup your Express server, visithttp://localhost:3000

If you see "Hello from sgID", congratulations! Your express server is working properly 🎉

Step 2: Initialize the SDK

After setting up the base Express 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>

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

Next, initialize the SDK by calling the constructor and passing in the environment variables.

index.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 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.

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

index.ts
import NodeCache from "node-cache";
import { v4 as uuidv4 } from "uuid";

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 4: Create the /api/callback 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: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);
    
    // Exchange the auth code for the access token
    const { accessToken, sub } = await sgidClient.callback({
        code,
        codeVerifier: memoryObject.codeVerifier
    });
    
    // Store the access token in memory
    const newMemoryObject = { ...memoryObject, 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 5: Create the /api/userinfo endpoint

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);
    
    // Request the userinfo using the access token
    const data = await sgidClient.userinfo({ 
        accessToken: memoryObject.accessToken
    });
    
    // Return the user info
    res.json(data);
});

Step 6: Integrate the frontend and backend

Now that your backend is setup properly, you will need to wire your frontend application to the backend server. As part of this example, we will be using this frontend repo built with React (you can use any framework to build your own frontend).

Firstly, clone the repository by running

git clone https://github.com/opengovsg/sgid-demo-frontend.git

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

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";

dotenv.config();

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',
})

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
  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);

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

  // Store the access token in memory
  const newMemoryObject = { ...memoryObject, 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);

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

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

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

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';
import dotenv from "dotenv";

dotenv.config();

const sgidClient = new SgidClient({
  clientId: process.env.SGID_CLIENT_ID,
  clientSecret: process.env.SGID_CLIENT_SECRET,
  privateKey: process.env.SGID_PRIVATE_KEY,
  redirectUri: 'http://localhost:3000/api/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 = { ...memoryObject, 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`);
});

Last updated