NextJS Notes: Difference between revisions

From bibbleWiki
Jump to navigation Jump to search
 
(4 intermediate revisions by the same user not shown)
Line 214: Line 214:
</syntaxhighlight>
</syntaxhighlight>


==Server Example ===
==Server Example==
On the server we use getServerSession
On the server we use getServerSession
<syntaxhighlight lang="ts">
<syntaxhighlight lang="ts">
Line 229: Line 229:
     fetchStuff2(accessToken)
     fetchStuff2(accessToken)
...
...
</syntaxhighlight>
==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.
<syntaxhighlight lang="json">
{
  "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"
      }
    }
  ]
}
</syntaxhighlight>
</syntaxhighlight>

Latest revision as of 03:24, 21 October 2023

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"
      }
    }
  ]
}