feat(core): improved logging / renames / new exports (#6085)

- Cleans up logging. Logs are now color-coded, added more debug logs, and errors can include some simple metadata (like provider id) to know which provider caused an issue.
- All errors are exposed via `@auth/core/errors`. Each error has a URL like: https://errors.authjs.dev#errorcode in the terminal, which points to the documentation explaining the problem in detail, suggesting a fix.
- Added a bunch of documentation that autogenerates the pages under https://authjs.dev/reference/core/modules/main
- Renames `AuthHandler`  to `Auth` and `AuthOptions` to `AuthConfig`
- Throwing an error in `signIn` callback will now be caught as a general error and will redirect to `/error?error=Configuration`. If the callback returns `false`, it will redirect to `/error?error=AccessDenied`.
This commit is contained in:
Balázs Orbán
2022-12-22 03:36:54 +01:00
committed by GitHub
parent 2ba5314e35
commit 6c45abf383
62 changed files with 2225 additions and 2241 deletions

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2018-2021, Iain Collins
Copyright (c) 2022-2023, Balázs Orbán
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,4 +1,4 @@
import { AuthHandler, type AuthOptions } from "@auth/core"
import { Auth, type AuthConfig } from "@auth/core"
// Providers
import Apple from "@auth/core/providers/apple"
@@ -66,7 +66,7 @@ import WorkOS from "@auth/core/providers/workos"
// secret: process.env.SUPABASE_SERVICE_ROLE_KEY,
// })
export const authOptions: AuthOptions = {
export const authConfig: AuthConfig = {
// adapter,
// debug: process.env.NODE_ENV !== "production",
theme: {
@@ -118,9 +118,10 @@ export const authOptions: AuthOptions = {
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
],
// debug: process.env.NODE_ENV !== "production",
}
if (authOptions.adapter) {
if (authConfig.adapter) {
// TODO:
// authOptions.providers.unshift(
// // NOTE: You can start a fake e-mail server with `pnpm email`
@@ -130,25 +131,21 @@ if (authOptions.adapter) {
}
// TODO: move to next-auth/edge
function Auth(...args: any[]) {
function AuthHandler(...args: any[]) {
const envSecret = process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET
const envTrustHost = !!(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL ?? process.env.NODE_ENV !== "production")
if (args.length === 1) {
return async (req: Request) => {
args[0].secret ??= envSecret
args[0].trustHost ??= envTrustHost
return await AuthHandler(req, args[0])
return Auth(req, args[0])
}
}
args[1].secret ??= envSecret
args[1].trustHost ??= envTrustHost
return AuthHandler(args[0], args[1])
return Auth(args[0], args[1])
}
// export default Auth(authOptions)
export default function handle(request: Request) {
return Auth(request, authOptions)
}
export default AuthHandler(authConfig)
export const config = { runtime: "experimental-edge" }

View File

@@ -1,173 +0,0 @@
---
id: errors
title: Errors
---
This is a list of errors output from Auth.js.
All errors indicate an unexpected problem, you should not expect to see errors.
If you are seeing any of these errors in the console, something is wrong.
---
## Client
These errors are returned from the client. As the client is [Universal JavaScript (or "Isomorphic JavaScript")](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) it can be run on the client or server, so these errors can occur both in the terminal and in the browser console.
#### `CLIENT_SESSION_ERROR`
This error occurs when the `SessionProvider` Context has a problem fetching session data.
#### `CLIENT_FETCH_ERROR`
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` environment variable.
---
## Server
These errors are displayed on the terminal.
### OAuth
#### `OAUTH_GET_ACCESS_TOKEN_ERROR`
This occurs when there was an error in the POST request to the OAuth provider and we were not able to retrieve the access token.
Please double check your provider settings.
#### `OAUTH_V1_GET_ACCESS_TOKEN_ERROR`
This error is explicitly related to older OAuth v1.x providers, if you are using one of these, please double check all available settings.
#### `OAUTH_GET_PROFILE_ERROR`
N/A
#### `OAUTH_PARSE_PROFILE_ERROR`
This error is a result of either a problem with the provider response or the user canceling the action with the provider, unfortunately, we can't discern which with the information we have.
This error should also log the exception and available `profileData` to further aid debugging.
#### `OAUTH_CALLBACK_HANDLER_ERROR`
This error will occur when there was an issue parsing the JSON request body, for example.
There should also be further details logged when this occurs, such as the error is thrown, and the request body itself to aid in debugging.
---
### Signin / Callback
#### `GET_AUTHORIZATION_URL_ERROR`
This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL.
Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### `SIGNIN_OAUTH_ERROR`
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 settings and make sure your URLs and other options are correctly set on the provider side.
#### `CALLBACK_OAUTH_ERROR`
This can occur during the handling of the callback if the `code_verifier` cookie was not found or an invalid state was returned from the OAuth provider.
#### `SIGNIN_EMAIL_ERROR`
This error can occur when a user tries to sign in via an email link; for example, if the email token could not be generated or the verification request failed.
Please double check your email settings.
#### `CALLBACK_EMAIL_ERROR`
This can occur during the email callback process. Specifically, if there was an error signing the user in via email, encoding the jwt, etc.
Please double check your Email settings.
#### `EMAIL_REQUIRES_ADAPTER_ERROR`
The Email authentication provider can only be used if a database is configured.
This is required to store the verification token. Please see the [Email provider tutorial](/getting-started/email-tutorial) for more details.
#### `CALLBACK_CREDENTIALS_JWT_ERROR`
The Credentials Provider can only be used if JSON Web Tokens are used for sessions.
JSON Web Tokens are used for Sessions by default if you have not specified a database. However, if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](/reference/configuration/auth-config#session) to use the Credentials Provider.
If you are using a Credentials Provider, Auth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and managed outside of Auth.js.
In _most cases_ it does not make sense to specify a database in Auth.js options and support a Credentials Provider.
#### `CALLBACK_CREDENTIALS_HANDLER_ERROR`
This error occurs when there was no `authorize()` handler defined on the credential authentication provider.
#### `PKCE_ERROR`
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4).
The `code_verifier` is saved in a cookie called (by default) `__Secure-next-auth.pkce.code_verifier` which expires after 15 minutes.
Check if `cookies.pkceCodeVerifier` is configured correctly.
The default `code_challenge_method` is `"S256"`. This is currently not configurable to `"plain"`, [as per RFC7636](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2):
> If the client is capable of using "S256", it MUST use "S256", as
> S256" is Mandatory To Implement (MTI) on the server.
#### `INVALID_CALLBACK_URL_ERROR`
The `callbackUrl` provided was either invalid or not defined. See [specifying a `callbackUrl`](/reference/utilities/#specifying-a-callbackurl) for more information.
---
### Session Handling
#### `JWT_SESSION_ERROR`
JWKKeySupport: the key does not support HS512 verify algorithm
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
```
jose newkey -s 512 -t oct -a HS512
```
#### `SESSION_ERROR`
---
### Signout
#### `SIGNOUT_ERROR`
This error occurs when there was an issue deleting the session from the database, for example.
---
### 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`.
Make sure the file is there and the filename is written correctly.
#### `NO_SECRET`
In production, we expect you to define a `secret` property in your configuration. In development, this is shown as a warning for convenience. [Read more](/reference/configuration/auth-config#secret)
#### `oauth_callback_error expected 200 OK with body but no body was returned`
This error might happen with some of the providers. It happens due to `openid-client`(which is peer dependency) node version mismatch. For instance, `openid-client` requires `>=14.2.0` for `lts/fermium` and has similar limits for the other versions. For the full list of the compatible node versions please see [package.json](https://github.com/panva/node-openid-client/blob/2a84e46992e1ebeaf685c3f87b65663d126e81aa/package.json#L78)

View File

@@ -202,7 +202,13 @@ module.exports = {
{
...typedocConfig,
plugin: ["./tyepdoc"],
entryPoints: ["index.ts", "adapters.ts", "jwt.ts", "lib/types.ts"]
entryPoints: [
"index.ts",
"adapters.ts",
"errors.ts",
"jwt.ts",
"types.ts",
]
.map((e) => `${coreSrc}/${e}`)
.concat(providers),
tsconfig: "../packages/core/tsconfig.json",

View File

@@ -83,7 +83,6 @@ module.exports = {
},
"reference/utilities/client",
"reference/warnings",
"reference/errors",
],
concepts: [
{

View File

@@ -285,4 +285,13 @@ html[data-theme="dark"] #carbonads .carbon-poweredby {
*/
.reflection-category, .theme-doc-sidebar-item-link-level-2 [href="/reference/core/modules/main"] {
display: none;
}
/*
HACK: to hide the "Classes" header and duplicate items together with the "typedoc-plugin-markdown" patch.
See: https://github.com/TypeStrong/typedoc/issues/2006
*/
#classes, h3.anchor + p:has(code, strong) {
display: none;
}

View File

@@ -78,7 +78,7 @@
"value": "errors.authjs.dev"
}
],
"destination": "https://authjs.dev/reference/errors/:path*"
"destination": "https://authjs.dev/reference/core/modules/errors/:path*"
},
{
"source": "/:path(.*)",

View File

@@ -66,6 +66,9 @@
"pnpm": {
"overrides": {
"undici": "5.11.0"
},
"patchedDependencies": {
"typedoc-plugin-markdown@3.14.0": "patches/typedoc-plugin-markdown@3.14.0.patch"
}
}
}

View File

@@ -15,10 +15,8 @@
"type": "module",
"types": "./index.d.ts",
"files": [
"adapters.*",
"index.*",
"jwt.*",
"types.*",
"*.js",
"*.d.ts",
"lib",
"providers",
"src"
@@ -31,6 +29,10 @@
"./adapters": {
"types": "./adapters.d.ts"
},
"./errors": {
"import": "./errors.js",
"types": "./errors.d.ts"
},
"./jwt": {
"types": "./jwt.d.ts",
"import": "./jwt.js"
@@ -65,9 +67,9 @@
},
"scripts": {
"build": "pnpm clean && pnpm css && tsc",
"clean": "rm -rf adapters.* index.* jwt.* types.* lib providers",
"clean": "rm -rf *.js *.d.ts lib providers",
"css": "node ./scripts/generate-css.js",
"lint": "eslint src",
"lint": "pnpm prettier --check src && eslint src",
"format": "pnpm lint --fix",
"dev": "pnpm css && tsc -w"
},

View File

@@ -6,7 +6,7 @@ import autoprefixer from "autoprefixer"
import postCssNested from "postcss-nested"
import cssNano from "cssnano"
const from = path.join(process.cwd(), "src/lib/styles/index.css")
const from = path.join(process.cwd(), "src/lib/pages/styles.css")
const css = fs.readFileSync(from)
const processedCss = await postcss([
@@ -16,7 +16,7 @@ const processedCss = await postcss([
]).process(css, { from })
fs.writeFileSync(
path.join(process.cwd(), "src/lib/styles/index.ts"),
path.join(process.cwd(), "src/lib/pages/styles.ts"),
`export default \`${processedCss.css}\`
// Generated by \`pnpm css\``
)

View File

@@ -1,8 +1,9 @@
/**
* The `@auth/core/adapters` module contains useful helpers that a database adapter
* can incorporate in order to be compatible with Auth.js.
* You can think of an adapter as a way to normalize database implementation details to a common interface
* that Auth.js can use to interact with the database.
* This module contains functions and types that a database adapter
* can use to be compatible with Auth.js.
*
* A database adapter provides a common interface for Auth.js so that it can work with
* _any_ database/ORM adapter without concerning itself with the implementation details of the database/ORM.
*
* Auth.js supports 2 session strtategies to persist the login state of a user.
* The default is to use a cookie + {@link https://authjs.dev/concepts/session-strategies#jwt JWT}
@@ -11,7 +12,7 @@
*
* :::info Note
* Auth.js _currently_ does **not** implement {@link https://authjs.dev/concepts/session-strategies#federated-logout federated logout}.
* So even if the session is deleted from the database, the user will still be logged in to the provider.
* So even if the session is deleted from the database, the user will still be logged in to the provider (but will be logged out of the app).
* See [this discussion](https://github.com/nextauthjs/next-auth/discussions/3938) for more information.
* :::
*
@@ -21,33 +22,52 @@
* npm install @auth/core
* ```
*
* You can then import this submodule from `@auth/core/adapters`.
*
* ## Usage
*
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interface, so you likely won't need to
* {@link https://authjs.dev/reference/adapters/overview Built-in adapters} already implement this interfac, so you likely won't need to
* implement it yourself. If you do, you can use the following example as a
* starting point.
*
* ```ts
* // src/your-adapter.ts
* ```ts title=your-adapter.ts
* import { type Adapter } from "@auth/core/adapters"
*
* export function MyAdapter(options: any): Adapter {
* export function MyAdapter(config: {}): Adapter {
* // implement the adapter methods
* }
* ```
*
* // src/index.ts
* ```ts title=index.ts
* import { MyAdapter } from "./your-adapter"
*
* const response = Auth({
* adapter: MyAdapter({ ...adapter options }),
* ... auth options
* adapter: MyAdapter({ /* ...adapter config *\/ }),
* // ... auth config
* })
* ```
*
* :::caution Note
* Although `@auth/core` is framework/runtime agnostic, an adapter might rely on a client/ORM package,
* that is not yet compatible with your runtime
* (E.g. it might rely on [Node.js-specific APIs](https://nodejs.org/docs/latest/api)) when you are trying to use it elsewhere.
* Related issues should be reported to the corresponding package maintainers.
* :::
*
* ### Testing
* :::tip
* If you are writing your own adapter, there is a test suite [available](https://github.com/nextauthjs/next-auth/tree/main/packages/adapter-test)
* to ensure that your adapter is compatible with Auth.js.
* :::
*
* ## Resources
*
* - [What is a database session strategy?](https://authjs.dev/concepts/session-strategies#database)
*
* @module adapters
*/
import type { Account, Awaitable, User } from "./lib/types"
import type { Account, Awaitable, User } from "./types.js"
// TODO: Discuss if we should expose methods to serialize and deserialize
// the data? Many adapters share this logic, so it could be useful to
@@ -64,7 +84,7 @@ export interface AdapterAccount extends Account {
}
/**
* The session object implementing this interface is
* The session object implementing this interface
* is used to look up the user in the database.
*/
export interface AdapterSession {
@@ -93,87 +113,66 @@ export interface VerificationToken {
}
/**
* Using a custom adapter you can connect to any database backend or even
* several different databases. Custom adapters created and maintained by our
* community can be found in the adapters repository. Feel free to add a custom
* adapter from your project to the repository, or even become a maintainer of a
* certain adapter. Custom adapters can still be created and used in a project
* without being added to the repository.
* Using a custom adapter you can connect to any database backend or even several different databases.
* Custom adapters created and maintained by our community can be found in the adapters repository.
* Feel free to add a custom adapter from your project to the repository,
* or even become a maintainer of a certain adapter.
* Custom adapters can still be created and used in a project without being added to the repository.
*
* ## Useful resources
* ## Resources
*
* @see [Session strategies](https://authjs.dev/concepts/session-strategies#database)
* @see [Using a database adapter](https://authjs.dev/guides/adapters/using-a-database-adapter)
* @see [Creating a database adapter](https://authjs.dev/guides/adapters/creating-a-database-adapter)
* - [Session strategies](https://authjs.dev/concepts/session-strategies#database)
* - [Using a database adapter](https://authjs.dev/guides/adapters/using-a-database-adapter)
* - [Creating a database adapter](https://authjs.dev/guides/adapters/creating-a-database-adapter)
*/
export type Adapter<WithVerificationToken = boolean> = DefaultAdapter &
(WithVerificationToken extends true
? {
createVerificationToken: (
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database and delete it so it
* cannot be used again.
*/
useVerificationToken: (params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}
: {})
export interface DefaultAdapter {
createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
getUser: (id: string) => Awaitable<AdapterUser | null>
getUserByEmail: (email: string) => Awaitable<AdapterUser | null>
/**
* Using the provider id and the id of the user for a specific account, get
* the user.
*/
getUserByAccount: (
export interface Adapter {
createUser(user: Omit<AdapterUser, "id">): Awaitable<AdapterUser>
getUser(id: string): Awaitable<AdapterUser | null>
getUserByEmail(email: string): Awaitable<AdapterUser | null>
/** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount(
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
/** @todo Implement */
deleteUser?: (
): Awaitable<AdapterUser | null>
updateUser(user: Partial<AdapterUser>): Awaitable<AdapterUser>
/** @todo This method is currently not implemented. Defining it will have no effect */
deleteUser?(
userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: (
): Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount(
account: AdapterAccount
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
/** @todo Implement */
unlinkAccount?: (
): Promise<void> | Awaitable<AdapterAccount | null | undefined>
/** @todo This method is currently not implemented. Defining it will have no effect */
unlinkAccount?(
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<AdapterAccount | undefined>
): Promise<void> | Awaitable<AdapterAccount | undefined>
/** Creates a session for the user and returns it. */
createSession: (session: {
createSession(session: {
sessionToken: string
userId: string
expires: Date
}) => Awaitable<AdapterSession>
getSessionAndUser: (
}): Awaitable<AdapterSession>
getSessionAndUser(
sessionToken: string
) => Awaitable<{ session: AdapterSession; user: AdapterUser } | null>
updateSession: (
): Awaitable<{ session: AdapterSession; user: AdapterUser } | null>
updateSession(
session: Partial<AdapterSession> & Pick<AdapterSession, "sessionToken">
) => Awaitable<AdapterSession | null | undefined>
): Awaitable<AdapterSession | null | undefined>
/**
* Deletes a session from the database. It is preferred that this method also
* returns the session that is being deleted for logging purposes.
*/
deleteSession: (
deleteSession(
sessionToken: string
) => Promise<void> | Awaitable<AdapterSession | null | undefined>
createVerificationToken?: (
): Promise<void> | Awaitable<AdapterSession | null | undefined>
createVerificationToken?(
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
): Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database and delete it so it cannot be
* used again.
*/
useVerificationToken?: (params: {
useVerificationToken?(params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}): Awaitable<VerificationToken | null>
}

View File

@@ -0,0 +1,95 @@
/** @internal */
export class AuthError extends Error {
metadata?: Record<string, unknown>
constructor(message: Error | string, metadata?: Record<string, unknown>) {
if (message instanceof Error) {
super(message.message)
this.stack = message.stack
} else super(message)
this.name = this.constructor.name
this.metadata = metadata
Error.captureStackTrace?.(this, this.constructor)
}
}
/**
* @todo
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinked extends AuthError {}
/**
* @todo
* One of the database `Adapter` methods failed.
*/
export class AdapterError extends AuthError {}
/** @todo */
export class AuthorizedCallbackError extends AuthError {}
/** @todo */
export class CallbackRouteError extends AuthError {}
/** @todo */
export class ErrorPageLoop extends AuthError {}
/** @todo */
export class EventError extends AuthError {}
/** @todo */
export class InvalidCallbackUrl extends AuthError {}
/** @todo */
export class InvalidEndpoints extends AuthError {}
/** @todo */
export class InvalidState extends AuthError {}
/** @todo */
export class JWTSessionError extends AuthError {}
/** @todo */
export class MissingAdapter extends AuthError {}
/** @todo */
export class MissingAdapterMethods extends AuthError {}
/** @todo */
export class MissingAPIRoute extends AuthError {}
/** @todo */
export class MissingAuthorize extends AuthError {}
/** @todo */
export class MissingSecret extends AuthError {}
/** @todo */
export class OAuthSignInError extends AuthError {}
/** @todo */
export class OAuthCallbackError extends AuthError {}
/** @todo */
export class OAuthCreateUserError extends AuthError {}
/** @todo */
export class OAuthProfileParseError extends AuthError {}
/** @todo */
export class SessionTokenError extends AuthError {}
/** @todo */
export class SignInError extends AuthError {}
/** @todo */
export class SignOutError extends AuthError {}
/** @todo */
export class UnknownAction extends AuthError {}
/** @todo */
export class UnsupportedStrategy extends AuthError {}
/** @todo */
export class UntrustedHost extends AuthError {}

View File

@@ -1,9 +1,19 @@
/**
*
* `@auth/core` is the main entry point for the Auth.js library.
* This is the main entry point to the Auth.js library.
*
* Based on the {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Request}
* and {@link https://developer.mozilla.org/en-US/docs/Web/API/Response Response} Web standard APIs.
* Primarily used to implement [framework](https://authjs.dev/concepts/frameworks)-specific packages,
* but it can also be used directly.
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install @auth/core
* ```
*
* ## Usage
*
* ```ts
* import { Auth } from "@auth/core"
@@ -14,293 +24,113 @@
* console.log(response instanceof Response) // true
* ```
*
* Primarily used to implement [framework](https://authjs.dev/concepts/frameworks)-specific packages,
* but it can also be used directly.
* ## Resources
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install @auth/core
* ```
* - [Gettint started](https://authjs.dev/getting-started/introduction)
* - [Most common use case guides](https://authjs.dev/guides/overview)
*
* @module main
*/
import { init } from "./lib/init.js"
import { assertConfig } from "./lib/assert.js"
import { SessionStore } from "./lib/cookie.js"
import { toInternalRequest, toResponse } from "./lib/web.js"
import { ErrorPageLoop } from "./errors.js"
import { AuthInternal } from "./lib/index.js"
import renderPage from "./lib/pages/index.js"
import * as routes from "./lib/routes/index.js"
import logger, { setLogger } from "./lib/utils/logger.js"
import { logger, setLogger, type LoggerInstance } from "./lib/utils/logger.js"
import { toInternalRequest, toResponse } from "./lib/web.js"
import type { ErrorType } from "./lib/pages/error.js"
import type { Adapter } from "./adapters.js"
import type {
AuthOptions,
RequestInternal,
ResponseInternal,
} from "./lib/types.js"
import { UntrustedHost } from "./lib/errors.js"
// Only thing exported from this file should be `AuthHandler` and `AuthOptions`
// TODO Don't re-export, just add `@auth/core/types` exports in package.json and change references these types
export * from "./lib/types.js"
const configErrorMessage =
"There is a problem with the server configuration. Check the server logs for more information."
async function AuthHandlerInternal<
Body extends string | Record<string, any> | any[]
>(params: {
req: RequestInternal
options: AuthOptions
/** REVIEW: Is this the best way to skip parsing the body in Node.js? */
parsedBody?: any
}): Promise<ResponseInternal<Body>> {
const { options: authOptions, req } = params
const assertionResult = assertConfig({ options: authOptions, req })
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
logger.error((assertionResult as any).code, assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
return {
status: 500,
headers: { "Content-Type": "application/json" },
body: { message: configErrorMessage } as any,
}
}
const { pages, theme } = authOptions
const authOnErrorPage =
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {
logger.error(
"AUTH_ON_ERROR_PAGE_ERROR",
new Error(
`The error page ${pages?.error} should not require authentication`
)
)
}
const render = renderPage({ theme })
return render.error({ error: "configuration" })
}
return {
redirect: `${pages.error}?error=Configuration`,
}
}
const { action, providerId, error, method } = req
const { options, cookies } = await init({
authOptions,
action,
providerId,
url: req.url,
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
csrfToken: req.body?.csrfToken,
cookies: req.cookies,
isPost: method === "POST",
})
const sessionStore = new SessionStore(
options.cookies.sessionToken,
req,
options.logger
)
if (method === "GET") {
const render = renderPage({ ...options, query: req.query, cookies })
const { pages } = options
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session({ options, sessionStore })
if (session.cookies) cookies.push(...session.cookies)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
}
case "csrf":
return {
headers: { "Content-Type": "application/json" },
body: { csrfToken: options.csrfToken } as any,
cookies,
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
if (error)
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
return { redirect: signinUrl, cookies }
}
return render.signin()
case "signout":
if (pages.signOut) return { redirect: pages.signOut, cookies }
return render.signout()
case "callback":
if (options.provider) {
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "verify-request":
if (pages.verifyRequest) {
return { redirect: pages.verifyRequest, cookies }
}
return render.verifyRequest()
case "error":
// These error messages are displayed in line on the sign in page
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
return render.error({ error: error as ErrorType })
default:
}
} else if (method === "POST") {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin({
query: req.query,
body: req.body,
options,
})
if (signin.cookies) cookies.push(...signin.cookies)
return { ...signin, cookies }
}
return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout({ options, sessionStore })
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
}
return { redirect: `${options.url}/signout?csrf=true`, cookies }
case "callback":
if (options.provider) {
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
}
const callback = await routes.callback({
body: req.body,
query: req.query,
headers: req.headers,
cookies: req.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "_log":
if (authOptions.logger) {
try {
const { code, level, ...metadata } = req.body ?? {}
logger[level](code, metadata)
} catch (error) {
// If logging itself failed...
logger.error("LOGGER_ERROR", error as Error)
}
}
return {}
default:
}
}
return {
status: 400,
body: `Error: This action with HTTP ${method} is not supported by NextAuth.js` as any,
}
}
CallbacksOptions,
CookiesOptions,
EventCallbacks,
PagesOptions,
SessionOptions,
Theme,
} from "./types.js"
import type { Provider } from "./providers/index.js"
import { JWTOptions } from "./jwt.js"
/**
* The core functionality of Auth.js. It receives a standard
* [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and
* returns a standard
* [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
* Core functionality provided by Auth.js.
*
* Receives a standard {@link Request} and returns a {@link Response}.
*
* @example
* ```ts
* import Auth from "@auth/core"
*
* const request = new Request("https://example.com")
* const resposne = await AuthHandler(request, {
* providers: [...],
* secret: "...",
* trustHost: true,
* })
*```
* @see [Documentation](https://authjs.dev)
*/
export async function AuthHandler(
export async function Auth(
request: Request,
options: AuthOptions
config: AuthConfig
): Promise<Response> {
setLogger(options.logger, options.debug)
setLogger(config.logger, config.debug)
if (!options.trustHost) {
const error = new UntrustedHost(
`Host must be trusted. URL was: ${request.url}`
)
logger.error(error.code, error)
return new Response(JSON.stringify({ message: configErrorMessage }), {
status: 500,
headers: { "Content-Type": "application/json" },
})
}
const req = await toInternalRequest(request)
if (req instanceof Error) {
logger.error((req as any).code, req)
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 }
)
}
const internalResponse = await AuthHandlerInternal({ req, options })
const assertionResult = assertConfig(internalRequest, config)
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
logger.error(assertionResult)
const htmlPages = ["signin", "signout", "error", "verify-request"]
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.",
code: assertionResult.name,
}),
{ status: 500, headers: { "Content-Type": "application/json" } }
)
}
const { pages, theme } = config
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`
)
)
}
const render = renderPage({ theme })
const page = render.error({ error: "Configuration" })
return toResponse(page)
}
return Response.redirect(`${pages.error}?error=Configuration`)
}
const internalResponse = await AuthInternal(internalRequest, config)
const response = await toResponse(internalResponse)
@@ -316,3 +146,210 @@ export async function AuthHandler(
}
return response
}
/**
* Configure the {@link Auth} method.
*
* @example
* ```ts
* import Auth, { type AuthConfig } from "@auth/core"
*
* export const authConfig: AuthConfig = {...}
*
* const request = new Request("https://example.com")
* const resposne = await AuthHandler(request, authConfig)
*
* ```
*
* @see [Initiailzation](https://authjs.dev/reference/configuration/auth-options)
*/
export interface AuthConfig {
/**
* List of authentication providers for signing in
* (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order.
* This can be one of the built-in providers or an object with a custom provider.
* * **Default value**: `[]`
* * **Required**: *Yes*
*
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: Provider[]
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified, it falls back to `AUTH_SECRET` or `NEXTAUTH_SECRET` from environment variables.
* To generate a random string, you can use the following command:
*
* On Unix systems: `openssl rand -hex 32`
* Or go to https://generate-secret.vercel.app/32
*
* @default process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET
*
* [Documentation](https://next-auth.js.org/configuration/options#secret)
*/
secret?: string
/**
* Configure your session like if you want to use JWT or a database,
* how long until an idle session expires, or to throttle write operations in case you are using a database.
* * **Default value**: See the documentation page
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#session)
*/
session?: Partial<SessionOptions>
/**
* JSON Web Tokens are enabled by default if you have not specified an adapter.
* JSON Web Tokens are encrypted (JWE) by default. We recommend you keep this behaviour.
* * **Default value**: See the documentation page
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#jwt)
*/
jwt?: Partial<JWTOptions>
/**
* Specify URLs to be used if you want to create custom sign in, sign out and error pages.
* Pages specified will override the corresponding built-in page.
* * **Default value**: `{}`
* * **Required**: *No*
* @example
*
* ```ts
* pages: {
* signIn: '/auth/signin',
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: '/auth/new-user'
* }
* ```
*
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: Partial<PagesOptions>
/**
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
* Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens
* as they **allow you to implement access controls without a database** and to **integrate with external databases or APIs**.
* * **Default value**: See the Callbacks documentation
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: Partial<CallbacksOptions>
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
* The content of the message object varies depending on the flow
* (e.g. OAuth or Email authentication flow, JWT or database sessions, etc),
* but typically contains a user object and/or contents of the JSON Web Token
* and other information relevant to the event.
* * **Default value**: `{}`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: Partial<EventCallbacks>
/**
* You can use the adapter option to pass in your database adapter.
*
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Adapters Overview](https://next-auth.js.org/adapters/overview)
*/
adapter?: Adapter
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
* * **Required**: *No*
*
* - ⚠ If you added a custom `logger`, this setting is ignored.
*
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
*/
debug?: boolean
/**
* Override any of the logger levels (`undefined` levels will use the built-in logger),
* and intercept logs in NextAuth. You can use this option to send NextAuth logs to a third-party logging service.
* * **Default value**: `console`
* * **Required**: *No*
*
* @example
*
* ```ts
* // /pages/api/auth/[...nextauth].js
* import log from "logging-service"
* export default NextAuth({
* logger: {
* error(code, ...message) {
* log.error(code, message)
* },
* warn(code, ...message) {
* log.warn(code, message)
* },
* debug(code, ...message) {
* log.debug(code, message)
* }
* }
* })
* ```
*
* - ⚠ When set, the `debug` option is ignored
*
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
*/
logger?: Partial<LoggerInstance>
/**
* Changes the theme of pages.
* Set to `"light"` if you want to force pages to always be light.
* Set to `"dark"` if you want to force pages to always be dark.
* Set to `"auto"`, (or leave this option out)if you want the pages to follow the preferred system theme.
* * **Default value**: `"auto"`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: Theme
/**
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
* You can manually set this option to `false` to disable this security feature and allow cookies
* to be accessible from non-secured URLs (this is not recommended).
* * **Default value**: `true` for HTTPS and `false` for HTTP sites
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#usesecurecookies)
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*/
useSecureCookies?: boolean
/**
* You can override the default cookie names and options for any of the cookies used by NextAuth.js.
* You can specify one or more cookies with custom properties,
* but if you specify custom options for a cookie you must provide all the options for that cookie.
* If you use this feature, you will likely want to create conditional behavior
* to support setting different cookies policies in development and production builds,
* as you will be opting out of the built-in dynamic policy.
* * **Default value**: `{}`
* * **Required**: No
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: Partial<CookiesOptions>
/**
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
* instead of `NEXTAUTH_URL`
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
* @default Boolean(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
*/
trustHost?: boolean
}

View File

@@ -1,13 +1,37 @@
/**
* `@authjs/core/jwt` provides functions
* to encode and decode {@link https://authjs.dev/concepts/session-strategies#jwt JWT}s
* issued and used by Auth.js. It is meant for being used in the app only.
* If you need JWT authentication for your API, you should rely on your Identity Provider.
*
* The JWT created by Auth.js is encrypted using the `A256GCM` algorithm ({@link https://www.rfc-editor.org/rfc/rfc7516 JWE}). by default.
*
* This module contains functions and types
* to encode and decode {@link https://authjs.dev/concepts/session-strategies#jwt JWT}s
* issued and used by Auth.js.
*
* The JWT issued by Auth.js is _encrypted by default_, using the _A256GCM_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7516 JWE}).
* It uses the `AUTH_SECRET` environment variable to dervice a sufficient encryption key.
*
* @see [RFC7519 - JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
* :::info Note
* Auth.js JWTs are meant to be used by the same app that issued them.
* If you need JWT authentication for your third-party API, you should rely on your Identity Provider instead.
* :::
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install @auth/core
* ```
*
* You can then import this submodule from `@auth/core/jwt`.
*
* ## Usage
*
* :::warning Warning
* This module *will* be refactored/changed. We do not recommend relying on it right now.
* :::
*
*
* ## Resources
*
* - [What is a JWT session strategy](https://authjs.dev/concepts/session-strategies#jwt)
* - [RFC7519 - JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
*
* @module jwt
*/
@@ -15,7 +39,7 @@
import { hkdf } from "@panva/hkdf"
import { EncryptJWT, jwtDecrypt } from "jose"
import { SessionStore } from "./lib/cookie.js"
import { Awaitable } from "./lib/types.js"
import { Awaitable } from "./types.js"
import type { LoggerInstance } from "./lib/utils/logger.js"
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days

View File

@@ -1,27 +1,25 @@
import { defaultCookies } from "./cookie.js"
import {
InvalidCallbackUrl,
InvalidEndpoints,
MissingAdapter,
MissingAdapterMethods,
MissingAPIRoute,
MissingAuthorize,
MissingSecret,
UnsupportedStrategy,
} from "./errors.js"
import { defaultCookies } from "./cookie.js"
UntrustedHost,
} from "../errors.js"
import type { AuthOptions, RequestInternal } from "../index.js"
import type { AuthConfig, RequestInternal } from "../types.js"
import type { WarningCode } from "./utils/logger.js"
type ConfigError =
| InvalidCallbackUrl
| InvalidEndpoints
| MissingAdapter
| MissingAdapterMethods
| MissingAPIRoute
| MissingAuthorize
| MissingSecret
| InvalidCallbackUrl
| UnsupportedStrategy
| InvalidEndpoints
| UnsupportedStrategy
let warned = false
@@ -39,34 +37,25 @@ function isValidHttpUrl(url: string, baseUrl: string) {
/**
* Verify that the user configured Auth.js correctly.
* Good place to mention deprecations as well.
*
* REVIEW: Make some of these and corresponding docs less Next.js specific?
*/
export function assertConfig(params: {
options: AuthOptions
req: RequestInternal
}): ConfigError | WarningCode[] {
const { options, req } = params
const { url } = req
export function assertConfig(
request: RequestInternal,
options: AuthConfig
): ConfigError | WarningCode[] {
const { url } = request
const warnings: WarningCode[] = []
if (!warned) {
if (!url.origin) warnings.push("NEXTAUTH_URL")
if (options.debug) warnings.push("DEBUG_ENABLED")
if (!warned && options.debug) warnings.push("debug_enabled")
if (!options.trustHost) {
return new UntrustedHost(`Host must be trusted. URL was: ${request.url}`)
}
if (!options.secret) {
return new MissingSecret("Please define a `secret`.")
}
// req.query isn't defined when asserting `unstable_getServerSession` for example
if (!req.query?.nextauth && !req.action) {
return new MissingAPIRoute(
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
)
}
const callbackUrlParam = req.query?.callbackUrl as string | undefined
const callbackUrlParam = request.query?.callbackUrl as string | undefined
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, url.origin)) {
return new InvalidCallbackUrl(
@@ -78,7 +67,9 @@ export function assertConfig(params: {
options.useSecureCookies ?? url.protocol === "https://"
)
const callbackUrlCookie =
req.cookies?.[options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
request.cookies?.[
options.cookies?.callbackUrl?.name ?? defaultCallbackUrl.name
]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, url.origin)) {
return new InvalidCallbackUrl(

View File

@@ -1,8 +1,8 @@
import { AccountNotLinkedError } from "./errors.js"
import { AccountNotLinked } from "../errors.js"
import { fromDate } from "./utils/date.js"
import type { Account, InternalOptions, User } from "../index.js"
import type { AdapterSession, AdapterUser } from "../adapters.js"
import type { Account, InternalOptions, User } from "../types.js"
import type { JWT } from "../jwt.js"
import type { OAuthConfig } from "../providers/index.js"
import type { SessionToken } from "./cookie.js"
@@ -19,13 +19,12 @@ import type { SessionToken } from "./cookie.js"
* done prior to this handler being called to avoid additonal complexity in this
* handler.
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User | AdapterUser | { email: string }
account: Account | null
export async function handleLogin(
sessionToken: SessionToken,
_profile: User | AdapterUser | { email: string },
account: Account | null,
options: InternalOptions
}) {
const { sessionToken, profile: _profile, account, options } = params
) {
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
@@ -133,7 +132,7 @@ export default async function callbackHandler(params: {
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another user, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError(
throw new AccountNotLinked(
"The account is already associated with another user"
)
}
@@ -193,7 +192,7 @@ export default async function callbackHandler(params: {
// We don't want to have two accounts with the same email address, and we don't
// 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 AccountNotLinkedError(
throw new AccountNotLinked(
"Another account already exists with the same e-mail address"
)
}

View File

@@ -1,4 +1,4 @@
import type { InternalOptions } from "../index.js"
import type { InternalOptions } from "../types.js"
interface CreateCallbackUrlParams {
options: InternalOptions

View File

@@ -3,7 +3,7 @@ import type {
CookiesOptions,
LoggerInstance,
SessionStrategy,
} from "../index.js"
} from "../types.js"
// Uncomment to recalculate the estimated size
// of an empty session cookie
@@ -160,6 +160,10 @@ export class SessionStore {
}
}
/**
* The JWT Session or database Session ID
* constructed from the cookie chunks.
*/
get value() {
return Object.values(this.#chunks)?.join("")
}

View File

@@ -1,6 +1,6 @@
import { createHash, randomString } from "./web.js"
import type { InternalOptions } from "./types.js"
import type { InternalOptions } from "../types.js"
interface CreateCSRFTokenParams {
options: InternalOptions
cookieValue?: string

View File

@@ -1,4 +1,4 @@
import type { CallbacksOptions } from "../index.js"
import type { CallbacksOptions } from "../types.js"
export const defaultCallbacks: CallbacksOptions = {
signIn() {

View File

@@ -1,20 +0,0 @@
import type { AdapterUser } from "../../adapters.js"
import type { InternalOptions } from "../../index.js"
/**
* Query the database for a user by email address.
* If is an existing user return a user object (otherwise use placeholder).
*/
export default async function getAdapterUserFromEmail({
email,
adapter,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
}): Promise<AdapterUser> {
const { getUserByEmail } = adapter
const adapterUser = email ? await getUserByEmail(email) : null
if (adapterUser) return adapterUser
return { id: email, email, emailVerified: null }
}

View File

@@ -1,6 +1,6 @@
import { randomString, createHash } from "../web.js"
import type { InternalOptions } from "../../index.js"
import { createHash, randomString } from "../web.js"
import type { InternalOptions } from "../../types.js"
/**
* Starts an e-mail login flow, by generating a token,
* and sending it to the user's e-mail (with the help of a DB adapter)
@@ -10,7 +10,6 @@ export default async function email(
options: InternalOptions<"email">
): Promise<string> {
const { url, adapter, provider, callbackUrl, theme } = options
// Generate token
const token =
(await provider.generateVerificationToken?.()) ?? randomString(32)
@@ -25,7 +24,6 @@ export default async function email(
const secret = provider.secret ?? options.secret
await Promise.all([
// Send to user
provider.sendVerificationRequest({
identifier,
token,
@@ -34,8 +32,8 @@ export default async function email(
provider,
theme,
}),
// Save in database
adapter.createVerificationToken({
// @ts-expect-error -- Verified in `assertConfig`.
adapter.createVerificationToken?.({
identifier,
token: await createHash(`${token}${secret}`),
expires,

View File

@@ -1,141 +0,0 @@
import type { EventCallbacks, LoggerInstance } from "./types.js"
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
code: string
constructor(error: Error | string) {
// Support passing error or string
super((error as Error)?.message ?? error)
this.name = "UnknownError"
this.code = (error as any).code
if (error instanceof Error) {
this.stack = error.stack
}
}
toJSON() {
return {
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
export class OAuthCallbackError extends UnknownError {
name = "OAuthCallbackError"
}
/**
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class MissingAPIRoute extends UnknownError {
name = "MissingAPIRouteError"
code = "MISSING_NEXTAUTH_API_ROUTE_ERROR"
}
export class MissingSecret extends UnknownError {
name = "MissingSecretError"
code = "NO_SECRET"
}
export class MissingAuthorize extends UnknownError {
name = "MissingAuthorizeError"
code = "CALLBACK_CREDENTIALS_HANDLER_ERROR"
}
export class MissingAdapter extends UnknownError {
name = "MissingAdapterError"
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
}
export class InvalidCallbackUrl extends UnknownError {
name = "InvalidCallbackUrlError"
code = "INVALID_CALLBACK_URL_ERROR"
}
export class InvalidEndpoints extends UnknownError {
name = "InvalidEndpoints"
code = "INVALID_ENDPOINTS_ERROR"
}
export class UnknownAction extends UnknownError {
name = "UnknownAction"
code = "UNKNOWN_ACTION_ERROR"
}
export class UntrustedHost extends UnknownError {
name = "UntrustedHost"
code = "UNTRUST_HOST_ERROR"
}
type Method = (...args: any[]) => Promise<any>
export function upperSnake(s: string) {
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
}
export function capitalize(s: string) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
/**
* Wraps an object of methods and adds error handling.
*/
export function eventsErrorHandler(
methods: Partial<EventCallbacks>,
logger: LoggerInstance
): Partial<EventCallbacks> {
return Object.keys(methods).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
const method: Method = methods[name as keyof Method]
return await method(...args)
} catch (e) {
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e as Error)
}
}
return acc
}, {})
}
/** Handles adapter induced errors. */
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
logger: LoggerInstance
): TAdapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
logger.debug(`adapter_${name}`, { args })
const method: Method = adapter[name as keyof Method]
return await method(...args)
} catch (error) {
logger.error(`adapter_error_${name}`, error as Error)
const e = new UnknownError(error as Error)
e.name = `${capitalize(name)}Error`
throw e
}
}
return acc
}, {})
}

View File

@@ -0,0 +1,175 @@
import { SessionStore } from "./cookie.js"
import { UnknownAction } from "../errors.js"
import { init } from "./init.js"
import renderPage from "./pages/index.js"
import * as routes from "./routes/index.js"
import type {
RequestInternal,
ResponseInternal,
AuthConfig,
ErrorPageParam,
} from "../types.js"
export async function AuthInternal<
Body extends string | Record<string, any> | any[]
>(
request: RequestInternal,
authOptions: AuthConfig
): Promise<ResponseInternal<Body>> {
const { action, providerId, error, method } = request
const { options, cookies } = await init({
authOptions,
action,
providerId,
url: request.url,
callbackUrl: request.body?.callbackUrl ?? request.query?.callbackUrl,
csrfToken: request.body?.csrfToken,
cookies: request.cookies,
isPost: method === "POST",
})
const sessionStore = new SessionStore(
options.cookies.sessionToken,
request,
options.logger
)
if (method === "GET") {
const render = renderPage({ ...options, query: request.query, cookies })
const { pages } = options
switch (action) {
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session(sessionStore, options)
if (session.cookies) cookies.push(...session.cookies)
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
return { ...session, cookies } as any
}
case "csrf":
return {
headers: { "Content-Type": "application/json" },
body: { csrfToken: options.csrfToken } as any,
cookies,
}
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(options.callbackUrl)}`
if (error)
signinUrl = `${signinUrl}&error=${encodeURIComponent(error)}`
return { redirect: signinUrl, cookies }
}
return render.signin()
case "signout":
if (pages.signOut) return { redirect: pages.signOut, cookies }
return render.signout()
case "callback":
if (options.provider) {
const callback = await routes.callback({
body: request.body,
query: request.query,
headers: request.headers,
cookies: request.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
case "verify-request":
if (pages.verifyRequest) {
return { redirect: pages.verifyRequest, cookies }
}
return render.verifyRequest()
case "error":
// These error messages are displayed in line on the sign in page
// TODO: verify these. We should redirect these to signin directly, instead of
// first to error and then to signin.
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
return { redirect: `${options.url}/signin?error=${error}`, cookies }
}
if (pages.error) {
return {
redirect: `${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`,
cookies,
}
}
return render.error({ error: error as ErrorPageParam })
default:
}
} else {
switch (action) {
case "signin":
// Verified CSRF Token required for all sign in routes
if (options.csrfTokenVerified && options.provider) {
const signin = await routes.signin(
request.query,
request.body,
options
)
if (signin.cookies) cookies.push(...signin.cookies)
return { ...signin, cookies }
}
return { redirect: `${options.url}/signin?csrf=true`, cookies }
case "signout":
// Verified CSRF Token required for signout
if (options.csrfTokenVerified) {
const signout = await routes.signout(sessionStore, options)
if (signout.cookies) cookies.push(...signout.cookies)
return { ...signout, cookies }
}
return { redirect: `${options.url}/signout?csrf=true`, cookies }
case "callback":
if (options.provider) {
// Verified CSRF Token required for credentials providers only
if (
options.provider.type === "credentials" &&
!options.csrfTokenVerified
) {
return { redirect: `${options.url}/signin?csrf=true`, cookies }
}
const callback = await routes.callback({
body: request.body,
query: request.query,
headers: request.headers,
cookies: request.cookies,
method,
options,
sessionStore,
})
if (callback.cookies) cookies.push(...callback.cookies)
return { ...callback, cookies }
}
break
default:
}
}
throw new UnknownAction(`Cannot handle action: ${action}`)
}

View File

@@ -1,18 +1,23 @@
import { adapterErrorHandler, eventsErrorHandler } from "./errors.js"
import * as jwt from "../jwt.js"
import { createCallbackUrl } from "./callback-url.js"
import * as cookie from "./cookie.js"
import { createCSRFToken } from "./csrf-token.js"
import { defaultCallbacks } from "./default-callbacks.js"
import { AdapterError, EventError } from "../errors.js"
import parseProviders from "./providers.js"
import logger from "./utils/logger.js"
import { logger, type LoggerInstance } from "./utils/logger.js"
import parseUrl from "./utils/parse-url.js"
import type { AuthOptions, InternalOptions, RequestInternal } from "../index.js"
import type {
AuthConfig,
EventCallbacks,
InternalOptions,
RequestInternal,
} from "../types.js"
interface InitParams {
url: URL
authOptions: AuthOptions
authOptions: AuthConfig
providerId?: string
action: InternalOptions["action"]
/** Callback URL value extracted from the incoming request. */
@@ -150,3 +155,46 @@ export async function init({
return { options, cookies }
}
type Method = (...args: any[]) => Promise<any>
/** Wraps an object of methods and adds error handling. */
function eventsErrorHandler(
methods: Partial<EventCallbacks>,
logger: LoggerInstance
): Partial<EventCallbacks> {
return Object.keys(methods).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
const method: Method = methods[name as keyof Method]
return await method(...args)
} catch (e) {
logger.error(new EventError(e))
}
}
return acc
}, {})
}
/** Handles adapter induced errors. */
function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
logger: LoggerInstance
): TAdapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {
acc[name] = async (...args: any[]) => {
try {
logger.debug(`adapter_${name}`, { args })
const method: Method = adapter[name as keyof Method]
return await method(...args)
} catch (e) {
const error = new AdapterError(e)
logger.error(error)
throw error
}
}
return acc
}, {})
}

View File

@@ -5,7 +5,7 @@ import type {
InternalOptions,
RequestInternal,
ResponseInternal,
} from "../../index.js"
} from "../../types.js"
import type { Cookie } from "../cookie.js"
/**
@@ -13,13 +13,10 @@ import type { Cookie } from "../cookie.js"
*
* [OAuth 2](https://www.oauth.com/oauth2-servers/authorization/the-authorization-request/)
*/
export async function getAuthorizationUrl({
options,
query,
}: {
export async function getAuthorizationUrl(
query: RequestInternal["query"],
options: InternalOptions<"oauth">
query: RequestInternal["query"]
}): Promise<ResponseInternal> {
): Promise<ResponseInternal> {
const { logger, provider } = options
let url = provider.authorization?.url
@@ -85,15 +82,13 @@ export async function getAuthorizationUrl({
cookies.push(nonce)
}
url.searchParams.delete("nextauth")
// TODO: This does not work in normalizeOAuth because authorization endpoint can come from discovery
// Need to make normalizeOAuth async
if (provider.type === "oidc" && !url.searchParams.has("scope")) {
url.searchParams.set("scope", "openid profile email")
}
logger.debug("GET_AUTHORIZATION_URL", { url, cookies, provider })
logger.debug("authorization url is ready", { url, cookies, provider })
return { redirect: url, cookies }
}

View File

@@ -1,8 +1,8 @@
import { OAuthCallbackError } from "../errors.js"
import * as o from "oauth4webapi"
import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js"
import { useNonce } from "./nonce-handler.js"
import { usePKCECodeVerifier } from "./pkce-handler.js"
import { useState } from "./state-handler.js"
import * as o from "oauth4webapi"
import type {
InternalOptions,
@@ -10,191 +10,173 @@ import type {
Profile,
RequestInternal,
TokenSet,
} from "../../index.js"
} from "../../types.js"
import type { OAuthConfigInternal } from "../../providers/index.js"
import type { Cookie } from "../cookie.js"
export async function handleOAuthCallback(params: {
/**
* Handles the following OAuth steps.
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.1
* https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3
* https://openid.net/specs/openid-connect-core-1_0.html#UserInfoRequest
*
* @note Although requesting userinfo is not required by the OAuth2.0 spec,
* we fetch it anyway. This is because we always want a user profile.
*/
export async function handleOAuth(
query: RequestInternal["query"],
cookies: RequestInternal["cookies"],
options: InternalOptions<"oauth">
query: RequestInternal["query"]
body: RequestInternal["body"]
cookies: RequestInternal["cookies"]
}) {
const { options, query, body, cookies } = params
) {
const { logger, provider } = options
let as: o.AuthorizationServer
const errorMessage = body?.error ?? query?.error
if (errorMessage) {
const error = new Error(errorMessage)
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
error,
error_description: query?.error_description,
if (!provider.token?.url && !provider.userinfo?.url) {
// We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer!)
const discoveryResponse = await o.discoveryRequest(issuer)
const discoveredAs = await o.processDiscoveryResponse(
issuer,
discoveryResponse
)
if (!discoveredAs.token_endpoint)
throw new TypeError(
"TODO: Authorization server did not provide a token endpoint."
)
if (!discoveredAs.userinfo_endpoint)
throw new TypeError(
"TODO: Authorization server did not provide a userinfo endpoint."
)
as = discoveredAs
} else {
as = {
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer
token_endpoint: provider.token?.url.toString(),
userinfo_endpoint: provider.userinfo?.url.toString(),
}
}
const client: o.Client = {
client_id: provider.clientId,
client_secret: provider.clientSecret,
...provider.client,
}
const resCookies: Cookie[] = []
const state = await useState(cookies, resCookies, options)
const parameters = o.validateAuthResponse(
as,
client,
new URLSearchParams(query),
provider.checks.includes("state") ? state : o.skipStateCheck
)
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
if (o.isOAuth2Error(parameters)) {
logger.debug("OAuthCallbackError", {
providerId: provider.id,
...parameters,
})
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
throw new OAuthCallbackError(parameters.error)
}
try {
let as: o.AuthorizationServer
const codeVerifier = await usePKCECodeVerifier(
cookies?.[options.cookies.pkceCodeVerifier.name],
options
)
if (!provider.token?.url && !provider.userinfo?.url) {
// We assume that issuer is always defined as this has been asserted earlier
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const issuer = new URL(provider.issuer!)
const discoveryResponse = await o.discoveryRequest(issuer)
const discoveredAs = await o.processDiscoveryResponse(
issuer,
discoveryResponse
)
if (codeVerifier) resCookies.push(codeVerifier.cookie)
if (!discoveredAs.token_endpoint)
throw new TypeError(
"TODO: Authorization server did not provide a token endpoint."
)
if (!discoveredAs.userinfo_endpoint)
throw new TypeError(
"TODO: Authorization server did not provide a userinfo endpoint."
)
as = discoveredAs
} else {
as = {
issuer: provider.issuer ?? "https://a", // TODO: review fallback issuer
token_endpoint: provider.token?.url.toString(),
userinfo_endpoint: provider.userinfo?.url.toString(),
}
}
const client: o.Client = {
client_id: provider.clientId,
client_secret: provider.clientSecret,
...provider.client,
}
const resCookies: Cookie[] = []
const state = await useState(cookies?.[options.cookies.state.name], options)
if (state) resCookies.push(state.cookie)
const codeVerifier = await usePKCECodeVerifier(
cookies?.[options.cookies.pkceCodeVerifier.name],
options
)
if (codeVerifier) resCookies.push(codeVerifier.cookie)
// TODO:
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.type === "oidc") {
resCookies.push(nonce.cookie)
}
const parameters = o.validateAuthResponse(
as,
client,
new URLSearchParams(query),
provider.checks.includes("state") ? state?.value : o.skipStateCheck
)
if (o.isOAuth2Error(parameters)) {
console.log("error", parameters)
throw new Error("TODO: Handle OAuth 2.0 redirect error")
}
const codeGrantResponse = await o.authorizationCodeGrantRequest(
as,
client,
parameters,
provider.callbackUrl,
codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier
)
let challenges: o.WWWAuthenticateChallenge[] | undefined
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
for (const challenge of challenges) {
console.log("challenge", challenge)
}
throw new Error("TODO: Handle www-authenticate challenges as needed")
}
let profile: Profile = {}
let tokens: TokenSet
if (provider.type === "oidc") {
const result = await o.processAuthorizationCodeOpenIDResponse(
as,
client,
codeGrantResponse
)
if (o.isOAuth2Error(result)) {
console.log("error", result)
throw new Error("TODO: Handle OIDC response body error")
}
profile = o.getValidatedIdTokenClaims(result)
tokens = result
} else {
tokens = await o.processAuthorizationCodeOAuth2Response(
as,
client,
codeGrantResponse
)
if (o.isOAuth2Error(tokens as any)) {
console.log("error", tokens)
throw new Error("TODO: Handle OAuth 2.0 response body error")
}
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider })
} else if (provider.userinfo?.url) {
const userinfoResponse = await o.userInfoRequest(
as,
client,
(tokens as any).access_token
)
profile = await userinfoResponse.json()
}
}
const profileResult = await getProfile({
profile,
provider,
tokens,
logger,
})
return { ...profileResult, cookies: resCookies }
} catch (error) {
throw new OAuthCallbackError(error as Error)
// TODO:
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.type === "oidc") {
resCookies.push(nonce.cookie)
}
}
interface GetProfileParams {
profile: Profile
tokens: TokenSet
provider: OAuthConfigInternal<any>
logger: LoggerInstance
const codeGrantResponse = await o.authorizationCodeGrantRequest(
as,
client,
parameters,
provider.callbackUrl,
codeVerifier?.codeVerifier ?? "auth" // TODO: review fallback code verifier
)
let challenges: o.WWWAuthenticateChallenge[] | undefined
if ((challenges = o.parseWwwAuthenticateChallenges(codeGrantResponse))) {
for (const challenge of challenges) {
console.log("challenge", challenge)
}
throw new Error("TODO: Handle www-authenticate challenges as needed")
}
let profile: Profile = {}
let tokens: TokenSet
if (provider.type === "oidc") {
const result = await o.processAuthorizationCodeOpenIDResponse(
as,
client,
codeGrantResponse
)
if (o.isOAuth2Error(result)) {
console.log("error", result)
throw new Error("TODO: Handle OIDC response body error")
}
profile = o.getValidatedIdTokenClaims(result)
tokens = result
} else {
tokens = await o.processAuthorizationCodeOAuth2Response(
as,
client,
codeGrantResponse
)
if (o.isOAuth2Error(tokens as any)) {
console.log("error", tokens)
throw new Error("TODO: Handle OAuth 2.0 response body error")
}
if (provider.userinfo?.request) {
profile = await provider.userinfo.request({ tokens, provider })
} else if (provider.userinfo?.url) {
const userinfoResponse = await o.userInfoRequest(
as,
client,
(tokens as any).access_token
)
profile = await userinfoResponse.json()
}
}
const profileResult = await getProfile(profile, provider, tokens, logger)
return { ...profileResult, cookies: resCookies }
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams) {
async function getProfile(
OAuthProfile: Profile,
provider: OAuthConfigInternal<any>,
tokens: TokenSet,
logger: LoggerInstance
) {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
if (!profile.id) {
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
}
// Return profile, raw profile and auth provider details
return {
profile,
account: {
@@ -213,9 +195,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.debug("getProfile error details", OAuthProfile)
logger.error(new OAuthProfileParseError(error))
}
}

View File

@@ -1,7 +1,7 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../index.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const NONCE_MAX_AGE = 60 * 15 // 15 minutes in seconds

View File

@@ -1,7 +1,7 @@
import * as o from "oauth4webapi"
import * as jwt from "../../jwt.js"
import type { InternalOptions } from "../../index.js"
import type { InternalOptions } from "../../types.js"
import type { Cookie } from "../cookie.js"
const PKCE_CODE_CHALLENGE_METHOD = "S256"

View File

@@ -1,6 +1,7 @@
import type { InternalOptions } from "../../index.js"
import type { Cookie } from "../cookie.js"
import * as o from "oauth4webapi"
import type { InternalOptions, RequestInternal } from "../../types.js"
import type { Cookie } from "../cookie.js"
import { InvalidState } from "../../errors.js"
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
@@ -39,25 +40,33 @@ export async function createState(
}
/**
* Returns state from if the provider supports states,
* Returns state from the saved cookie
* if the provider supports states,
* and clears the container cookie afterwards.
*/
export async function useState(
state: string | undefined,
cookies: RequestInternal["cookies"],
resCookies: Cookie[],
options: InternalOptions<"oauth">
): Promise<{ value: string; cookie: Cookie } | undefined> {
const { cookies, provider, jwt } = options
): Promise<string | undefined> {
const { provider, jwt } = options
if (!provider.checks.includes("state")) return
if (!provider.checks?.includes("state") || !state) return
const state = cookies?.[options.cookies.state.name]
if (!state) throw new InvalidState("State was missing from the cookies.")
// IDEA: Let the user do something with the returned state
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
return {
value: value?.value ?? undefined,
cookie: {
name: cookies.state.name,
value: "",
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
},
}
if (!value?.value) throw new InvalidState("Could not parse state cookie.")
// Clear the state cookie after use
resCookies.push({
name: options.cookies.state.name,
value: "",
options: { ...options.cookies.state.options, maxAge: 0 },
})
return value.value
}

View File

@@ -1,19 +1,15 @@
import type { Theme } from "../../index.js"
import type { ErrorPageParam, Theme } from "../../types.js"
/**
* The following errors are passed as error query parameters to the default or overridden error page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
export type ErrorType =
| "default"
| "configuration"
| "accessdenied"
| "verification"
* [Documentation](https://next-auth.js.org/configuration/pages#error-page)
*/
export interface ErrorProps {
url?: URL
theme?: Theme
error?: ErrorType
error?: ErrorPageParam
}
interface ErrorView {
@@ -28,7 +24,7 @@ export default function ErrorPage(props: ErrorProps) {
const { url, error = "default", theme } = props
const signinPageUrl = `${url}/signin`
const errors: Record<ErrorType, ErrorView> = {
const errors: Record<Lowercase<ErrorPageParam | "default">, ErrorView> = {
default: {
status: 200,
heading: "Error",

View File

@@ -1,17 +1,28 @@
import { renderToString } from "preact-render-to-string"
import css from "../styles/index.js"
import ErrorPage from "./error.js"
import SigninPage from "./signin.js"
import SignoutPage from "./signout.js"
import css from "./styles.js"
import VerifyRequestPage from "./verify-request.js"
import type {
ErrorPageParam,
InternalOptions,
RequestInternal,
ResponseInternal,
} from "../../index.js"
} from "../../types.js"
import type { Cookie } from "../cookie.js"
import type { ErrorType } from "./error.js"
function send({ html, title, status, cookies, theme }: any): ResponseInternal {
return {
cookies,
status,
headers: { "Content-Type": "text/html" },
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css}</style><title>${title}</title></head><body class="__next-auth-theme-${
theme?.colorScheme ?? "auto"
}"><div class="page">${renderToString(html)}</div></body></html>`,
}
}
type RenderPageParams = {
query?: RequestInternal["query"]
@@ -30,20 +41,11 @@ type RenderPageParams = {
export default function renderPage(params: RenderPageParams) {
const { url, theme, query, cookies } = params
function send({ html, title, status }: any): ResponseInternal {
return {
cookies,
status,
headers: { "Content-Type": "text/html" },
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css}</style><title>${title}</title></head><body class="__next-auth-theme-${
theme?.colorScheme ?? "auto"
}"><div class="page">${renderToString(html)}</div></body></html>`,
}
}
return {
signin(props?: any) {
return send({
cookies,
theme,
html: SigninPage({
csrfToken: params.csrfToken,
// We only want to render providers
@@ -66,6 +68,8 @@ export default function renderPage(params: RenderPageParams) {
},
signout(props?: any) {
return send({
cookies,
theme,
html: SignoutPage({
csrfToken: params.csrfToken,
url,
@@ -77,12 +81,16 @@ export default function renderPage(params: RenderPageParams) {
},
verifyRequest(props?: any) {
return send({
cookies,
theme,
html: VerifyRequestPage({ url, theme, ...props }),
title: "Verify Request",
})
},
error(props?: { error?: ErrorType }) {
error(props?: { error?: ErrorPageParam }) {
return send({
cookies,
theme,
...ErrorPage({ url, theme, ...props }),
title: "Error",
})

View File

@@ -1,32 +1,36 @@
import type { InternalProvider, Theme } from "../../index.js"
import type {
InternalProvider,
SignInPageErrorParam,
Theme,
} from "../../types.js"
/**
* The following errors are passed as error query parameters to the default or overridden sign-in page.
*
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
export type SignInErrorTypes =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
| "OAuthAccountNotLinked"
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
| "default"
const signinErrors: Record<
Lowercase<SignInPageErrorParam | "default">,
string
> = {
default: "Unable to sign in.",
signin: "Try signing in with a different account.",
oauthsignin: "Try signing in with a different account.",
oauthcallback: "Try signing in with a different account.",
oauthcreateaccount: "Try signing in with a different account.",
emailcreateaccount: "Try signing in with a different account.",
callback: "Try signing in with a different account.",
oauthaccountnotlinked:
"To confirm your identity, sign in with the same account you used originally.",
emailsignin: "The e-mail could not be sent.",
credentialssignin:
"Sign in failed. Check the details you provided are correct.",
sessionrequired: "Please sign in to access this page.",
}
export interface SignInServerPageParams {
export default function SigninPage(props: {
csrfToken: string
providers: InternalProvider[]
callbackUrl: string
email: string
error: SignInErrorTypes
error?: SignInPageErrorParam
theme: Theme
}
export default function SigninPage(props: SignInServerPageParams) {
}) {
const {
csrfToken,
providers = [],
@@ -43,23 +47,8 @@ export default function SigninPage(props: SignInServerPageParams) {
)
}
const errors: Record<SignInErrorTypes, string> = {
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
OAuthCreateAccount: "Try signing in with a different account.",
EmailCreateAccount: "Try signing in with a different account.",
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "The e-mail could not be sent.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}
const error = errorType && (errors[errorType] ?? errors.default)
const error =
errorType && (signinErrors[errorType.toLowerCase()] ?? signinErrors.default)
// TODO: move logos
const logos =

View File

@@ -1,4 +1,4 @@
import type { Theme } from "../../index.js"
import type { Theme } from "../../types.js"
export interface SignoutProps {
url: URL

View File

@@ -1,4 +1,4 @@
import type { Theme } from "../../index.js"
import type { Theme } from "../../types.js"
interface VerifyRequestPageProps {
url: URL

View File

@@ -1,6 +1,6 @@
import { merge } from "./utils/merge.js"
import type { InternalProvider } from "../index.js"
import type { InternalProvider } from "../types.js"
import type {
OAuthConfig,
OAuthConfigInternal,

View File

@@ -1,12 +1,13 @@
import callbackHandler from "../callback-handler.js"
import getAdapterUserFromEmail from "../email/getUserFromEmail.js"
import { handleOAuthCallback } from "../oauth/callback.js"
import { handleLogin } from "../callback-handler.js"
import { CallbackRouteError } from "../../errors.js"
import { handleOAuth } from "../oauth/callback.js"
import { createHash } from "../web.js"
import { handleAuthorized } from "./shared.js"
import type { RequestInternal, ResponseInternal, User } from "../../index.js"
import type { AdapterSession } from "../../adapters.js"
import type { RequestInternal, ResponseInternal, User } from "../../types.js"
import type { Cookie, SessionStore } from "../cookie.js"
import type { InternalOptions } from "../types.js"
import type { InternalOptions } from "../../types.js"
/** Handle callbacks from login services */
export async function callback(params: {
@@ -36,166 +37,116 @@ export async function callback(params: {
const useJwtSession = sessionStrategy === "jwt"
if (provider.type === "oauth" || provider.type === "oidc") {
try {
const {
try {
if (provider.type === "oauth" || provider.type === "oidc") {
const authorizationResult = await handleOAuth(
query,
params.cookies,
options
)
if (authorizationResult.cookies.length) {
cookies.push(...authorizationResult.cookies)
}
logger.debug("authroization result", authorizationResult)
const { profile, account, OAuthProfile } = authorizationResult
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
// direct the user to the signin page for now. We could do something
// else in future.
// TODO: Handle user cancelling signin
if (!profile || !account || !OAuthProfile) {
return { redirect: `${url}/signin`, cookies }
}
// Check if user is allowed to sign in
// Attempt to get Profile from OAuth provider details before invoking
// signIn callback - but if no user object is returned, that is fine
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: provider.id,
})
if (userByAccount) userOrProfile = userByAccount
}
const unauthorizedOrError = await handleAuthorized(
{ user: userOrProfile, account, profile: OAuthProfile },
options
)
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
// Sign user in
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
account,
OAuthProfile,
cookies: oauthCookies,
} = await handleOAuthCallback({
query,
body,
options,
cookies: params.cookies,
})
options
)
if (oauthCookies.length) cookies.push(...oauthCookies)
try {
// Make it easier to debug when adding a new provider
logger.debug("OAUTH_CALLBACK_RESPONSE", {
profile,
if (useJwtSession) {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
OAuthProfile,
profile: OAuthProfile,
isNewUser,
})
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
// direct the user to the signin page for now. We could do something
// else in future.
//
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile || !account || !OAuthProfile) {
return { redirect: `${url}/signin`, cookies }
}
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Check if user is allowed to sign in
// Attempt to get Profile from OAuth provider details before invoking
// signIn callback - but if no user object is returned, that is fine
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: provider.id,
})
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
if (userByAccount) userOrProfile = userByAccount
}
try {
const isAllowed = await callbacks.signIn({
user: userOrProfile,
account,
profile: OAuthProfile,
})
if (!isAllowed) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
} else if (typeof isAllowed === "string") {
return { redirect: isAllowed, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message
)}`,
cookies,
}
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
account,
options,
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
},
})
if (useJwtSession) {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
profile: OAuthProfile,
isNewUser,
})
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
} else {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: (session as AdapterSession).expires,
},
})
}
// @ts-expect-error
await events.signIn?.({ user, account, profile, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
// Callback URL is already verified at this point, so safe to use if specified
return { redirect: callbackUrl, cookies }
} catch (error) {
if ((error as Error).name === "AccountNotLinkedError") {
// If the email on the account is already linked, but not with this OAuth account
return {
redirect: `${url}/error?error=OAuthAccountNotLinked`,
cookies,
}
} else if ((error as Error).name === "CreateUserError") {
return { redirect: `${url}/error?error=OAuthCreateAccount`, cookies }
}
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} catch (error) {
if ((error as Error).name === "OAuthCallbackError") {
logger.error("OAUTH_CALLBACK_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=OAuthCallback`, cookies }
// @ts-expect-error
await events.signIn?.({ user, account, profile, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return {
redirect: `${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`,
cookies,
}
}
logger.error("OAUTH_CALLBACK_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} else if (provider.type === "email") {
try {
return { redirect: callbackUrl, cookies }
} else if (provider.type === "email") {
const token = query?.token as string | undefined
const identifier = query?.email as string | undefined
@@ -205,7 +156,7 @@ export async function callback(params: {
}
const secret = provider.secret ?? options.secret
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
// @ts-expect-error -- Verified in `assertConfig`.
const invite = await adapter.useVerificationToken({
identifier,
token: await createHash(`${token}${secret}`),
@@ -216,11 +167,8 @@ export async function callback(params: {
return { redirect: `${url}/error?error=Verification`, cookies }
}
const profile = await getAdapterUserFromEmail({
email: identifier,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter,
})
// @ts-expect-error -- Verified in `assertConfig`.
const profile = await getAdapterUserFromEmail(identifier, adapter)
const account = {
providerAccountId: profile.email,
@@ -229,32 +177,20 @@ export async function callback(params: {
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user: profile,
account,
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
} else if (typeof signInCallbackResponse === "string") {
return { redirect: signInCallbackResponse, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message
)}`,
cookies,
}
}
const unauthorizedOrError = await handleAuthorized(
{ user: profile, account },
options
)
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
// Sign user in
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
account,
options,
})
options
)
if (useJwtSession) {
const defaultToken = {
@@ -309,112 +245,99 @@ export async function callback(params: {
// Callback URL is already verified at this point, so safe to use if specified
return { redirect: callbackUrl, cookies }
} catch (error) {
if ((error as Error).name === "CreateUserError") {
return { redirect: `${url}/error?error=EmailCreateAccount`, cookies }
}
logger.error("CALLBACK_EMAIL_ERROR", error as Error)
return { redirect: `${url}/error?error=Callback`, cookies }
}
} else if (provider.type === "credentials" && method === "POST") {
const credentials = body
} else if (provider.type === "credentials" && method === "POST") {
const credentials = body
let user: User | null
try {
user = await provider.authorize(credentials, {
query,
body,
headers,
method,
})
if (!user) {
let user: User | null
try {
// TODO: Forward the original request as is, instead of reconstructing it
// 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 (error) {
return {
status: 401,
redirect: `${url}/error?${new URLSearchParams({
error: "CredentialsSignin",
provider: provider.id,
})}`,
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message
)}`,
cookies,
}
}
} catch (error) {
return {
status: 401,
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message
)}`,
cookies,
/** @type {import("src").Account} */
const account = {
providerAccountId: user.id,
type: "credentials",
provider: provider.id,
}
}
/** @type {import("src").Account} */
const account = {
providerAccountId: user.id,
type: "credentials",
provider: provider.id,
}
const unauthorizedOrError = await handleAuthorized(
{ user, account, credentials },
options
)
try {
const isAllowed = await callbacks.signIn({
if (unauthorizedOrError) return { ...unauthorizedOrError, cookies }
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
credentials,
isNewUser: false,
})
if (!isAllowed) {
return {
status: 403,
redirect: `${url}/error?error=AccessDenied`,
cookies,
}
} else if (typeof isAllowed === "string") {
return { redirect: isAllowed, cookies }
}
} catch (error) {
return {
redirect: `${url}/error?error=${encodeURIComponent(
(error as Error).message
)}`,
cookies,
}
}
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser: false,
})
await events.signIn?.({ user, account })
// Encode token
const newToken = await jwt.encode({ ...jwt, token })
return { redirect: callbackUrl, cookies }
}
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
return {
status: 500,
body: `Error: Callback for provider type ${provider.type} not supported`,
cookies,
}
} catch (e) {
const error = new CallbackRouteError(e, { provider: provider.id })
const sessionCookies = sessionStore.chunk(newToken, {
expires: cookieExpires,
})
cookies.push(...sessionCookies)
// @ts-expect-error
await events.signIn?.({ user, account })
return { redirect: callbackUrl, cookies }
}
return {
status: 500,
body: `Error: Callback for provider type ${provider.type} not supported`,
cookies,
logger.error(error)
url.searchParams.set("error", CallbackRouteError.name)
url.pathname += "/error"
return { redirect: url, cookies }
}
}

View File

@@ -1,4 +1,4 @@
import type { InternalProvider, ResponseInternal } from "../../index.js"
import type { InternalProvider, ResponseInternal } from "../../types.js"
export interface PublicProvider {
id: string

View File

@@ -1,23 +1,15 @@
import { JWTSessionError, SessionTokenError } from "../../errors.js"
import { fromDate } from "../utils/date.js"
import type { InternalOptions, ResponseInternal, Session } from "../../index.js"
import type { Adapter } from "../../adapters.js"
import type { InternalOptions, ResponseInternal, Session } from "../../types.js"
import type { SessionStore } from "../cookie.js"
interface SessionParams {
options: InternalOptions
sessionStore: SessionStore
}
/**
* Return a session object (without any private fields)
* for Single Page App clients
*/
/** Return a session object filtered via `callbacks.session` */
export async function session(
params: SessionParams
sessionStore: SessionStore,
options: InternalOptions
): Promise<ResponseInternal<Session | {}>> {
const { options, sessionStore } = params
const {
adapter,
jwt,
@@ -39,10 +31,7 @@ export async function session(
if (sessionStrategy === "jwt") {
try {
const decodedToken = await jwt.decode({
...jwt,
token: sessionToken,
})
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
const newExpires = fromDate(sessionMaxAge)
@@ -81,84 +70,89 @@ export async function session(
await events.session?.({ session: newSession, token })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error as Error)
logger.error(new JWTSessionError(error))
// If the JWT is not verifiable remove the broken session cookie(s).
response.cookies?.push(...sessionStore.clean())
}
} else {
try {
const { getSessionAndUser, deleteSession, updateSession } =
adapter as Adapter
let userAndSession = await getSessionAndUser(sessionToken)
// If session has expired, clean up the database
if (
userAndSession &&
userAndSession.session.expires.valueOf() < Date.now()
) {
await deleteSession(sessionToken)
userAndSession = null
}
return response
}
if (userAndSession) {
const { user, session } = userAndSession
// Retrieve session from database
try {
const { getSessionAndUser, deleteSession, updateSession } =
adapter as Adapter
let userAndSession = await getSessionAndUser(sessionToken)
const sessionUpdateAge = options.session.updateAge
// Calculate last updated date to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
const sessionIsDueToBeUpdatedDate =
session.expires.valueOf() -
sessionMaxAge * 1000 +
sessionUpdateAge * 1000
const newExpires = fromDate(sessionMaxAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (sessionIsDueToBeUpdatedDate <= Date.now()) {
await updateSession({ sessionToken, expires: newExpires })
}
// Pass Session through to the session callback
// @ts-expect-error
const sessionPayload = await callbacks.session({
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
session: {
user: {
name: user.name,
email: user.email,
image: user.image,
},
expires: session.expires.toISOString(),
},
user,
})
// Return session payload as response
response.body = sessionPayload
// Set cookie again to update expiry
response.cookies?.push({
name: options.cookies.sessionToken.name,
value: sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: newExpires,
},
})
// @ts-expect-error
await events.session?.({ session: sessionPayload })
} else if (sessionToken) {
// If `sessionToken` was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
response.cookies?.push(...sessionStore.clean())
}
} catch (error) {
logger.error("SESSION_ERROR", error as Error)
// If session has expired, clean up the database
if (
userAndSession &&
userAndSession.session.expires.valueOf() < Date.now()
) {
await deleteSession(sessionToken)
userAndSession = null
}
if (userAndSession) {
const { user, session } = userAndSession
const sessionUpdateAge = options.session.updateAge
// Calculate last updated date to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
const sessionIsDueToBeUpdatedDate =
session.expires.valueOf() -
sessionMaxAge * 1000 +
sessionUpdateAge * 1000
const newExpires = fromDate(sessionMaxAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (sessionIsDueToBeUpdatedDate <= Date.now()) {
await updateSession({
sessionToken: sessionToken,
expires: newExpires,
})
}
// Pass Session through to the session callback
// @ts-expect-error
const sessionPayload = await callbacks.session({
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
session: {
user: {
name: user.name,
email: user.email,
image: user.image,
},
expires: session.expires.toISOString(),
},
user,
})
// Return session payload as response
response.body = sessionPayload
// Set cookie again to update expiry
response.cookies?.push({
name: options.cookies.sessionToken.name,
value: sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: newExpires,
},
})
// @ts-expect-error
await events.session?.({ session: sessionPayload })
} else if (sessionToken) {
// If `sessionToken` was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
response.cookies?.push(...sessionStore.clean())
}
} catch (error) {
logger.error(new SessionTokenError(error))
}
return response

View File

@@ -0,0 +1,37 @@
import { AuthorizedCallbackError } from "../../errors.js"
import { InternalOptions } from "../../types.js"
import type { Adapter, AdapterUser } from "../../adapters.js"
export async function handleAuthorized(
params: any,
{ url, logger, callbacks: { signIn } }: InternalOptions
) {
url.pathname += "/error"
try {
const authorized = await signIn(params)
if (!authorized) {
logger.debug("User not authorized", params)
url.searchParams.set("error", "AccessDenied")
return { status: 403 as const, redirect: url }
}
} catch (e) {
const error = new AuthorizedCallbackError(e)
logger.error(error)
url.searchParams.set("error", "Configuration")
return { status: 500 as const, redirect: url }
}
}
/**
* Query the database for a user by email address.
* If it's an existing user, return a user object,
* otherwise use placeholder.
*/
export async function getAdapterUserFromEmail(
email: string,
adapter: Adapter
): Promise<AdapterUser> {
const user = await adapter.getUserByEmail(email)
return user ?? { id: email, email, emailVerified: null }
}

View File

@@ -1,103 +1,70 @@
import getAdapterUserFromEmail from "../email/getUserFromEmail.js"
import emailSignin from "../email/signin.js"
import { SignInError } from "../../errors.js"
import { getAuthorizationUrl } from "../oauth/authorization-url.js"
import { getAdapterUserFromEmail, handleAuthorized } from "./shared.js"
import type {
Account,
InternalOptions,
RequestInternal,
ResponseInternal,
} from "../../index.js"
} from "../../types.js"
/** Handle requests to /api/auth/signin */
export async function signin(params: {
/**
* Initiates the sign in process for OAuth and Email flows .
* For OAuth, redirects to the provider's authorization URL.
* For Email, sends an email with a sign in link.
*/
export async function signin(
query: RequestInternal["query"],
body: RequestInternal["body"],
options: InternalOptions<"oauth" | "email">
query: RequestInternal["query"]
body: RequestInternal["body"]
}): Promise<ResponseInternal> {
const { options, query, body } = params
const { url, callbacks, logger, provider } = options
): Promise<ResponseInternal> {
const { url, logger, provider } = options
try {
if (provider.type === "oauth" || provider.type === "oidc") {
return await getAuthorizationUrl(query, options)
} else if (provider.type === "email") {
const normalizer = provider.normalizeIdentifier ?? defaultNormalizer
const email = normalizer(body?.email)
if (!provider.type) {
return {
status: 500,
// @ts-expect-error
text: `Error: Type not specified for ${provider.name}`,
}
}
// @ts-expect-error -- Verified in `assertConfig`
const user = await getAdapterUserFromEmail(email, options.adapter)
if (provider.type === "oauth" || provider.type === "oidc") {
try {
return await getAuthorizationUrl({ options, query })
} catch (error) {
logger.error("SIGNIN_OAUTH_ERROR", {
error: error as Error,
providerId: provider.id,
})
return { redirect: `${url}/error?error=OAuthSignin` }
}
} else if (provider.type === "email") {
let email: string = body?.email
if (!email) return { redirect: `${url}/error?error=EmailSignin` }
const normalizer: (identifier: string) => string =
provider.normalizeIdentifier ??
((identifier) => {
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = identifier.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
})
try {
email = normalizer(body?.email)
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
const user = await getAdapterUserFromEmail({
email,
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter: options.adapter,
})
const account: Account = {
providerAccountId: email,
userId: email,
type: "email",
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { verificationRequest: true },
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied` }
} else if (typeof signInCallbackResponse === "string") {
return { redirect: signInCallbackResponse }
const account: Account = {
providerAccountId: email,
userId: email,
type: "email",
provider: provider.id,
}
} catch (error) {
return {
redirect: `${url}/error?${new URLSearchParams({
error: error as string,
})}`,
}
}
try {
const unauthorizedOrError = await handleAuthorized(
{ user, account, email: { verificationRequest: true } },
options
)
if (unauthorizedOrError) return unauthorizedOrError
const redirect = await emailSignin(email, options)
return { redirect }
} catch (error) {
logger.error("SIGNIN_EMAIL_ERROR", { error, providerId: provider.id })
return { redirect: `${url}/error?error=EmailSignin` }
}
return { redirect: `${url}/signin` }
} catch (e) {
const error = new SignInError(e, { provider: provider.id })
logger.error(error)
url.searchParams.set("error", error.name)
url.pathname += "/error"
return { redirect: url }
}
return { redirect: `${url}/signin` }
}
function defaultNormalizer(email?: string) {
if (!email) throw new Error("Missing email from request body.")
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = email.toLowerCase().trim().split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
return `${local}@${domain}`
}

View File

@@ -1,44 +1,35 @@
import type { InternalOptions, ResponseInternal } from "../../index.js"
import type { Adapter } from "../../adapters.js"
import { SignOutError } from "../../errors.js"
import type { InternalOptions, ResponseInternal } from "../../types.js"
import type { SessionStore } from "../cookie.js"
/** Handle requests to /api/auth/signout */
export async function signout(params: {
/**
* Destroys the session.
* If the session strategy is database,
* The session is also deleted from the database.
* In any case, the session cookie is cleared and
* an `events.signOut` is emitted.
*/
export async function signout(
sessionStore: SessionStore,
options: InternalOptions
sessionStore: SessionStore
}): Promise<ResponseInternal> {
const { options, sessionStore } = params
const { adapter, events, jwt, callbackUrl, logger, session } = options
): Promise<ResponseInternal> {
const { jwt, events, callbackUrl: redirect, logger, session } = options
const sessionToken = sessionStore?.value
if (!sessionToken) {
return { redirect: callbackUrl }
}
const sessionToken = sessionStore.value
if (!sessionToken) return { redirect }
if (session.strategy === "jwt") {
// Dispatch signout event
try {
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
// @ts-expect-error
await events.signOut?.({ token: decodedJwt })
} catch (error) {
// Do nothing if decoding the JWT fails
logger.error("SIGNOUT_ERROR", error)
}
} else {
try {
const session = await (adapter as Adapter).deleteSession(sessionToken)
// Dispatch signout event
// @ts-expect-error
try {
if (session.strategy === "jwt") {
const token = await jwt.decode({ ...jwt, token: sessionToken })
await events.signOut?.({ token })
} else {
const session = await options.adapter?.deleteSession(sessionToken)
await events.signOut?.({ session })
} catch (error) {
// If error, log it but continue
logger.error("SIGNOUT_ERROR", error as Error)
}
} catch (error) {
logger.error(new SignOutError(error))
}
// Remove Session Token
const sessionCookies = sessionStore.clean()
return { redirect: callbackUrl, cookies: sessionCookies }
return { redirect, cookies: sessionStore.clean() }
}

View File

@@ -1,587 +0,0 @@
/**
*
* The `@auth/core/types` module contains all public types and interfaces of the core package.
*
* @module types
*/
import type { CookieSerializeOptions } from "cookie"
import type { Adapter, AdapterUser } from "../adapters.js"
import type {
CredentialInput,
CredentialsConfig,
EmailConfig,
OAuthConfigInternal,
Provider,
ProviderType,
} from "../providers/index.js"
import type {
OAuth2TokenEndpointResponse,
OpenIDTokenEndpointResponse,
} from "oauth4webapi"
import type { JWT, JWTOptions } from "../jwt.js"
import type { Cookie } from "./cookie.js"
import type { LoggerInstance } from "./utils/logger.js"
export type Awaitable<T> = T | PromiseLike<T>
export type { LoggerInstance }
/**
* Configure your NextAuth instance
*
* [Documentation](https://next-auth.js.org/configuration/options#options)
*/
export interface AuthOptions {
/**
* An array of authentication providers for signing in
* (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order.
* This can be one of the built-in providers or an object with a custom provider.
* * **Default value**: `[]`
* * **Required**: *Yes*
*
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: Provider[]
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified, it falls back to `AUTH_SECRET` or `NEXTAUTH_SECRET` from environment variables.
* To generate a random string, you can use the following command:
*
* On Unix systems: `openssl rand -hex 32`
* Or go to https://generate-secret.vercel.app/32
*
* @default process.env.AUTH_SECRET ?? process.env.NEXTAUTH_SECRET
*
* [Documentation](https://next-auth.js.org/configuration/options#secret)
*/
secret?: string
/**
* Configure your session like if you want to use JWT or a database,
* how long until an idle session expires, or to throttle write operations in case you are using a database.
* * **Default value**: See the documentation page
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#session)
*/
session?: Partial<SessionOptions>
/**
* JSON Web Tokens are enabled by default if you have not specified an adapter.
* JSON Web Tokens are encrypted (JWE) by default. We recommend you keep this behaviour.
* * **Default value**: See the documentation page
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#jwt)
*/
jwt?: Partial<JWTOptions>
/**
* Specify URLs to be used if you want to create custom sign in, sign out and error pages.
* Pages specified will override the corresponding built-in page.
* * **Default value**: `{}`
* * **Required**: *No*
* @example
*
* ```js
* pages: {
* signIn: '/auth/signin',
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: '/auth/new-user'
* }
* ```
*
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: Partial<PagesOptions>
/**
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
* Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens
* as they **allow you to implement access controls without a database** and to **integrate with external databases or APIs**.
* * **Default value**: See the Callbacks documentation
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: Partial<CallbacksOptions>
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
* The content of the message object varies depending on the flow
* (e.g. OAuth or Email authentication flow, JWT or database sessions, etc),
* but typically contains a user object and/or contents of the JSON Web Token
* and other information relevant to the event.
* * **Default value**: `{}`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: Partial<EventCallbacks>
/**
* You can use the adapter option to pass in your database adapter.
*
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Adapters Overview](https://next-auth.js.org/adapters/overview)
*/
adapter?: Adapter
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
* * **Required**: *No*
*
* - ⚠ If you added a custom `logger`, this setting is ignored.
*
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
*/
debug?: boolean
/**
* Override any of the logger levels (`undefined` levels will use the built-in logger),
* and intercept logs in NextAuth. You can use this option to send NextAuth logs to a third-party logging service.
* * **Default value**: `console`
* * **Required**: *No*
*
* @example
*
* ```js
* // /pages/api/auth/[...nextauth].js
* import log from "logging-service"
* export default NextAuth({
* logger: {
* error(code, ...message) {
* log.error(code, message)
* },
* warn(code, ...message) {
* log.warn(code, message)
* },
* debug(code, ...message) {
* log.debug(code, message)
* }
* }
* })
* ```
*
* - ⚠ When set, the `debug` option is ignored
*
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
*/
logger?: Partial<LoggerInstance>
/**
* Changes the theme of pages.
* Set to `"light"` if you want to force pages to always be light.
* Set to `"dark"` if you want to force pages to always be dark.
* Set to `"auto"`, (or leave this option out)if you want the pages to follow the preferred system theme.
* * **Default value**: `"auto"`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: Theme
/**
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
* You can manually set this option to `false` to disable this security feature and allow cookies
* to be accessible from non-secured URLs (this is not recommended).
* * **Default value**: `true` for HTTPS and `false` for HTTP sites
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#usesecurecookies)
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*/
useSecureCookies?: boolean
/**
* You can override the default cookie names and options for any of the cookies used by NextAuth.js.
* You can specify one or more cookies with custom properties,
* but if you specify custom options for a cookie you must provide all the options for that cookie.
* If you use this feature, you will likely want to create conditional behavior
* to support setting different cookies policies in development and production builds,
* as you will be opting out of the built-in dynamic policy.
* * **Default value**: `{}`
* * **Required**: No
*
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: Partial<CookiesOptions>
/**
* If set to `true`, NextAuth.js will use either the `x-forwarded-host` or `host` headers,
* instead of `NEXTAUTH_URL`
* Make sure that reading `x-forwarded-host` on your hosting platform can be trusted.
* - ⚠ **This is an advanced option.** Advanced options are passed the same way as basic options,
* but **may have complex implications** or side effects.
* You should **try to avoid using advanced options** unless you are very comfortable using them.
* @default Boolean(process.env.NEXTAUTH_URL ?? process.env.AUTH_TRUST_HOST ?? process.env.VERCEL)
*/
trustHost?: boolean
}
/**
* Change the theme of the built-in pages.
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
* [Pages](https://next-auth.js.org/configuration/pages)
*/
export interface Theme {
colorScheme?: "auto" | "dark" | "light"
logo?: string
brandColor?: string
buttonText?: string
}
/**
* Different tokens returned by OAuth Providers.
* Some of them are available with different casing,
* but they refer to the same value.
*/
export type TokenSet = Partial<
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse
>
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends Partial<OpenIDTokenEndpointResponse> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
* - email: The user's email address.
* - credentials: `id` returned from the `authorize()` callback
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId?: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
/** The OAuth profile returned from your provider */
export interface Profile {
sub?: string
name?: string
email?: string
image?: string
}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<P = Profile, A = Account> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
* Throwing an error or returning a string will stop the flow, and redirect the user.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn: (params: {
user: User | AdapterUser
account: A | null
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile?: P
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
* When the callback is invoked after a user has clicked on a sign in link,
* this property will not be present. You can check for the `verificationRequest` property
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email?: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials?: Record<string, CredentialInput>
}) => Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
* you can use this callback to customise that behaviour.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect: (params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
baseUrl: string
}) => Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* ⚠ By default, only a subset (email, name, image)
* of the token is returned for increased security.
*
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitly forward it here to make it available to the client.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#session-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (params: {
token: JWT
user?: User | AdapterUser
account?: A | null
profile?: P
isNewUser?: boolean
}) => Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookieOption {
name: string
options: CookieSerializeOptions
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
state: CookieOption
nonce: CookieOption
}
/**
* The various event callbacks you can register for from next-auth
*
* [Documentation](https://next-auth.js.org/configuration/events)
*/
export interface EventCallbacks {
/**
* If using a `credentials` type auth, the user is the raw response from your
* credential provider.
* For other providers, you'll get the User object from your adapter, the account,
* and an indicator if the user was new to your Adapter.
*/
signIn: (message: {
user: User
account: Account | null
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter that is being ended.
*/
signOut: (message: { session: Session; token: JWT }) => Awaitable<void>
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser
account: Account
profile: User | AdapterUser
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter.
*/
session: (message: { session: Session; token: JWT }) => Awaitable<void>
}
export type EventType = keyof EventCallbacks
/** [Documentation](https://next-auth.js.org/configuration/pages) */
export interface PagesOptions {
signIn: string
signOut: string
/** Error code passed in query string as ?error= */
error: string
verifyRequest: string
/** If set, new users will be directed here on first sign in */
newUser: string
}
type ISODateString = string
export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
}
expires: ISODateString
}
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends DefaultSession {}
export type SessionStrategy = "jwt" | "database"
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
export interface SessionOptions {
/**
* Choose how you want to save the user session.
* The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie.
*
* If you use an `adapter` however, we default it to `"database"` instead.
* You can still force a JWT session by explicitly defining `"jwt"`.
*
* When using `"database"`, the session cookie will only contain a `sessionToken` value,
* which is used to look up the session in the database.
*
* [Documentation](https://next-auth.js.org/configuration/options#session) | [Adapter](https://next-auth.js.org/configuration/options#adapter) | [About JSON Web Tokens](https://next-auth.js.org/faq#json-web-tokens)
*/
strategy: SessionStrategy
/**
* Relative time from now in seconds when to expire the session
*
* @default 2592000 // 30 days
*/
maxAge: number
/**
* How often the session should be updated in seconds.
* If set to `0`, session is updated every time.
*
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
*
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => string
}
export interface DefaultUser {
id: string
name?: string | null
email?: string | null
image?: string | null
}
/**
* The shape of the returned object in the OAuth providers' `profile` callback,
* available in the `jwt` and `session` callbacks,
* or the second parameter of the `session` callback, when using a database.
*
* [`signIn` callback](https://next-auth.js.org/configuration/callbacks#sign-in-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User extends DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? OAuthConfigInternal<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
? CredentialsConfig
: never) & {
signinUrl: string
callbackUrl: string
}
export type AuthAction =
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
| "_log"
/** @internal */
export interface RequestInternal {
url: URL
method?: string
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
action: AuthAction
providerId?: string
error?: string
}
/** @internal */
export interface ResponseInternal<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: Headers | HeadersInit
body?: Body
redirect?: URL | string
cookies?: Cookie[]
}
/** @internal */
export interface InternalOptions<
TProviderType = ProviderType,
WithVerificationToken = TProviderType extends "email" ? true : false
> {
providers: InternalProvider[]
url: URL
action: AuthAction
provider: InternalProvider<TProviderType>
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
theme: Theme
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: Partial<PagesOptions>
jwt: JWTOptions
events: Partial<EventCallbacks>
adapter: WithVerificationToken extends true
? Adapter<WithVerificationToken>
: Adapter<WithVerificationToken> | undefined
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}

View File

@@ -1,62 +1,41 @@
import { UnknownError } from "../errors.js"
import { AuthError } from "../../errors.js"
/** Makes sure that error is always serializable */
function formatError(o: unknown): unknown {
if (o instanceof Error && !(o instanceof UnknownError)) {
return { message: o.message, stack: o.stack, name: o.name }
}
if (hasErrorProperty(o)) {
o.error = formatError(o.error) as Error
o.message = o.message ?? o.error.message
}
return o
}
function hasErrorProperty(
x: unknown
): x is { error: Error; [key: string]: unknown } {
return !!(x as any)?.error
}
export type WarningCode = "NEXTAUTH_URL" | "DEBUG_ENABLED"
export type WarningCode = "debug_enabled"
/**
* Override any of the methods, and the rest will use the default logger.
*
* [Documentation](https://next-auth.js.org/configuration/options#logger)
* [Documentation](https://authjs.dev/configuration/options#logger)
*/
export interface LoggerInstance extends Record<string, Function> {
warn: (code: WarningCode) => 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
error: (error: AuthError) => void
debug: (message: string, metadata?: unknown) => void
}
const _logger: LoggerInstance = {
error(code, metadata) {
metadata = formatError(metadata) as Error
const red = "\x1b[31m"
const yellow = "\x1b[33m"
const grey = "\x1b[90m"
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(
`[next-auth][error][${code}]`,
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
metadata.message,
metadata
`${red}[auth][error][${error.name}]${reset}: Read more at ${url}`
)
error.metadata && console.error(JSON.stringify(error.metadata, null, 2))
},
warn(code) {
console.warn(
`[next-auth][warn][${code}]`,
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`
)
const url = `https://errors.authjs.dev#${code}`
console.warn(`${yellow}[auth][warn][${code}]${reset}`, `Read more: ${url}`)
},
debug(code, metadata) {
console.log(`[next-auth][debug][${code}]`, metadata)
debug(message, metadata) {
console.log(
`${grey}[auth][debug]:${reset} ${message}`,
JSON.stringify(metadata, null, 2)
)
},
}
@@ -69,44 +48,9 @@ export function setLogger(
debug?: boolean
) {
// Turn off debug logging if `debug` isn't set to `true`
if (!debug) _logger.debug = () => {}
if (!debug) logger.debug = () => {}
if (newLogger.error) _logger.error = newLogger.error
if (newLogger.warn) _logger.warn = newLogger.warn
if (newLogger.debug) _logger.debug = newLogger.debug
}
export default _logger
/** Serializes client-side log messages and sends them to the server */
export function proxyLogger(
logger: LoggerInstance = _logger,
basePath?: string
): LoggerInstance {
try {
if (typeof window === "undefined") {
return logger
}
const clientLogger: Record<string, unknown> = {}
for (const level in logger) {
clientLogger[level] = (code: string, metadata: Error) => {
_logger[level](code, metadata) // Logs to console
if (level === "error") {
metadata = formatError(metadata) as Error
}
;(metadata as any).client = true
const url = `${basePath}/_log`
const body = new URLSearchParams({ level, code, ...(metadata as any) })
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, body)
}
return fetch(url, { method: "POST", body, keepalive: true })
}
}
return clientLogger as unknown as LoggerInstance
} catch {
return _logger
}
if (newLogger.error) logger.error = newLogger.error
if (newLogger.warn) logger.warn = newLogger.warn
if (newLogger.debug) logger.debug = newLogger.debug
}

View File

@@ -1,7 +1,7 @@
import { parse as parseCookie, serialize } from "cookie"
import type { RequestInternal, ResponseInternal } from "../index.js"
import { UnknownAction } from "./errors.js"
import type { AuthAction } from "./types.js"
import { AuthError, UnknownAction } from "../errors.js"
import type { AuthAction, RequestInternal, ResponseInternal } from "../types.js"
async function getBody(req: Request): Promise<Record<string, any> | undefined> {
if (!("body" in req) || !req.body || req.method !== "POST") return
@@ -14,12 +14,21 @@ async function getBody(req: Request): Promise<Record<string, any> | undefined> {
return Object.fromEntries(params)
}
}
// prettier-ignore
const actions: AuthAction[] = [ "providers", "session", "csrf", "signin", "signout", "callback", "verify-request", "error", "_log" ]
const actions: AuthAction[] = [
"providers",
"session",
"csrf",
"signin",
"signout",
"callback",
"verify-request",
"error",
]
export async function toInternalRequest(
req: Request
): Promise<RequestInternal | Error> {
): Promise<RequestInternal | AuthError> {
try {
// TODO: url.toString() should not include action and providerId
// see init.ts
@@ -31,6 +40,10 @@ export async function toInternalRequest(
throw new UnknownAction("Cannot detect action.")
}
if (req.method !== "GET" && req.method !== "POST") {
throw new UnknownAction("Only GET and POST requests are supported.")
}
const providerIdOrAction = pathname.split("/").pop()
let providerId
if (
@@ -45,7 +58,7 @@ export async function toInternalRequest(
url,
action,
providerId,
method: req.method ?? "GET",
method: req.method,
headers: Object.fromEntries(req.headers),
body: req.body ? await getBody(req) : undefined,
cookies: parseCookie(req.headers.get("cookie") ?? "") ?? {},

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "./index.js"
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
/**
* See more at:

View File

@@ -1,44 +1,117 @@
import type { CommonProviderOptions } from "./index.js"
import type { Awaitable, RequestInternal, User } from "../lib/types.js"
import type { Awaitable, RequestInternal, User } from "../types.js"
import type { JSXInternal } from "preact/src/jsx.js"
export interface CredentialInput {
/**
* Besieds providing type safety inside {@link CredentialsConfig.authorize}
* it also determines how the credentials input fields will be rendered
* on the default sign in page.
*/
export interface CredentialInput
extends Partial<JSXInternal.IntrinsicElements["input"]> {
label?: string
type?: string
value?: string
placeholder?: string
}
/** The Credentials Provider needs to be configured. */
export interface CredentialsConfig<
C extends Record<string, CredentialInput> = Record<string, CredentialInput>
CredentialsInputs extends Record<string, CredentialInput> = Record<
string,
CredentialInput
>
> extends CommonProviderOptions {
type: "credentials"
credentials: C
credentials: CredentialsInputs
/**
* Gives full control over how you handle the credentials received from the user.
*
* :::warning
* There is no validation on the user inputs by default, so make sure you do so
* by a popular library like [Zod](https://zod.dev)
* :::
*
* @example
* ```ts
* //...
* async authorize(, request) {
* const response = await fetch(request)
* if(!response.ok) return null
* return await response.json() ?? null
* }
* //...
*/
authorize: (
credentials: Record<keyof C, string> | undefined,
req: Pick<RequestInternal, "body" | "query" | "headers" | "method">
/** See {@link CredentialInput} */
credentials: Record<keyof CredentialsInputs, string> | undefined,
/** The original request is forward for convenience */
request: Request
) => Awaitable<User | null>
}
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(
options: Partial<CredentialsConfig<C>>
) => CredentialsConfig<C>
export type CredentialsProviderType = "Credentials"
type UserCredentialsConfig<C extends Record<string, CredentialInput>> = Partial<
Omit<CredentialsConfig<C>, "options">
> &
Pick<CredentialsConfig<C>, "authorize" | "credentials">
export default function Credentials<
export type CredentialsConfigInternal<
C extends Record<string, CredentialInput> = Record<string, CredentialInput>
>(options: UserCredentialsConfig<C>): CredentialsConfig<C> {
> = CredentialsConfig<C> & { options: CredentialsConfig<C> }
/**
* The Credentials provider allows you to handle signing in with arbitrary credentials,
* such as a username and password, domain, or two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
*
* It is intended to support use cases where you have an existing system you need to authenticate users against.
*
* It comes with the constraint that users authenticated in this manner are not persisted in the database,
* and consequently that the Credentials provider can only be used if JSON Web Tokens are enabled for sessions.
*
* :::warning **NOTE**
*
* The functionality provided for credentials based authentication is
* **intentionally limited** to _discourage_ use of passwords
* due to the _inherent security risks_ associated with them
* and the _additional complexity_ associated
* with supporting usernames and passwords.
*
* :::
*
* @example
* ```js
* import Auth from "@auth/core"
* import { Credentials } from "@auth/core/providers/credentials"
*
* const request = new Request("https://example.com")
* const resposne = await AuthHandler(request, {
* providers: [
* Credentials({
* credentials: {
* username: { label: "Username" },
* password: { label: "Password", type: "password" }
* },
* async authorize({ request }) {
* const response = await fetch(request)
* if(!response.ok) return null
* return await response.json() ?? null
* }
* })
* ],
* secret: "...",
* trustHost: true,
* })
* ```
* @see [Username/Password Example](https://authjs.dev/guides/providers/credentials#example---username--password)
* @see [Web3/Signin With Ethereum Example](https://authjs.dev/guides/providers/credentials#example---web3--signin-with-ethereum)
*/
export default function Credentials<
CredentialsInputs extends Record<string, CredentialInput> = Record<
string,
CredentialInput
>
>(config: Partial<CredentialsConfig<CredentialsInputs>>): CredentialsConfig {
return {
id: "credentials",
name: "Credentials",
type: "credentials",
credentials: {} as any,
credentials: {},
authorize: () => null,
options,
// @ts-expect-error
options: config,
}
}

View File

@@ -1,4 +1,4 @@
import type { OAuthConfig, OAuthUserConfig } from "./oauth"
import type { OAuthConfig, OAuthUserConfig } from "./oauth.js"
export interface DuendeISUser extends Record<string, any> {
email: string

View File

@@ -2,7 +2,7 @@ import { createTransport } from "nodemailer"
import type { CommonProviderOptions } from "./index.js"
import type { Options as SMTPTransportOptions } from "nodemailer/lib/smtp-transport"
import type { Awaitable, Theme } from "../index.js"
import type { Awaitable, Theme } from "../types.js"
export interface SendVerificationRequestParams {
identifier: string
@@ -13,11 +13,24 @@ export interface SendVerificationRequestParams {
theme: Theme
}
/**
* The Email Provider needs to be configured with an e-mail client.
* By default, it uses `nodemailer`, which you have to install if this
* provider is present.
*
* You can use a other services as well, like:
* - [Postmark](https://postmarkapp.com)
* - [Mailgun](https://www.mailgun.com)
* - [SendGrid](https://sendgrid.com)
* - etc.
*
* @see [Custom email service with Auth.js](https://authjs.dev/guides/providers/email#custom-email-service)
*/
export interface EmailConfig extends CommonProviderOptions {
type: "email"
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
server: string | SMTPTransportOptions
/** @default "NextAuth <no-reply@example.com>" */
/** @default `"Auth.js <no-reply@authjs.dev>"` */
from?: string
/**
* How long until the e-mail can be used to log the user in,
@@ -35,7 +48,7 @@ export interface EmailConfig extends CommonProviderOptions {
* You can make it predictable or modify it as you like with this method.
*
* @example
* ```js
* ```ts
* Providers.Email({
* async generateVerificationToken() {
* return "ABC123"
@@ -62,25 +75,20 @@ export interface EmailConfig extends CommonProviderOptions {
* [Documentation](https://next-auth.js.org/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
*/
normalizeIdentifier?: (identifier: string) => string
options: EmailUserConfig
}
export type EmailUserConfig = Partial<Omit<EmailConfig, "options">>
export type EmailProvider = (options: EmailUserConfig) => EmailConfig
// TODO: Rename to Token provider
// when started working on https://github.com/nextauthjs/next-auth/discussions/1465
export type EmailProviderType = "Email"
export type EmailProviderType = "email"
export default function Email(options: EmailUserConfig): EmailConfig {
/** TODO: */
export function Email(config: EmailConfig): EmailConfig {
return {
id: "email",
type: "email",
name: "Email",
// Server can be an SMTP connection string or a nodemailer config object
server: { host: "localhost", port: 25, auth: { user: "", pass: "" } },
from: "NextAuth <no-reply@example.com>",
from: "Auth.js <no-reply@authjs.dev>",
maxAge: 24 * 60 * 60,
async sendVerificationRequest(params) {
const { identifier, url, provider, theme } = params
@@ -98,7 +106,8 @@ export default function Email(options: EmailUserConfig): EmailConfig {
throw new Error(`Email (${failed.join(", ")}) could not be sent`)
}
},
options,
// @ts-expect-error
options: config,
}
}

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "./oauth"
import type { OAuthConfig, OAuthUserConfig } from "./oauth.js"
/**
* This is the default openid signature returned from FusionAuth

View File

@@ -57,14 +57,13 @@ export interface GithubProfile extends Record<string, any> {
}
/**
* Add GitHub login to your page and make requests to [GitHub
* APIs](https://docs.github.com/en/rest).
* Add GitHub login to your page and make requests to [GitHub APIs](https://docs.github.com/en/rest).
*
* ## Example
*
* @example
*
* ```js
* ```ts
* import Auth from "@auth/core"
* import { GitHub } from "@auth/core/providers/github"
*
@@ -80,7 +79,7 @@ export interface GithubProfile extends Record<string, any> {
* @see [GitHub - Authorizing OAuth Apps](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps)
* @see [GitHub - Configure your GitHub OAuth Apps](https://github.com/settings/developers)
* @see [Learn more about OAuth](https://authjs.dev/concepts/oauth)
* @see [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/github.ts) ---
* @see [Source code](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/github.ts)
*
* ## Notes
*

View File

@@ -45,6 +45,51 @@ export interface GitLabProfile extends Record<string, any> {
extra_shared_runners_minutes_limit: number
}
/**
* Add GitLab login to your page.
*
* ## Example
*
* @example
*
* ```js
* import Auth from "@auth/core"
* import { GitLab } from "@auth/core/providers/gitlab"
*
* const request = new Request("https://example.com")
* const resposne = await AuthHandler(request, {
* providers: [
* GitLab({clientId: "", clientSecret: ""})
* ]
* })
* ```
*
* ## Resources
*
* @see [Link 1](https://example.com)
*
* ## Notes
*
* By default, Auth.js assumes that the GitLab provider is
* based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification.
*
* :::tip
*
* The GitLab provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/gitlab.ts).
* To override the defaults for your use case, check out [customizing a built-in OAuth provider](https://authjs.dev/guides/providers/custom-provider#override-default-options).
*
* :::
*
* :::info **Disclaimer**
*
* If you think you found a bug in the default configuration, you can [open an issue](https://authjs.dev/new/provider-issue).
*
* Auth.js strictly adheres to the specification and it cannot take responsibility for any deviation from
* the spec by the provider. You can open an issue, but if the problem is non-compliance with the spec,
* we might not pursue a resolution. You can ask for more help in [Discussions](https://authjs.dev/new/github-discussions).
*
* :::
*/
export default function GitLab<P extends GitLabProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {

View File

@@ -1,29 +1,79 @@
import type { OAuthConfig, OAuthProvider, OAuthProviderType } from "./oauth.js"
import type { EmailConfig, EmailProvider, EmailProviderType } from "./email.js"
import { Profile } from "../types.js"
import type {
default as CredentialsProvider,
CredentialsConfig,
CredentialsProvider,
CredentialsProviderType,
} from "./credentials.js"
import type {
Email as EmailProvider,
EmailConfig,
EmailProviderType,
} from "./email.js"
import type {
OAuth2Config,
OAuthConfig,
OAuthProviderType,
OIDCConfig,
} from "./oauth.js"
export * from "./credentials.js"
export * from "./email.js"
export * from "./oauth.js"
/**
* Providers passed to Auth.js must define one of these types.
*
* @see [RFC 6749 - The OAuth 2.0 Authorization Framework](https://www.rfc-editor.org/rfc/rfc6749.html#section-2.3)
* @see [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication)
* @see [Email or Passwordless Authentication](https://authjs.dev/concepts/oauth)
* @see [Credentials-based Authentication](https://authjs.dev/concepts/credentials)
*/
export type ProviderType = "oidc" | "oauth" | "email" | "credentials"
/** Shared across all {@link ProviderType} */
export interface CommonProviderOptions {
/**
* Uniquely identifies the provider in {@link AuthConfig.providers}
* It's also part of the URL
*/
id: string
/**
* The provider name used on the default sign-in page's sign-in button.
* For example if it's "Google", the corresponding button will say:
* "Sign in with Google"
*/
name: string
/** See {@link ProviderType} */
type: ProviderType
options?: Record<string, unknown>
}
export type Provider = OAuthConfig<any> | EmailConfig | CredentialsConfig
/**
* Must be a supported authentication provider config:
* - {@link OAuthConfig}
* - {@link EmailConfigInternal}
* - {@link CredentialsConfigInternal}
*
* For more information, see the guides:
*
* @see [OAuth/OIDC guide](https://authjs.dev/guides/providers/custom-provider)
* @see [Email (Passwordless) guide](https://authjs.dev/guides/providers/email)
* @see [Credentials guide](https://authjs.dev/guides/providers/credentials)
*/
export type Provider<P extends Profile = Profile> = (
| OIDCConfig<P>
| OAuth2Config<P>
| EmailConfig
| CredentialsConfig
) & {
options: Record<string, unknown>
}
export type BuiltInProviders = Record<OAuthProviderType, OAuthProvider> &
Record<CredentialsProviderType, CredentialsProvider> &
Record<EmailProviderType, EmailProvider>
export type BuiltInProviders = Record<
OAuthProviderType,
(options: Partial<OAuthConfig<any>>) => OAuthConfig<any>
> &
Record<CredentialsProviderType, typeof CredentialsProvider> &
Record<EmailProviderType, typeof EmailProvider>
export type AppProviders = Array<
Provider | ReturnType<BuiltInProviders[keyof BuiltInProviders]>

View File

@@ -1,6 +1,5 @@
// THIS FILE IS AUTOGENERATED. DO NOT EDIT.
export type OAuthProviderType =
export type OAuthProviderType =
| "42-school"
| "apple"
| "atlassian"
@@ -41,7 +40,7 @@ export type OAuthProviderType =
| "medium"
| "naver"
| "netlify"
| "oauth-types"
| "oauth-types.js"
| "oauth"
| "okta"
| "onelogin"
@@ -67,4 +66,4 @@ export type OAuthProviderType =
| "yandex"
| "zitadel"
| "zoho"
| "zoom"
| "zoom"

View File

@@ -1,5 +1,5 @@
import type { Client } from "oauth4webapi"
import type { Awaitable, Profile, TokenSet, User } from "../index.js"
import type { Awaitable, Profile, TokenSet, User } from "../types.js"
import type { CommonProviderOptions } from "../providers/index.js"
// TODO:
@@ -95,13 +95,14 @@ export interface OAuthProviderButtonStyles {
textDark: string
}
/** TODO: */
export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
/**
* Identifies the provider when you want to sign in to
* a specific provider.
*
* @example
* ```js
* ```ts
* signIn('github') // "github" is the provider ID
* ```
*/
@@ -163,6 +164,7 @@ export interface OAuth2Config<P> extends CommonProviderOptions, PartialIssuer {
options?: OAuthUserConfig<P>
}
/** TODO: */
export interface OIDCConfig<P> extends Omit<OAuth2Config<P>, "type"> {
type: "oidc"
}
@@ -192,7 +194,3 @@ export type OAuthUserConfig<P> = Omit<
"options" | "type"
> &
Required<Pick<OAuthConfig<P>, "clientId" | "clientSecret">>
export type OAuthProvider = (
options: Partial<OAuthConfig<any>>
) => OAuthConfig<any>

View File

@@ -1,4 +1,4 @@
import { OAuthConfig, OAuthUserConfig } from "./index.js"
import type { OAuthConfig, OAuthUserConfig } from "./index.js"
export interface PinterestProfile extends Record<string, any> {
account_type: "BUSINESS" | "PINNER"

View File

@@ -18,19 +18,24 @@ export interface SpotifyProfile extends Record<string, any> {
*
* @example
*
* ```js
* ```ts
* import Auth from "@auth/core"
* import { Spotify } from "@auth/core/providers/spotify"
*
* const request = new Request("https://example.com")
* const resposne = await AuthHandler(request, {
* providers: [Spotify({ clientId: "", clientSecret: "" })],
* providers: [
* Spotify({clientId: "", clientSecret: ""})
* ]
* })
* ```
*
* ## Resources
* ---
*
* @see [Link 1](https://example.com) ---
* ## Resources
* @see [Link 1](https://example.com)
*
* ---
*
* ## Notes
*

View File

@@ -1,9 +1,475 @@
/**
* `@auth/core/types` contains all public types and interfaces of the core package.
*
* This module contains public types and interfaces of the core package.
*
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install @auth/core
* ```
*
* You can then import this submodule from `@auth/core/type`.
*
* ## Usage
*
* Even if you don't use TypeScript, IDEs like VSCode will pick up types to provide you with a better developer experience.
* While you are typing, you will get suggestions about what certain objects/functions look like,
* and sometimes links to documentation, examples, and other valuable resources.
*
* Generally, you will not need to import types from this module.
* Mostly when using the `Auth` function and optionally the `AuthConfig` interface,
* everything inside there will already be typed.
*
* :::tip
* Inside the `Auth` function, you won't need to use a single type from this module.
*
* @example
* ```ts title=index.ts
* import { Auth } from "@auth/core"
*
* const request = new Request("https://example.com")
* const response = await Auth(request, {
* callbacks: {
* jwt(): JWT { // <-- This is unnecessary!
* return { foo: "bar" }
* },
* session(
* { session, token }: { session: Session; token: JWT } // <-- This is unnecessary!
* ) {
* return session
* },
* }
* })
* ```
* :::
*
* :::info
* We are advocates of TypeScript, as it will help you catch errors at build-time, before your users do. 😉
* :::
*
* ## Resources
*
* - [TypeScript - The Basics](https://www.typescriptlang.org/docs/handbook/2/basic-types.html)
* - [Extending built-in types](https://authjs.dev/getting-started/typescript#module-augmentation)
*
* @module types
*/
// TODO: move types directly into this file.
import type { CookieSerializeOptions } from "cookie"
import type {
OAuth2TokenEndpointResponse,
OpenIDTokenEndpointResponse,
} from "oauth4webapi"
import type { Adapter, AdapterUser } from "./adapters.js"
import type {
CredentialInput,
CredentialsConfig,
EmailConfig,
OAuthConfigInternal,
ProviderType,
} from "./providers/index.js"
import type { JWT, JWTOptions } from "./jwt.js"
import type { Cookie } from "./lib/cookie.js"
import type { LoggerInstance } from "./lib/utils/logger.js"
export * from "./lib/types.js"
export type { AuthConfig } from "./index.js"
export type Awaitable<T> = T | PromiseLike<T>
export type { LoggerInstance }
/**
* Change the theme of the built-in pages.
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
* [Pages](https://next-auth.js.org/configuration/pages)
*/
export interface Theme {
colorScheme?: "auto" | "dark" | "light"
logo?: string
brandColor?: string
buttonText?: string
}
/**
* Different tokens returned by OAuth Providers.
* Some of them are available with different casing,
* but they refer to the same value.
*/
export type TokenSet = Partial<
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse
>
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends Partial<OpenIDTokenEndpointResponse> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
* - email: The user's email address.
* - credentials: `id` returned from the `authorize()` callback
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId?: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
/** The OAuth profile returned from your provider */
export interface Profile {
sub?: string
name?: string
email?: string
image?: string
}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<P = Profile, A = Account> {
/**
* Control whether a user is allowed to sign in or not.
* Returning `true` continues the sign-in flow, while
* returning `false` redirects to the {@link PagesOptions.error error page}.
* The `error` {@link ErrorPageParam parameter} is set to `AccessDenied`.
*
* Unhandled errors are redirected to the error page
* The `error` parameter is set to `Configuration`.
* an `AuthorizedCallbackError` is logged on the server.
*
* @see https://authjs.dev/reference/errors#authorizedcallbackerror
* @todo rename to `authorized`
*/
signIn: (params: {
user: User | AdapterUser
account: A | null
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile?: P
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
* When the callback is invoked after a user has clicked on a sign in link,
* this property will not be present. You can check for the `verificationRequest` property
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email?: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials?: Record<string, CredentialInput>
}) => Awaitable<boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
* you can use this callback to customise that behaviour.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect: (params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
baseUrl: string
}) => Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* ⚠ By default, only a subset (email, name, image)
* of the token is returned for increased security.
*
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitly forward it here to make it available to the client.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#session-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (params: {
token: JWT
user?: User | AdapterUser
account?: A | null
profile?: P
isNewUser?: boolean
}) => Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookieOption {
name: string
options: CookieSerializeOptions
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
state: CookieOption
nonce: CookieOption
}
/**
* The various event callbacks you can register for from next-auth
*
* [Documentation](https://next-auth.js.org/configuration/events)
*/
export interface EventCallbacks {
/**
* If using a `credentials` type auth, the user is the raw response from your
* credential provider.
* For other providers, you'll get the User object from your adapter, the account,
* and an indicator if the user was new to your Adapter.
*/
signIn: (message: {
user: User
account: Account | null
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter that is being ended.
*/
signOut: (
message:
| { session: Awaited<ReturnType<Adapter["deleteSession"]>> }
| { token: Awaited<ReturnType<JWTOptions["decode"]>> }
) => Awaitable<void>
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User | AdapterUser
account: Account
profile: User | AdapterUser
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
* if you use JWT or database persisted sessions:
* - `token`: The JWT token for this session.
* - `session`: The session object from your adapter.
*/
session: (message: { session: Session; token: JWT }) => Awaitable<void>
}
export type EventType = keyof EventCallbacks
/** TODO: Check if all these are used/correct */
export type ErrorPageParam = "Configuration" | "AccessDenied" | "Verification"
/** TODO: Check if all these are used/correct */
export type SignInPageErrorParam =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
| "OAuthAccountNotLinked"
| "EmailSignin"
| "CredentialsSignin"
| "SessionRequired"
export interface PagesOptions {
/**
* The path to the sign in page.
*
* The optional "error" query parameter is set to
* one of the {@link SignInPageErrorParam available} values.
*
* @default "/signin"
*/
signIn: string
signOut: string
/**
* The path to the error page.
*
* The optional "error" query parameter is set to
* one of the {@link ErrorPageParam available} values.
*
* @default "/error"
*/
error: string
verifyRequest: string
/** If set, new users will be directed here on first sign in */
newUser: string
}
type ISODateString = string
export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
image?: string | null
}
expires: ISODateString
}
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends DefaultSession {}
export type SessionStrategy = "jwt" | "database"
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
export interface SessionOptions {
/**
* Choose how you want to save the user session.
* The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie.
*
* If you use an `adapter` however, we default it to `"database"` instead.
* You can still force a JWT session by explicitly defining `"jwt"`.
*
* When using `"database"`, the session cookie will only contain a `sessionToken` value,
* which is used to look up the session in the database.
*
* [Documentation](https://next-auth.js.org/configuration/options#session) | [Adapter](https://next-auth.js.org/configuration/options#adapter) | [About JSON Web Tokens](https://next-auth.js.org/faq#json-web-tokens)
*/
strategy: SessionStrategy
/**
* Relative time from now in seconds when to expire the session
*
* @default 2592000 // 30 days
*/
maxAge: number
/**
* How often the session should be updated in seconds.
* If set to `0`, session is updated every time.
*
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
*
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => string
}
export interface DefaultUser {
id: string
name?: string | null
email?: string | null
image?: string | null
}
/**
* The shape of the returned object in the OAuth providers' `profile` callback,
* available in the `jwt` and `session` callbacks,
* or the second parameter of the `session` callback, when using a database.
*
* [`signIn` callback](https://next-auth.js.org/configuration/callbacks#sign-in-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User extends DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? OAuthConfigInternal<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
? CredentialsConfig
: never) & {
signinUrl: string
callbackUrl: string
}
export type AuthAction =
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
/** @internal */
export interface RequestInternal {
url: URL
method: "GET" | "POST"
cookies?: Partial<Record<string, string>>
headers?: Record<string, any>
query?: Record<string, any>
body?: Record<string, any>
action: AuthAction
providerId?: string
error?: string
}
/** @internal */
export interface ResponseInternal<
Body extends string | Record<string, any> | any[] = any
> {
status?: number
headers?: Headers | HeadersInit
body?: Body
redirect?: URL | string
cookies?: Cookie[]
}
// TODO: rename to AuthConfigInternal
/** @internal */
export interface InternalOptions<TProviderType = ProviderType> {
providers: InternalProvider[]
url: URL
action: AuthAction
provider: InternalProvider<TProviderType>
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
theme: Theme
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: Partial<PagesOptions>
jwt: JWTOptions
events: Partial<EventCallbacks>
adapter: Adapter | undefined
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}

View File

@@ -0,0 +1,23 @@
diff --git a/dist/theme.js b/dist/theme.js
index 1483a4b4ec69583aa3086eac83b2b31ae8bb6777..c30e7a4f7785fc230099e8b904040dd4aa57c38e 100644
--- a/dist/theme.js
+++ b/dist/theme.js
@@ -221,12 +221,12 @@ class MarkdownTheme extends typedoc_1.Theme {
directory: 'enums',
template: this.getReflectionTemplate(),
},
- {
- kind: [typedoc_1.ReflectionKind.Class],
- isLeaf: false,
- directory: 'classes',
- template: this.getReflectionTemplate(),
- },
+ // {
+ // kind: [typedoc_1.ReflectionKind.Class],
+ // isLeaf: false,
+ // directory: 'classes',
+ // template: this.getReflectionTemplate(),
+ // },
{
kind: [typedoc_1.ReflectionKind.Interface],
isLeaf: false,

10
pnpm-lock.yaml generated
View File

@@ -3,6 +3,11 @@ lockfileVersion: 5.4
overrides:
undici: 5.11.0
patchedDependencies:
typedoc-plugin-markdown@3.14.0:
hash: p7xjg2asbi3h7h4efxnoor54kq
path: patches/typedoc-plugin-markdown@3.14.0.patch
importers:
.:
@@ -54,7 +59,7 @@ importers:
ts-node: 10.5.0_ksn4eycaeggbrckn3ykh37hwf4
turbo: 1.3.1
typedoc: 0.23.22_typescript@4.8.4
typedoc-plugin-markdown: 3.14.0_typedoc@0.23.22
typedoc-plugin-markdown: 3.14.0_p7xjg2asbi3h7h4efxnoor54kq_typedoc@0.23.22
typescript: 4.8.4
apps/dev:
@@ -27473,7 +27478,7 @@ packages:
dependencies:
is-typedarray: 1.0.0
/typedoc-plugin-markdown/3.14.0_typedoc@0.23.22:
/typedoc-plugin-markdown/3.14.0_p7xjg2asbi3h7h4efxnoor54kq_typedoc@0.23.22:
resolution: {integrity: sha512-UyQLkLRkfTFhLdhSf3RRpA3nNInGn+k6sll2vRXjflaMNwQAAiB61SYbisNZTg16t4K1dt1bPQMMGLrxS0GZ0Q==}
peerDependencies:
typedoc: '>=0.23.0'
@@ -27481,6 +27486,7 @@ packages:
handlebars: 4.7.7
typedoc: 0.23.22_typescript@4.8.4
dev: true
patched: true
/typedoc/0.23.22_typescript@4.8.4:
resolution: {integrity: sha512-5sJkjK60xp8A7YpcYniu3+Wf0QcgojEnhzHuCN+CkdpQkKRhOspon/9+sGTkGI8kjVkZs3KHrhltpQyVhRMVfw==}