mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
5 Commits
feat/auth-
...
fix/improv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f42ef7cc62 | ||
|
|
35a708a2c3 | ||
|
|
4f919ca76f | ||
|
|
036e34b4b6 | ||
|
|
d2288ee4cc |
@@ -9,7 +9,6 @@ module.exports = {
|
||||
files: [
|
||||
"apps/dev/pages/api/auth/[...nextauth].ts",
|
||||
"docs/{sidebars,docusaurus.config}.js",
|
||||
"packages/core/src/index.ts",
|
||||
],
|
||||
options: { printWidth: 150 },
|
||||
},
|
||||
|
||||
8
apps/dev/nextjs/pages/api/examples/session.js
Normal file
8
apps/dev/nextjs/pages/api/examples/session.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// This is an example of how to access a session from an API route
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "../auth/[...nextauth]"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
res.json(session)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Auth, SessionRequest } from "@auth/core"
|
||||
import { authConfig } from "../auth/[...nextauth]"
|
||||
|
||||
export default async function handle(req: Request) {
|
||||
authConfig.secret = process.env.AUTH_SECRET
|
||||
|
||||
const response = await Auth(new SessionRequest(req), authConfig)
|
||||
const session = await response.session()
|
||||
if (!session) {
|
||||
return new Response("Not authenticated", { status: 401 })
|
||||
}
|
||||
|
||||
console.log(session.user) // Do something with the session
|
||||
// Pass the original headers to set cookies (eg.: updating the session expiry)
|
||||
response.headers.set("content-type", "text/plain")
|
||||
return new Response("Authenticated", { headers: response.headers })
|
||||
}
|
||||
|
||||
export const config = {
|
||||
runtime: "experimental-edge",
|
||||
}
|
||||
@@ -293,6 +293,6 @@ html[data-theme="dark"] #carbonads .carbon-poweredby {
|
||||
See: https://github.com/TypeStrong/typedoc/issues/2006
|
||||
*/
|
||||
/* h3.anchor + p:has(code, strong), */ /** hack did not work as it hides property types elsewhere */
|
||||
/* #classes {
|
||||
#classes {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
interface ErrorCause extends Record<string, unknown> {}
|
||||
|
||||
/** @internal */
|
||||
export class AuthError extends Error {
|
||||
metadata?: Record<string, unknown>
|
||||
constructor(message: Error | string, metadata?: Record<string, unknown>) {
|
||||
constructor(message: string | Error | ErrorCause, cause?: ErrorCause) {
|
||||
if (message instanceof Error) {
|
||||
super(message.message)
|
||||
this.stack = message.stack
|
||||
} else super(message)
|
||||
this.name = this.constructor.name
|
||||
this.metadata = metadata
|
||||
super(undefined, {
|
||||
cause: { err: message, ...(message.cause as any), ...cause },
|
||||
})
|
||||
} else if (typeof message === "string") {
|
||||
if (cause instanceof Error) {
|
||||
cause = { err: cause, ...(cause.cause as any) }
|
||||
}
|
||||
super(message, cause)
|
||||
} else {
|
||||
super(undefined, message)
|
||||
}
|
||||
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 */
|
||||
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 {}
|
||||
|
||||
/** @todo */
|
||||
@@ -93,3 +140,10 @@ export class UnsupportedStrategy extends AuthError {}
|
||||
|
||||
/** @todo */
|
||||
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 {}
|
||||
|
||||
@@ -40,17 +40,17 @@ import { logger, setLogger, type LoggerInstance } from "./lib/utils/logger.js"
|
||||
import { toInternalRequest, toResponse } from "./lib/web.js"
|
||||
|
||||
import type { Adapter } from "./adapters.js"
|
||||
import type { CallbacksOptions, CookiesOptions, EventCallbacks, PagesOptions, SessionOptions, Theme } from "./types.js"
|
||||
import type {
|
||||
CallbacksOptions,
|
||||
CookiesOptions,
|
||||
EventCallbacks,
|
||||
PagesOptions,
|
||||
SessionOptions,
|
||||
Theme,
|
||||
} from "./types.js"
|
||||
import type { Provider } from "./providers/index.js"
|
||||
import { JWTOptions } from "./jwt.js"
|
||||
import { ProvidersRequest, ProvidersResponse, SessionRequest, SessionResponse } from "./lib/web-extension.js"
|
||||
|
||||
export * from "./lib/web-extension.js"
|
||||
|
||||
/** Returns a special {@link SessionResponse} instance to read the session from the request. */
|
||||
export function Auth(request: SessionRequest, config: AuthConfig): Promise<SessionResponse>
|
||||
/** Returns a special {@link ProvidersResponse} instance to read the list of providers in a client-safe way. */
|
||||
export function Auth(request: ProvidersRequest, config: AuthConfig): Promise<ProvidersResponse>
|
||||
/**
|
||||
* Core functionality provided by Auth.js.
|
||||
*
|
||||
@@ -69,18 +69,19 @@ export function Auth(request: ProvidersRequest, config: AuthConfig): Promise<Pro
|
||||
*```
|
||||
* @see [Documentation](https://authjs.dev)
|
||||
*/
|
||||
export function Auth(request: Request, config: AuthConfig): Promise<Response>
|
||||
export async function Auth(request: Request, config: AuthConfig): Promise<Response> {
|
||||
export async function Auth(
|
||||
request: Request,
|
||||
config: AuthConfig
|
||||
): Promise<Response> {
|
||||
setLogger(config.logger, config.debug)
|
||||
|
||||
const isAuthRequest = request instanceof SessionRequest || request instanceof ProvidersRequest
|
||||
|
||||
if (isAuthRequest) config.trustHost = true
|
||||
|
||||
const internalRequest = await toInternalRequest(request)
|
||||
if (internalRequest instanceof Error) {
|
||||
logger.error(internalRequest)
|
||||
return new Response(`Error: This action with HTTP ${request.method} is not supported.`, { status: 400 })
|
||||
return new Response(
|
||||
`Error: This action with HTTP ${request.method} is not supported.`,
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const assertionResult = assertConfig(internalRequest, config)
|
||||
@@ -91,10 +92,14 @@ export async function Auth(request: Request, config: AuthConfig): Promise<Respon
|
||||
// Bail out early if there's an error in the user config
|
||||
logger.error(assertionResult)
|
||||
const htmlPages = ["signin", "signout", "error", "verify-request"]
|
||||
if (!htmlPages.includes(internalRequest.action) || internalRequest.method !== "GET") {
|
||||
if (
|
||||
!htmlPages.includes(internalRequest.action) ||
|
||||
internalRequest.method !== "GET"
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "There was a problem with the server configuration. Check the server logs for more information.",
|
||||
message:
|
||||
"There was a problem with the server configuration. Check the server logs for more information.",
|
||||
code: assertionResult.name,
|
||||
}),
|
||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
||||
@@ -103,11 +108,19 @@ export async function Auth(request: Request, config: AuthConfig): Promise<Respon
|
||||
|
||||
const { pages, theme } = config
|
||||
|
||||
const authOnErrorPage = pages?.error && internalRequest.url.searchParams.get("callbackUrl")?.startsWith(pages.error)
|
||||
const authOnErrorPage =
|
||||
pages?.error &&
|
||||
internalRequest.url.searchParams
|
||||
.get("callbackUrl")
|
||||
?.startsWith(pages.error)
|
||||
|
||||
if (!pages?.error || authOnErrorPage) {
|
||||
if (authOnErrorPage) {
|
||||
logger.error(new ErrorPageLoop(`The error page ${pages?.error} should not require authentication`))
|
||||
logger.error(
|
||||
new ErrorPageLoop(
|
||||
`The error page ${pages?.error} should not require authentication`
|
||||
)
|
||||
)
|
||||
}
|
||||
const render = renderPage({ theme })
|
||||
const page = render.error({ error: "Configuration" })
|
||||
@@ -131,18 +144,6 @@ export async function Auth(request: Request, config: AuthConfig): Promise<Respon
|
||||
headers: response.headers,
|
||||
})
|
||||
}
|
||||
|
||||
if (isAuthRequest) {
|
||||
switch (request.action) {
|
||||
case "session":
|
||||
return new SessionResponse(response.body, response)
|
||||
case "providers":
|
||||
return new ProvidersResponse(response.body, response)
|
||||
default:
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,8 @@ export async function handleLogin(
|
||||
// with is already associated with another user, then we cannot link them
|
||||
// and need to return an error.
|
||||
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
|
||||
@@ -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
|
||||
// to sign in via email to verify their identity and then link the accounts.
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { handleLogin } from "../callback-handler.js"
|
||||
import { CallbackRouteError } from "../../errors.js"
|
||||
import { CallbackRouteError, Verification } from "../../errors.js"
|
||||
import { handleOAuth } from "../oauth/callback.js"
|
||||
import { createHash } from "../web.js"
|
||||
import { handleAuthorized } from "./shared.js"
|
||||
@@ -8,7 +8,6 @@ import type { AdapterSession } from "../../adapters.js"
|
||||
import type {
|
||||
RequestInternal,
|
||||
ResponseInternal,
|
||||
User,
|
||||
InternalOptions,
|
||||
} from "../../types.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 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) {
|
||||
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
|
||||
@@ -166,10 +169,10 @@ export async function callback(params: {
|
||||
token: await createHash(`${token}${secret}`),
|
||||
})
|
||||
|
||||
const invalidInvite = !invite || invite.expires.valueOf() < Date.now()
|
||||
if (invalidInvite) {
|
||||
return { redirect: `${url}/error?error=Verification`, cookies }
|
||||
}
|
||||
const hasInvite = !!invite
|
||||
const expired = invite ? invite.expires.valueOf() < Date.now() : undefined
|
||||
const invalidInvite = !hasInvite || expired
|
||||
if (invalidInvite) throw new Verification({ hasInvite, expired })
|
||||
|
||||
// @ts-expect-error -- Verified in `assertConfig`.
|
||||
const profile = await getAdapterUserFromEmail(identifier, adapter)
|
||||
@@ -252,33 +255,22 @@ export async function callback(params: {
|
||||
} else if (provider.type === "credentials" && method === "POST") {
|
||||
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
|
||||
Object.entries(query ?? {}).forEach(([k, v]) =>
|
||||
url.searchParams.set(k, v)
|
||||
)
|
||||
const user = await provider.authorize(
|
||||
credentials,
|
||||
// prettier-ignore
|
||||
Object.entries(query ?? {}).forEach(([k, v]) => url.searchParams.set(k, v))
|
||||
user = await provider.authorize(
|
||||
credentials,
|
||||
// prettier-ignore
|
||||
new Request(url, { headers, method, body: JSON.stringify(body) })
|
||||
)
|
||||
if (!user) {
|
||||
return {
|
||||
status: 401,
|
||||
redirect: `${url}/error?${new URLSearchParams({
|
||||
error: "CredentialsSignin",
|
||||
provider: provider.id,
|
||||
})}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
new Request(url, { headers, method, body: JSON.stringify(body) })
|
||||
)
|
||||
if (!user) {
|
||||
return {
|
||||
status: 401,
|
||||
redirect: `${url}/error?error=${encodeURIComponent(
|
||||
(e as Error).message
|
||||
)}`,
|
||||
redirect: `${url}/error?${new URLSearchParams({
|
||||
error: "CredentialsSignin",
|
||||
provider: provider.id,
|
||||
})}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,21 @@ const reset = "\x1b[0m"
|
||||
export const logger: LoggerInstance = {
|
||||
error(error: AuthError) {
|
||||
const url = `https://errors.authjs.dev#${error.name.toLowerCase()}`
|
||||
console.error(error.stack)
|
||||
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) {
|
||||
const url = `https://errors.authjs.dev#${code}`
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import type { AuthAction, Session } from "../types.js"
|
||||
import { PublicProvider } from "./routes/providers.js"
|
||||
|
||||
/** @internal */
|
||||
export abstract class AuthRequest extends Request {
|
||||
abstract action: AuthAction
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link Request} to add a `session()` method on the response
|
||||
* for retrieving the {@link Session} object.
|
||||
*/
|
||||
export class SessionRequest extends AuthRequest {
|
||||
action = "session" as const
|
||||
constructor(req: Request) {
|
||||
super(req.url, req)
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionResponse extends Response {
|
||||
action = "session" as const
|
||||
|
||||
/**
|
||||
* Returns the {@link Session} object from the response, or `null`
|
||||
* if the session is unavailable (config error, not authenticated, etc.).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* export default async function handle(req: Request) {
|
||||
* const response = await Auth(new SessionRequest(req), authConfig)
|
||||
* const session = await response.session()
|
||||
*
|
||||
* if (!session) {
|
||||
* return new Response("Not authenticated", { status: 401 })
|
||||
* }
|
||||
*
|
||||
* console.log(session.user) // Do something with the session
|
||||
* return response // or return whatever you want.
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async session(): Promise<Session | null> {
|
||||
try {
|
||||
const data = await this.clone().json()
|
||||
if (!this.ok || !data || !Object.keys(data).length) {
|
||||
return null
|
||||
}
|
||||
return data
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the standard {@link Request} to add a `providers()` method on the response
|
||||
* for retrieving a list of client-safe provider configuration. Useful for
|
||||
* rendering a list of sign-in options.
|
||||
*/
|
||||
export class ProvidersRequest extends AuthRequest {
|
||||
action = "providers" as const
|
||||
constructor(req: Request) {
|
||||
super(req.url, req)
|
||||
}
|
||||
}
|
||||
|
||||
export class ProvidersResponse extends Response {
|
||||
action = "providers" as const
|
||||
|
||||
/**
|
||||
* Returns the list of providers from the response, or `null`
|
||||
* if the providers are unavailable (config error, etc.).
|
||||
* @example
|
||||
* ```ts
|
||||
* export default async function handle(req: Request) {
|
||||
* const response = await Auth(new ProvidersRequest(req), authConfig)
|
||||
* const providers = await response.providers()
|
||||
* if (!providers) {
|
||||
* return new Response("Providers unavailable", { status: 500 })
|
||||
*
|
||||
*
|
||||
* console.log(providers) // Do something with the providers
|
||||
* return response // or return whatever you want.
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async providers(): Promise<PublicProvider[]> {
|
||||
try {
|
||||
if (!this.ok) return []
|
||||
return Object.values(await this.clone().json())
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { parse as parseCookie, serialize } from "cookie"
|
||||
import { AuthError, UnknownAction } from "../errors.js"
|
||||
|
||||
import type { AuthAction, RequestInternal, ResponseInternal } from "../types.js"
|
||||
import { ProvidersRequest, SessionRequest } from "./web-extension.js"
|
||||
|
||||
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
|
||||
if (!("body" in req) || !req.body || req.method !== "POST") return
|
||||
@@ -35,11 +34,8 @@ export async function toInternalRequest(
|
||||
// see init.ts
|
||||
const url = new URL(req.url.replace(/\/$/, ""))
|
||||
const { pathname } = url
|
||||
let action: AuthAction | undefined
|
||||
if (req instanceof SessionRequest || req instanceof ProvidersRequest) {
|
||||
action = req.action
|
||||
} else action = actions.find((a) => pathname.includes(a))
|
||||
|
||||
const action = actions.find((a) => pathname.includes(a))
|
||||
if (!action) {
|
||||
throw new UnknownAction("Cannot detect action.")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* ## Usage
|
||||
*
|
||||
* ```ts title="src/hooks.server.ts"
|
||||
* import SvelteKitAuth from "@auth/sveltekit"
|
||||
* import { SvelteKitAuth } from "@auth/sveltekit"
|
||||
* import GitHub from "@auth/core/providers/github"
|
||||
* import { GITHUB_ID, GITHUB_SECRET } from "$env/static/private"
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user