diff --git a/docs/docs/concepts/faq.md b/docs/docs/concepts/faq.md index 4279eb7e..3c86a4c4 100644 --- a/docs/docs/concepts/faq.md +++ b/docs/docs/concepts/faq.md @@ -269,7 +269,7 @@ Ultimately if your request is not accepted or is not actively in development, yo

-Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWT tokens are now encrypted by default with A256GCM. +Auth.js by default uses JSON Web Tokens for saving the user's session. However, if you use a [database adapter](/guides/adapters/using-a-database-adapter), the database will be used to persist the user's session. You can force the usage of JWT when using a database [through the configuration options](/reference/configuration/auth-config#session). Since v4 all our JWTs are now encrypted by default with A256GCM.

diff --git a/docs/docs/guides/adapters/creating-a-database-adapter.md b/docs/docs/guides/adapters/creating-a-database-adapter.md index e8620008..b31ca33f 100644 --- a/docs/docs/guides/adapters/creating-a-database-adapter.md +++ b/docs/docs/guides/adapters/creating-a-database-adapter.md @@ -6,7 +6,7 @@ Using a custom adapter you can connect to any database back-end or even several ## How to create an adapter -For more information about the data these methods need to manage see [models](/reference/adapters/models). +For more information about the data these methods need to manage see [models](/reference/adapters#models). _See the code below for practical example._ diff --git a/docs/docs/guides/basics/events.md b/docs/docs/guides/basics/events.md index 33305562..2d699eac 100644 --- a/docs/docs/guides/basics/events.md +++ b/docs/docs/guides/basics/events.md @@ -29,7 +29,7 @@ Sent when the user signs out. 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. +- `token`: The JWT for this session. - `session`: The session object from your adapter that is being ended ### createUser @@ -60,5 +60,5 @@ Sent at the end of a request for the current session. 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. +- `token`: The JWT for this session. - `session`: The session object from your adapter. diff --git a/docs/docs/reference/adapters/index.md b/docs/docs/reference/adapters/index.md index 1ad11728..17662141 100644 --- a/docs/docs/reference/adapters/index.md +++ b/docs/docs/reference/adapters/index.md @@ -2,7 +2,7 @@ title: Overview --- -Using a Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community: +Using an Auth.js / NextAuth.js adapter you can connect to any database service or even several different services at the same time. The following listed official adapters are created and maintained by the community:
@@ -71,7 +71,7 @@ If you don't find an adapter for the database or service you use, you can always ## Models -Auth.js can be used with any database. Models tell you what structures Auth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general, will look something like this. Each adapter's model/schema will be slightly adapted for its needs, but will look very much like this schema below: +Auth.js can be used with any database. Models tell you what structures Auth.js expects from your database. Models will vary slightly depending on which adapter you use, but in general, will look something like this: ```mermaid erDiagram @@ -96,15 +96,8 @@ erDiagram string type string provider string providerAccountId - string refresh_token string access_token - int expires_at - string token_type - string scope string id_token - string session_state - string oauth_token_secret - string oauth_token } VerificationToken { string identifier @@ -113,10 +106,10 @@ erDiagram } ``` -More information about each Model / Table can be found below. +More information about each Model/Table can be found below. :::note -You can [create your own adapter](/guides/adapters/creating-a-database-adapter) if you want to use Auth.js with a database that is not supported out of the box, or you have to change fields on any of the models. +You can [create your adapter](/guides/adapters/creating-a-database-adapter) if you want to use Auth.js with a database that is not supported out of the box, or you have to change fields on any of the models. ::: --- @@ -125,30 +118,31 @@ You can [create your own adapter](/guides/adapters/creating-a-database-adapter) The User model is for information such as the user's name and email address. -Email address is optional, but if one is specified for a User then it must be unique. +Email address is optional, but if one is specified for a User, then it must be unique. :::note -If a user first signs in with OAuth then their email address is automatically populated using the one from their OAuth profile, if the OAuth provider returns one. +If a user first signs in with an OAuth provider, then their email address is automatically populated using the one from their OAuth profile if the OAuth provider returns one. -This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if the [Email Provider](/getting-started/email-tutorial) is configured). +This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in the future (if the [Email Provider](/reference/core/providers_email) is configured). ::: -User creation in the database is automatic, and happens when the user is logging in for the first time with a provider. The default data saved is `id`, `name`, `email` and `image`. You can add more profile data by returning extra fields in your [OAuth provider](/guides/providers/custom-provider)'s [`profile()`](/reference/core/providers#profile) callback. +User creation in the database is automatic and happens when the user is logging in for the first time with a provider. +If the first sign-in is via the [OAuth Provider](/reference/core/providers_oauth), the default data saved is `id`, `name`, `email` and `image`. You can add more profile data by returning extra fields in your [OAuth provider](/guides/providers/custom-provider)'s [`profile()`](/reference/core/providers#profile) callback. + +If the first sign-in is via the [Email Provider](/reference/core/providers_email), then the saved user will have `id`, `email`, `emailVerified`, where `emailVerified` is the timestamp of when the user was created. ### Account -The Account model is for information about OAuth accounts associated with a User. It will usually contain `access_token`, `id_token` and other OAuth specific data. [`TokenSet`](https://github.com/panva/node-openid-client/blob/main/docs/README.md#new-tokensetinput) from `openid-client` might give you an idea of all the fields. - -:::note -In case of an OAuth 1.0 provider (like Twitter), you will have to look for `oauth_token` and `oauth_token_secret` string fields. GitHub also has an extra `refresh_token_expires_in` integer field. You have to make sure that your database schema includes these fields. -::: +The Account model is for information about OAuth accounts associated with a User A single User can have multiple Accounts, but each Account can only have one User. -Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/concepts/faq#security) for more information why this is a requirement. +Account creation in the database is automatic and happens when the user is logging in for the first time with a provider, or the [`Adapter.linkAccount`](/reference/core/adapters#linkaccount) method is invoked. The default data saved is `access_token`, `refresh_token`, `id_token` and `expires_at`. You can save other fields by returning them in the [OAuth provider](/guides/providers/custom-provider)'s [`account()`](/reference/core/providers#account) callback. + +Linking Accounts to Users happen automatically, only when they have the same e-mail address, and the user is currently signed in. Check the [FAQ](/concepts/faq#security) for more information on why this is a requirement. :::tip -You can manually unlink accounts, if your adapter implements the `unlinkAccount` method. Make sure to take all the necessary security steps to avoid data loss. +You can manually unlink accounts if your adapter implements the `unlinkAccount` method. Make sure to take all the necessary security steps to avoid data loss. ::: :::note @@ -162,7 +156,7 @@ The Session model is used for database sessions. It is not used if JSON Web Toke A single User can have multiple Sessions, each Session can only have one User. :::tip -When a Session is read, we check if it's `expires` field indicates an invalid session, and delete it from the database. You can also do this clean-up periodically in the background to avoid our extra delete call to the database during an active session retrieval. This might result in a slight performance increase in a few cases. +When a Session is read, we check if its `expires` field indicates an invalid session, and delete it from the database. You can also do this clean-up periodically in the background to avoid our extra delete call to the database during an active session retrieval. This might result in a slight performance increase in a few cases. ::: ### Verification Token @@ -171,7 +165,7 @@ The Verification Token model is used to store tokens for passwordless sign in. A single User can have multiple open Verification Tokens (e.g. to sign in to different devices). -It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / short codes). +It has been designed to be extendable for other verification purposes in the future (e.g. 2FA / magic codes, etc.). :::note Auth.js makes sure that every token is usable only once, and by default has a short (1 day, can be configured by [`maxAge`](/guides/providers/email)) lifetime. If your user did not manage to finish the sign-in flow in time, they will have to start the sign-in process again. @@ -183,8 +177,7 @@ Due to users forgetting or failing at the sign-in flow, you might end up with un ## RDBMS Naming Convention -Auth.js / NextAuth.js uses `camelCase` for its own database rows, while respecting the conventional `snake_case` formatting for OAuth related values. If mixed casing is an issue for you, most adapters have a dedicated section on how to use a single naming convention. - +Auth.js / NextAuth.js uses `camelCase` for its database rows while respecting the conventional `snake_case` formatting for OAuth-related values. If the mixed casing is an issue for you, most adapters have a dedicated documentation section on how to force a casing convention. ## TypeScript diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index eca23d45..bb600d45 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -7,7 +7,7 @@ const path = require("path") const coreSrc = "../packages/core/src" const providers = fs .readdirSync(path.join(__dirname, coreSrc, "/providers")) - .filter((file) => file.endsWith(".ts") && !file.startsWith("oauth")) + .filter((file) => file.endsWith(".ts")) .map((p) => `${coreSrc}/providers/${p}`) const typedocConfig = require("./typedoc.json") diff --git a/docs/static/img/providers/authentik.svg b/docs/static/img/providers/authentik.svg new file mode 100644 index 00000000..517eb179 --- /dev/null +++ b/docs/static/img/providers/authentik.svg @@ -0,0 +1 @@ + diff --git a/packages/adapter-neo4j/.npmrc b/packages/adapter-neo4j/.npmrc new file mode 100644 index 00000000..ae643592 --- /dev/null +++ b/packages/adapter-neo4j/.npmrc @@ -0,0 +1 @@ +//registry.npmjs.org/:_authToken=${NPM_TOKEN} diff --git a/packages/adapter-neo4j/package.json b/packages/adapter-neo4j/package.json index 531ad58d..0133bdc8 100644 --- a/packages/adapter-neo4j/package.json +++ b/packages/adapter-neo4j/package.json @@ -1,6 +1,6 @@ { "name": "@next-auth/neo4j-adapter", - "version": "1.0.5", + "version": "1.0.6", "description": "neo4j adapter for next-auth.", "homepage": "https://authjs.dev", "repository": "https://github.com/nextauthjs/next-auth", @@ -33,7 +33,7 @@ "dist" ], "peerDependencies": { - "neo4j-driver": "^4.0.0", + "neo4j-driver": "^4.0.0 || ^5.7.0", "next-auth": "^4" }, "devDependencies": { @@ -41,7 +41,7 @@ "@next-auth/tsconfig": "workspace:*", "@types/uuid": "^8.3.3", "jest": "^27.4.3", - "neo4j-driver": "^4.4.0", + "neo4j-driver": "^5.7.0", "next-auth": "workspace:*" }, "dependencies": { @@ -50,4 +50,4 @@ "jest": { "preset": "@next-auth/adapter-test/jest" } -} +} \ No newline at end of file diff --git a/packages/adapter-xata/src/index.ts b/packages/adapter-xata/src/index.ts index 860c728d..fefea69c 100644 --- a/packages/adapter-xata/src/index.ts +++ b/packages/adapter-xata/src/index.ts @@ -195,7 +195,7 @@ import type { XataClient } from "./xata" * xata init --schema=./path/to/your/schema.json * ``` * - * The CLI will walk you through a setup process where you choose a [workspace](https://docs.xata.io/concepts/workspaces) (kind of like a GitHub org or a Vercel team) and an appropriate database. We recommend using a fresh database for this, as we'll augment it with tables that Auth.js needs. + * The CLI will walk you through a setup process where you choose a [workspace](https://xata.io/docs/api-reference/workspaces) (kind of like a GitHub org or a Vercel team) and an appropriate database. We recommend using a fresh database for this, as we'll augment it with tables that Auth.js needs. * * Once you're done, you can continue using Auth.js in your project as expected, like creating a `./pages/api/auth/[...nextauth]` route. * diff --git a/packages/core/package.json b/packages/core/package.json index ffcdbbd5..9c3253ad 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@auth/core", - "version": "0.7.0", + "version": "0.7.1", "description": "Authentication for the Web.", "keywords": [ "authentication", @@ -93,4 +93,4 @@ "postcss": "8.4.19", "postcss-nested": "6.0.0" } -} +} \ No newline at end of file diff --git a/packages/core/src/adapters.ts b/packages/core/src/adapters.ts index 4d67cbce..8dfa65db 100644 --- a/packages/core/src/adapters.ts +++ b/packages/core/src/adapters.ts @@ -228,6 +228,10 @@ export interface Adapter { deleteUser?( userId: string ): Promise | Awaitable + /** + * This method is invoked internally (but optionally can be used for manual linking). + * It creates an [Account](https://authjs.dev/reference/adapters#models) in the database. + */ linkAccount?( account: AdapterAccount ): Promise | Awaitable diff --git a/packages/core/src/errors.ts b/packages/core/src/errors.ts index 35589432..d6c2d5dd 100644 --- a/packages/core/src/errors.ts +++ b/packages/core/src/errors.ts @@ -20,13 +20,6 @@ export class AuthError extends Error { } } -/** - * @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. @@ -37,8 +30,8 @@ export class AdapterError extends AuthError {} export class AuthorizedCallbackError extends AuthError {} /** - * There was an error while trying to finish up authenticating the user. - * Depending on the type of provider, this could be for multiple reasons. + * This error occurs when the user cannot finish the sign-in process. + * Depending on the provider type, this could have happened for multiple reasons. * * :::tip * Check out `[auth][details]` in the error message to know which provider failed. @@ -48,7 +41,7 @@ export class AuthorizedCallbackError extends AuthError {} * ``` * ::: * - * For an **OAuth provider**, possible causes are: + * For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: * - The user denied access to the application * - There was an error parsing the OAuth Profile: * Check out the provider's `profile` or `userinfo.request` method to make sure @@ -56,7 +49,7 @@ export class AuthorizedCallbackError extends AuthError {} * - The `signIn` or `jwt` callback methods threw an uncaught error: * Check the callback method implementations. * - * For an **Email provider**, possible causes are: + * For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: * - The provided email/token combination was invalid/missing: * Check if the provider's `sendVerificationRequest` method correctly sends the email. * - The provided email/token combination has expired: @@ -64,7 +57,7 @@ export class AuthorizedCallbackError extends AuthError {} * - There was an error with the database: * Check the database logs. * - * For a **Credentials provider**, possible causes are: + * For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), possible causes are: * - The `authorize` method threw an uncaught error: * Check the provider's `authorize` method. * - The `signIn` or `jwt` callback methods threw an uncaught error: @@ -107,11 +100,30 @@ export class MissingAPIRoute extends AuthError {} /** @todo */ export class MissingAuthorize extends AuthError {} -/** @todo */ +/** + * Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data. + * + * :::note + * If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable. + * Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret). + * ::: + * + * + * :::tip + * You can generate a good secret value: + * - On Unix systems: type `openssl rand -hex 32` in the terminal + * - Or generate one [online](https://generate-secret.vercel.app/32) + * + * ::: + */ export class MissingSecret extends AuthError {} -/** @todo */ -export class OAuthSignInError extends AuthError {} +/** + * @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 OAuthAccountNotLinked extends AuthError {} /** @todo */ export class OAuthCallbackError extends AuthError {} @@ -119,19 +131,51 @@ export class OAuthCallbackError extends AuthError {} /** @todo */ export class OAuthCreateUserError extends AuthError {} -/** @todo */ +/** + * This error occurs during an OAuth sign in attempt when the provdier's + * response could not be parsed. This could for example happen if the provider's API + * changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly. + */ export class OAuthProfileParseError extends AuthError {} /** @todo */ export class SessionTokenError extends AuthError {} -/** @todo */ +/** + * This error occurs when the user cannot initiate the sign-in process. + * Depending on the provider type, this could have happened for multiple reasons. + * + * :::tip + * Check out `[auth][details]` in the error message to know which provider failed. + * @example + * ```sh + * [auth][details]: { "provider": "github" } + * ``` + * ::: + * + * For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: + * - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html) + * Check the details in the error message. + * - A runtime error occurred in Auth.js. This should be reported as a bug. + * + * For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: + * - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier) + * - The provided email/token combination has expired: + * Ask the user to log in again. + * - There was an error with the database: + * Check the database logs. + * + */ export class SignInError extends AuthError {} /** @todo */ export class SignOutError extends AuthError {} -/** @todo */ +/** + * Auth.js was requested to handle an operation that it does not support. + * + * See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions. + */ export class UnknownAction extends AuthError {} /** @todo */ diff --git a/packages/core/src/jwt.ts b/packages/core/src/jwt.ts index 73f566ce..87f8cd1a 100644 --- a/packages/core/src/jwt.ts +++ b/packages/core/src/jwt.ts @@ -190,7 +190,7 @@ export interface JWTEncodeParams { /** * The maximum age of the Auth.js issued JWT in seconds. * - * @default 30 * 24 * 30 * 60 // 30 days + * @default 30 * 24 * 60 * 60 // 30 days */ maxAge?: number } @@ -213,7 +213,7 @@ export interface JWTOptions { /** * The maximum age of the Auth.js issued JWT in seconds. * - * @default 30 * 24 * 30 * 60 // 30 days + * @default 30 * 24 * 60 * 60 // 30 days */ maxAge: number /** Override this method to control the Auth.js issued JWT encoding. */ diff --git a/packages/core/src/lib/callback-handler.ts b/packages/core/src/lib/callback-handler.ts index 2344a81d..bae3ade9 100644 --- a/packages/core/src/lib/callback-handler.ts +++ b/packages/core/src/lib/callback-handler.ts @@ -1,4 +1,4 @@ -import { AccountNotLinked } from "../errors.js" +import { OAuthAccountNotLinked } from "../errors.js" import { fromDate } from "./utils/date.js" import type { @@ -49,7 +49,7 @@ export async function handleLogin( } const profile = _profile as AdapterUser - const account = _account as AdapterAccount + let account = _account as AdapterAccount const { createUser, @@ -122,113 +122,116 @@ export async function handleLogin( }) return { session, user, isNewUser } - } else if (account.type === "oauth" || account.type === "oidc") { - // If signing in with OAuth account, check to see if the account exists already - const userByAccount = await getUserByAccount({ - providerAccountId: account.providerAccountId, - provider: account.provider, - }) - if (userByAccount) { - if (user) { - // If the user is already signed in with this account, we don't need to do anything - if (userByAccount.id === user.id) { - return { session, user, isNewUser } - } - // 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 AccountNotLinked( - "The account is already associated with another user", - { provider: account.provider } - ) - } - // If there is no active session, but the account being signed in with is already - // associated with a valid user then create session to sign the user in. - session = useJwtSession - ? {} - : await createSession({ - sessionToken: generateSessionToken(), - userId: userByAccount.id, - expires: fromDate(options.session.maxAge), - }) + } - return { session, user: userByAccount, isNewUser } - } else { - if (user) { - // If the user is already signed in and the OAuth account isn't already associated - // with another user account then we can go ahead and link the accounts safely. - await linkAccount({ ...account, userId: user.id }) - await events.linkAccount?.({ user, account, profile }) - - // As they are already signed in, we don't need to do anything after linking them + // If signing in with OAuth account, check to see if the account exists already + const userByAccount = await getUserByAccount({ + providerAccountId: account.providerAccountId, + provider: account.provider, + }) + if (userByAccount) { + if (user) { + // If the user is already signed in with this account, we don't need to do anything + if (userByAccount.id === user.id) { return { session, user, isNewUser } } + // 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 OAuthAccountNotLinked( + "The account is already associated with another user", + { provider: account.provider } + ) + } + // If there is no active session, but the account being signed in with is already + // associated with a valid user then create session to sign the user in. + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: userByAccount.id, + expires: fromDate(options.session.maxAge), + }) - // If the user is not signed in and it looks like a new OAuth account then we - // check there also isn't an user account already associated with the same - // email address as the one in the OAuth profile. - // - // This step is often overlooked in OAuth implementations, but covers the following cases: - // - // 1. It makes it harder for someone to accidentally create two accounts. - // e.g. by signin in with email, then again with an oauth account connected to the same email. - // 2. It makes it harder to hijack a user account using a 3rd party OAuth account. - // e.g. by creating an oauth account then changing the email address associated with it. - // - // It's quite common for services to automatically link accounts in this case, but it's - // better practice to require the user to sign in *then* link accounts to be sure - // someone is not exploiting a problem with a third party OAuth service. - // - // OAuth providers should require email address verification to prevent this, but in - // practice that is not always the case; this helps protect against that. - const userByEmail = profile.email - ? await getUserByEmail(profile.email) - : null - if (userByEmail) { - const provider = options.provider as OAuthConfig - if (provider?.allowDangerousEmailAccountLinking) { - // If you trust the oauth provider to correctly verify email addresses, you can opt-in to - // account linking even when the user is not signed-in. - user = userByEmail - } else { - // We end up here when we don't have an account with the same [provider].id *BUT* - // we do already have an account with the same email address as the one in the - // OAuth profile the user has just tried to sign in with. - // - // 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 AccountNotLinked( - "Another account already exists with the same e-mail address", - { provider: account.provider } - ) - } - } else { - // If the current user is not logged in and the profile isn't linked to any user - // accounts (by email or provider account id)... - // - // If no account matching the same [provider].id or .email exists, we can - // create a new account for the user, link it to the OAuth account and - // create a new session for them so they are signed in with it. - const { id: _, ...newUser } = { ...profile, emailVerified: null } - user = await createUser(newUser) - } - await events.createUser?.({ user }) + return { session, user: userByAccount, isNewUser } + } else { + const { provider: p } = options as InternalOptions<"oauth" | "oidc"> + const { type, provider, providerAccountId, userId, ...tokenSet } = account + const defaults = { providerAccountId, provider, type, userId } + account = Object.assign(p.account(tokenSet), defaults) + if (user) { + // If the user is already signed in and the OAuth account isn't already associated + // with another user account then we can go ahead and link the accounts safely. await linkAccount({ ...account, userId: user.id }) await events.linkAccount?.({ user, account, profile }) - session = useJwtSession - ? {} - : await createSession({ - sessionToken: generateSessionToken(), - userId: user.id, - expires: fromDate(options.session.maxAge), - }) - - return { session, user, isNewUser: true } + // As they are already signed in, we don't need to do anything after linking them + return { session, user, isNewUser } } - } - throw new Error("Unsupported account type") + // If the user is not signed in and it looks like a new OAuth account then we + // check there also isn't an user account already associated with the same + // email address as the one in the OAuth profile. + // + // This step is often overlooked in OAuth implementations, but covers the following cases: + // + // 1. It makes it harder for someone to accidentally create two accounts. + // e.g. by signin in with email, then again with an oauth account connected to the same email. + // 2. It makes it harder to hijack a user account using a 3rd party OAuth account. + // e.g. by creating an oauth account then changing the email address associated with it. + // + // It's quite common for services to automatically link accounts in this case, but it's + // better practice to require the user to sign in *then* link accounts to be sure + // someone is not exploiting a problem with a third party OAuth service. + // + // OAuth providers should require email address verification to prevent this, but in + // practice that is not always the case; this helps protect against that. + const userByEmail = profile.email + ? await getUserByEmail(profile.email) + : null + if (userByEmail) { + const provider = options.provider as OAuthConfig + if (provider?.allowDangerousEmailAccountLinking) { + // If you trust the oauth provider to correctly verify email addresses, you can opt-in to + // account linking even when the user is not signed-in. + user = userByEmail + } else { + // We end up here when we don't have an account with the same [provider].id *BUT* + // we do already have an account with the same email address as the one in the + // OAuth profile the user has just tried to sign in with. + // + // 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 OAuthAccountNotLinked( + "Another account already exists with the same e-mail address", + { provider: account.provider } + ) + } + } else { + // If the current user is not logged in and the profile isn't linked to any user + // accounts (by email or provider account id)... + // + // If no account matching the same [provider].id or .email exists, we can + // create a new account for the user, link it to the OAuth account and + // create a new session for them so they are signed in with it. + const { id: _, ...newUser } = { ...profile, emailVerified: null } + user = await createUser(newUser) + } + await events.createUser?.({ user }) + + await linkAccount({ ...account, userId: user.id }) + await events.linkAccount?.({ user, account, profile }) + + session = useJwtSession + ? {} + : await createSession({ + sessionToken: generateSessionToken(), + userId: user.id, + expires: fromDate(options.session.maxAge), + }) + + return { session, user, isNewUser: true } + } } diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 1bcb468b..2a44f7c1 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -110,14 +110,11 @@ export async function AuthInternal< if ( [ "Signin", - "OAuthSignin", "OAuthCallback", "OAuthCreateAccount", "EmailCreateAccount", "Callback", "OAuthAccountNotLinked", - "EmailSignin", - "CredentialsSignin", "SessionRequired", ].includes(error as string) ) { diff --git a/packages/core/src/lib/oauth/callback.ts b/packages/core/src/lib/oauth/callback.ts index 04f5079d..44ed06d3 100644 --- a/packages/core/src/lib/oauth/callback.ts +++ b/packages/core/src/lib/oauth/callback.ts @@ -3,6 +3,7 @@ import * as o from "oauth4webapi" import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js" import type { + Account, InternalOptions, LoggerInstance, Profile, @@ -123,8 +124,8 @@ export async function handleOAuth( throw new Error("TODO: Handle www-authenticate challenges as needed") } - let profile: Profile = {} - let tokens: TokenSet + let profile: Profile + let tokens: TokenSet & Pick if (provider.type === "oidc") { const nonce = await checks.nonce.use(cookies, resCookies, options) @@ -162,37 +163,49 @@ export async function handleOAuth( (tokens as any).access_token ) profile = await userinfoResponse.json() + } else { + throw new TypeError("No userinfo endpoint configured") } } - const profileResult = await getProfile(profile, provider, tokens, logger) + if (tokens.expires_in) { + tokens.expires_at = + Math.floor(Date.now() / 1000) + Number(tokens.expires_in) + } + + const profileResult = await getUserAndProfile( + profile, + provider, + tokens, + logger + ) return { ...profileResult, cookies: resCookies } } /** Returns profile, raw profile and auth provider details */ -async function getProfile( +async function getUserAndProfile( OAuthProfile: Profile, provider: OAuthConfigInternal, tokens: TokenSet, logger: LoggerInstance ) { try { - const profile = await provider.profile(OAuthProfile, tokens) - profile.email = profile.email?.toLowerCase() + const user = await provider.profile(OAuthProfile, tokens) + user.email = user.email?.toLowerCase() - if (!profile.id) { + if (!user.id) { throw new TypeError( - `Profile id is missing in ${provider.name} OAuth profile response` + `User id is missing in ${provider.name} OAuth profile response` ) } return { - profile, + user, account: { provider: provider.id, type: provider.type, - providerAccountId: profile.id.toString(), + providerAccountId: user.id.toString(), ...tokens, }, OAuthProfile, @@ -206,6 +219,8 @@ async function getProfile( // 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.debug("getProfile error details", OAuthProfile) - logger.error(new OAuthProfileParseError(e as Error)) + logger.error( + new OAuthProfileParseError(e as Error, { provider: provider.id }) + ) } } diff --git a/packages/core/src/lib/providers.ts b/packages/core/src/lib/providers.ts index 2da2d80b..496e34b2 100644 --- a/packages/core/src/lib/providers.ts +++ b/packages/core/src/lib/providers.ts @@ -1,13 +1,15 @@ import { merge } from "./utils/merge.js" import type { + AccountCallback, OAuthConfig, OAuthConfigInternal, OAuthEndpointType, OAuthUserConfig, + ProfileCallback, Provider, } from "../providers/index.js" -import type { AuthConfig, InternalProvider } from "../types.js" +import type { AuthConfig, InternalProvider, Profile } from "../types.js" /** * Adds `signinUrl` and `callbackUrl` to each provider @@ -77,18 +79,47 @@ function normalizeOAuth( checks, userinfo, profile: c.profile ?? defaultProfile, + account: c.account ?? defaultAccount, } } -function defaultProfile(profile: any) { - return { +/** + * Returns basic user profile from the userinfo response/`id_token` claims. + * @see https://authjs.dev/reference/adapters#user + * @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken + * @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo + */ +const defaultProfile: ProfileCallback = (profile) => { + return stripUndefined({ id: profile.sub ?? profile.id, - name: - profile.name ?? profile.nickname ?? profile.preferred_username ?? null, - email: profile.email ?? null, - image: profile.picture ?? null, - } + name: profile.name ?? profile.nickname ?? profile.preferred_username, + email: profile.email, + image: profile.picture, + }) } + +/** + * Returns basic OAuth/OIDC values from the token response. + * @see https://www.ietf.org/rfc/rfc6749.html#section-5.1 + * @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + * @see https://authjs.dev/reference/adapters#account + * + * @todo Return `refresh_token` and `expires_at` as well when built-in + * refresh token support is added. (Can make it opt-in first with a flag). + */ +const defaultAccount: AccountCallback = (account) => { + return stripUndefined({ + access_token: account.access_token, + id_token: account.id_token, + }) +} + +function stripUndefined(o: T): T { + const result = {} as any + for (let [k, v] of Object.entries(o)) v !== undefined && (result[k] = v) + return result as T +} + function normalizeEndpoint( e?: OAuthConfig[OAuthEndpointType], issuer?: string diff --git a/packages/core/src/lib/routes/callback.ts b/packages/core/src/lib/routes/callback.ts index 4b0fead9..8b708a45 100644 --- a/packages/core/src/lib/routes/callback.ts +++ b/packages/core/src/lib/routes/callback.ts @@ -68,14 +68,18 @@ export async function callback(params: { logger.debug("authorization result", authorizationResult) - const { profile, account, OAuthProfile } = authorizationResult + const { + user: userFromProvider, + 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) { + if (!userFromProvider || !account || !OAuthProfile) { return { redirect: `${url}/signin`, cookies } } @@ -83,7 +87,7 @@ export async function callback(params: { // 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 + let userByAccountOrFromProvider if (adapter) { const { getUserByAccount } = adapter const userByAccount = await getUserByAccount({ @@ -91,11 +95,15 @@ export async function callback(params: { provider: provider.id, }) - if (userByAccount) userOrProfile = userByAccount + if (userByAccount) userByAccountOrFromProvider = userByAccount } const unauthorizedOrError = await handleAuthorized( - { user: userOrProfile, account, profile: OAuthProfile }, + { + user: userByAccountOrFromProvider, + account, + profile: OAuthProfile, + }, options ) @@ -104,7 +112,7 @@ export async function callback(params: { // Sign user in const { user, session, isNewUser } = await handleLogin( sessionStore.value, - profile, + userFromProvider, account, options ) @@ -152,7 +160,7 @@ export async function callback(params: { }) } - await events.signIn?.({ user, account, profile, isNewUser }) + await events.signIn?.({ user, account, profile: OAuthProfile, isNewUser }) // Handle first logins on new accounts // e.g. option to send users to a new account landing page on initial login @@ -362,6 +370,7 @@ export async function callback(params: { } catch (e) { const error = new CallbackRouteError(e as Error, { provider: provider.id }) + logger.debug("callback route error details", { method, query, body }) logger.error(error) url.searchParams.set("error", CallbackRouteError.name) url.pathname += "/error" diff --git a/packages/core/src/lib/routes/signin.ts b/packages/core/src/lib/routes/signin.ts index f5533236..f31aeb8d 100644 --- a/packages/core/src/lib/routes/signin.ts +++ b/packages/core/src/lib/routes/signin.ts @@ -55,8 +55,9 @@ export async function signin( } catch (e) { const error = new SignInError(e as Error, { provider: provider.id }) logger.error(error) - url.searchParams.set("error", error.name) - url.pathname += "/error" + const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin" + url.searchParams.set("error", code) + url.pathname += "/signin" return { redirect: url.toString() } } } diff --git a/packages/core/src/lib/web.ts b/packages/core/src/lib/web.ts index 5191d15e..475c35a3 100644 --- a/packages/core/src/lib/web.ts +++ b/packages/core/src/lib/web.ts @@ -33,6 +33,8 @@ export async function toInternalRequest( // TODO: url.toString() should not include action and providerId // see init.ts const url = new URL(req.url.replace(/\/$/, "")) + // FIXME: Upstream issue in Next.js, pathname segments get included as part of the query string + url.searchParams.delete("nextauth") const { pathname } = url const action = actions.find((a) => pathname.includes(a)) diff --git a/packages/core/src/providers/42-school.ts b/packages/core/src/providers/42-school.ts index 59ce0a63..76f0e519 100644 --- a/packages/core/src/providers/42-school.ts +++ b/packages/core/src/providers/42-school.ts @@ -1,13 +1,11 @@ /** - *
+ *
* Built-in 42School integration. - * TODO: SVG logo * * * *
* - * --- * @module providers/42-school */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -167,9 +165,15 @@ export interface FortyTwoProfile extends UserData, Record { /** * Add 42School login to your page. * - * @example + * ### Setup * - * ```js + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/42-school + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import 42School from "@auth/core/providers/42-school" * @@ -179,13 +183,13 @@ export interface FortyTwoProfile extends UserData, Record { * }) * ``` * - * ## Resources + * ### Resources * * - [42School OAuth documentation](https://api.intra.42.fr/apidoc/guides/web_application_flow) * - * ## Notes + * ### Notes + * * - * * :::note * 42 returns a field on `Account` called `created_at` which is a number. See the [docs](https://api.intra.42.fr/apidoc/guides/getting_started#make-basic-requests). Make sure to add this field to your database schema, in case if you are using an [Adapter](https://authjs.dev/reference/adapters). * ::: diff --git a/packages/core/src/providers/apple.ts b/packages/core/src/providers/apple.ts index ed71dcc7..9391cf4c 100644 --- a/packages/core/src/providers/apple.ts +++ b/packages/core/src/providers/apple.ts @@ -97,7 +97,14 @@ export interface AppleProfile extends Record { } /** - * ## Setup + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/apple + * ``` + * + * #### Configuration * * Import the provider and configure it in your **Auth.js** initialization file: * @@ -115,14 +122,14 @@ export interface AppleProfile extends Record { * }) * ``` * - * ## Resources + * ### Resources * * - Sign in with Apple [Overview](https://developer.apple.com/sign-in-with-apple/get-started/) * - Sign in with Apple [REST API](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api) * - [How to retrieve](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773) the user's information from Apple ID servers * - [Learn more about OAuth](https://authjs.dev/concepts/oauth) - * ## Notes + * ### Notes * * The Apple provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/apple.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). * diff --git a/packages/core/src/providers/asgardeo.ts b/packages/core/src/providers/asgardeo.ts index 2acf5514..2b8fb4db 100644 --- a/packages/core/src/providers/asgardeo.ts +++ b/packages/core/src/providers/asgardeo.ts @@ -35,7 +35,14 @@ export interface AsgardeoProfile extends Record { /** * - * ## Setup + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/asgardeo + * ``` + * + * #### Configuration * * Import the provider and configure it in your **Auth.js** initialization file: * @@ -75,12 +82,12 @@ export interface AsgardeoProfile extends Record { * ASGARDEO_ISSUER="Copy the issuer url from the info tab here" * ``` * - * ## Resources + * ### Resources * * - [Asgardeo - Authentication Guide](https://wso2.com/asgardeo/docs/guides/authentication) * - [Learn more about OAuth](https://authjs.dev/concepts/oauth) * - * ## Notes + * ### Notes * * The Asgardeo provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/asgardeo.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). * diff --git a/packages/core/src/providers/atlassian.ts b/packages/core/src/providers/atlassian.ts index 38daa260..c3f4daef 100644 --- a/packages/core/src/providers/atlassian.ts +++ b/packages/core/src/providers/atlassian.ts @@ -33,7 +33,14 @@ export interface AtlassianProfile extends Record { } /** - * ## Setup + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/atlassian + * ``` + * + * #### Configuration * * Import the provider and configure it in your **Auth.js** initialization file: * @@ -51,11 +58,11 @@ export interface AtlassianProfile extends Record { * }) * ``` * - * ## Resources + * ### Resources * * - [Atlassian docs](https://developer.atlassian.com/server/jira/platform/oauth/) * - * ## Notes + * ### Notes * * The Atlassian provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/atlassian.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). * diff --git a/packages/core/src/providers/auth0.ts b/packages/core/src/providers/auth0.ts index cf0aa8ba..956ddca3 100644 --- a/packages/core/src/providers/auth0.ts +++ b/packages/core/src/providers/auth0.ts @@ -75,7 +75,14 @@ export interface Auth0Profile extends Record { } /** - * ## Setup + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/auth0 + * ``` + * + * #### Configuration * * Import the provider and configure it in your **Auth.js** initialization file: * @@ -93,11 +100,11 @@ export interface Auth0Profile extends Record { * }) * ``` * - * ## Resources + * ### Resources * * - [Auth0 docs](https://auth0.com/docs/authenticate) * - * ## Notes + * ### Notes * * The Auth0 provider comes with a [default configuration](https://github.com/nextauthjs/next-auth/blob/main/packages/core/src/providers/auth0.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). * diff --git a/packages/core/src/providers/authentik.ts b/packages/core/src/providers/authentik.ts index 948c6a9c..767998bb 100644 --- a/packages/core/src/providers/authentik.ts +++ b/packages/core/src/providers/authentik.ts @@ -1,13 +1,11 @@ /** - *
+ *
* Built-in Authentik integration. - * TODO: SVG logo * * * *
* - * --- * @module providers/authentik */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -36,9 +34,15 @@ export interface AuthentikProfile extends Record { /** * Add Authentik login to your page. * - * @example + * ### Setup * - * ```js + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/authentik + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import Authentik from "@auth/core/providers/authentik" * @@ -47,16 +51,16 @@ export interface AuthentikProfile extends Record { * providers: [Authentik({ clientId: AUTHENTIK_CLIENT_ID, clientSecret: AUTHENTIK_CLIENT_SECRET, issuer: AUTHENTIK_ISSUER })], * }) * ``` - * + * * :::note * issuer should include the slug without a trailing slash – e.g., https://my-authentik-domain.com/application/o/My_Slug * ::: * - * ## Resources + * ### Resources * * - [Authentik OAuth documentation](https://goauthentik.io/docs/providers/oauth2) * - * ## Notes + * ### Notes * * By default, Auth.js assumes that the Authentik provider is * based on the [Open ID Connect](https://openid.net/specs/openid-connect-core-1_0.html) specification. diff --git a/packages/core/src/providers/azure-ad-b2c.ts b/packages/core/src/providers/azure-ad-b2c.ts index 3d341794..c817346d 100644 --- a/packages/core/src/providers/azure-ad-b2c.ts +++ b/packages/core/src/providers/azure-ad-b2c.ts @@ -6,7 +6,6 @@ * *
* - * --- * @module providers/azure-ad-b2c */ @@ -60,7 +59,7 @@ export interface AzureADB2CProfile { * - Identity Provider Access Token * - User's Object ID * - * ## Example + * @example * * ```ts * import { Auth } from "@auth/core" @@ -75,13 +74,13 @@ export interface AzureADB2CProfile { * * --- * - * ## Resources + * ### Resources * * - [Azure Active Directory B2C documentation](https://learn.microsoft.com/en-us/azure/active-directory-b2c) * * --- * - * ## Notes + * ### Notes * * By default, Auth.js assumes that the Azure AD B2C provider is * based on the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification. diff --git a/packages/core/src/providers/azure-ad.ts b/packages/core/src/providers/azure-ad.ts index 9e38d39c..6ac6bbde 100644 --- a/packages/core/src/providers/azure-ad.ts +++ b/packages/core/src/providers/azure-ad.ts @@ -6,7 +6,6 @@ * *
* - * --- * @module providers/azure-ad */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -21,9 +20,15 @@ export interface AzureADProfile extends Record { /** * Add AzureAd login to your page. * - * @example + * ### Setup * - * ```js + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/azure-ad + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import AzureAd from "@auth/core/providers/azure-ad" * @@ -33,15 +38,15 @@ export interface AzureADProfile extends Record { * }) * ``` * - * ## Resources + * ### Resources * * - [AzureAd OAuth documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow/) * - [AzureAd OAuth apps](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app/) * - * ## Example - * + * @example + * * ### To allow specific Active Directory users access: - * + * * - In https://portal.azure.com/ search for "Azure Active Directory", and select your organization. * - Next, go to "App Registration" in the left menu, and create a new one. * - Pay close attention to "Who can use this application or access this API?" @@ -53,26 +58,26 @@ export interface AzureADProfile extends Record { * - Application (client) ID * - Directory (tenant) ID * - Client secret (value) - * + * * In `.env.local` create the following entries: - * + * * ``` * AZURE_AD_CLIENT_ID= * AZURE_AD_CLIENT_SECRET= * AZURE_AD_TENANT_ID= * ``` - * + * * That will default the tenant to use the `common` authorization endpoint. [For more details see here](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints). - * + * * :::note * Azure AD returns the profile picture in an ArrayBuffer, instead of just a URL to the image, so our provider converts it to a base64 encoded image string and returns that instead. See: https://docs.microsoft.com/en-us/graph/api/profilephoto-get?view=graph-rest-1.0#examples. The default image size is 48x48 to avoid [running out of space](https://next-auth.js.org/faq#:~:text=What%20are%20the%20disadvantages%20of%20JSON%20Web%20Tokens%3F) in case the session is saved as a JWT. * ::: - * + * * In `pages/api/auth/[...nextauth].js` find or add the `AzureAD` entries: - * + * * ```js * import AzureADProvider from "next-auth/providers/azure-ad"; - * + * * ... * providers: [ * AzureADProvider({ @@ -82,10 +87,10 @@ export interface AzureADProfile extends Record { * }), * ] * ... - * + * * ``` - * - * ## Notes + * + * ### Notes * * By default, Auth.js assumes that the AzureAd provider is * based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. diff --git a/packages/core/src/providers/battlenet.ts b/packages/core/src/providers/battlenet.ts index b06aa9fb..36712359 100644 --- a/packages/core/src/providers/battlenet.ts +++ b/packages/core/src/providers/battlenet.ts @@ -1,13 +1,11 @@ /** *
* Built-in Battle.net integration. - * TODO: SVG logo * * * *
* - * --- * @module providers/battlenet */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -25,9 +23,15 @@ export type BattleNetIssuer = /** * Add Battle.net login to your page. * - * @example + * ### Setup * - * ```js + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/battlenet + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import BattleNet from "@auth/core/providers/battlenet" * @@ -46,11 +50,11 @@ export type BattleNetIssuer = * | "https://tw.battle.net/oauth" * ``` * - * ## Resources + * ### Resources * * - [BattleNet OAuth documentation](https://develop.battle.net/documentation/guides/using-oauth) * - * ## Notes + * ### Notes * * By default, Auth.js assumes that the BattleNet provider is * based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. diff --git a/packages/core/src/providers/beyondidentity.ts b/packages/core/src/providers/beyondidentity.ts index b26b1859..8b39a377 100644 --- a/packages/core/src/providers/beyondidentity.ts +++ b/packages/core/src/providers/beyondidentity.ts @@ -6,7 +6,6 @@ * *
* - * --- * @module providers/beyondidentity */ @@ -27,7 +26,7 @@ export interface BeyondIdentityProfile { /** * Add Beyond Identity login to your page. * - * ## Example + * @example * * ```ts * import { Auth } from "@auth/core" @@ -41,13 +40,13 @@ export interface BeyondIdentityProfile { * * --- * - * ## Resources + * ### Resources * * - [Beyond Identity Developer Docs](https://developer.beyondidentity.com/) * * --- * - * ## Notes + * ### Notes * * By default, Auth.js assumes that the BeyondIdentity provider is * based on the [OIDC](https://openid.net/specs/openid-connect-core-1_0.html) specification. diff --git a/packages/core/src/providers/box.ts b/packages/core/src/providers/box.ts index 490fa59d..d0e55721 100644 --- a/packages/core/src/providers/box.ts +++ b/packages/core/src/providers/box.ts @@ -1,13 +1,11 @@ /** *
* Built-in Box integration. - * TODO: SVG logo * * * *
* - * --- * @module providers/box */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -15,9 +13,15 @@ import type { OAuthConfig, OAuthUserConfig } from "./index.js" /** * Add Box login to your page. * - * @example + * ### Setup * - * ```js + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/box + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import Box from "@auth/core/providers/box" * @@ -27,12 +31,12 @@ import type { OAuthConfig, OAuthUserConfig } from "./index.js" * }) * ``` * - * ## Resources + * ### Resources * * - [Box developers documentation](https://developer.box.com/reference/) * - [Box OAuth documentation](https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/) * - * ## Notes + * ### Notes * * By default, Auth.js assumes that the Box provider is * based on the [OAuth 2](https://www.rfc-editor.org/rfc/rfc6749.html) specification. @@ -54,7 +58,8 @@ import type { OAuthConfig, OAuthUserConfig } from "./index.js" * * ::: */ -export default function Box(options: OAuthUserConfig> +export default function Box( + options: OAuthUserConfig> ): OAuthConfig> { return { id: "box", diff --git a/packages/core/src/providers/boxyhq-saml.ts b/packages/core/src/providers/boxyhq-saml.ts index 3bf0ca93..e53b8701 100644 --- a/packages/core/src/providers/boxyhq-saml.ts +++ b/packages/core/src/providers/boxyhq-saml.ts @@ -1,13 +1,11 @@ /** *
* Built-in BoxyHQ SAML integration. - * TODO: SVG logo * * * *
* - * --- * @module providers/boxyhq-saml */ import type { OAuthConfig, OAuthUserConfig } from "./index.js" @@ -23,12 +21,18 @@ export interface BoxyHQSAMLProfile extends Record { * Add BoxyHQ SAML login to your page. * * BoxyHQ SAML is an open source service that handles the SAML login flow as an OAuth 2.0 flow, abstracting away all the complexities of the SAML protocol. - * - * You can deploy BoxyHQ SAML as a separate service or embed it into your app using our NPM library. [Check out the documentation for more details](https://boxyhq.com/docs/jackson/deploy) - * - * @example * - * ```js + * You can deploy BoxyHQ SAML as a separate service or embed it into your app using our NPM library. [Check out the documentation for more details](https://boxyhq.com/docs/jackson/deploy) + * + * ### Setup + * + * #### Callback URL + * ``` + * https://example.com/api/auth/callback/boxyhq-saml + * ``` + * + * #### Configuration + *```js * import Auth from "@auth/core" * import BoxyHQ from "@auth/core/providers/boxyhq-saml" * @@ -38,23 +42,23 @@ export interface BoxyHQSAMLProfile extends Record { * }) * ``` * - * ## Resources + * ### Resources * * - [BoxyHQ OAuth documentation](https://example.com) * * ## Configuration - * + * * SAML login requires a configuration for every tenant of yours. One common method is to use the domain for an email address to figure out which tenant they belong to. You can also use a unique tenant ID (string) from your backend for this, typically some kind of account or organization ID. - * + * * Check out the [documentation](https://boxyhq.com/docs/jackson/saml-flow#2-saml-config-api) for more details. - * - * + * + * * On the client side you'll need to pass additional parameters `tenant` and `product` to the `signIn` function. This will allow BoxyHQL SAML to figure out the right SAML configuration and take your user to the right SAML Identity Provider to sign them in. - * + * * ```tsx * import { signIn } from "next-auth/react"; * ... - * + * * // Map your users's email to a tenant and product * const tenant = email.split("@")[1]; * const product = 'my_awesome_product'; @@ -62,12 +66,12 @@ export interface BoxyHQSAMLProfile extends Record { *