@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
146 lines (145 loc) • 5.6 kB
JavaScript
import { NextResponse } from "next/server.js";
import { InvalidRequestError, MfaChallengeError, MfaGetAuthenticatorsError, MfaNoAvailableFactorsError, MfaRequiredError, MfaTokenExpiredError, MfaTokenInvalidError, MfaVerifyError, OAuth2Error, SdkError } from "../errors/index.js";
import { decrypt, encrypt } from "../server/cookies.js";
/**
* Encrypt mfa_token with full context before exposing to application.
* Uses same encryption as session cookies (JWE with AES-256-GCM).
* The encrypted token is self-contained with audience, scope, and requirements.
*
* @param mfaToken - Raw mfa_token from Auth0
* @param audience - The API audience the token is for
* @param scope - The requested scope
* @param mfaRequirements - MFA requirements from Auth0
* @param secret - Cookie secret for encryption
* @param ttlSeconds - TTL in seconds for JWE expiration
* @returns Encrypted JWE string containing full MFA context
*/
export async function encryptMfaToken(mfaToken, audience, scope, mfaRequirements, secret, ttlSeconds) {
const context = {
mfaToken,
audience,
scope,
mfaRequirements,
createdAt: Date.now()
};
return await encrypt(context, secret, Math.floor(Date.now() / 1000) + ttlSeconds);
}
/**
* Decrypt encrypted mfa_token from application to extract full context.
*
* @param encryptedToken - Encrypted JWE from MfaRequiredError
* @param secret - Cookie secret for decryption
* @returns MfaContext with mfaToken, audience, scope, and requirements
* @throws MfaTokenExpiredError if JWE TTL exceeded
* @throws MfaTokenInvalidError if token is tampered/malformed
*/
export async function decryptMfaToken(encryptedToken, secret) {
try {
const result = await decrypt(encryptedToken, secret, undefined, true);
return result.payload;
}
catch (e) {
if (e.code === "ERR_JWT_EXPIRED") {
throw new MfaTokenExpiredError();
}
// ERR_JWE_DECRYPTION_FAILED or any other error means tampered, malformed, or wrong secret
throw new MfaTokenInvalidError();
}
}
/**
* Detect if an OAuth error response indicates MFA is required.
* Works with oauth4webapi's ResponseBodyError which has `error` property directly.
*
* @param error - Error object from oauth4webapi
* @returns True if error indicates mfa_required
*/
export function isMfaRequiredError(error) {
if (!error || typeof error !== "object")
return false;
const err = error;
return err.error === "mfa_required" || err.code === "mfa_required";
}
/**
* Extract mfa_token and error details from Auth0's mfa_required response.
* oauth4webapi's ResponseBodyError puts custom fields (mfa_token, mfa_requirements)
* in the `cause` property, while `error` and `error_description` are directly on the error.
*
* @param error - Error object from oauth4webapi containing Auth0 response
* @returns Object with mfa_token, error_description, and mfa_requirements if present
*/
export function extractMfaErrorDetails(error) {
if (!error || typeof error !== "object") {
return {
mfa_token: undefined,
error_description: undefined,
mfa_requirements: undefined
};
}
const err = error;
// oauth4webapi's ResponseBodyError has:
// - error, error_description: directly on the error object
// - cause: contains the full response body with mfa_token, mfa_requirements
const cause = err.cause;
return {
// mfa_token and mfa_requirements are in the cause (response body)
mfa_token: cause?.mfa_token ??
err.mfa_token,
// error_description is directly on the error
error_description: err.error_description,
// mfa_requirements is in the cause (response body)
mfa_requirements: cause?.mfa_requirements ??
err.mfa_requirements
};
}
/**
* Get HTTP status code for MFA error.
*
* Centralized mapping: 401 (auth), 400 (validation), 500 (unexpected)
*
* @param error - Error instance
* @returns HTTP status code
*/
export function getMfaErrorStatusCode(error) {
if (error instanceof MfaTokenExpiredError ||
error instanceof MfaTokenInvalidError) {
return 401;
}
if (error instanceof InvalidRequestError ||
error instanceof MfaNoAvailableFactorsError ||
error instanceof MfaGetAuthenticatorsError ||
error instanceof MfaChallengeError ||
error instanceof MfaVerifyError) {
return 400;
}
if (error instanceof MfaRequiredError) {
return 403;
}
return 500;
}
/**
* Handle MFA errors and format response.
*
* Wraps non-SDK errors for consistent shape, uses error.toJSON() for serialization.
*
* @param e - Error thrown by business logic
* @returns NextResponse with error details
*/
export function handleMfaError(e) {
// Wrap non-SDK errors in OAuth2Error for consistent shape
if (!(e instanceof SdkError)) {
e = new OAuth2Error({
code: "server_error",
message: e instanceof Error ? e.message : "Internal server error"
});
}
const error = e;
const status = getMfaErrorStatusCode(error);
// MfaRequiredError has toJSON() with mfa_token + mfa_requirements
// MfaError subclasses have toJSON() with error + error_description
// Other SdkErrors fallback to generic shape
const body = error.toJSON?.() ?? {
error: error.code || "server_error",
error_description: error.message || "Internal server error"
};
return NextResponse.json(body, { status });
}