# Express (with Single-Page App frontend)

This page provides a step-by-step guide on how to integrate the TypeScript SDK in a simple Express server. This Express server will be used as a [backend server for a SPA frontend](https://docs.id.gov.sg/learn-the-basics/integration-patterns/backend-for-single-page-app-spa-frontend-bff).

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.

<div align="center" data-full-width="false"><figure><img src="https://2214909052-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FpBW92htBuXTrDYoKovQ2%2Fuploads%2Fvo7BNfjodVRm1j1izAsm%2Fimage.png?alt=media&#x26;token=3064f678-e7a6-4992-bb3b-291e5e0ac34d" alt="" width="375"><figcaption><p>sgID login page with an ice cream flavour selector</p></figcaption></figure></div>

{% hint style="warning" %}
If you have not already obtained your client credentials via registration, please [register your client](https://docs.id.gov.sg/introduction/getting-started/register-your-application) 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
{% endhint %}

## Running the example locally

### Step 1: Clone the repo

To run the example locally, clone from our [source code](https://github.com/opengovsg/sgid-client/tree/develop/examples/express) by running:

```bash
# Clone the frontend repository
git clone https://github.com/opengovsg/sgid-demo-frontend-spa.git
cd sgid-demo-frontend-spa
cat .env.example > .env # Copy the `.env.example` file
npm install

cd ..

# Clone the backend repository
git clone https://github.com/opengovsg/sgid-client.git
cd sgid-client/examples/express
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.

{% code title="examples/express/.env" %}

```
SGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>
```

{% endcode %}

### Step 3: Run the example

In separate terminals, run the frontend and the backend.

```bash
# In the /sgid-client/examples/express directory
npm run dev

# Open a new terminal and in the /sgid-demo-frontend-spa directory
npm run dev 
```

Ensure that your backend Express server is running on <http://localhost:5001> and visit <http://localhost:5173>.

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.

1. [Initialize the SDK](#step-1-initialize-the-sdk)
2. [Create the `/api/auth-url` endpoint](#step-2-create-the-api-auth-url-endpoint)
3. [Create the`/api/redirect` endpoint](#step-3-create-the-api-redirect-endpoint)
4. [Create the`/api/userinfo` endpoint](#step-4-create-the-api-userinfo-endpoint)
5. [Test it out](#step-5-test-it-out)

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

{% code title="examples/express/.env" %}

```
SGID_CLIENT_ID=<your client id>
SGID_CLIENT_SECRET=<your client secret>
SGID_PRIVATE_KEY=<your private key>
```

{% endcode %}

{% hint style="info" %}
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.
{% endhint %}

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

{% code title="index.ts" %}

```typescript
import { SgidClient } from '@opengovsg/sgid-client'

const PORT = 5001

const sgid = 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:${PORT}/api/redirect`,
})
```

{% endcode %}

Before we can create the endpoints, we will need to configure the Express app.

{% code title="index.ts" %}

```typescript
import express, { Router } from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import * as dotenv from 'dotenv'
import open from 'open'

dotenv.config()

const PORT = 5001

// Initialize the sgID SDK here

const app = express()

const apiRouter = Router()

const SESSION_COOKIE_NAME = 'exampleAppSession'
const SESSION_COOKIE_OPTIONS = {
  httpOnly: true,
}

type SessionData = Record<
  string,
  | {
      nonce?: string
      // Store state as search params to easily stringify key-value pairs
      state?: URLSearchParams
      accessToken?: string
      codeVerifier?: string
      sub?: string
    }
  | undefined
>

/**
 * In-memory store for session data.
 * In a real application, this would be a database.
 */
const sessionData: SessionData = {}

app.use(
  cors({
    credentials: true,
    origin: 'http://localhost:5173',
  }),
)

const initServer = async (): Promise<void> => {
  try {
    app.use(cookieParser())
    app.use('/api', apiRouter)

    app.listen(PORT, () => {
      console.log(`Server listening on port ${PORT}`)
      void open(`http://localhost:${PORT}`)
    })
  } catch (error) {
    console.error(
      'Something went wrong while starting the server. Please restart the server.',
    )
    console.error(error)
  }
}

void initServer()
```

{% endcode %}

### Step 2: 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 make a `GET` request to this endpoint to retrieve the authorization URL. The browser is then redirected to this authorization URL.

<figure><img src="https://2214909052-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FpBW92htBuXTrDYoKovQ2%2Fuploads%2Fp17CgPCG1EPDgaiVDK81%2Fimage.png?alt=media&#x26;token=f6161986-3837-4f6b-89b0-4e019d1c5787" alt="" width="375"><figcaption><p>Clicking 'Login with Singpass app' makes a <code>GET</code> request to the /api/auth-url endpoint</p></figcaption></figure>

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 session
* Set the session ID in the browser's cookies
* Return the authorization URL

{% code title="index.ts" %}

```typescript
import crypto from "crypto"
import { generatePkcePair } from "@opengovsg/sgid-client"

apiRouter.get('/auth-url', (req, res) => {
  const iceCreamSelection = String(req.query.icecream)
  
  // Generate a session ID
  const sessionId = crypto.randomUUID()
  
  // Generate a PKCE pair
  const { codeChallenge, codeVerifier } = generatePkcePair()
  
  // Use search params to store state so other key-value pairs can be added easily
  const state = new URLSearchParams({
    icecream: iceCreamSelection,
  })
  
  // Generate an authorization URL
  const { url, nonce } = sgid.authorizationUrl({
    // We pass the user's ice cream preference as the state,
    // so after they log in, we can display it together with the
    // other user info.
    state: state.toString(),
    codeChallenge,
    // Scopes that all sgID relying parties can access by default
    scope: ['openid', 'myinfo.name'],
  })
  
  // Store code verifier, state, and nonce
  sessionData[sessionId] = {
    state,
    nonce,
    codeVerifier
  }
  
  // Return the authorization URL
  return res
    .cookie(SESSION_COOKIE_NAME, sessionId, SESSION_COOKIE_OPTIONS)
    .json({ url })
})
```

{% endcode %}

### Step 3: 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 session
* Exchange the authorization code and code verifier for the access token
* Store the access token and sub in session
* Redirect the browser to a logged in page (or any page of your choice)

{% code title="index.ts" %}

```typescript
apiRouter.get('/redirect', async (req, res): Promise<void> => {
  // Retrieve the authorization code and session ID
  const authCode = String(req.query.code)
  const state = String(req.query.state)
  const sessionId = String(req.cookies[SESSION_COOKIE_NAME])

  // Retrieve the code verifier from memory
  const session = sessionData[sessionId]
  
  // Validate that the state matches what we passed to sgID for this session
  if (session?.state.toString() !== state) {
    res.redirect('/error')
    return
  }
  
  // Validate that the code verifier exists for this session
  if (!session?.codeVerifier) {
    res.redirect(`${frontendHost}/error`)
    return
  }

  // Exchange the authorization code and code verifier for the access token
  const { accessToken, sub } = await sgid.callback({
    code: authCode,
    nonce: session.nonce,
    codeVerifier: session.codeVerifier,
  })
  
  // Store the access token and sub in session
  session.accessToken = accessToken
  session.sub = sub
  sessionData[sessionId] = session

  // Successful login, redirect to logged in state
  res.redirect('/logged-in')
})

```

{% endcode %}

{% hint style="info" %}
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 3) and utilize the `sub` value to identify the user.
{% endhint %}

### Step 4: 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 memory using the session ID
* Request user info using the access token
* Return the user info

{% code title="index.ts" %}

```typescript
apiRouter.get('/userinfo', async (req, res) => {
  // Retrieve the session ID
  const sessionId = String(req.cookies[SESSION_COOKIE_NAME])
  
  // Retrieve the access token and sub
  const session = sessionData[sessionId]
  const accessToken = session?.accessToken
  const sub = session?.sub

  // User is not authenticated
  if (session === undefined || accessToken === undefined || sub === undefined) {
    return res.sendStatus(401)
  }
  
  // Request user info using the access token
  const userinfo = await sgid.userinfo({
    accessToken,
    sub
  })

  // Add ice cream flavour (state) to userinfo
  userinfo.data.iceCream = session.state?.get('icecream') ?? 'None'
  
  // Return the user info
  return res.json(userinfo)
})
```

{% endcode %}

### Step 5: Integrate the frontend and backend

Now that your Express server has been set up properly, you will need to integrate your frontend application with it.&#x20;

If you have followed the steps from [Running the example locally](#running-the-example-locally), the frontend and backend examples have already been integrated for you.&#x20;

However, if you would like to integrate with your own frontend application, there are two main steps you need to implement:

1. A page with a 'Login with Singpass' button
   1. Click [here](https://github.com/opengovsg/sgid-demo-frontend-spa/blob/develop/src/pages/Home.tsx) for the relevant code in the frontend repo.
   2. The button will need to make a `GET` request to the `/api/auth-url` endpoint and then redirect the browser to the received authorization URL.
2. Fetching the user info after logging in
   1. Click [here](https://github.com/opengovsg/sgid-demo-frontend-spa/blob/develop/src/hooks/useAuth.ts) for the relevant code in the frontend repo.
   2. After the user logs in, the frontend can make a `GET` request to the `/api/userinfo` endpoint to retrieve the user info.

## Congratulations! :tada:

You have reached the end of the Express step-by-step guide.&#x20;

{% hint style="danger" %}
While these examples should work seamlessly in a local environment (i.e. localhost), they may not work if deployed (specifically if the frontend and backend are deployed on different domains).\
\
This is due to the [`SameSite`](https://web.dev/samesite-cookies-explained/) attribute on cookies. For these examples to work in a deployed environment, you would need to either

1. Utilize a reverse proxy to deploy the frontend and backend on the same domain; or
2. Set the `SameSite` attribute as `None` to be able to set cookies on a different domain
   {% endhint %}

If you want to find out more about how sgID works, click here to [**learn about the sgID protocol**](https://docs.id.gov.sg/learn-the-basics/protocols/sgid).&#x20;

If you have more questions about sgID, check out our [**FAQ**](https://docs.id.gov.sg/faq-developers) for answers to common questions.
