From 036d87c9d078fd419fa5521d0c693a3371335829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 15:46:06 +0200 Subject: [PATCH 1/7] refactor(logger): use named params in error, simplify others --- src/client/react.js | 7 ++-- src/lib/logger.js | 30 ++++++++++++---- src/server/index.js | 8 ++--- src/server/lib/oauth/callback.js | 31 +++++++++------- src/server/lib/oauth/client.js | 6 +++- src/server/lib/oauth/pkce-handler.js | 51 ++++++++++++++------------- src/server/lib/oauth/state-handler.js | 44 +++++++++-------------- src/server/lib/signin/oauth.js | 4 +-- src/server/routes/callback.js | 8 +++-- 9 files changed, 105 insertions(+), 84 deletions(-) diff --git a/src/client/react.js b/src/client/react.js index ee83a3aabb..360eb15712 100644 --- a/src/client/react.js +++ b/src/client/react.js @@ -320,14 +320,13 @@ 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, headers: req.headers }) return null } } @@ -336,7 +335,7 @@ 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") + logger.warn("NEXTAUTH_URL") } // Return absolute path when called server side diff --git a/src/lib/logger.js b/src/lib/logger.js index 7e2f448337..39f1fb4dcc 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -1,22 +1,38 @@ /** @type {import("types").LoggerInstance} */ + +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 } + } + return o +} + const _logger = { - error(code, ...message) { + error(code, metadata) { + if (metadata.error) { + metadata.error = formatError(metadata.error) + metadata.message = metadata.message ?? metadata.error.message + } else 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) }, } diff --git a/src/server/index.js b/src/server/index.js index a1cfa06a88..69d9aa45ba 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 { 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 2c9c2c1c5a..bb6b353de6 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 366fd45c39..81a4118a34 100644 --- a/src/server/lib/oauth/pkce-handler.js +++ b/src/server/lib/oauth/pkce-handler.js @@ -1,11 +1,11 @@ -import pkceChallenge from 'pkce-challenge' -import * as cookie from '../cookie' -import jwt from '../../../lib/jwt' -import logger from '../../../lib/logger' -import { OAuthCallbackError } from '../../../lib/errors' +import pkceChallenge from "pkce-challenge" +import * as cookie from "../cookie" +import jwt from "../../../lib/jwt" +import logger from "../../../lib/logger" +import { OAuthCallbackError } from "../../../lib/errors" const PKCE_LENGTH = 64 -const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2 +const PKCE_CODE_CHALLENGE_METHOD = "S256" // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2 const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds /** @@ -13,36 +13,36 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds * @param {import("types/internals").NextAuthRequest} req * @param {import("types/internals").NextAuthResponse} res */ -export async function handleCallback (req, res) { +export async function handleCallback(req, res) { const { cookies, provider, baseUrl, basePath } = req.options try { // Provider does not support PKCE, nothing to do. - if (!provider.protection?.includes('pkce')) { + if (!provider.protection?.includes("pkce")) { return } if (!(cookies.pkceCodeVerifier.name in req.cookies)) { - throw new OAuthCallbackError('The code_verifier cookie was not found.') + throw new OAuthCallbackError("The code_verifier cookie was not found.") } const pkce = await jwt.decode({ ...req.options.jwt, token: req.cookies[cookies.pkceCodeVerifier.name], maxAge: PKCE_MAX_AGE, - encryption: true + encryption: true, }) req.options.pkce = pkce - logger.debug('OAUTH_CALLBACK_PROTECTION', 'Read PKCE verifier from cookie', { + logger.debug("PKCE_VERIFIER_FROM_COOKIE", { code_verifier: pkce.code_verifier, pkceLength: PKCE_LENGTH, - method: PKCE_CODE_CHALLENGE_METHOD + 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) + logger.error("CALLBACK_OAUTH_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`) } } @@ -52,42 +52,43 @@ export async function handleCallback (req, res) { * @param {import("types/internals").NextAuthRequest} req * @param {import("types/internals").NextAuthResponse} res */ -export async function handleSignin (req, res) { +export async function handleSignin(req, res) { const { cookies, provider, baseUrl, basePath } = req.options try { - if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do. + if (!provider.protection?.includes("pkce")) { + // Provider does not support PKCE, nothing to do. return } // 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_PROTECTION', 'Created PKCE challenge/verifier', { + logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", { ...pkce, pkceLength: PKCE_LENGTH, - method: PKCE_CODE_CHALLENGE_METHOD + method: PKCE_CODE_CHALLENGE_METHOD, }) provider.authorizationParams = { ...provider.authorizationParams, code_challenge: pkce.code_challenge, - code_challenge_method: PKCE_CODE_CHALLENGE_METHOD + code_challenge_method: PKCE_CODE_CHALLENGE_METHOD, } const encryptedCodeVerifier = await jwt.encode({ ...req.options.jwt, maxAge: PKCE_MAX_AGE, token: { code_verifier: pkce.code_verifier }, - encryption: true + encryption: true, }) const cookieExpires = new Date() - cookieExpires.setTime(cookieExpires.getTime() + (PKCE_MAX_AGE * 1000)) + cookieExpires.setTime(cookieExpires.getTime() + PKCE_MAX_AGE * 1000) cookie.set(res, cookies.pkceCodeVerifier.name, encryptedCodeVerifier, { expires: cookieExpires.toISOString(), - ...cookies.pkceCodeVerifier.options + ...cookies.pkceCodeVerifier.options, }) - logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE code_verifier saved in cookie') + logger.debug("PKCE_CODE_VERIFIER_SAVED", { encryptedCodeVerifier }) } catch (error) { - logger.error('SIGNIN_OAUTH_ERROR', 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 a1654644da..25a1f7cadc 100644 --- a/src/server/lib/oauth/state-handler.js +++ b/src/server/lib/oauth/state-handler.js @@ -1,6 +1,6 @@ -import { createHash } from 'crypto' -import logger from '../../../lib/logger' -import { OAuthCallbackError } from '../../../lib/errors' +import { createHash } from "crypto" +import logger from "../../../lib/logger" +import { OAuthCallbackError } from "../../../lib/errors" /** * For OAuth 2.0 flows, if the provider supports state, @@ -9,27 +9,23 @@ import { OAuthCallbackError } from '../../../lib/errors' * @param {import("types/internals").NextAuthRequest} req * @param {import("types/internals").NextAuthResponse} res */ -export async function handleCallback (req, res) { +export async function handleCallback(req, res) { const { csrfToken, provider, baseUrl, basePath } = req.options try { // Provider does not support state, nothing to do. - if (!provider.protection?.includes('state')) { + if (!provider.protection?.includes("state")) { return } const state = req.query.state || req.body.state - const expectedState = createHash('sha256').update(csrfToken).digest('hex') + const expectedState = createHash("sha256").update(csrfToken).digest("hex") - logger.debug( - 'OAUTH_CALLBACK_PROTECTION', - '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') + throw new OAuthCallbackError("Invalid state returned from OAuth provider") } } catch (error) { - logger.error('STATE_ERROR', error) + logger.error("STATE_ERROR", error) return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`) } } @@ -39,31 +35,25 @@ export async function handleCallback (req, res) { * @param {import("types/internals").NextAuthRequest} req * @param {import("types/internals").NextAuthResponse} res */ -export async function handleSignin (req, res) { +export async function handleSignin(req, res) { const { provider, baseUrl, basePath, csrfToken } = req.options try { - if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do. + if (!provider.protection?.includes("state")) { + // Provider does not support state, nothing to do. return } - if ('state' in provider) { - logger.warn( - 'STATE_OPTION_DEPRECATION', - 'The `state` provider option is being replaced with `protection`. See the docs.' - ) + if ("state" in provider) { + logger.warn("STATE_OPTION_DEPRECATION") } // A hash of the NextAuth.js CSRF token is used as the state - const state = createHash('sha256').update(csrfToken).digest('hex') + const state = createHash("sha256").update(csrfToken).digest("hex") provider.authorizationParams = { ...provider.authorizationParams, state } - logger.debug( - 'OAUTH_CALLBACK_PROTECTION', - 'Added state to authorization params', - { state } - ) + logger.debug("STATE_ADDED_TO_PARAMS", { state }) } catch (error) { - logger.error('SIGNIN_OAUTH_ERROR', 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) From dd59db80c78259c7947483e4e130ae377ed0fec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 15:46:20 +0200 Subject: [PATCH 2/7] docs(ts): update `logger` method signatures --- types/index.d.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 8b2d4fd086..80471494ea 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -213,9 +213,17 @@ 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: string): 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 } /** From 5ee982ab4ae4b202266754f8cd9453abfc80999b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 15:46:34 +0200 Subject: [PATCH 3/7] docs(logger): update `logger` documentation --- www/docs/configuration/options.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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) } } ... From f3f97be06c092eadd62249e1e5e4d5d40b80d8e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 15:52:53 +0200 Subject: [PATCH 4/7] chore(errors): remove unused errors These adapter specific errors are now generated on-the-fly. --- src/lib/errors.js | 64 ----------------------------------------------- 1 file changed, 64 deletions(-) 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" -} From 286c5e5d2c60ef0c0962a0462b8c49893f56b3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 16:27:49 +0200 Subject: [PATCH 5/7] refactor(ogger): clean up client-side logging --- src/client/react.js | 11 +++++------ src/lib/logger.js | 38 +++++++++++++------------------------- src/server/index.js | 10 +++------- 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/client/react.js b/src/client/react.js index 360eb15712..2fe6bec422 100644 --- a/src/client/react.js +++ b/src/client/react.js @@ -326,18 +326,17 @@ async function _fetchData(path, { ctx, req = ctx?.req } = {}) { 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", { error, path, headers: req.headers }) + 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") - } - // Return absolute path when called server side return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}` } diff --git a/src/lib/logger.js b/src/lib/logger.js index 39f1fb4dcc..44fcf2af8b 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -7,16 +7,16 @@ 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 } const _logger = { error(code, metadata) { - if (metadata.error) { - metadata.error = formatError(metadata.error) - metadata.message = metadata.message ?? metadata.error.message - } else metadata = formatError(metadata) - + metadata = formatError(metadata) console.error( `[next-auth][error][${code.toLowerCase()}]`, `\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`, @@ -63,31 +63,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) + } 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 69d9aa45ba..f1401052f3 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -254,13 +254,9 @@ 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 + metadata.client = true // Indicate that these logs are from the client + logger[level](code, metadata) } catch (error) { // If logging itself failed... logger.error("LOGGER_ERROR", error) From 3f6eb526664669436f3721c509be0b4167429e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sat, 10 Jul 2021 16:28:59 +0200 Subject: [PATCH 6/7] test(logger): fix client-side logging tests --- src/client/__tests__/csrf.test.js | 9 ++++----- src/client/__tests__/providers.test.js | 9 ++++----- src/client/__tests__/session.test.js | 9 ++++----- src/client/__tests__/sign-in.test.js | 9 ++++----- src/lib/logger.js | 2 +- src/server/index.js | 1 - 6 files changed, 17 insertions(+), 22 deletions(-) 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/lib/logger.js b/src/lib/logger.js index 44fcf2af8b..75e63929a5 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -69,7 +69,7 @@ export function proxyLogger(logger = _logger, basePath) { if (level === "error") { metadata = formatError(metadata) } - + metadata.client = true const url = `${basePath}/_log` const body = new URLSearchParams({ level, code, ...metadata }) if (navigator.sendBeacon) { diff --git a/src/server/index.js b/src/server/index.js index f1401052f3..2c3c837096 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -255,7 +255,6 @@ async function NextAuthHandler(req, res, userOptions) { if (userOptions.logger) { try { const { code, level, ...metadata } = req.body - metadata.client = true // Indicate that these logs are from the client logger[level](code, metadata) } catch (error) { // If logging itself failed... From eb0f1608cb07eb492b3c3206c7a0ebd8b38d8c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 12 Jul 2021 00:11:01 +0200 Subject: [PATCH 7/7] docs(ts): strongly type `logger.warn` code --- src/lib/logger.js | 3 +-- types/index.d.ts | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/logger.js b/src/lib/logger.js index 75e63929a5..630cb27ba5 100644 --- a/src/lib/logger.js +++ b/src/lib/logger.js @@ -1,5 +1,3 @@ -/** @type {import("types").LoggerInstance} */ - import { UnknownError } from "./errors" /** Makes sure that error is always serializable */ @@ -14,6 +12,7 @@ function formatError(o) { return o } +/** @type {import("types").LoggerInstance} */ const _logger = { error(code, metadata) { metadata = formatError(metadata) diff --git a/types/index.d.ts b/types/index.d.ts index 80471494ea..e5d0a737f6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -213,7 +213,12 @@ export type Theme = "auto" | "dark" | "light" * [Documentation](https://next-auth.js.org/configuration/options#logger) */ export interface LoggerInstance { - warn(code: string): void + warn( + code: + | "JWT_AUTO_GENERATED_SIGNING_KEY" + | "JWT_AUTO_GENERATED_ENCRYPTION_KEY" + | "NEXTAUTH_URL" + ): void error( code: string, /**