Compare commits

...

4 Commits

Author SHA1 Message Date
Balázs Orbán
f42ef7cc62 apply suggestion 2023-01-06 18:44:48 +01:00
Balázs Orbán
35a708a2c3 remove prettier-ignore 2023-01-03 07:36:59 +01:00
Balázs Orbán
4f919ca76f fix error name 2023-01-03 07:35:46 +01:00
Balázs Orbán
036e34b4b6 fix(core): improve stack traces 2023-01-03 07:24:12 +01:00
4 changed files with 103 additions and 45 deletions

View File

@@ -1,14 +1,23 @@
interface ErrorCause extends Record<string, unknown> {}
/** @internal */ /** @internal */
export class AuthError extends Error { export class AuthError extends Error {
metadata?: Record<string, unknown> constructor(message: string | Error | ErrorCause, cause?: ErrorCause) {
constructor(message: Error | string, metadata?: Record<string, unknown>) {
if (message instanceof Error) { if (message instanceof Error) {
super(message.message) super(undefined, {
this.stack = message.stack cause: { err: message, ...(message.cause as any), ...cause },
} else super(message) })
this.name = this.constructor.name } else if (typeof message === "string") {
this.metadata = metadata if (cause instanceof Error) {
cause = { err: cause, ...(cause.cause as any) }
}
super(message, cause)
} else {
super(undefined, message)
}
Error.captureStackTrace?.(this, this.constructor) Error.captureStackTrace?.(this, this.constructor)
this.name =
message instanceof AuthError ? message.name : this.constructor.name
} }
} }
@@ -28,7 +37,45 @@ export class AdapterError extends AuthError {}
/** @todo */ /** @todo */
export class AuthorizedCallbackError extends AuthError {} export class AuthorizedCallbackError extends AuthError {}
/** @todo */ /**
* There was an error while trying to finish up authenticating the user.
* Depending on the type of provider, this could be for multiple reasons.
*
* :::tip
* Check out `[auth][details]` in the error message to know which provider failed.
* @example
* ```sh
* [auth][details]: { "provider": "github" }
* ```
* :::
*
* For an **OAuth provider**, possible causes are:
* - The user denied access to the application
* - There was an error parsing the OAuth Profile:
* Check out the provider's `profile` or `userinfo.request` method to make sure
* it correctly fetches the user's profile.
* - The `signIn` or `jwt` callback methods threw an uncaught error:
* Check the callback method implementations.
*
* For an **Email provider**, possible causes are:
* - The provided email/token combination was invalid/missing:
* Check if the provider's `sendVerificationRequest` method correctly sends the email.
* - The provided email/token combination has expired:
* Ask the user to log in again.
* - There was an error with the database:
* Check the database logs.
*
* For a **Credentials provider**, possible causes are:
* - The `authorize` method threw an uncaught error:
* Check the provider's `authorize` method.
* - The `signIn` or `jwt` callback methods threw an uncaught error:
* Check the callback method implementations.
*
* :::tip
* Check out `[auth][cause]` in the error message for more details.
* It will show the original stack trace.
* :::
*/
export class CallbackRouteError extends AuthError {} export class CallbackRouteError extends AuthError {}
/** @todo */ /** @todo */
@@ -93,3 +140,10 @@ export class UnsupportedStrategy extends AuthError {}
/** @todo */ /** @todo */
export class UntrustedHost extends AuthError {} export class UntrustedHost extends AuthError {}
/**
* The user's email/token combination was invalid.
* This could be because the email/token combination was not found in the database,
* or because it token has expired. Ask the user to log in again.
*/
export class Verification extends AuthError {}

View File

@@ -133,7 +133,8 @@ export async function handleLogin(
// with is already associated with another user, then we cannot link them // with is already associated with another user, then we cannot link them
// and need to return an error. // and need to return an error.
throw new AccountNotLinked( throw new AccountNotLinked(
"The account is already associated with another user" "The account is already associated with another user",
{ provider: account.provider }
) )
} }
// If there is no active session, but the account being signed in with is already // If there is no active session, but the account being signed in with is already
@@ -193,7 +194,8 @@ export async function handleLogin(
// want to link them in case it's not safe to do so, so instead we prompt the user // want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts. // to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinked( throw new AccountNotLinked(
"Another account already exists with the same e-mail address" "Another account already exists with the same e-mail address",
{ provider: account.provider }
) )
} }
} else { } else {

View File

@@ -1,5 +1,5 @@
import { handleLogin } from "../callback-handler.js" import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError } from "../../errors.js" import { CallbackRouteError, Verification } from "../../errors.js"
import { handleOAuth } from "../oauth/callback.js" import { handleOAuth } from "../oauth/callback.js"
import { createHash } from "../web.js" import { createHash } from "../web.js"
import { handleAuthorized } from "./shared.js" import { handleAuthorized } from "./shared.js"
@@ -8,7 +8,6 @@ import type { AdapterSession } from "../../adapters.js"
import type { import type {
RequestInternal, RequestInternal,
ResponseInternal, ResponseInternal,
User,
InternalOptions, InternalOptions,
} from "../../types.js" } from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js" import type { Cookie, SessionStore } from "../cookie.js"
@@ -154,9 +153,13 @@ export async function callback(params: {
const token = query?.token as string | undefined const token = query?.token as string | undefined
const identifier = query?.email as string | undefined const identifier = query?.email as string | undefined
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email.
if (!token || !identifier) { if (!token || !identifier) {
return { redirect: `${url}/error?error=configuration`, cookies } const e = new TypeError(
"Missing token or email. The sign-in URL was manually opened without token/identifier or the link was not sent correctly in the email.",
{ cause: { hasToken: !!token, hasEmail: !!identifier } }
)
e.name = "Configuration"
throw e
} }
const secret = provider.secret ?? options.secret const secret = provider.secret ?? options.secret
@@ -166,10 +169,10 @@ export async function callback(params: {
token: await createHash(`${token}${secret}`), token: await createHash(`${token}${secret}`),
}) })
const invalidInvite = !invite || invite.expires.valueOf() < Date.now() const hasInvite = !!invite
if (invalidInvite) { const expired = invite ? invite.expires.valueOf() < Date.now() : undefined
return { redirect: `${url}/error?error=Verification`, cookies } const invalidInvite = !hasInvite || expired
} if (invalidInvite) throw new Verification({ hasInvite, expired })
// @ts-expect-error -- Verified in `assertConfig`. // @ts-expect-error -- Verified in `assertConfig`.
const profile = await getAdapterUserFromEmail(identifier, adapter) const profile = await getAdapterUserFromEmail(identifier, adapter)
@@ -252,13 +255,11 @@ export async function callback(params: {
} else if (provider.type === "credentials" && method === "POST") { } else if (provider.type === "credentials" && method === "POST") {
const credentials = body const credentials = body
let user: User | null
try {
// TODO: Forward the original request as is, instead of reconstructing it // TODO: Forward the original request as is, instead of reconstructing it
// prettier-ignore Object.entries(query ?? {}).forEach(([k, v]) =>
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v)) url.searchParams.set(k, v)
user = await provider.authorize( )
const user = await provider.authorize(
credentials, credentials,
// prettier-ignore // prettier-ignore
new Request(url, { headers, method, body: JSON.stringify(body) }) new Request(url, { headers, method, body: JSON.stringify(body) })
@@ -273,15 +274,6 @@ export async function callback(params: {
cookies, cookies,
} }
} }
} catch (e) {
return {
status: 401,
redirect: `${url}/error?error=${encodeURIComponent(
(e as Error).message
)}`,
cookies,
}
}
/** @type {import("src").Account} */ /** @type {import("src").Account} */
const account = { const account = {

View File

@@ -21,11 +21,21 @@ const reset = "\x1b[0m"
export const logger: LoggerInstance = { export const logger: LoggerInstance = {
error(error: AuthError) { error(error: AuthError) {
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}` const url = `https://errors.authjs.dev#${error.name.toLowerCase()}`
console.error(error.stack)
console.error( console.error(
`${red}[auth][error][${error.name}]${reset}: Read more at ${url}` `${red}[auth][error][${error.name}]${reset}:${
error.message ? ` ${error.message}.` : ""
} Read more at ${url}`
) )
error.metadata && console.error(JSON.stringify(error.metadata, null, 2)) if (error.cause) {
const { err, ...data } = error.cause as any
console.error(`${red}[auth][cause]${reset}:`, (err as Error).stack)
console.error(
`${red}[auth][details]${reset}:`,
JSON.stringify(data, null, 2)
)
} else if (error.stack) {
console.error(error.stack.replace(/.*/, "").substring(1))
}
}, },
warn(code) { warn(code) {
const url = `https://errors.authjs.dev#${code}` const url = `https://errors.authjs.dev#${code}`