# Next.js (server-side rendering)

This page provides a step-by-step guide on how to integrate the TypeScript SDK in a [Next.js (>= 13.4)](https://nextjs.org/blog/next-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.

<figure><img src="https://2214909052-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FpBW92htBuXTrDYoKovQ2%2Fuploads%2FKzsUAC4m4cUq9rJ1FQUc%2Fimage.png?alt=media&#x26;token=73750d0e-cda9-4e9a-aaa1-3906164944f9" alt="" width="375"><figcaption><p>sgID login page with an ice cream flavour selector</p></figcaption></figure>

{% 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/success` 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/nextjs-ssr) by running:

```bash
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 install
```

### Step 2: Update your environment variables

Update your `.env` file with your client credentials.

{% code title="examples/nextjs-ssr/.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

```bash
# In the /sgid-client/examples/nextjs-ssr 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.

1. [Initialize the SDK](#step-1-initialize-the-sdk)
2. [Initialize an in-memory store for sessions](#step-2-initialize-an-in-memory-store-for-sessions)
3. [Create the middleware for setting a session ID in cookies](#step-3-create-the-middleware-for-setting-a-session-id-in-cookies)
4. [Create a button to redirect to `/login`](#step-4-create-a-button-to-redirect-to-login)
5. [Create the `/login` page which redirects to the authorization URL](#step-5-create-the-login-page-which-redirects-to-the-authorization-url)
6. [Create the login `/success` page](#step-6-create-the-login-success-page)
7. [Create a `/userinfo` page to fetch user info from session store](#step-7-create-a-userinfo-page-to-fetch-user-info-from-session-store)

### 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/nextjs-ssr/.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 providing your client credentials.

{% code title="src/lib/sgidClient.ts" %}

```typescript
// 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 }
```

{% endcode %}

### 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.&#x20;

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

{% code title="src/lib/store.ts" %}

```typescript
/**
 * 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 }
```

{% endcode %}

### Step 3: Create the middleware for setting a session ID in cookies

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.

{% code title="src/middleware.tsx" lineNumbers="true" %}

```typescript

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;
  }
}
```

{% endcode %}

### Step 4: Create a button to redirect to /login

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 [source code](https://github.com/opengovsg/sgid-client/tree/feat/example-next-csr/examples/nextjs-csr).

As we will be using `useState`, this component will be a [client component](https://nextjs.org/docs/getting-started/react-essentials).

{% code title="src/components/LoginWithIceCream.tsx" lineNumbers="true" %}

```typescript
// 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;
```

{% endcode %}

Next, add the log in button to the home page as such.

{% code title="src/app/page.tsx" %}

```typescript
// Server-side code
import LoginWithIceCream from "@/components/LoginWithIceCream";

export default function Home() {
  return (
    <main>
      <LoginWithIceCream />
    </main>
  );
}
```

{% endcode %}

### Step 5: Create the `/login` page which redirects to the authorization URL

When 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
* Redirect the browser to the authorization URL

{% code title="src/app/login/page.tsx" lineNumbers="true" %}

```typescript
// 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 <></>;
}
```

{% endcode %}

### Step 6: Create the login `/success` page

Once 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.&#x20;

When the browser visits this URL, the following will happen.

1. The authorization code is retrieved from the URL search params
2. The code is exchanged for an access token
3. The user info is requested with the access token
4. The user info is stored in-memory for subsequent requests
5. The user info is used to generate the HTML page on the server

{% code title="src/app/success/page.tsx" lineNumbers="true" %}

```typescript
// 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>
  )
}
```

{% endcode %}

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.

{% hint style="info" %}
If this is sufficient for your application and you do not need user data for any other further actions, congratulations! You can stop reading here. However, if you would like to use user data on other routes and pages, continue on to the next step.
{% endhint %}

### Step 7: Create a /userinfo page to fetch user info from session store

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.

{% code title="src/app/user-info/page.tsx" lineNumbers="true" %}

```typescript
// 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>
  );
}
```

{% endcode %}

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.

## Congratulations! :tada:

You have reached the end of the Next.js (SSR) step-by-step guide.&#x20;

The [source code](https://github.com/opengovsg/sgid-client/tree/develop/examples/nextjs-ssr) 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 [**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.
