NextJS Notes

From bibbleWiki
Jump to navigation Jump to search

Introduction

This page is meant to capture parts of NextJS not covered by React

External Images

When using external images on a page you next to specify the allowed domains in nextjs.config.js

const nextConfig = {
    images: {
      remotePatterns: [
        {
          protocol: "https",
          hostname: "images.unsplash.com",
        },
      ],
    },
    experimental: {
      serverActions: true,
    },
  };

Middleware

Protecting routes can be achieved using a file called middleware.ts at the same level as project/src/

export {default} from "next-auth/middleware";

// NextAuth config
export const config = {
  // Add protected routes here
  matcher: [
    "/",
    "/admin/:path*",
    "/course/:path*",
  ]

Authentication

nextjs seems to use next-auth for this. You first create a session object and a provider and then you can access these in the server and client

Session

Create a file called src/lib/next-auth.d.ts. This is an object to share between client and server. The example creates additional fields, it can be anything provide you can generate in the provider (see below)

import 'next-auth'

declare module 'next-auth' {
    interface Session extends DefaultSession {
        email: string
        name: string
        accessTokenExpires: number
        graphAccessToken?: string
        apiToken?: string
    }
}

Provider

This differs depending on the provider but here is the Azure-ad example.

import { AuthOptions } from 'next-auth'
import { JWT } from 'next-auth/jwt'
import AzureADProvider from 'next-auth/providers/azure-ad'

interface AccessTokenResponse {
    access_token: string
}

export const getAccessTokenRequestOptions = (data: string): RequestInit => {
    return {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: data,
    }
}

const refreshAccessToken = async (oldAccessToken: JWT): Promise<string> => {
    const operation = 'Getting TEST API Refresh Access Token'

    let accessToken = ''

    try {
        const url = `${process.env.TEST_API_OAUTH_HOST}/${process.env.TEST_API_TENANT_ID}/oauth2/v2.0/token`

        const data = `client_id=${process.env.AZURE_AD_CLIENT_ID}&Client_secret=${process.env.AZURE_AD_CLIENT_SECRET}&grant_type=refresh_token&refresh_token=${oldAccessToken.refreshToken}`
        const requestOptions: RequestInit = getAccessTokenRequestOptions(data)

        const response = await fetch(url, requestOptions)

        if (!response.ok)
            throw new Error(
                `Error ${operation}. Error getting Access Token. Status returned: ${
                    response.statusText ? response.statusText : 'Unknown Status'
                }`
            )

        const jsonResponse = (await response.json()) as AccessTokenResponse

        if (!jsonResponse.access_token)
            throw new Error(`Error ${operation}. Error getting Access Token. Malformed response`)

        accessToken = jsonResponse.access_token
    } catch (err) {
        console.error('getAccessToken():error:', err)
        throw err
    }

    return accessToken
}

const getClientCredentialsToken = async (): Promise<string> => {
    const operation = 'Getting TEST API Access Token'

    let accessToken = ''

    try {
        const url = `${process.env.TEST_API_OAUTH_HOST}/${process.env.TEST_API_TENANT_ID}/oauth2/v2.0/token`

        const data = `client_id=${process.env.TEST_API_CLIENT_ID}&Client_secret=${process.env.TEST_API_CLIENT_SECRET}&grant_type=client_credentials&scope=${process.env.TEST_API_SCOPE}`
        const requestOptions: RequestInit = getAccessTokenRequestOptions(data)

        const response = await fetch(url, requestOptions)
        if (!response.ok)
            throw new Error(
                `Error ${operation}. Error getting Access Token. Status returned: ${
                    response.statusText ? response.statusText : 'Unknown Status'
                }`
            )

        const jsonResponse = (await response.json()) as AccessTokenResponse

        if (!jsonResponse.access_token)
            throw new Error(`Error ${operation}. Error getting Access Token. Malformed response`)

        accessToken = jsonResponse.access_token
    } catch (err) {
        console.error('getAccessToken():error:', err)
        throw err
    }

    return accessToken
}

export const authOptions: AuthOptions = {
    providers: [
        AzureADProvider({
            clientId: process.env.AZURE_AD_CLIENT_ID || '',
            clientSecret: process.env.AZURE_AD_CLIENT_SECRET || '',
            tenantId: process.env.AZURE_AD_TENANT_ID || '',
            authorization: {
                params: {
                    scope: `offline_access openid profile email User.Read`,
                },
            },
        }),
    ],

    session: { strategy: 'jwt' },

    debug: false,

    callbacks: {
        async jwt({ token, account }) {

            if (account?.access_token && account.expires_at) {
                token.graphAccessToken = account.access_token
                token.idToken = account.id_token
                token.refreshToken = account.refresh_token
                token.accessTokenExpires = account.expires_at * 1000
            }

            // Return previous token if the access token has not expired yet
            // @ts-ignore

            if (Date.now() < token.accessTokenExpires) {
                token.apiToken = await getClientCredentialsToken()
                return token
            }

            token.graphAccessToken = await refreshAccessToken(token)
            token.apiToken = await getClientCredentialsToken()

            return token

            // Access token has expired, refresh it
            // return token
        },
        async session({ session, token }) {
            const newSession = {
                ...session,
                name: token.name,
                email: token.email,
                accessTokenExpires: token.accessTokenExpires,
                graphAccessToken: token.graphAccessToken,
                apiToken: token.apiToken,
            }
            return newSession
        },
    },
    pages: {
    },
}

Client Example

The useSession hook gives access to the session object and standard session properties Here is example of client

...
    useSession({
        required: true,
        onUnauthenticated() {
            handleSignIn().catch(error => {
                console.error('There was an error signing in. Error was', error)
            })
        },
    })
...

Server Example

On the server we use getServerSession

...
    const session = await getServerSession(authOptions)
    const accessToken = session?.apiToken

    if (!accessToken) {
        return redirect('/signIn')
    }

    // Now we can fetch stuff
    fetchStuff1(accessToken)
    fetchStuff2(accessToken)
...

Helpful links

Here are some links

https://github.com/nextauthjs/next-auth/discussions/3940
https://github.com/nextauthjs/next-auth/issues/6462
https://next-auth.js.org/v3/tutorials/refresh-token-rotation
https://github.com/gitdagray/next-auth-intro

Debugging

Back and frontend can be debugged with the following. Make sure you select the right debug target. e.g.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Next.js: debug server-side",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev"
    },
    {
      "name": "Next.js: debug client-side",
      "type": "chrome",
      "request": "launch",
      "url": "http://localhost:3000"
    },
    {
      "name": "Next.js: debug full stack",
      "type": "node-terminal",
      "request": "launch",
      "command": "npm run dev",
      "serverReadyAction": {
        "pattern": "- Local:.+(https?://.+)",
        "uriFormat": "%s",
        "action": "debugWithChrome"
      }
    }
  ]
}