NextJS Notes
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)
...