@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
625 lines (624 loc) • 30.4 kB
JavaScript
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Auth0Client_options, _Auth0Client_tokenRequestCache;
import { cookies } from "next/headers.js";
import { NextRequest, NextResponse } from "next/server.js";
import { AccessTokenError, AccessTokenErrorCode, AccessTokenForConnectionError, AccessTokenForConnectionErrorCode, ConnectAccountError, ConnectAccountErrorCodes } from "../errors/index.js";
import { DEFAULT_SCOPES } from "../utils/constants.js";
import { validateDpopConfiguration } from "../utils/dpopUtils.js";
import { isRequest } from "../utils/request.js";
import { getSessionChangesAfterGetAccessToken } from "../utils/session-changes-helpers.js";
import { AuthClient } from "./auth-client.js";
import { RequestCookies, ResponseCookies } from "./cookies.js";
import * as withApiAuthRequired from "./helpers/with-api-auth-required.js";
import { appRouteHandlerFactory, pageRouteHandlerFactory } from "./helpers/with-page-auth-required.js";
import { StatefulSessionStore } from "./session/stateful-session-store.js";
import { StatelessSessionStore } from "./session/stateless-session-store.js";
import { TokenRequestCache } from "./token-request-cache.js";
import { TransactionStore } from "./transaction-store.js";
export class Auth0Client {
constructor(options = {}) {
_Auth0Client_options.set(this, void 0);
// Cache for in-flight token requests to prevent race conditions
_Auth0Client_tokenRequestCache.set(this, new TokenRequestCache());
__classPrivateFieldSet(this, _Auth0Client_options, options, "f");
// Extract and validate required options
const { domain, clientId, clientSecret, appBaseUrl, secret, clientAssertionSigningKey } = this.validateAndExtractRequiredOptions(options);
this.domain = domain;
const clientAssertionSigningAlg = options.clientAssertionSigningAlg ||
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_ALG;
// Validate DPoP configuration and resolve from environment variables if needed
const { dpopKeyPair: resolvedDpopKeyPair, dpopOptions: resolvedDpopOptions } = validateDpopConfiguration(options);
// Auto-detect base path for cookie configuration
const basePath = process.env.NEXT_PUBLIC_BASE_PATH;
const sessionCookieOptions = {
name: options.session?.cookie?.name ?? "__session",
secure: options.session?.cookie?.secure ??
process.env.AUTH0_COOKIE_SECURE === "true",
sameSite: options.session?.cookie?.sameSite ??
process.env.AUTH0_COOKIE_SAME_SITE ??
"lax",
path: options.session?.cookie?.path ??
process.env.AUTH0_COOKIE_PATH ??
basePath ??
"/",
transient: options.session?.cookie?.transient ??
process.env.AUTH0_COOKIE_TRANSIENT === "true",
domain: options.session?.cookie?.domain ?? process.env.AUTH0_COOKIE_DOMAIN
};
const transactionCookieOptions = {
prefix: options.transactionCookie?.prefix ?? "__txn_",
secure: options.transactionCookie?.secure ?? false,
sameSite: options.transactionCookie?.sameSite ?? "lax",
path: options.transactionCookie?.path ?? basePath ?? "/",
maxAge: options.transactionCookie?.maxAge ?? 3600
};
if (appBaseUrl) {
const { protocol } = new URL(appBaseUrl);
if (protocol === "https:") {
sessionCookieOptions.secure = true;
transactionCookieOptions.secure = true;
}
}
this.routes = {
login: process.env.NEXT_PUBLIC_LOGIN_ROUTE || "/auth/login",
logout: "/auth/logout",
callback: "/auth/callback",
backChannelLogout: "/auth/backchannel-logout",
profile: process.env.NEXT_PUBLIC_PROFILE_ROUTE || "/auth/profile",
accessToken: process.env.NEXT_PUBLIC_ACCESS_TOKEN_ROUTE || "/auth/access-token",
connectAccount: "/auth/connect",
...options.routes
};
this.transactionStore = new TransactionStore({
secret,
cookieOptions: transactionCookieOptions,
enableParallelTransactions: options.enableParallelTransactions ?? true
});
this.sessionStore = options.sessionStore
? new StatefulSessionStore({
...options.session,
secret,
store: options.sessionStore,
cookieOptions: sessionCookieOptions
})
: new StatelessSessionStore({
...options.session,
secret,
cookieOptions: sessionCookieOptions
});
this.authClient = new AuthClient({
transactionStore: this.transactionStore,
sessionStore: this.sessionStore,
domain,
clientId,
clientSecret,
clientAssertionSigningKey,
clientAssertionSigningAlg,
authorizationParameters: options.authorizationParameters,
pushedAuthorizationRequests: options.pushedAuthorizationRequests,
appBaseUrl,
secret,
signInReturnToPath: options.signInReturnToPath,
logoutStrategy: options.logoutStrategy,
includeIdTokenHintInOIDCLogoutUrl: options.includeIdTokenHintInOIDCLogoutUrl,
beforeSessionSaved: options.beforeSessionSaved,
onCallback: options.onCallback,
routes: this.routes,
allowInsecureRequests: options.allowInsecureRequests,
httpTimeout: options.httpTimeout,
enableTelemetry: options.enableTelemetry,
enableAccessTokenEndpoint: options.enableAccessTokenEndpoint,
noContentProfileResponseWhenUnauthenticated: options.noContentProfileResponseWhenUnauthenticated,
enableConnectAccountEndpoint: options.enableConnectAccountEndpoint,
useDPoP: options.useDPoP || false,
dpopKeyPair: options.dpopKeyPair || resolvedDpopKeyPair,
dpopOptions: options.dpopOptions || resolvedDpopOptions
});
}
/**
* middleware mounts the SDK routes to run as a middleware function.
*/
middleware(req) {
return this.authClient.handler.bind(this.authClient)(req);
}
/**
* getSession returns the session data for the current request.
*/
async getSession(req) {
if (req) {
// middleware usage
if (req instanceof NextRequest) {
return this.sessionStore.get(req.cookies);
}
// pages router usage
return this.sessionStore.get(this.createRequestCookies(req));
}
// app router usage: Server Components, Server Actions, Route Handlers
return this.sessionStore.get(await cookies());
}
/**
* getAccessToken returns the access token.
*
* Please note: If you are passing audience, ensure that the used audiences and scopes are
* part of the Application's Refresh Token Policies in Auth0 when configuring Multi-Resource Refresh Tokens (MRRT).
* {@link https://auth0.com/docs/secure/tokens/refresh-tokens/multi-resource-refresh-token|See Auth0 Documentation on Multi-resource Refresh Tokens}
*
* NOTE: Server Components cannot set cookies. Calling `getAccessToken()` in a Server Component will cause the access token to be refreshed, if it is expired, and the updated token set will not to be persisted.
* It is recommended to call `getAccessToken(req, res)` in the middleware if you need to retrieve the access token in a Server Component to ensure the updated token set is persisted.
*/
async getAccessToken(arg1, arg2, arg3) {
const defaultOptions = {
refresh: false
};
let req = undefined;
let res = undefined;
let options = {};
// Determine which overload was called based on arguments
if (arg1 &&
(arg1 instanceof Request || typeof arg1.headers === "object")) {
// Case: getAccessToken(req, res, options?)
req = arg1;
res = arg2; // arg2 must be Response if arg1 is Request
// Merge provided options (arg3) with defaults
options = { ...defaultOptions, ...(arg3 ?? {}) };
if (!res) {
throw new TypeError("getAccessToken(req, res): The 'res' argument is missing. Both 'req' and 'res' must be provided together for Pages Router or middleware usage.");
}
}
else {
// Case: getAccessToken(options?) or getAccessToken()
// arg1 (if present) must be options, arg2 and arg3 must be undefined.
if (arg2 !== undefined || arg3 !== undefined) {
throw new TypeError("getAccessToken: Invalid arguments. Valid signatures are getAccessToken(), getAccessToken(options), or getAccessToken(req, res, options).");
}
// Merge provided options (arg1) with defaults
options = {
...defaultOptions,
...(arg1 ?? {})
};
}
// Execute the token request with caching to avoid duplicate in-flight requests
return __classPrivateFieldGet(this, _Auth0Client_tokenRequestCache, "f").execute(() => this.executeGetAccessToken(req, res, options), {
options,
authorizationParameters: __classPrivateFieldGet(this, _Auth0Client_options, "f").authorizationParameters
});
}
/**
* Core implementation of getAccessToken that performs the actual token retrieval.
* This is separated to enable request coalescing via the cache.
*/
async executeGetAccessToken(req, res, options) {
const session = req
? await this.getSession(req)
: await this.getSession();
if (!session) {
throw new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, "The user does not have an active session.");
}
const [error, getTokenSetResponse] = await this.authClient.getTokenSet(session, options);
if (error) {
throw error;
}
const { tokenSet, idTokenClaims } = getTokenSetResponse;
// update the session with the new token set, if necessary
const sessionChanges = getSessionChangesAfterGetAccessToken(session, tokenSet, {
scope: __classPrivateFieldGet(this, _Auth0Client_options, "f").authorizationParameters?.scope ?? DEFAULT_SCOPES,
audience: __classPrivateFieldGet(this, _Auth0Client_options, "f").authorizationParameters?.audience
});
if (sessionChanges) {
if (idTokenClaims) {
session.user = idTokenClaims;
}
// call beforeSessionSaved callback if present
// if not then filter id_token claims with default rules
const finalSession = await this.authClient.finalizeSession(session, tokenSet.idToken);
await this.saveToSession({
...finalSession,
...sessionChanges
}, req, res);
}
return {
token: tokenSet.accessToken,
scope: tokenSet.scope,
expiresAt: tokenSet.expiresAt,
token_type: tokenSet.token_type,
audience: tokenSet.audience
};
}
/**
* Retrieves an access token for a connection.
*
* This method attempts to obtain an access token for a specified connection.
* It first checks if a session exists, either from the provided request or from cookies.
* If no session is found, it throws a `AccessTokenForConnectionError` indicating
* that the user does not have an active session.
*
* @param {AccessTokenForConnectionOptions} options - Options for retrieving an access token for a connection.
* @param {PagesRouterRequest | NextRequest} [req] - An optional request object from which to extract session information.
* @param {PagesRouterResponse | NextResponse} [res] - An optional response object from which to extract session information.
*
* @throws {AccessTokenForConnectionError} If the user does not have an active session.
* @throws {Error} If there is an error during the token exchange process.
*
* @returns {Promise<{ token: string; expiresAt: number; scope?: string }} An object containing the access token and its expiration time.
*/
async getAccessTokenForConnection(options, req, res) {
const session = req
? await this.getSession(req)
: await this.getSession();
if (!session) {
throw new AccessTokenForConnectionError(AccessTokenForConnectionErrorCode.MISSING_SESSION, "The user does not have an active session.");
}
// Find the connection token set in the session
const existingTokenSet = session.connectionTokenSets?.find((tokenSet) => tokenSet.connection === options.connection);
const [error, retrievedTokenSet] = await this.authClient.getConnectionTokenSet(session.tokenSet, existingTokenSet, options);
if (error !== null) {
throw error;
}
// If we didn't have a corresponding connection token set in the session
// or if the one we have in the session does not match the one we received
// We want to update the store incase we retrieved a token set.
if (retrievedTokenSet &&
(!existingTokenSet ||
retrievedTokenSet.accessToken !== existingTokenSet.accessToken ||
retrievedTokenSet.expiresAt !== existingTokenSet.expiresAt ||
retrievedTokenSet.scope !== existingTokenSet.scope)) {
let tokenSets;
// If we already had the connection token set in the session
// we need to update the item in the array
// If not, we need to add it.
if (existingTokenSet) {
tokenSets = session.connectionTokenSets?.map((tokenSet) => tokenSet.connection === options.connection
? retrievedTokenSet
: tokenSet);
}
else {
tokenSets = [...(session.connectionTokenSets || []), retrievedTokenSet];
}
await this.saveToSession({
...session,
connectionTokenSets: tokenSets
}, req, res);
}
return {
token: retrievedTokenSet.accessToken,
scope: retrievedTokenSet.scope,
expiresAt: retrievedTokenSet.expiresAt
};
}
/**
* updateSession updates the session of the currently authenticated user. If the user does not have a session, an error is thrown.
*/
async updateSession(reqOrSession, res, sessionData) {
if (!res) {
// app router: Server Actions, Route Handlers
const existingSession = await this.getSession();
if (!existingSession) {
throw new Error("The user is not authenticated.");
}
const updatedSession = reqOrSession;
if (!updatedSession) {
throw new Error("The session data is missing.");
}
await this.sessionStore.set(await cookies(), await cookies(), {
...updatedSession,
internal: {
...existingSession.internal
}
});
}
else {
const req = reqOrSession;
if (!sessionData) {
throw new Error("The session data is missing.");
}
if (req instanceof NextRequest && res instanceof NextResponse) {
// middleware usage
const existingSession = await this.getSession(req);
if (!existingSession) {
throw new Error("The user is not authenticated.");
}
await this.sessionStore.set(req.cookies, res.cookies, {
...sessionData,
internal: {
...existingSession.internal
}
});
}
else {
// pages router usage
const existingSession = await this.getSession(req);
if (!existingSession) {
throw new Error("The user is not authenticated.");
}
const resHeaders = new Headers();
const resCookies = new ResponseCookies(resHeaders);
const updatedSession = sessionData;
const reqCookies = this.createRequestCookies(req);
const pagesRouterRes = res;
await this.sessionStore.set(reqCookies, resCookies, {
...updatedSession,
internal: {
...existingSession.internal
}
});
// Handle multiple set-cookie headers properly
// resHeaders.entries() yields each set-cookie header separately,
// but res.setHeader() overwrites previous values. We need to collect
// all set-cookie values and set them as an array.
// Note: Per the Web API specification, the Headers API normalizes header names
// to lowercase, so comparing key.toLowerCase() === "set-cookie" is safe.
const setCookieValues = [];
const otherHeaders = {};
for (const [key, value] of resHeaders.entries()) {
if (key.toLowerCase() === "set-cookie") {
setCookieValues.push(value);
}
else {
otherHeaders[key] = value;
}
}
// Set all cookies at once as an array if any exist
if (setCookieValues.length > 0) {
pagesRouterRes.setHeader("set-cookie", setCookieValues);
}
// Set non-cookie headers normally
for (const [key, value] of Object.entries(otherHeaders)) {
pagesRouterRes.setHeader(key, value);
}
}
}
}
createRequestCookies(req) {
const headers = new Headers();
for (const key in req.headers) {
if (Array.isArray(req.headers[key])) {
for (const value of req.headers[key]) {
headers.append(key, value);
}
}
else {
headers.append(key, req.headers[key] ?? "");
}
}
return new RequestCookies(headers);
}
async startInteractiveLogin(options = {}) {
return this.authClient.startInteractiveLogin(options);
}
/**
* Authenticates using Client-Initiated Backchannel Authentication and returns the token set and optionally the ID token claims and authorization details.
*
* This method will initialize the backchannel authentication process with Auth0, and poll the token endpoint until the authentication is complete.
*
* Using Client-Initiated Backchannel Authentication requires the feature to be enabled in the Auth0 dashboard.
* @see https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow
*/
async getTokenByBackchannelAuth(options) {
const [error, response] = await this.authClient.backchannelAuthentication(options);
if (error) {
throw error;
}
return response;
}
/**
* Initiates the Connect Account flow to connect a third-party account to the user's profile.
* If the user does not have an active session, a `ConnectAccountError` is thrown.
*
* This method first attempts to obtain an access token with the `create:me:connected_accounts` scope
* for the My Account API to create a connected account for the user.
*
* The user will then be redirected to authorize the connection with the third-party provider.
*/
async connectAccount(options) {
const session = await this.getSession();
if (!session) {
throw new ConnectAccountError({
code: ConnectAccountErrorCodes.MISSING_SESSION,
message: "The user does not have an active session."
});
}
const getMyAccountTokenOpts = {
audience: `${this.issuer}/me/`,
scope: "create:me:connected_accounts"
};
const accessToken = await this.getAccessToken(getMyAccountTokenOpts);
const [error, connectAccountResponse] = await this.authClient.connectAccount({
...options,
tokenSet: {
accessToken: accessToken.token,
expiresAt: accessToken.expiresAt,
scope: getMyAccountTokenOpts.scope,
audience: accessToken.audience
}
});
if (error) {
throw error;
}
return connectAccountResponse;
}
withPageAuthRequired(fnOrOpts, opts) {
const config = {
loginUrl: this.routes.login
};
const appRouteHandler = appRouteHandlerFactory(this, config);
const pageRouteHandler = pageRouteHandlerFactory(this, config);
if (typeof fnOrOpts === "function") {
return appRouteHandler(fnOrOpts, opts);
}
return pageRouteHandler(fnOrOpts);
}
withApiAuthRequired(apiRoute) {
const pageRouteHandler = withApiAuthRequired.pageRouteHandlerFactory(this);
const appRouteHandler = withApiAuthRequired.appRouteHandlerFactory(this);
return (req, resOrParams) => {
if (isRequest(req)) {
return appRouteHandler(apiRoute)(req, resOrParams);
}
return pageRouteHandler(apiRoute)(req, resOrParams);
};
}
async saveToSession(data, req, res) {
if (req && res) {
if (req instanceof NextRequest && res instanceof NextResponse) {
// middleware usage
await this.sessionStore.set(req.cookies, res.cookies, data);
}
else {
// pages router usage
const resHeaders = new Headers();
const resCookies = new ResponseCookies(resHeaders);
const pagesRouterRes = res;
await this.sessionStore.set(this.createRequestCookies(req), resCookies, data);
for (const [key, value] of resHeaders.entries()) {
pagesRouterRes.setHeader(key, value);
}
}
}
else {
// app router usage: Server Components, Server Actions, Route Handlers
try {
await this.sessionStore.set(await cookies(), await cookies(), data);
}
catch (e) {
if (process.env.NODE_ENV === "development") {
console.warn("Failed to persist the updated token set. `getAccessToken()` was likely called from a Server Component which cannot set cookies.");
}
}
}
}
/**
* Validates and extracts required configuration options.
* @param options The client options
* @returns The validated required options
* @throws ConfigurationError if any required option is missing
*/
validateAndExtractRequiredOptions(options) {
// Base required options that are always needed
const requiredOptions = {
domain: options.domain ?? process.env.AUTH0_DOMAIN,
clientId: options.clientId ?? process.env.AUTH0_CLIENT_ID,
appBaseUrl: options.appBaseUrl ?? process.env.APP_BASE_URL,
secret: options.secret ?? process.env.AUTH0_SECRET
};
// Check client authentication options - either clientSecret OR clientAssertionSigningKey must be provided
const clientSecret = options.clientSecret ?? process.env.AUTH0_CLIENT_SECRET;
const clientAssertionSigningKey = options.clientAssertionSigningKey ??
process.env.AUTH0_CLIENT_ASSERTION_SIGNING_KEY;
const hasClientAuthentication = !!(clientSecret || clientAssertionSigningKey);
const missing = Object.entries(requiredOptions)
.filter(([, value]) => !value)
.map(([key]) => key);
// Add client authentication error if neither option is provided
if (!hasClientAuthentication) {
missing.push("clientAuthentication");
}
if (missing.length) {
// Map of option keys to their exact environment variable names
const envVarNames = {
domain: "AUTH0_DOMAIN",
clientId: "AUTH0_CLIENT_ID",
appBaseUrl: "APP_BASE_URL",
secret: "AUTH0_SECRET"
};
// Standard intro message explaining the issue
let errorMessage = "WARNING: Not all required options were provided when creating an instance of Auth0Client. Ensure to provide all missing options, either by passing it to the Auth0Client constructor, or by setting the corresponding environment variable.\n";
// Add specific details for each missing option
missing.forEach((key) => {
if (key === "clientAuthentication") {
errorMessage += `Missing: clientAuthentication: Set either AUTH0_CLIENT_SECRET env var or AUTH0_CLIENT_ASSERTION_SIGNING_KEY env var, or pass clientSecret or clientAssertionSigningKey in options\n`;
}
else if (envVarNames[key]) {
errorMessage += `Missing: ${key}: Set ${envVarNames[key]} env var or pass ${key} in options\n`;
}
else {
errorMessage += `Missing: ${key}\n`;
}
});
console.error(errorMessage.trim());
}
// Prepare the result object with all validated options
const result = {
...requiredOptions,
clientSecret,
clientAssertionSigningKey
};
// Type-safe assignment after validation
return result;
}
/**
* Creates a configured Fetcher instance for making authenticated API requests.
*
* This method creates a specialized HTTP client that handles:
* - Automatic access token retrieval and injection
* - DPoP (Demonstrating Proof-of-Possession) proof generation when enabled
* - Token refresh and session management
* - Error handling and retry logic for DPoP nonce errors
* - Base URL resolution for relative requests
*
* The fetcher provides a high-level interface for making requests to protected resources
* without manually handling authentication details.
*
* @template TOutput - Response type that extends the standard Response interface
* @param req - Request object for session context (required for Pages Router, optional for App Router)
* @param options - Configuration options for the fetcher
* @param options.useDPoP - Enable DPoP for this fetcher instance (overrides global setting)
* @param options.baseUrl - Base URL for resolving relative requests
* @param options.getAccessToken - Custom access token factory function
* @param options.fetch - Custom fetch implementation
* @returns Promise that resolves to a configured Fetcher instance
* @throws AccessTokenError when no active session exists
*
* @example
* ```typescript
* import { auth0 } from "@/lib/auth0";
*
* const fetcher = await auth0.createFetcher(undefined, {
* baseUrl: "https://api.example.com",
* useDPoP: true
* });
*
* const response = await fetcher.fetchWithAuth("/users");
* const users = await response.json();
* ```
*
* @see {@link Fetcher} for details on using the returned fetcher instance
* @see {@link FetcherMinimalConfig} for available configuration options
*/
async createFetcher(req, options) {
const session = req
? await this.getSession(req)
: await this.getSession();
if (!session) {
throw new AccessTokenError(AccessTokenErrorCode.MISSING_SESSION, "The user does not have an active session.");
}
const getAccessToken = async (getAccessTokenOptions) => {
const [error, getTokenSetResponse] = await this.authClient.getTokenSet(session, getAccessTokenOptions || {});
if (error) {
throw error;
}
return getTokenSetResponse.tokenSet;
};
const fetcher = await this.authClient.fetcherFactory({
...options,
getAccessToken
});
return fetcher;
}
get issuer() {
return this.domain.startsWith("http://") ||
this.domain.startsWith("https://")
? this.domain
: `https://${this.domain}`;
}
}
_Auth0Client_options = new WeakMap(), _Auth0Client_tokenRequestCache = new WeakMap();