diff --git a/docs/docs/errors.md b/docs/docs/errors.md index 076c2b7993..3fd1e39f20 100644 --- a/docs/docs/errors.md +++ b/docs/docs/errors.md @@ -61,17 +61,20 @@ There should also be further details logged when this occurs, such as the error ### Signin / Callback -#### GET_AUTHORIZATION_URL_ERROR +#### SIGNIN_OAUTH_ERROR -This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL. +This error occurs during the redirection to the authorization URL of the OAuth provider. Possible causes: -Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret. +1. Cookie handling +Either PKCE code verifier or the generation of the CSRF token hash in the internal state failed. -#### SIGNIN_OAUTH_ERROR +If set, check your [`cookies` configuration](/configuration/options#cookies), and make sure the browser is not blocking/restricting cookies. + +2. OAuth misconfiguration -This error can occur in one of a few places, first during the redirect to the authorization URL of the provider. Next, in the signin flow while creating the PKCE code verifier. Finally, during the generation of the CSRF Token hash in the internal state during signin. +Please check your OAuth provider and make sure your URLs and other options are correctly set. -Please check your OAuth provider settings and make sure your URLs and other options are correctly set on the provider side. +If you are using an OAuth v1 provider, check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret. #### CALLBACK_OAUTH_ERROR @@ -151,12 +154,6 @@ This error occurs when there was an issue deleting the session from the database ### Other -#### SEND_VERIFICATION_EMAIL_ERROR - -This error occurs when the Email Authentication Provider is unable to send an email. - -Check your mail server configuration. - #### MISSING_NEXTAUTH_API_ROUTE_ERROR This error happens when `[...nextauth].js` file is not found inside `pages/api/auth`. diff --git a/docs/docs/warnings.md b/docs/docs/warnings.md index dc2e66baea..25f060689f 100644 --- a/docs/docs/warnings.md +++ b/docs/docs/warnings.md @@ -37,6 +37,12 @@ Twitter OAuth 2.0 is currently in beta as certain changes might still be necessa Some APIs are still experimental; they may be changed or removed in the future. Use at your own risk. +#### DEBUG_ENABLED + +You have enabled the `debug` option. It is meant for development only, to help you catch issues in your authentication flow and you should consider removing this option when deploying to production. One way of only allowing debugging while not in production is to set `debug: process.env.NODE_ENV !== "production"`, so you can commit this without needing to change the value. + +If you want to log debug messages during production anyway, we recommend setting the [`logger` option](/configuration/options#logger) with proper sanitization of potentially sensitive user information. + ## Adapter ### ADAPTER_TYPEORM_UPDATING_ENTITIES diff --git a/packages/next-auth/src/client/__tests__/csrf.test.js b/packages/next-auth/src/client/__tests__/csrf.test.js index ad335a45c5..1d6b12618e 100644 --- a/packages/next-auth/src/client/__tests__/csrf.test.js +++ b/packages/next-auth/src/client/__tests__/csrf.test.js @@ -79,7 +79,7 @@ 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", { - path: "csrf", + url: "/api/auth/csrf", error: new SyntaxError("Unexpected token s in JSON at position 0"), }) }) diff --git a/packages/next-auth/src/client/__tests__/providers.test.js b/packages/next-auth/src/client/__tests__/providers.test.js index cd9613ee12..45d055089f 100644 --- a/packages/next-auth/src/client/__tests__/providers.test.js +++ b/packages/next-auth/src/client/__tests__/providers.test.js @@ -57,7 +57,7 @@ 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", { - path: "providers", + url: "/api/auth/providers", error: new SyntaxError("Unexpected token s in JSON at position 0"), }) }) diff --git a/packages/next-auth/src/client/__tests__/session.test.js b/packages/next-auth/src/client/__tests__/session.test.js index 1787de15ee..4940f81b15 100644 --- a/packages/next-auth/src/client/__tests__/session.test.js +++ b/packages/next-auth/src/client/__tests__/session.test.js @@ -71,7 +71,7 @@ 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", { - path: "session", + url: "/api/auth/session", error: new SyntaxError("Unexpected token S in JSON at position 0"), }) }) diff --git a/packages/next-auth/src/client/__tests__/sign-in.test.js b/packages/next-auth/src/client/__tests__/sign-in.test.js index b7278a1d25..0422fe90a4 100644 --- a/packages/next-auth/src/client/__tests__/sign-in.test.js +++ b/packages/next-auth/src/client/__tests__/sign-in.test.js @@ -256,7 +256,7 @@ test("when it fails to fetch the providers, it redirected back to signin page", expect(logger.error).toHaveBeenCalledTimes(1) expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", { error: "Error when retrieving providers", - path: "providers", + url: "/api/auth/providers", }) }) }) diff --git a/packages/next-auth/src/client/_utils.ts b/packages/next-auth/src/client/_utils.ts index 5b29d843d3..f2eeb3140a 100644 --- a/packages/next-auth/src/client/_utils.ts +++ b/packages/next-auth/src/client/_utils.ts @@ -35,20 +35,17 @@ export async function fetchData( logger: LoggerInstance, { ctx, req = ctx?.req }: CtxOrReq = {} ): Promise { + const url = `${apiBaseUrl(__NEXTAUTH)}/${path}` try { const options = req?.headers.cookie ? { headers: { cookie: req.headers.cookie } } : {} - const res = await fetch(`${apiBaseUrl(__NEXTAUTH)}/${path}`, options) + const res = await fetch(url, 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", { - error: error as Error, - path, - ...(req ? { header: req.headers } : {}), - }) + logger.error("CLIENT_FETCH_ERROR", { error: error as Error, url }) return null } } diff --git a/packages/next-auth/src/core/index.ts b/packages/next-auth/src/core/index.ts index 370443c77b..7acd7454b9 100644 --- a/packages/next-auth/src/core/index.ts +++ b/packages/next-auth/src/core/index.ts @@ -90,8 +90,8 @@ export async function NextAuthHandler< const assertionResult = assertConfig({ options: userOptions, req }) - if (typeof assertionResult === "string") { - logger.warn(assertionResult) + if (Array.isArray(assertionResult)) { + assertionResult.forEach(logger.warn) } else if (assertionResult instanceof Error) { // Bail out early if there's an error in the user config const { pages, theme } = userOptions diff --git a/packages/next-auth/src/core/lib/assert.ts b/packages/next-auth/src/core/lib/assert.ts index 49f058c43d..a22790d359 100644 --- a/packages/next-auth/src/core/lib/assert.ts +++ b/packages/next-auth/src/core/lib/assert.ts @@ -9,8 +9,9 @@ import { import parseUrl from "../../utils/parse-url" import { defaultCookies } from "./cookie" -import type { NextAuthHandlerParams, RequestInternal } from ".." +import type { RequestInternal } from ".." import type { WarningCode } from "../../utils/logger" +import type { NextAuthOptions } from "../types" type ConfigError = | MissingAPIRoute @@ -19,7 +20,7 @@ type ConfigError = | MissingAuthorize | MissingAdapter -let twitterWarned = false +let warned = false function isValidHttpUrl(url: string, baseUrl: string) { try { @@ -37,13 +38,28 @@ function isValidHttpUrl(url: string, baseUrl: string) { * * REVIEW: Make some of these and corresponding docs less Next.js specific? */ -export function assertConfig( - params: NextAuthHandlerParams & { - req: RequestInternal - } -): ConfigError | WarningCode | undefined { +export function assertConfig(params: { + options: NextAuthOptions + req: RequestInternal +}): ConfigError | WarningCode[] { const { options, req } = params + const warnings: WarningCode[] = [] + + if (!warned) { + if (!req.host) warnings.push("NEXTAUTH_URL") + + // TODO: Make this throw an error in next major. This will also get rid of `NODE_ENV` + if (!options.secret && process.env.NODE_ENV !== "production") + warnings.push("NO_SECRET") + + if (options.debug) warnings.push("DEBUG_ENABLED") + } + + if (!options.secret && process.env.NODE_ENV === "production") { + return new MissingSecret("Please define a `secret` in production.") + } + // req.query isn't defined when asserting `unstable_getServerSession` for example if (!req.query?.nextauth && !req.action) { return new MissingAPIRoute( @@ -51,14 +67,6 @@ export function assertConfig( ) } - if (!options.secret) { - if (process.env.NODE_ENV === "production") { - return new MissingSecret("Please define a `secret` in production.") - } else { - return "NO_SECRET" - } - } - const callbackUrlParam = req.query?.callbackUrl as string | undefined const url = parseUrl(req.host) @@ -69,9 +77,6 @@ export function assertConfig( ) } - // This is below the callbackUrlParam check because it would obscure the error - if (!req.host) return "NEXTAUTH_URL" - const { callbackUrl: defaultCallbackUrl } = defaultCookies( options.useSecureCookies ?? url.base.startsWith("https://") ) @@ -119,8 +124,10 @@ export function assertConfig( return new MissingAdapter("E-mail login requires an adapter.") } - if (!twitterWarned && hasTwitterOAuth2) { - twitterWarned = true - return "TWITTER_OAUTH_2_BETA" + if (!warned) { + if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA") + warned = true } + + return warnings } diff --git a/packages/next-auth/src/core/lib/email/signin.ts b/packages/next-auth/src/core/lib/email/signin.ts index cd58f9262e..3be14f407f 100644 --- a/packages/next-auth/src/core/lib/email/signin.ts +++ b/packages/next-auth/src/core/lib/email/signin.ts @@ -9,9 +9,8 @@ import type { InternalOptions } from "../../types" export default async function email( identifier: string, options: InternalOptions<"email"> -) { - const { url, adapter, provider, logger, callbackUrl, theme } = options - +): Promise { + const { url, adapter, provider, callbackUrl, theme } = options // Generate token const token = (await provider.generateVerificationToken?.()) ?? @@ -34,22 +33,18 @@ export default async function email( const params = new URLSearchParams({ callbackUrl, token, email: identifier }) const _url = `${url}/callback/${provider.id}?${params}` - try { - // Send to user - await provider.sendVerificationRequest({ - identifier, - token, - expires, - url: _url, - provider, - theme, - }) - } catch (error) { - logger.error("SEND_VERIFICATION_EMAIL_ERROR", { - identifier, - url, - error: error as Error, - }) - throw new Error("SEND_VERIFICATION_EMAIL_ERROR") - } + // Send to user + await provider.sendVerificationRequest({ + identifier, + token, + expires, + url: _url, + provider, + theme, + }) + + return `${url}/verify-request?${new URLSearchParams({ + provider: provider.id, + type: provider.type, + })}` } diff --git a/packages/next-auth/src/core/lib/oauth/authorization-url.ts b/packages/next-auth/src/core/lib/oauth/authorization-url.ts index c98da9eb78..1f74acf630 100644 --- a/packages/next-auth/src/core/lib/oauth/authorization-url.ts +++ b/packages/next-auth/src/core/lib/oauth/authorization-url.ts @@ -14,66 +14,63 @@ import type { Cookie } from "../cookie" * * [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/) | [OAuth 1](https://oauth.net/core/1.0a/#auth_step2) */ -export default async function getAuthorizationUrl(params: { +export default async function getAuthorizationUrl({ + options, + query, +}: { options: InternalOptions<"oauth"> query: RequestInternal["query"] }) { - const { options, query } = params const { logger, provider } = options - try { - let params: any = {} + let params: any = {} - if (typeof provider.authorization === "string") { - const parsedUrl = new URL(provider.authorization) - const parsedParams = Object.fromEntries(parsedUrl.searchParams.entries()) - params = { ...params, ...parsedParams } - } else { - params = { ...params, ...provider.authorization?.params } - } + if (typeof provider.authorization === "string") { + const parsedUrl = new URL(provider.authorization) + const parsedParams = Object.fromEntries(parsedUrl.searchParams.entries()) + params = { ...params, ...parsedParams } + } else { + params = { ...params, ...provider.authorization?.params } + } - params = { ...params, ...query } + params = { ...params, ...query } - // Handle OAuth v1.x - if (provider.version?.startsWith("1.")) { - const client = oAuth1Client(options) - const tokens = (await client.getOAuthRequestToken(params)) as any - const url = `${ - // @ts-expect-error - provider.authorization?.url ?? provider.authorization - }?${new URLSearchParams({ - oauth_token: tokens.oauth_token, - oauth_token_secret: tokens.oauth_token_secret, - ...tokens.params, - })}` + // Handle OAuth v1.x + if (provider.version?.startsWith("1.")) { + const client = oAuth1Client(options) + const tokens = (await client.getOAuthRequestToken(params)) as any + const url = `${ + // @ts-expect-error + provider.authorization?.url ?? provider.authorization + }?${new URLSearchParams({ + oauth_token: tokens.oauth_token, + oauth_token_secret: tokens.oauth_token_secret, + ...tokens.params, + })}` - logger.debug("GET_AUTHORIZATION_URL", { url }) - return { redirect: url } - } + logger.debug("GET_AUTHORIZATION_URL", { url, provider }) + return { redirect: url } + } - const client = await openidClient(options) + const client = await openidClient(options) - const authorizationParams: AuthorizationParameters = params - const cookies: Cookie[] = [] + const authorizationParams: AuthorizationParameters = params + const cookies: Cookie[] = [] - const state = await createState(options) - if (state) { - authorizationParams.state = state.value - cookies.push(state.cookie) - } + const state = await createState(options) + if (state) { + authorizationParams.state = state.value + cookies.push(state.cookie) + } - const pkce = await createPKCE(options) - if (pkce) { - authorizationParams.code_challenge = pkce.code_challenge - authorizationParams.code_challenge_method = pkce.code_challenge_method - cookies.push(pkce.cookie) - } + const pkce = await createPKCE(options) + if (pkce) { + authorizationParams.code_challenge = pkce.code_challenge + authorizationParams.code_challenge_method = pkce.code_challenge_method + cookies.push(pkce.cookie) + } - const url = client.authorizationUrl(authorizationParams) + const url = client.authorizationUrl(authorizationParams) - logger.debug("GET_AUTHORIZATION_URL", { url, cookies }) - return { redirect: url, cookies } - } catch (error) { - logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error) - throw error - } + logger.debug("GET_AUTHORIZATION_URL", { url, cookies, provider }) + return { redirect: url, cookies } } diff --git a/packages/next-auth/src/core/lib/oauth/callback.ts b/packages/next-auth/src/core/lib/oauth/callback.ts index 333ed3d887..0904216d7e 100644 --- a/packages/next-auth/src/core/lib/oauth/callback.ts +++ b/packages/next-auth/src/core/lib/oauth/callback.ts @@ -28,9 +28,9 @@ export default async function oAuthCallback(params: { logger.error("OAUTH_CALLBACK_HANDLER_ERROR", { error, error_description: query?.error_description, - body, providerId: provider.id, }) + logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body }) throw error } @@ -199,10 +199,7 @@ async function getProfile({ // 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", { - error: error as Error, - OAuthProfile, - }) + logger.error("OAUTH_PARSE_PROFILE_ERROR", error as Error) return { profile: null, account: null, diff --git a/packages/next-auth/src/core/routes/signin.ts b/packages/next-auth/src/core/routes/signin.ts index cd1547015e..739ea1090c 100644 --- a/packages/next-auth/src/core/routes/signin.ts +++ b/packages/next-auth/src/core/routes/signin.ts @@ -26,7 +26,10 @@ export default async function signin(params: { const response = await getAuthorizationUrl({ options, query }) return response } catch (error) { - logger.error("SIGNIN_OAUTH_ERROR", { error: error as Error, provider }) + logger.error("SIGNIN_OAUTH_ERROR", { + error: error as Error, + providerId: provider.id, + }) return { redirect: `${url}/error?error=OAuthSignin` } } } else if (provider.type === "email") { @@ -79,18 +82,15 @@ export default async function signin(params: { } try { - await emailSignin(email, options) + const redirect = await emailSignin(email, options) + return { redirect } } catch (error) { - logger.error("SIGNIN_EMAIL_ERROR", error as Error) + logger.error("SIGNIN_EMAIL_ERROR", { + error: error as Error, + providerId: provider.id, + }) return { redirect: `${url}/error?error=EmailSignin` } } - - const params = new URLSearchParams({ - provider: provider.id, - type: provider.type, - }) - - return { redirect: `${url}/verify-request?${params}` } } return { redirect: `${url}/signin` } } diff --git a/packages/next-auth/src/utils/logger.ts b/packages/next-auth/src/utils/logger.ts index b6cb6d964e..278a5010f4 100644 --- a/packages/next-auth/src/utils/logger.ts +++ b/packages/next-auth/src/utils/logger.ts @@ -19,7 +19,11 @@ function hasErrorProperty( return !!(x as any)?.error } -export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET" | "TWITTER_OAUTH_2_BETA" +export type WarningCode = + | "NEXTAUTH_URL" + | "NO_SECRET" + | "TWITTER_OAUTH_2_BETA" + | "DEBUG_ENABLED" /** * Override any of the methods, and the rest will use the default logger.