diff --git a/.eslintrc.json b/.eslintrc.json index b1e516f..3dcc82d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,6 +5,9 @@ "node": true, "jest/globals": true }, + "globals": { + "RequestInit": true + }, "ignorePatterns": [ "build/" ], diff --git a/nextjs/server/package.json b/nextjs/server/package.json deleted file mode 100644 index bb5b084..0000000 --- a/nextjs/server/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@fief/fief/nextjs/server", - "private": true, - "main": "../../build/cjs/nextjs/server.js", - "module": "../../build/esm/nextjs/server.js", - "types": "../../build/esm/nextjs/server.d.ts" -} diff --git a/rollup.config.mjs b/rollup.config.mjs index 2bc2b3f..ea3261d 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -45,7 +45,6 @@ export default [ 'src/express/index.ts', 'src/nextjs/index.ts', 'src/nextjs/react.tsx', - 'src/nextjs/server.ts', ], plugins: [ typescript({ @@ -73,7 +72,6 @@ export default [ 'src/express/index.ts', 'src/nextjs/index.ts', 'src/nextjs/react.tsx', - 'src/nextjs/server.ts', ], plugins: [ typescript({ diff --git a/src/client.ts b/src/client.ts index 8e4d0a4..4f78ee1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -228,6 +228,16 @@ export interface FiefParameters { * @see [ID Token encryption](https://docs.fief.dev/going-further/id-token-encryption/) */ encryptionKey?: string; + + /** + * Additional fetch init options for `getOpenIDConfiguration` and `getJWKS` requests. + * + * Mostly useful to control fetch cache. + * + * @see [fetch cache property](https://developer.mozilla.org/en-US/docs/Web/API/Request/cache) + * @see [Next.js fetch](https://nextjs.org/docs/app/api-reference/functions/fetch#fetchurl-options) + */ + requestInit?: RequestInit; } /** @@ -253,6 +263,8 @@ export class Fief { private fetch: typeof fetch; + private requestInit?: RequestInit; + private openIDConfiguration?: Record; private jwks?: jose.JSONWebKeySet; @@ -274,6 +286,7 @@ export class Fief { } this.fetch = getFetch(); + this.requestInit = parameters.requestInit; this.crypto = getCrypto(); } @@ -348,6 +361,7 @@ export class Fief { * @param redirectURI - The exact same `redirectURI` you passed to the authorization URL. * @param codeVerifier - The raw [PKCE](https://docs.fief.dev/going-further/pkce/) code * used to generate the code challenge during authorization. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns A token response and user information. * @@ -360,6 +374,7 @@ export class Fief { code: string, redirectURI: string, codeVerifier?: string, + requestInit?: RequestInit, ): Promise<[FiefTokenResponse, FiefUserInfo]> { const openIDConfiguration = await this.getOpenIDConfiguration(); const payload = serializeQueryString({ @@ -374,9 +389,11 @@ export class Fief { const response = await this.fetch( openIDConfiguration.token_endpoint, { + ...requestInit || {}, method: 'POST', body: payload, headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/x-www-form-urlencoded', }, }, @@ -402,6 +419,7 @@ export class Fief { * If not provided, the access token will share the same list of scopes * as requested the first time. * Otherwise, it should be a subset of the original list of scopes. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns A token response and user information. * @@ -413,6 +431,7 @@ export class Fief { public async authRefreshToken( refreshToken: string, scope?: string[], + requestInit?: RequestInit, ): Promise<[FiefTokenResponse, FiefUserInfo]> { const openIDConfiguration = await this.getOpenIDConfiguration(); const payload = serializeQueryString({ @@ -425,9 +444,11 @@ export class Fief { const response = await this.fetch( openIDConfiguration.token_endpoint, { + ...requestInit || {}, method: 'POST', body: payload, headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/x-www-form-urlencoded', }, }, @@ -452,6 +473,7 @@ export class Fief { * @param requiredScopes - Optional list of scopes to check for. * @param requiredACR - Optional minimum ACR level required. Read more: https://docs.fief.dev/going-further/acr/ * @param requiredPermissions - Optional list of permissions to check for. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns {@link FiefAccessTokenInfo} * @throws {@link FiefAccessTokenInvalid} if the access token is invalid. @@ -548,6 +570,7 @@ export class Fief { * Return fresh {@link FiefUserInfo} from the Fief API using a valid access token. * * @param accessToken - A valid access token. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns Fresh user information. * @@ -556,13 +579,15 @@ export class Fief { * userinfo = await fief.userinfo('ACCESS_TOKEN'); * ``` */ - public async userinfo(accessToken: string): Promise { + public async userinfo(accessToken: string, requestInit?: RequestInit): Promise { const openIDConfiguration = await this.getOpenIDConfiguration(); const response = await this.fetch( openIDConfiguration.userinfo_endpoint, { + ...requestInit || {}, method: 'GET', headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, Authorization: `Bearer ${accessToken}`, }, }, @@ -577,6 +602,7 @@ export class Fief { * * @param accessToken - A valid access token. * @param data - An object containing the data to update. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns Updated user information. * @@ -590,14 +616,17 @@ export class Fief { public async updateProfile( accessToken: string, data: Record, + requestInit?: RequestInit, ): Promise { const updateProfileEndpoint = `${this.baseURL}/api/profile`; const response = await this.fetch( updateProfileEndpoint, { + ...requestInit || {}, method: 'PATCH', body: JSON.stringify(data), headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, @@ -615,6 +644,7 @@ export class Fief { * * @param accessToken - A valid access token. * @param newPassword - The new password. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns Updated user information. * @@ -626,14 +656,17 @@ export class Fief { public async changePassword( accessToken: string, newPassword: string, + requestInit?: RequestInit, ): Promise { const updateProfileEndpoint = `${this.baseURL}/api/password`; const response = await this.fetch( updateProfileEndpoint, { + ...requestInit || {}, method: 'PATCH', body: JSON.stringify({ password: newPassword }), headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, @@ -654,6 +687,7 @@ export class Fief { * * @param accessToken - A valid access token. * @param newPassword - The new email address. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns Updated user information. * @@ -665,14 +699,17 @@ export class Fief { public async emailChange( accessToken: string, email: string, + requestInit?: RequestInit, ): Promise { const updateProfileEndpoint = `${this.baseURL}/api/email/change`; const response = await this.fetch( updateProfileEndpoint, { + ...requestInit || {}, method: 'PATCH', body: JSON.stringify({ email }), headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, @@ -691,6 +728,7 @@ export class Fief { * * @param accessToken - A valid access token. * @param newPassword - The new email address. + * @param requestInit - Additional fetch init options. Mostly useful to control fetch cache. * * @returns Updated user information. * @@ -702,14 +740,17 @@ export class Fief { public async emailVerify( accessToken: string, code: string, + requestInit?: RequestInit, ): Promise { const updateProfileEndpoint = `${this.baseURL}/api/email/verify`; const response = await this.fetch( updateProfileEndpoint, { + ...requestInit || {}, method: 'POST', body: JSON.stringify({ code }), headers: { + ...requestInit && requestInit.headers ? requestInit.headers : {}, 'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`, }, @@ -752,6 +793,7 @@ export class Fief { const response = await this.fetch( `${this.baseURL}/.well-known/openid-configuration`, { + ...this.requestInit || {}, method: 'GET', }, ); @@ -771,6 +813,7 @@ export class Fief { const response = await this.fetch( openIDConfiguration.jwks_uri, { + ...this.requestInit || {}, method: 'GET', }, ); diff --git a/src/nextjs/index.ts b/src/nextjs/index.ts index 3b2a053..5901a55 100644 --- a/src/nextjs/index.ts +++ b/src/nextjs/index.ts @@ -5,6 +5,8 @@ */ import type { IncomingMessage, OutgoingMessage } from 'http'; import type { NextApiRequest, NextApiResponse } from 'next'; +import { revalidateTag } from 'next/cache'; +import { headers } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; import { pathToRegexp } from 'path-to-regexp'; @@ -31,6 +33,16 @@ const defaultAPIForbiddenResponse = async (req: NextApiRequest, res: NextApiResp res.status(403).send('Forbidden'); }; +const getServerSideHeaders = (headerName: string, req?: IncomingMessage): string | null => { + // "Legacy" Next.js, req object is required + if (req) { + return req.headers[headerName.toLowerCase()] as string | null; + } + // Next.js 14 style + const headersList = headers(); + return headersList.get(headerName); +}; + type FiefNextApiHandler = ( req: NextApiRequest & AuthenticateRequestResult, res: NextApiResponse, @@ -144,13 +156,6 @@ export interface FiefAuthParameters { */ userIdHeaderName?: string; - /** - * Name of the request header where user information is made available by middleware. - * - * Defaults to `X-FiefAuth-User-Info`. - */ - userInfoHeaderName?: string; - /** * Name of the request header where access token is made available by middleware. * @@ -238,8 +243,6 @@ class FiefAuth { private userIdHeaderName: string; - private userInfoHeaderName: string; - private accessTokenHeaderName: string; private accessTokenInfoHeaderName: string; @@ -282,7 +285,6 @@ class FiefAuth { ; this.userIdHeaderName = parameters.userIdHeaderName ? parameters.userIdHeaderName : 'X-FiefAuth-User-Id'; - this.userInfoHeaderName = parameters.userInfoHeaderName ? parameters.userInfoHeaderName : 'X-FiefAuth-User-Info'; this.accessTokenHeaderName = parameters.accessTokenHeaderName ? parameters.accessTokenHeaderName : 'X-FiefAuth-Access-Token'; this.accessTokenInfoHeaderName = parameters.accessTokenInfoHeaderName ? parameters.accessTokenInfoHeaderName : 'X-FiefAuth-Access-Token-Info'; } @@ -408,9 +410,6 @@ class FiefAuth { JSON.stringify(result.accessTokenInfo), ); } - if (result.user) { - requestHeaders.set(this.userInfoHeaderName, JSON.stringify(result.user)); - } return NextResponse.next({ request: { headers: requestHeaders } }); } catch (err) { if (err instanceof FiefAuthUnauthorized) { @@ -533,6 +532,64 @@ class FiefAuth { )(req, res as NextApiResponse); }; } + + /** + * Return the user ID set in headers by the Fief middleware, or `null` if not authenticated. + * + * This function is suitable for server-side rendering in Next.js. + * + * @param req - Next.js request object. Required for older versions of Next.js + * not supporting the `headers()` function. + * @returns The user ID, or null if not available. + */ + public getUserId(req?: IncomingMessage): string | null { + return getServerSideHeaders(this.userIdHeaderName, req); + } + + /** + * Return the access token information set in headers by the Fief middleware, + * or `null` if not authenticated. + * + * This function is suitable for server-side rendering in Next.js. + * + * @param req - Next.js request object. Required for older versions of Next.js + * not supporting the `headers()` function. + * @returns he access token information, or null if not available. + */ + public getAccessTokenInfo(req?: IncomingMessage): FiefAccessTokenInfo | null { + const rawAccessTokenInfo = getServerSideHeaders(this.accessTokenInfoHeaderName, req); + return rawAccessTokenInfo ? JSON.parse(rawAccessTokenInfo) : null; + } + + /** + * Fetch the user information object from the Fief API, if access token is available. + * + * This function is suitable for server-side rendering in Next.js. + * + * @param req - Next.js request object. Required for older versions of Next.js + * not supporting the `headers()` function. + * @param refresh - If `true`, the user information will be refreshed from the Fief API. + * Otherwise, Next.js fetch cache will be used. + * @returns The user information, or null if access token is not available. + */ + public async getUserInfo( + req?: IncomingMessage, + refresh: boolean = false, + ): Promise { + const accessTokenInfo = this.getAccessTokenInfo(req); + if (accessTokenInfo === null) { + return null; + } + const userId = accessTokenInfo.id; + if (refresh && !req) { + revalidateTag(userId); + } + const userinfo = await this.client.userinfo( + accessTokenInfo.access_token, + { cache: 'force-cache', next: { tags: [userId] } }, + ); + return userinfo; + } } export { diff --git a/src/nextjs/server.ts b/src/nextjs/server.ts deleted file mode 100644 index 628cc76..0000000 --- a/src/nextjs/server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { headers } from 'next/headers'; - -import { FiefAccessTokenInfo, FiefUserInfo } from '../client'; - -/** - * Return the user ID set in headers by the Fief middleware, or `null` if not authenticated. - * - * This function is suitable for server-side rendering in Next.js. - * - * @param headerName - Name of the request header. Defaults to `X-FiefAuth-User-Id`. - * @returns The user ID, or null if not available. - */ -export const fiefUserId = (headerName: string = 'X-FiefAuth-User-Id'): string | null => { - const headersList = headers(); - return headersList.get(headerName); -}; - -/** - * Return the user information object set in headers by the Fief middleware, - * or `null` if not authenticated. - * - * This function is suitable for server-side rendering in Next.js. - * - * @param headerName - Name of the request header. Defaults to `X-FiefAuth-User-Info`. - * @returns The user information, or null if not available. - */ -export const fiefUserInfo = (headerName: string = 'X-FiefAuth-User-Info'): FiefUserInfo | null => { - const headersList = headers(); - const rawUserInfo = headersList.get(headerName); - return rawUserInfo ? JSON.parse(rawUserInfo) : null; -}; - -/** - * Return the access token set in headers by the Fief middleware, - * or `null` if not authenticated. - * - * This function is suitable for server-side rendering in Next.js. - * - * @param headerName - Name of the request header. Defaults to `X-FiefAuth-Access-Token`. - * @returns The access token, or null if not available. - */ -export const fiefAccessToken = (headerName: string = 'X-FiefAuth-Access-Token'): string | null => { - const headersList = headers(); - return headersList.get(headerName); -}; - -/** - * Return the access token information set in headers by the Fief middleware, - * or `null` if not authenticated. - * - * This function is suitable for server-side rendering in Next.js. - * - * @param headerName - Name of the request header. Defaults to `X-FiefAuth-Access-Token-Info`. - * @returns The access token information, or null if not available. - */ -export const fiefAccessTokenInfo = (headerName: string = 'X-FiefAuth-Access-Token-Info'): FiefAccessTokenInfo | null => { - const headersList = headers(); - const rawAccessTokenInfo = headersList.get(headerName); - return rawAccessTokenInfo ? JSON.parse(rawAccessTokenInfo) : null; -};