diff --git a/src/client/__tests__/csrf.test.js b/src/client/__tests__/csrf.test.js index 7a7562529e..134c285489 100644 --- a/src/client/__tests__/csrf.test.js +++ b/src/client/__tests__/csrf.test.js @@ -78,11 +78,10 @@ test("when the fetch fails it'll throw a client fetch error", async () => { await waitFor(() => { expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toBeCalledWith( - "CLIENT_FETCH_ERROR", - "csrf", - new SyntaxError("Unexpected token s in JSON at position 0") - ) + expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", { + path: "csrf", + error: new SyntaxError("Unexpected token s in JSON at position 0"), + }) }) }) diff --git a/src/client/__tests__/providers.test.js b/src/client/__tests__/providers.test.js index acf9f2045f..3a7476814e 100644 --- a/src/client/__tests__/providers.test.js +++ b/src/client/__tests__/providers.test.js @@ -56,11 +56,10 @@ test("when failing to fetch the providers, it'll log the error", async () => { await waitFor(() => { expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toBeCalledWith( - "CLIENT_FETCH_ERROR", - "providers", - new SyntaxError("Unexpected token s in JSON at position 0") - ) + expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", { + path: "providers", + error: new SyntaxError("Unexpected token s in JSON at position 0"), + }) }) }) diff --git a/src/client/__tests__/session.test.js b/src/client/__tests__/session.test.js index 983c3f85ea..82e27b1dbe 100644 --- a/src/client/__tests__/session.test.js +++ b/src/client/__tests__/session.test.js @@ -70,11 +70,10 @@ test("if there's an error fetching the session, it should log it", async () => { await waitFor(() => { expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toBeCalledWith( - "CLIENT_FETCH_ERROR", - "session", - new SyntaxError("Unexpected token S in JSON at position 0") - ) + expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", { + path: "session", + error: new SyntaxError("Unexpected token S in JSON at position 0"), + }) }) }) diff --git a/src/client/__tests__/sign-in.test.js b/src/client/__tests__/sign-in.test.js index a80bbeb8bb..5be0cb5025 100644 --- a/src/client/__tests__/sign-in.test.js +++ b/src/client/__tests__/sign-in.test.js @@ -250,11 +250,10 @@ test("when it fails to fetch the providers, it redirected back to signin page", expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`) expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error).toBeCalledWith( - "CLIENT_FETCH_ERROR", - "providers", - errorMsg - ) + expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", { + error: "Error when retrieving providers", + path: "providers", + }) }) }) diff --git a/src/client/react.js b/src/client/react.js index ee83a3aabb..2fe6bec422 100644 --- a/src/client/react.js +++ b/src/client/react.js @@ -320,25 +320,23 @@ export function SessionProvider(props) { */ async function _fetchData(path, { ctx, req = ctx?.req } = {}) { try { - const baseUrl = await _apiBaseUrl() const options = req ? { headers: { cookie: req.headers.cookie } } : {} - const res = await fetch(`${baseUrl}/${path}`, options) + const res = await fetch(`${_apiBaseUrl()}/${path}`, options) const data = await res.json() if (!res.ok) throw data return Object.keys(data).length > 0 ? data : null // Return null if data empty } catch (error) { - logger.error("CLIENT_FETCH_ERROR", path, error) + logger.error("CLIENT_FETCH_ERROR", { + error, + path, + ...(req ? { header: req.headers } : {}), + }) return null } } function _apiBaseUrl() { if (typeof window === "undefined") { - // NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set - if (!process.env.NEXTAUTH_URL) { - logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set") - } - // Return absolute path when called server side return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}` } diff --git a/src/lib/errors.js b/src/lib/errors.js index 8532b556c0..3bac54613a 100644 --- a/src/lib/errors.js +++ b/src/lib/errors.js @@ -32,67 +32,3 @@ export class OAuthCallbackError extends UnknownError { export class AccountNotLinkedError extends UnknownError { name = "AccountNotLinkedError" } - -export class CreateUserError extends UnknownError { - name = "CreateUserError" -} - -export class GetUserError extends UnknownError { - name = "GetUserError" -} - -export class GetUserByEmailError extends UnknownError { - name = "GetUserByEmailError" -} - -export class GetUserByIdError extends UnknownError { - name = "GetUserByIdError" -} - -export class GetUserByProviderAccountIdError extends UnknownError { - name = "GetUserByProviderAccountIdError" -} - -export class UpdateUserError extends UnknownError { - name = "UpdateUserError" -} - -export class DeleteUserError extends UnknownError { - name = "DeleteUserError" -} - -export class LinkAccountError extends UnknownError { - name = "LinkAccountError" -} - -export class UnlinkAccountError extends UnknownError { - name = "UnlinkAccountError" -} - -export class CreateSessionError extends UnknownError { - name = "CreateSessionError" -} - -export class GetSessionError extends UnknownError { - name = "GetSessionError" -} - -export class UpdateSessionError extends UnknownError { - name = "UpdateSessionError" -} - -export class DeleteSessionError extends UnknownError { - name = "DeleteSessionError" -} - -export class CreateVerificationRequestError extends UnknownError { - name = "CreateVerificationRequestError" -} - -export class GetVerificationRequestError extends UnknownError { - name = "GetVerificationRequestError" -} - -export class DeleteVerificationRequestError extends UnknownError { - name = "DeleteVerificationRequestError" -} diff --git a/src/lib/logger.js b/src/lib/logger.js index 7e2f448337..630cb27ba5 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -1,22 +1,37 @@ +import { UnknownError } from "./errors" + +/** Makes sure that error is always serializable */ +function formatError(o) { + if (o instanceof Error && !(o instanceof UnknownError)) { + return { message: o.message, stack: o.stack, name: o.name } + } + if (o?.error) { + o.error = formatError(o.error) + o.message = o.message ?? o.error.message + } + return o +} + /** @type {import("types").LoggerInstance} */ const _logger = { - error(code, ...message) { + error(code, metadata) { + metadata = formatError(metadata) console.error( `[next-auth][error][${code.toLowerCase()}]`, `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`, - ...message + metadata.message, + metadata ) }, - warn(code, ...message) { + warn(code) { console.warn( `[next-auth][warn][${code.toLowerCase()}]`, - `\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`, - ...message + `\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}` ) }, - debug(code, ...message) { + debug(code, metadata) { if (!process?.env?._NEXTAUTH_DEBUG) return - console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message) + console.log(`[next-auth][debug][${code.toLowerCase()}]`, metadata) }, } @@ -47,31 +62,19 @@ export function proxyLogger(logger = _logger, basePath) { const clientLogger = {} for (const level in logger) { - clientLogger[level] = (code, ...message) => { - _logger[level](code, ...message) // Log on client as usual + clientLogger[level] = (code, metadata) => { + _logger[level](code, metadata) // Logs to console + if (level === "error") { + metadata = formatError(metadata) + } + metadata.client = true const url = `${basePath}/_log` - const body = new URLSearchParams({ - level, - code, - message: JSON.stringify( - message.map((m) => { - if (m instanceof Error) { - // Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af - return { name: m.name, message: m.message, stack: m.stack } - } - return m - }) - ), - }) + const body = new URLSearchParams({ level, code, ...metadata }) if (navigator.sendBeacon) { return navigator.sendBeacon(url, body) } - return fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body, - }) + return fetch(url, { method: "POST", body, keepalive: true }) } } return clientLogger diff --git a/src/server/index.js b/src/server/index.js index efb1cdc83a..e4d00771b4 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -17,7 +17,7 @@ import * as state from "./lib/oauth/state-handler" // To work properly in production with OAuth providers the NEXTAUTH_URL // environment variable must be set. if (!process.env.NEXTAUTH_URL) { - logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set") + logger.warn("NEXTAUTH_URL") } /** @@ -43,11 +43,11 @@ async function NextAuthHandler(req, res, userOptions) { extendRes(req, res, resolve) if (!req.query.nextauth) { - const error = + const message = "Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly." - logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error) - return res.status(500).end(`Error: ${error}`) + logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", new Error(message)) + return res.status(500).end(`Error: ${message}`) } const { @@ -244,13 +244,8 @@ async function NextAuthHandler(req, res, userOptions) { case "_log": if (userOptions.logger) { try { - const { - code = "CLIENT_ERROR", - level = "error", - message = "[]", - } = req.body - - logger[level](code, ...JSON.parse(message)) + const { code, level, ...metadata } = req.body + logger[level](code, metadata) } catch (error) { // If logging itself failed... logger.error("LOGGER_ERROR", error) diff --git a/src/server/lib/oauth/callback.js b/src/server/lib/oauth/callback.js index 910ac91203..6b9e7e6356 100644 --- a/src/server/lib/oauth/callback.js +++ b/src/server/lib/oauth/callback.js @@ -23,13 +23,12 @@ export default async function oAuthCallback(req) { code = body.code user = body.user != null ? JSON.parse(body.user) : null } catch (error) { - logger.error( - "OAUTH_CALLBACK_HANDLER_ERROR", + logger.error("OAUTH_CALLBACK_HANDLER_ERROR", { error, - req.body, - provider.id, - code - ) + body: req.body, + providerId: provider.id, + code, + }) throw error } } @@ -62,7 +61,11 @@ export default async function oAuthCallback(req) { return getProfile({ profileData, provider, tokens, user }) } catch (error) { - logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code) + logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", { + error, + providerId: provider.id, + code, + }) throw error } } @@ -74,7 +77,11 @@ export default async function oAuthCallback(req) { // eslint-disable-next-line camelcase const { token_secret } = await client.getOAuthRequestToken(provider.params) - const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier) + const tokens = await client.getOAuthAccessToken( + oauth_token, + token_secret, + oauth_verifier + ) const profileData = await client.get( provider.profileUrl, tokens.oauth_token, @@ -113,7 +120,7 @@ export default async function oAuthCallback(req) { async function getProfile({ profileData, tokens, provider, user }) { try { // Convert profileData into an object if it's a string - if (typeof profileData === "string" || profileData instanceof String) { + if (typeof profileData === "string") { profileData = JSON.parse(profileData) } @@ -122,7 +129,7 @@ async function getProfile({ profileData, tokens, provider, user }) { profileData.user = user } - logger.debug("PROFILE_DATA", profileData) + logger.debug("PROFILE_DATA", { profile: profileData }) const profile = await provider.profile(profileData, tokens) // Return profile, raw profile and auth provider details @@ -139,7 +146,7 @@ async function getProfile({ profileData, tokens, provider, user }) { }, OAuthProfile: profileData, } - } catch (exception) { + } catch (error) { // If we didn't get a response either there was a problem with the provider // response *or* the user cancelled the action with the provider. // @@ -147,7 +154,7 @@ async function getProfile({ profileData, tokens, provider, user }) { // all providers, so we return an empty object; the user should then be // redirected back to the sign up page. We log the error to help developers // who might be trying to debug this when configuring a new provider. - logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData) + logger.error("OAUTH_PARSE_PROFILE_ERROR", { error, profileData }) return { profile: null, account: null, diff --git a/src/server/lib/oauth/client.js b/src/server/lib/oauth/client.js index 076bf33258..6aeaa0288c 100644 --- a/src/server/lib/oauth/client.js +++ b/src/server/lib/oauth/client.js @@ -188,7 +188,11 @@ async function getOAuth2AccessToken(code, provider, codeVerifier) { null, (error, data, response) => { if (error) { - logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, data, response) + logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", { + error, + data, + response, + }) return reject(error) } diff --git a/src/server/lib/oauth/pkce-handler.js b/src/server/lib/oauth/pkce-handler.js index f15a2b0264..87ddab6f90 100644 --- a/src/server/lib/oauth/pkce-handler.js +++ b/src/server/lib/oauth/pkce-handler.js @@ -31,15 +31,15 @@ export async function handleCallback(req, res) { encryption: true, }) req.options.pkce = pkce - logger.debug("OAUTH_CALLBACK_CHECK", "Read PKCE verifier from cookie", { + logger.debug("PKCE_VERIFIER_FROM_COOKIE", { code_verifier: pkce.code_verifier, pkceLength: PKCE_LENGTH, method: PKCE_CODE_CHALLENGE_METHOD, }) // remove PKCE after it has been used - cookie.set(res, cookies.pkceCodeVerifier.name, "", { + cookie.set(res, cookies.pkceCodeVerifier.name, "", { ...cookies.pkceCodeVerifier.options, - maxAge: 0 + maxAge: 0, }) } catch (error) { logger.error("CALLBACK_OAUTH_ERROR", error) @@ -61,7 +61,7 @@ export async function handleSignin(req, res) { } // Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie const pkce = pkceChallenge(PKCE_LENGTH) - logger.debug("OAUTH_SIGNIN_CHECK", "Created PKCE challenge/verifier", { + logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", { ...pkce, pkceLength: PKCE_LENGTH, method: PKCE_CODE_CHALLENGE_METHOD, @@ -86,10 +86,7 @@ export async function handleSignin(req, res) { expires: cookieExpires.toISOString(), ...cookies.pkceCodeVerifier.options, }) - logger.debug( - "OAUTH_SIGNIN_CHECK", - "Created PKCE code_verifier saved in cookie" - ) + logger.debug("PKCE_CODE_VERIFIER_SAVED", { encryptedCodeVerifier }) } catch (error) { logger.error("SIGNIN_OAUTH_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`) diff --git a/src/server/lib/oauth/state-handler.js b/src/server/lib/oauth/state-handler.js index 722af0d309..a1cacf4c41 100644 --- a/src/server/lib/oauth/state-handler.js +++ b/src/server/lib/oauth/state-handler.js @@ -20,11 +20,7 @@ export async function handleCallback(req, res) { const state = req.query.state || req.body.state const expectedState = createHash("sha256").update(csrfToken).digest("hex") - logger.debug( - "OAUTH_CALLBACK_CHECK", - "Comparing received and expected state", - { state, expectedState } - ) + logger.debug("STATE_CHECK", { state, expectedState }) if (state !== expectedState) { throw new OAuthCallbackError("Invalid state returned from OAuth provider") } @@ -51,11 +47,7 @@ export async function handleSignin(req, res) { const state = createHash("sha256").update(csrfToken).digest("hex") provider.authorizationParams = { ...provider.authorizationParams, state } - logger.debug( - "OAUTH_CALLBACK_CHECK", - "Added state to authorization params", - { state } - ) + logger.debug("STATE_ADDED_TO_PARAMS", { state }) } catch (error) { logger.error("SIGNIN_OAUTH_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`) diff --git a/src/server/lib/signin/oauth.js b/src/server/lib/signin/oauth.js index 55f2138a78..3f08e3b578 100644 --- a/src/server/lib/signin/oauth.js +++ b/src/server/lib/signin/oauth.js @@ -33,7 +33,7 @@ export default async function getAuthorizationUrl(req) { url = url.replace(baseUrl, provider.authorizationUrl + "&") } - logger.debug("GET_AUTHORIZATION_URL", url) + logger.debug("GET_AUTHORIZATION_URL", { url, params }) return url } @@ -44,7 +44,7 @@ export default async function getAuthorizationUrl(req) { oauth_token_secret: tokens.oauth_token_secret, ...tokens.params, })}` - logger.debug("GET_AUTHORIZATION_URL", url) + logger.debug("GET_AUTHORIZATION_URL", { url, tokens }) return url } catch (error) { logger.error("GET_AUTHORIZATION_URL_ERROR", error) diff --git a/src/server/routes/callback.js b/src/server/routes/callback.js index 7f620f5ee7..510581fcc3 100644 --- a/src/server/routes/callback.js +++ b/src/server/routes/callback.js @@ -303,7 +303,9 @@ export default async function callback(req, res) { if (!useJwtSession) { logger.error( "CALLBACK_CREDENTIALS_JWT_ERROR", - "Signin in with credentials is only supported if JSON Web Tokens are enabled" + new Error( + "Signin in with credentials is only supported if JSON Web Tokens are enabled" + ) ) return res .status(500) @@ -313,7 +315,9 @@ export default async function callback(req, res) { if (!provider.authorize) { logger.error( "CALLBACK_CREDENTIALS_HANDLER_ERROR", - "Must define an authorize() handler to use credentials authentication provider" + new Error( + "Must define an authorize() handler to use credentials authentication provider" + ) ) return res .status(500) diff --git a/types/index.d.ts b/types/index.d.ts index 8b2d4fd086..e5d0a737f6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -213,9 +213,22 @@ export type Theme = "auto" | "dark" | "light" * [Documentation](https://next-auth.js.org/configuration/options#logger) */ export interface LoggerInstance { - warn(code: string, ...message: unknown[]): void - error(code: string, ...message: unknown[]): void - debug(code: string, ...message: unknown[]): void + warn( + code: + | "JWT_AUTO_GENERATED_SIGNING_KEY" + | "JWT_AUTO_GENERATED_ENCRYPTION_KEY" + | "NEXTAUTH_URL" + ): void + error( + code: string, + /** + * Either an instance of (JSON serializable) Error + * or an object that contains some debug information. + * (Error is still available through `metadata.error`) + */ + metadata: Error | { error: Error; [key: string]: unknown } + ): void + debug(code: string, metadata: unknown): void } /** diff --git a/www/docs/configuration/options.md b/www/docs/configuration/options.md index 401e782cfe..f1ebe05198 100644 --- a/www/docs/configuration/options.md +++ b/www/docs/configuration/options.md @@ -329,6 +329,8 @@ Set debug to `true` to enable debug messages for authentication and database ope Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service. +The `code` parameter for `error` and `warn` are explained in the [Warnings](/warnings) and [Errors](/errors) pages respectively. + Example: ```js title="/pages/api/auth/[...nextauth].js" @@ -337,14 +339,14 @@ import log from "logging-service" export default NextAuth({ ... logger: { - error(code, ...message) { - log.error(code, message) + error(code, metadata) { + log.error(code, metadata) }, - warn(code, ...message) { - log.warn(code, message) + warn(code) { + log.warn(code) }, - debug(code, ...message) { - log.debug(code, message) + debug(code, metadata) { + log.debug(code, metadata) } } ...