Compare commits

..

38 Commits

Author SHA1 Message Date
GitHub Actions
3b414bd7b5 chore(release): bump package version(s) [skip ci] 2023-06-01 14:19:07 +00:00
Balázs Orbán
37bb6ebd2c fix(docs): update code example 2023-06-01 16:08:21 +02:00
Balázs Orbán
2ecf52c342 feat: introduce @auth/prisma-adapter (#7703)
BREAKING CHANGE:
If you are coming from the previous adapter, change your `package.json`:

```diff
-  "@next-auth/prisma-adapter": "0.0.0",
+  "@auth/prisma-adapter": "0.0.0",
```

And run `npm install`, `yarn install` or `pnpm install` respectively.

**Note:** This packages is published as ESM-only.
2023-06-01 16:06:22 +02:00
Balázs Orbán
cda07c239e chore: remove "nuxt postinstall" 2023-06-01 15:17:58 +02:00
Balázs Orbán
fa60b79abe chore: upgrade turbo 2023-06-01 15:15:23 +02:00
GitHub Actions
39e1a76e8f chore(release): bump package version(s) [skip ci] 2023-06-01 12:59:53 +00:00
Balázs Orbán
953ef9d04a chore: re-add pnpm caching
Related: #7332
2023-06-01 14:49:45 +02:00
Balázs Orbán
94f3031765 chore: allow manual release of any @auth/* package 2023-06-01 14:49:45 +02:00
Balázs Orbán
ad7bf07ddf chore: update lock file 2023-06-01 14:49:45 +02:00
Graham Charles
f30308ac30 docs: fix info card rendering in oauth-tutorial.mdx (#7662)
Info box is not being rendered; the raw `:::info` is displayed. Blind guess: it needs a blank line before it.
2023-06-01 14:49:45 +02:00
Tashrik Anam
6eaaeb15e9 docs: adapter card text color on hover when on dark mode (#7672) 2023-06-01 14:49:45 +02:00
Robert Soriano
8b3f0696a5 chore(playgrounds): Nuxt 3.5.1 (#7626)
* bump Nuxt to 3.5.1

* follow playground package names

* chore: update nuxt playground scripts

* fix: imports and types

* fix: more nuxt type imports

* fix: nuxt auth options types

* fix: nuxt client fetch types
2023-06-01 14:49:45 +02:00
Doron Sharon
c69a157832 chore: Add Descope as a 🥉 bronze financial sponsor (#7615)
Add Descope as a bronze sponsor
2023-06-01 14:49:45 +02:00
TATHAGATA ROY
60af446338 docs: Cypress.Cookies.defaults removed (#7574) 2023-06-01 14:49:45 +02:00
Nirmalya Ghosh
ce85444760 chore: Move next.config.js file into the correct directory (#7580)
fix: moves next config file into the correct directory
2023-06-01 14:49:45 +02:00
Balázs Orbán
142abe3eea feat: allow empty account mapper 2023-06-01 14:49:45 +02:00
Balázs Orbán
da211e6cbe chore: revert picture to image 2023-06-01 14:49:45 +02:00
Balázs Orbán
79ad6156ed feat: add update session to core (#7505)
* feat: add update session to core

Integrates #7056 into `@auth/core`

* resolve default user after jwt callback
2023-06-01 14:49:45 +02:00
Rémi Robichet
28f287d63e docs(example): update broken link (#7504)
Co-authored-by: Nico Domino <yo@ndo.dev>
2023-06-01 14:49:45 +02:00
Balázs Orbán
1ab77d0e11 chore: move build to root 2023-06-01 14:49:45 +02:00
Balázs Orbán
787c1ff7d0 chore: add build to manual publish 2023-06-01 14:49:45 +02:00
Balázs Orbán
208b3b4a43 chore: reduce breaking changes on Account mapping
Reverts some changes on #7369 so DB migration won't be needed
2023-06-01 14:49:45 +02:00
Balázs Orbán
c4f6330f70 chore: tweak manual release version 2023-06-01 14:49:45 +02:00
Balázs Orbán
44127068e1 chore: tweaks 2023-06-01 14:49:45 +02:00
Balázs Orbán
9e3f1aacf7 chore: tweak 2023-06-01 14:49:45 +02:00
Balázs Orbán
83051c6862 chore: skip test for manual release 2023-06-01 14:49:45 +02:00
Balázs Orbán
f1acab67e6 chore: separate manual release job 2023-06-01 14:49:45 +02:00
Balázs Orbán
6a31ed3216 chore: support release any package as experimental 2023-06-01 14:49:45 +02:00
Balázs Orbán
0998fc0b98 chore: use @ts-ignore 2023-06-01 14:49:45 +02:00
Balázs Orbán
bd20d750c2 fix(docs): remove extra heading
Fixes #7426
2023-06-01 14:49:45 +02:00
Balázs Orbán
8e29b4df0c fix: allow handling OAuth callback error response
related #7407
2023-06-01 14:49:45 +02:00
Balázs Orbán
9632a56d45 chore: type fixes 2023-06-01 14:49:45 +02:00
Balázs Orbán
12161b9613 fix: loosen profile types 2023-06-01 14:49:45 +02:00
Balázs Orbán
a3b5276a5a chore: improve errors, add more docs (#7415)
* JWT Token -> JWT

* document some errors

* improve errors, docs
2023-06-01 14:49:45 +02:00
Balázs Orbán
7c1078b9a9 feat(adapters): add Account mapping before database write (#7369)
* feat: map Account before saving to database

* document `acconut()`, explain default behaviour

* generate `expires_at` based on `expires_in`

Fixes #6538

* rename

* strip undefined on `defaultProfile`

* don't forward defaults to account callback

* improve internal namings, types, docs
2023-06-01 14:49:45 +02:00
Victor
37d3461155 docs: fix default maxAge formula (#7406) 2023-06-01 14:49:40 +02:00
Balázs Orbán
6111662df7 docs: Update creating-a-database-adapter.md 2023-04-30 09:52:47 +02:00
Zack Reneau-Wedeen
5da6549c48 chore(docs): update xata docs link (#7397)
Update link to a working page (Workspaces API reference)

Co-authored-by: Nico Domino <yo@ndo.dev>
2023-04-28 22:26:30 +02:00
59 changed files with 3344 additions and 1406 deletions

View File

@@ -29,7 +29,7 @@ body:
- "@next-auth/mongodb-adapter"
- "@next-auth/neo4j-adapter"
- "@next-auth/pouchdb-adapter"
- "@next-auth/prisma-adapter"
- "@auth/prisma-adapter"
- "@next-auth/sequelize-adapter"
- "@next-auth/supabase-adapter"
- "@next-auth/typeorm-legacy-adapter"

View File

@@ -25,7 +25,7 @@ pouchdb:
- "@next-auth/pouchdb-adapter"
prisma:
- "@next-auth/prisma-adapter"
- "@auth/prisma-adapter"
sequelize:
- "@next-auth/sequelize-adapter"

View File

@@ -5,14 +5,15 @@ const core = require("@actions/core")
try {
const packageJSONPath = path.join(
process.cwd(),
"packages/next-auth/package.json"
`packages/${process.env.PACKAGE_PATH || "next-auth"}/package.json`
)
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
const sha8 = process.env.GITHUB_SHA.substring(0, 8)
const prNumber = process.env.PR_NUMBER
const packageVersion = `0.0.0-pr.${prNumber}.${sha8}`
const prefix = "0.0.0-"
const pr = process.env.PR_NUMBER
const source = pr ? `pr.${pr}` : "manual"
const packageVersion = `${prefix}${source}.${sha8}`
packageJSON.version = packageVersion
core.setOutput("version", packageVersion)
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))

View File

@@ -8,6 +8,54 @@ on:
- next
- 3.x
pull_request:
# TODO: Support latest releases
workflow_dispatch:
inputs:
name:
type: choice
description: Package name (npm)
options:
- "@auth/core"
- "@auth/nextjs"
- "@auth/dgraph-adapter"
- "@auth/drizzle-adapter"
- "@auth/dynamodb-adapter"
- "@auth/fauna-adapter"
- "@auth/firebase-adapter"
- "@auth/mikro-orm-adapter"
- "@auth/mongodb-adapter"
- "@auth/neo4j-adapter"
- "@auth/pouchdb-adapter"
- "@auth/prisma-adapter"
- "@auth/sequelize-adapter"
- "@auth/supabase-adapter"
- "@auth/typeorm-legacy-adapter"
- "@auth/upstash-redis-adapter"
- "@auth/xata-adapter"
- "next-auth"
# TODO: Infer from package name
path:
type: choice
description: Directory name (packages/*)
options:
- "core"
- "frameworks-nextjs"
- "adapter-dgraph"
- "adapter-drizzle"
- "adapter-dynamodb"
- "adapter-fauna"
- "adapter-firebase"
- "adapter-mikro-orm"
- "adapter-mongodb"
- "adapter-neo4j"
- "adapter-pouchdb"
- "adapter-prisma"
- "adapter-sequelize"
- "adapter-supabase"
- "adapter-typeorm-legacy"
- "adapter-upstash-redis"
- "adapter-xata"
- "next-auth"
jobs:
test:
@@ -24,6 +72,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run tests
@@ -73,6 +122,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Publish to npm and GitHub
@@ -97,6 +147,7 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
@@ -122,3 +173,36 @@ jobs:
env:
VERSION: ${{ steps.determine-version.outputs.version }}
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
release-manual:
name: Publish manually
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' }}
steps:
- name: Init
uses: actions/checkout@v3
- name: Install pnpm
uses: pnpm/action-setup@v2.2.4
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Determine version
uses: ./.github/version-pr
id: determine-version
env:
PACKAGE_PATH: ${{ github.event.inputs.path }}
- name: Publish to npm
run: |
pnpm build
cd packages/$PACKAGE_PATH
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
pnpm publish --no-git-checks --access public --tag experimental
echo "🎉 Experimental release published 📦️ on npm: https://npmjs.com/package/${{ github.event.inputs.name }}/v/${{ env.VERSION }}"
echo "Install via: pnpm add ${{ github.event.inputs.name }}@${{ env.VERSION }}"
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_PATH: ${{ github.event.inputs.path }}
VERSION: ${{ steps.determine-version.outputs.version }}

View File

@@ -15,7 +15,7 @@
"license": "ISC",
"dependencies": {
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",

View File

@@ -37,7 +37,7 @@ import WorkOS from "next-auth/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)

View File

@@ -16,7 +16,7 @@
"dependencies": {
"@auth/core": "workspace:*",
"@next-auth/fauna-adapter": "workspace:*",
"@next-auth/prisma-adapter": "workspace:*",
"@auth/prisma-adapter": "workspace:*",
"@next-auth/supabase-adapter": "workspace:*",
"@next-auth/typeorm-legacy-adapter": "workspace:*",
"@prisma/client": "^3",

View File

@@ -41,7 +41,7 @@ import WorkOS from "@auth/core/providers/workos"
// // Prisma
// import { PrismaClient } from "@prisma/client"
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
// import { PrismaAdapter } from "@auth/prisma-adapter"
// const client = globalThis.prisma || new PrismaClient()
// if (process.env.NODE_ENV !== "production") globalThis.prisma = client
// const adapter = PrismaAdapter(client)

View File

@@ -7,7 +7,7 @@ export default function Page() {
<p>Only admin users can see this page.</p>
<p>
To learn more about the NextAuth middleware see&nbsp;
<a href="https://docs-git-misc-docs-nextauthjs.vercel.app/configuration/nextjs#middleware">
<a href="https://next-auth.js.org/configuration/nextjs#middleware">
the docs
</a>
.

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core"
import { Session } from "@auth/core/types"
export default function useSession() {
return useState<Session | null>("session", () => null)

View File

@@ -43,7 +43,7 @@ export async function signIn<
// TODO: Handle custom base path
// TODO: Remove this since Sveltekit offers the CSRF protection via origin check
const { csrfToken } = await $fetch("/api/auth/csrf")
const { csrfToken } = await $fetch<{ csrfToken: string }>("/api/auth/csrf")
console.log(_signInUrl)

View File

@@ -1,13 +1,14 @@
import { AuthHandler, AuthOptions, Session } from "@auth/core"
import { AuthConfig, Session } from "@auth/core/types"
import { Auth } from "@auth/core"
import { fromNodeMiddleware, H3Event } from "h3"
import getURL from "requrl"
import { createMiddleware } from "@hattip/adapter-node"
export function NuxtAuthHandler(options: AuthOptions) {
export function NuxtAuthHandler(options: AuthConfig) {
async function handler(ctx: { request: Request }) {
options.trustHost ??= true
return AuthHandler(ctx.request, options)
return Auth(ctx.request, options)
}
const middleware = createMiddleware(handler)
@@ -17,7 +18,7 @@ export function NuxtAuthHandler(options: AuthOptions) {
export async function getSession(
event: H3Event,
options: AuthOptions
options: AuthConfig
): Promise<Session | null> {
options.trustHost ??= true
@@ -30,7 +31,7 @@ export async function getSession(
nodeHeaders.append(key, headers[key] as any)
})
const response = await AuthHandler(
const response = await Auth(
new Request(url, { headers: nodeHeaders }),
options
)

View File

@@ -1,21 +1,21 @@
{
"name": "playground-nuxt",
"name": "next-auth-nuxt",
"private": true,
"scripts": {
"build": "nuxt prepare && nuxt build",
"dev": "nuxt prepare && export NODE_OPTIONS='--no-experimental-fetch' && nuxt dev",
"build": "nuxt build",
"dev": "nuxt prepare && nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview"
},
"devDependencies": {
"@nuxt/eslint-config": "^0.1.1",
"eslint": "^8.29.0",
"h3": "1.0.2",
"nuxt": "3.0.0"
"h3": "1.6.6",
"nuxt": "3.5.1"
},
"dependencies": {
"@auth/core": "workspace:*",
"@hattip/adapter-node": "^0.0.22",
"@hattip/adapter-node": "^0.0.34",
"requrl": "^3.0.2"
}
}

View File

@@ -1,4 +1,4 @@
import { Session } from "@auth/core"
import { Session } from "@auth/core/types"
export default defineNuxtPlugin(async () => {
const session = useSession()

View File

@@ -1,10 +1,10 @@
import { NuxtAuthHandler } from "@/lib/auth/server"
import GithubProvider from "@auth/core/providers/github"
import type { AuthOptions } from "@auth/core"
import type { AuthConfig } from "@auth/core"
const runtimeConfig = useRuntimeConfig()
export const authOptions: AuthOptions = {
export const authOptions = {
secret: runtimeConfig.secret,
providers: [
GithubProvider({
@@ -12,6 +12,6 @@ export const authOptions: AuthOptions = {
clientSecret: runtimeConfig.github.clientSecret,
}),
],
}
} as AuthConfig
export default NuxtAuthHandler(authOptions)

View File

@@ -269,7 +269,7 @@ Ultimately if your request is not accepted or is not actively in development, yo
</summary>
<p>
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.
</p>
</details>

View File

@@ -243,10 +243,13 @@ http://localhost:5173/auth/callback/github
TODO Core
</TabItem>
</Tabs>
:::info
The last part of the URL, `[provider]`, is the ID of the provider you're using. In this case, we're using GitHub, so it's `github`. If you're using Google, it'll be `google`, etc... We keep track of the provider IDs internally.
The same id is used in the `signIn()` method we saw earlier.
:::
To register, tap on "Register application" button.
The next screen shows all the configurations for your newly created OAuth app. For now, we need two things from it - the **Client ID** and **Client Secret**:

View File

@@ -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._

View File

@@ -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.

View File

@@ -117,7 +117,7 @@ Using the database strategy is very similar, but instead of preserving the `acce
import { Auth } from "@auth/core"
import { type TokenSet } from "@auth/core/types"
import Google from "@auth/core/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()

View File

@@ -110,10 +110,6 @@ describe("Login page", () => {
secure: cookie.secure,
})
Cypress.Cookies.defaults({
preserve: cookieName,
})
// remove the two lines below if you need to stay logged in
// for your remaining tests
cy.visit("/api/auth/signout")

View File

@@ -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:
<div class="adapter-card-list">
<a href="/reference/adapter/dgraph" class="adapter-card">
@@ -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
@@ -103,8 +103,6 @@ erDiagram
string scope
string id_token
string session_state
string oauth_token_secret
string oauth_token
}
VerificationToken {
string identifier
@@ -113,10 +111,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 +123,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`, `expires_at`, `refresh_token`, `id_token`, `token_type`, `scope` and `session_state`. You can save other fields or remove the ones you don't need 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 +161,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 +170,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 +182,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

View File

@@ -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")

View File

@@ -29,6 +29,7 @@ html[data-theme="dark"] .adapter-card {
color: #f5f5f5;
}
html[data-theme="dark"] .adapter-card:hover,
.adapter-card:hover {
text-decoration: none;
color: black;

View File

@@ -43,7 +43,7 @@
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "2.8.1",
"prettier-plugin-svelte": "^2.8.1",
"turbo": "1.8.8",
"turbo": "1.10.1",
"typescript": "4.9.4"
},
"engines": {

View File

@@ -8,14 +8,14 @@
</a>
<h3 align="center"><b>Prisma Adapter</b> - NextAuth.js / Auth.js</a></h3>
<p align="center" style="align: center;">
<a href="https://npm.im/@next-auth/prisma-adapter">
<a href="https://npm.im/@auth/prisma-adapter">
<img src="https://img.shields.io/badge/TypeScript-blue?style=flat-square" alt="TypeScript" />
</a>
<a href="https://npm.im/@next-auth/prisma-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@next-auth/prisma-adapter?color=green&label=@next-auth/prisma-adapter&style=flat-square">
<a href="https://npm.im/@auth/prisma-adapter">
<img alt="npm" src="https://img.shields.io/npm/v/@auth/prisma-adapter?color=green&label=@auth/prisma-adapter&style=flat-square">
</a>
<a href="https://www.npmtrends.com/@next-auth/prisma-adapter">
<img src="https://img.shields.io/npm/dm/@next-auth/prisma-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
<a href="https://www.npmtrends.com/@auth/prisma-adapter">
<img src="https://img.shields.io/npm/dm/@auth/prisma-adapter?label=%20downloads&style=flat-square" alt="Downloads" />
</a>
<a href="https://github.com/nextauthjs/next-auth/stargazers">
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth?style=flat-square" alt="Github Stars" />

View File

@@ -1,14 +1,29 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.6",
"description": "Prisma adapter for next-auth.",
"homepage": "https://authjs.dev",
"name": "@auth/prisma-adapter",
"version": "1.0.0",
"description": "Prisma adapter for Auth.js",
"homepage": "https://authjs.dev/reference/adapter/prisma",
"repository": "https://github.com/nextauthjs/next-auth",
"bugs": {
"url": "https://github.com/nextauthjs/next-auth/issues"
},
"author": "William Luke",
"main": "dist/index.js",
"contributors": [
"Balázs Orbán <info@balazsorban.com>"
],
"type": "module",
"types": "./index.d.ts",
"files": [
"*.js",
"*.d.ts*",
"src"
],
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.js"
}
},
"license": "ISC",
"keywords": [
"next-auth",
@@ -32,22 +47,19 @@
"dev": "prisma generate && tsc -w",
"studio": "prisma studio"
},
"files": [
"README.md",
"dist"
],
"dependencies": {
"@auth/core": "workspace:*"
},
"peerDependencies": {
"@prisma/client": ">=2.26.0 || >=3",
"next-auth": "^4"
"@prisma/client": ">=2.26.0 || >=3 || >=4"
},
"devDependencies": {
"@next-auth/adapter-test": "workspace:*",
"@next-auth/tsconfig": "workspace:*",
"@prisma/client": "^3.10.0",
"@prisma/client": "^4.15.0",
"jest": "^27.4.3",
"mongodb": "^4.4.0",
"next-auth": "workspace:*",
"prisma": "^3.10.0"
"prisma": "^4.15.0"
},
"jest": {
"preset": "@next-auth/adapter-test/jest"

View File

@@ -9,14 +9,14 @@
* ## Installation
*
* ```bash npm2yarn2pnpm
* npm install next-auth @prisma/client @next-auth/prisma-adapter
* npm install @prisma/client @auth/prisma-adapter
* npm install prisma --save-dev
* ```
*
* @module @next-auth/prisma-adapter
* @module @auth/prisma-adapter
*/
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter, AdapterAccount } from "next-auth/adapters"
import type { Adapter, AdapterAccount } from "@auth/core/adapters"
/**
* ## Setup
@@ -26,7 +26,7 @@ import type { Adapter, AdapterAccount } from "next-auth/adapters"
* ```js title="pages/api/auth/[...nextauth].js"
* import NextAuth from "next-auth"
* import GoogleProvider from "next-auth/providers/google"
* import { PrismaAdapter } from "@next-auth/prisma-adapter"
* import { PrismaAdapter } from "@auth/prisma-adapter"
* import { PrismaClient } from "@prisma/client"
*
* const prisma = new PrismaClient()
@@ -42,8 +42,6 @@ import type { Adapter, AdapterAccount } from "next-auth/adapters"
* })
* ```
*
* ## Advanced usage
*
* ### Create the Prisma schema from scratch
*
* You need to use at least Prisma 2.26.0. Create a schema file in `prisma/schema.prisma` similar to this one:

View File

@@ -1,9 +1,25 @@
{
"extends": "@next-auth/tsconfig/tsconfig.adapters.json",
"extends": "@next-auth/tsconfig/tsconfig.base.json",
"compilerOptions": {
"allowJs": true,
"baseUrl": ".",
"isolatedModules": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": ".",
"rootDir": "src",
"outDir": "dist"
"skipDefaultLibCheck": true,
"strictNullChecks": true,
"stripInternal": true,
"declarationMap": true,
"declaration": true
},
"include": ["."],
"exclude": ["tests", "dist", "jest.config.js"]
}
"include": [
"src/**/*"
],
"exclude": [
"*.js",
"*.d.ts",
]
}

View File

@@ -1,4 +1,4 @@
import type { Adapter } from "next-auth/adapters"
import type { Adapter } from "@auth/core/adapters"
import { createHash, randomUUID } from "crypto"
const requiredMethods = [
@@ -58,7 +58,8 @@ export async function runBasicTests(options: TestOptions) {
await options.db.connect?.()
})
const { adapter, db, skipTests } = options
const { adapter: _adapter, db, skipTests } = options
const adapter = _adapter as Required<Adapter>
afterAll(async () => {
// @ts-expect-error This is only used for the TypeORM adapter
@@ -88,7 +89,7 @@ export async function runBasicTests(options: TestOptions) {
providerAccountId: randomUUID(),
type: "oauth",
access_token: randomUUID(),
expires_at: ONE_MONTH,
expires_at: ONE_MONTH / 1000,
id_token: randomUUID(),
refresh_token: randomUUID(),
token_type: "bearer",

View File

@@ -12,13 +12,13 @@
"license": "ISC",
"private": true,
"devDependencies": {
"@auth/core": "workspace:*",
"@babel/cli": "^7.14.3",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/preset-env": "^7.14.2",
"@types/jest": "^26.0.23",
"@types/nodemailer": "^6.4.4",
"jest": "^27.0.3",
"next-auth": "workspace:*",
"ts-jest": "^27.0.3",
"typescript": "^4.2.4"
}

View File

@@ -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.
*

View File

@@ -1,6 +1,6 @@
{
"name": "@auth/core",
"version": "0.7.1",
"version": "0.8.1",
"description": "Authentication for the Web.",
"keywords": [
"authentication",

View File

@@ -71,7 +71,7 @@
*
* ```ts title=my-adapter.ts
* import { type Adapter } from "@auth/core/adapters"
* import { PrismaAdapter } from "@next-auth/prisma-adapter"
* import { PrismaAdapter } from "@auth/prisma-adapter"
* import { PrismaClient } from "@prisma/client"
*
* const prisma = new PrismaClient()
@@ -228,6 +228,10 @@ export interface Adapter {
deleteUser?(
userId: string
): Promise<void> | Awaitable<AdapterUser | null | undefined>
/**
* 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<void> | Awaitable<AdapterAccount | null | undefined>

View File

@@ -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,31 +100,87 @@ 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 */
/**
* Thrown when an OAuth provider returns an error during the sign in process.
* This could happen for example if the user denied access to the application or there was a configuration error.
*
* For a full list of possible reasons, check out the specification [Authorization Code Grant: Error Response](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1)
*/
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 */

View File

@@ -190,7 +190,7 @@ export interface JWTEncodeParams<Payload = JWT> {
/**
* 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. */

View File

@@ -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<any>
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<any>
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 }
}
}

View File

@@ -47,7 +47,7 @@ export async function AuthInternal<
case "providers":
return (await routes.providers(options.providers)) as any
case "session": {
const session = await routes.session(sessionStore, options)
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
@@ -110,14 +110,10 @@ export async function AuthInternal<
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error as string)
) {
@@ -181,6 +177,22 @@ export async function AuthInternal<
return { ...callback, cookies }
}
break
case "session": {
if (options.csrfTokenVerified) {
const session = await routes.session({
options,
sessionStore,
newSession: request.body?.data,
isUpdate: true,
})
if (session.cookies) cookies.push(...session.cookies)
return { ...session, cookies } as any
}
// If CSRF token is invalid, return a 400 status code
// we should not redirect to a page as this is an API route
return { status: 400, cookies }
}
default:
}
}

View File

@@ -3,6 +3,7 @@ import * as o from "oauth4webapi"
import { OAuthCallbackError, OAuthProfileParseError } from "../../errors.js"
import type {
Account,
InternalOptions,
LoggerInstance,
Profile,
@@ -88,11 +89,9 @@ export async function handleOAuth(
/** https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1 */
if (o.isOAuth2Error(codeGrantParams)) {
logger.debug("OAuthCallbackError", {
providerId: provider.id,
...codeGrantParams,
})
throw new OAuthCallbackError(codeGrantParams.error)
const cause = { providerId: provider.id, ...codeGrantParams }
logger.debug("OAuthCallbackError", cause)
throw new OAuthCallbackError("OAuth Provider returned an error", cause)
}
const codeVerifier = await checks.pkce.use(cookies, resCookies, options)
@@ -123,8 +122,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<Account, "expires_at">
if (provider.type === "oidc") {
const nonce = await checks.nonce.use(cookies, resCookies, options)
@@ -162,40 +161,51 @@ 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)
}
return { ...profileResult, cookies: resCookies }
const profileResult = await getUserAndAccount(
profile,
provider,
tokens,
logger
)
return { ...profileResult, profile, cookies: resCookies }
}
/** Returns profile, raw profile and auth provider details */
async function getProfile(
/** Returns the user and account that is going to be created in the database. */
async function getUserAndAccount(
OAuthProfile: Profile,
provider: OAuthConfigInternal<any>,
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,
}
} catch (e) {
// If we didn't get a response either there was a problem with the provider
@@ -206,6 +216,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 })
)
}
}

View File

@@ -11,7 +11,7 @@ const signinErrors: Record<
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.",
oauthcallbackerror: "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.",

View File

@@ -1,13 +1,16 @@
import { OAuthProfileParseError } from "../errors.js"
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 +80,51 @@ function normalizeOAuth(
checks,
userinfo,
profile: c.profile ?? defaultProfile,
account: c.account ?? defaultAccount,
}
}
function defaultProfile(profile: any) {
return {
id: profile.sub ?? profile.id,
name:
profile.name ?? profile.nickname ?? profile.preferred_username ?? null,
email: profile.email ?? null,
image: profile.picture ?? null,
}
/**
* 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> = (profile) => {
const id = profile.sub ?? profile.id
if (!id) throw new OAuthProfileParseError("Missing user id")
return stripUndefined({
id: id.toString(),
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
*/
const defaultAccount: AccountCallback = (account) => {
return stripUndefined({
access_token: account.access_token,
id_token: account.id_token,
refresh_token: account.refresh_token,
expires_at: account.expires_at,
scope: account.scope,
token_type: account.token_type,
session_state: account.session_state,
})
}
function stripUndefined<T extends object>(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<any>[OAuthEndpointType],
issuer?: string

View File

@@ -1,4 +1,8 @@
import { CallbackRouteError, Verification } from "../../errors.js"
import {
CallbackRouteError,
OAuthCallbackError,
Verification,
} from "../../errors.js"
import { handleLogin } from "../callback-handler.js"
import { handleOAuth } from "../oauth/callback.js"
import { handleState } from "../oauth/handle-state.js"
@@ -68,14 +72,18 @@ export async function callback(params: {
logger.debug("authorization result", authorizationResult)
const { profile, account, OAuthProfile } = authorizationResult
const {
user: userFromProvider,
account,
profile: 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 +91,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 +99,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 +116,7 @@ export async function callback(params: {
// Sign user in
const { user, session, isNewUser } = await handleLogin(
sessionStore.value,
profile,
userFromProvider,
account,
options
)
@@ -122,6 +134,7 @@ export async function callback(params: {
account,
profile: OAuthProfile,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -152,7 +165,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
@@ -232,6 +245,7 @@ export async function callback(params: {
user: loggedInUser,
account,
isNewUser,
trigger: isNewUser ? "signUp" : "signIn",
})
// Clear cookies if token is null
@@ -328,6 +342,7 @@ export async function callback(params: {
// @ts-expect-error
account,
isNewUser: false,
trigger: "signIn",
})
// Clear cookies if token is null
@@ -360,8 +375,18 @@ export async function callback(params: {
cookies,
}
} catch (e) {
if (e instanceof OAuthCallbackError) {
logger.error(e)
// REVIEW: Should we expose original error= and error_description=
// Should we use a different name for error= then, since we already use it for all kind of errors?
url.searchParams.set("error", OAuthCallbackError.name)
url.pathname += "/signin"
return { redirect: url.toString(), cookies }
}
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"

View File

@@ -6,10 +6,13 @@ import type { InternalOptions, ResponseInternal, Session } from "../../types.js"
import type { SessionStore } from "../cookie.js"
/** Return a session object filtered via `callbacks.session` */
export async function session(
sessionStore: SessionStore,
export async function session(params: {
options: InternalOptions
): Promise<ResponseInternal<Session | null>> {
sessionStore: SessionStore
isUpdate?: boolean
newSession?: any
}): Promise<ResponseInternal<Session | null>> {
const { options, sessionStore, newSession, isUpdate } = params
const {
adapter,
jwt,
@@ -33,23 +36,24 @@ export async function session(
try {
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
const newExpires = fromDate(sessionMaxAge)
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
const session = {
user: {
name: decodedToken?.name,
email: decodedToken?.email,
image: decodedToken?.picture,
},
expires: newExpires.toISOString(),
}
if (!decodedToken) throw new Error("Invalid JWT")
// @ts-expect-error
const token = await callbacks.jwt({ token: decodedToken })
const token = await callbacks.jwt({
token: decodedToken,
...(isUpdate && { trigger: "update" }),
session: newSession,
})
const newExpires = fromDate(sessionMaxAge)
if (token !== null) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as...").
const session = {
user: { name: token.name, email: token.email, image: token.picture },
expires: newExpires.toISOString(),
}
// @ts-expect-error
const newSession = await callbacks.session({ session, token })
@@ -125,14 +129,12 @@ export async function 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,
},
user: { name: user.name, email: user.email, image: user.image },
expires: session.expires.toISOString(),
},
user,
newSession,
...(isUpdate ? { trigger: "update" } : {}),
})
// Return session payload as response

View File

@@ -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() }
}
}

View File

@@ -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))

View File

@@ -32,12 +32,14 @@ export interface CredentialsConfig<
* @example
* ```ts
* //...
* async authorize(, request) {
* async authorize(credentials, request) {
* if(!isValidCredentials(credentials)) return null
* const response = await fetch(request)
* if(!response.ok) return null
* return await response.json() ?? null
* }
* //...
* ```
*/
authorize: (
/**

View File

@@ -29,7 +29,7 @@ export interface SendVerificationRequestParams {
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
server?: string | SMTPTransportOptions
/** @default `"Auth.js <no-reply@authjs.dev>"` */
from?: string
/**
@@ -72,7 +72,7 @@ export interface EmailConfig extends CommonProviderOptions {
* By default, we treat email addresses as all lower case,
* but you can override this function to change this behavior.
*
* [Documentation](https://authjs.dev/guides/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)
* [Normalizing the email address](https://authjs.dev/reference/core/providers_email#normalizing-the-email-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax)
*/
normalizeIdentifier?: (identifier: string) => string
}
@@ -287,7 +287,7 @@ export type EmailProviderType = "email"
*
* ## Normalizing the email address
*
* By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
* By default, Auth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior:
* ```ts
* EmailProvider({
* // ...
@@ -301,7 +301,7 @@ export type EmailProviderType = "email"
* return `${local}@${domain}`
*
* // You can also throw an error, which will redirect the user
* // to the error page with error=EmailSignin in the URL
* // to the sign-in page with error=EmailSignin in the URL
* // if (identifier.split("@").length > 2) {
* // throw new Error("Only one email allowed")
* // }

View File

@@ -52,7 +52,7 @@ export interface EVEOnlineProfile extends Record<string, any> {
* :::
*
* :::tip
* If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example:
* If using JWT for the session, you can add the `CharacterID` to the JWT and session. Example:
* ```js
* options: {
* jwt: {

View File

@@ -65,6 +65,7 @@ export interface GitHubProfile {
space: number
private_repos: number
}
[claim: string]: unknown
}
/**

View File

@@ -52,7 +52,10 @@ interface AdvancedEndpointHandler<P extends UrlParams, C, R> {
conform?: (response: Response) => Awaitable<Response | undefined>
}
/** Either an URL (containing all the parameters) or an object with more granular control. */
/**
* Either an URL (containing all the parameters) or an object with more granular control.
* @internal
*/
export type EndpointHandler<
P extends UrlParams,
C = any,
@@ -92,6 +95,8 @@ export type ProfileCallback<Profile> = (
tokens: TokenSet
) => Awaitable<User>
export type AccountCallback = (tokens: TokenSet) => TokenSet | undefined | void
export interface OAuthProviderButtonStyles {
logo: string
logoDark: string
@@ -138,13 +143,49 @@ export interface OAuth2Config<Profile>
userinfo?: string | UserinfoEndpointHandler
type: "oauth"
/**
* Receives the profile object returned by the OAuth provider, and returns the user object.
* This will be used to create the user in the database.
* Receives the full {@link Profile} returned by the OAuth provider, and returns a subset.
* It is used to create the user in the database.
*
* Defaults to: `id`, `email`, `name`, `image`
*
* [Documentation](https://authjs.dev/reference/adapters/models#user)
* @see [Database Adapter: User model](https://authjs.dev/reference/adapters#user)
*/
profile?: ProfileCallback<Profile>
/**
* Receives the full {@link TokenSet} returned by the OAuth provider, and returns a subset.
* It is used to create the account associated with a user in the database.
*
* :::note
* You need to adjust your database's [Account model](https://authjs.dev/reference/adapters#account) to match the returned properties.
* Check out the documentation of your [database adapter](https://authjs.dev/reference/adapters) for more information.
* :::
*
* Defaults to: `access_token`, `id_token`, `refresh_token`, `expires_at`, `scope`, `token_type`, `session_state`
*
* @example
* ```ts
* import GitHub from "@auth/core/providers/github"
* // ...
* GitHub({
* account(account) {
* // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token
* const refresh_token_expires_at =
* Math.floor(Date.now() / 1000) + Number(account.refresh_token_expires_in)
* return {
* access_token: account.access_token,
* expires_at: account.expires_at,
* refresh_token: account.refresh_token,
* refresh_token_expires_at
* }
* }
* })
* ```
*
* @see [Database Adapter: Account model](https://authjs.dev/reference/adapters#account)
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1
*/
account?: AccountCallback
/**
* The CSRF protection performed on the callback endpoint.
* @default ["pkce"]
@@ -190,7 +231,11 @@ export interface OAuth2Config<Profile>
options?: OAuthUserConfig<Profile>
}
/** TODO: Document */
/**
* Extension of the {@link OAuth2Config}.
*
* @see https://openid.net/specs/openid-connect-core-1_0.html
*/
export interface OIDCConfig<Profile>
extends Omit<OAuth2Config<Profile>, "type" | "checks"> {
type: "oidc"
@@ -204,6 +249,7 @@ export type OAuthEndpointType = "authorization" | "token" | "userinfo"
/**
* We parsed `authorization`, `token` and `userinfo`
* to always contain a valid `URL`, with the params
* @internal
*/
export type OAuthConfigInternal<Profile> = Omit<
OAuthConfig<Profile>,
@@ -229,7 +275,10 @@ export type OAuthConfigInternal<Profile> = Omit<
*
*/
redirectProxyUrl?: OAuth2Config<Profile>["redirectProxyUrl"]
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">
} & Pick<
Required<OAuthConfig<Profile>>,
"clientId" | "checks" | "profile" | "account"
>
export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & {
checks: OIDCConfig<Profile>["checks"]
@@ -238,11 +287,9 @@ export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & {
export type OAuthUserConfig<Profile> = Omit<
Partial<OAuthConfig<Profile>>,
"options" | "type"
> &
Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>
>
export type OIDCUserConfig<Profile> = Omit<
Partial<OIDCConfig<Profile>>,
"options" | "type"
> &
Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>
>

View File

@@ -99,6 +99,7 @@ export interface TwitterProfile {
text: string
}>
}
[claims: string]: unknown
}
/**

View File

@@ -98,7 +98,15 @@ export interface Theme {
*/
export type TokenSet = Partial<
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse
>
> & {
/**
* Date of when the `access_token` expires in seconds.
* This value is calculated from the `expires_in` value.
*
* @see https://www.ietf.org/rfc/rfc6749.html#section-4.2.2
*/
expires_at?: number
}
/**
* Usually contains information about the provider being used
@@ -116,16 +124,58 @@ export interface Account extends Partial<OpenIDTokenEndpointResponse> {
providerAccountId: string
/** Provider's type for this account */
type: ProviderType
/** id of the user this account belongs to */
/**
* id of the user this account belongs to
*
* @see https://authjs.dev/reference/adapters#user
*/
userId?: string
/**
* Calculated value based on {@link OAuth2TokenEndpointResponse.expires_in}.
*
* It is the absolute timestamp (in seconds) when the {@link OAuth2TokenEndpointResponse.access_token} expires.
*
* This value can be used for implementing token rotation together with {@link OAuth2TokenEndpointResponse.refresh_token}.
*
* @see https://authjs.dev/guides/basics/refresh-token-rotation#database-strategy
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1
*/
expires_at?: number
}
/** The OAuth profile returned from your provider */
/**
* The user info returned from your OAuth provider.
*
* @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
*/
export interface Profile {
sub?: string | null
name?: string | null
given_name?: string | null
family_name?: string | null
middle_name?: string | null
nickname?: string | null
preferred_username?: string | null
profile?: string | null
picture?: string | null | any
website?: string | null
email?: string | null
image?: string | null
email_verified?: boolean | null
gender?: string | null
birthdate?: string | null
zoneinfo?: string | null
locale?: string | null
phone_number?: string | null
updated_at?: Date | string | number | null
address?: {
formatted?: string | null
street_address?: string | null
locality?: string | null
region?: string | null
postal_code?: string | null
country?: string | null
} | null
[claim: string]: unknown
}
/** [Documentation](https://authjs.dev/guides/basics/callbacks) */
@@ -188,40 +238,86 @@ export interface CallbacksOptions<P = Profile, A = Account> {
* 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://authjs.dev/guides/basics/callbacks#session-callback) |
* [`jwt` callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`useSession`](https://authjs.dev/reference/react/#usesession) |
* [`getSession`](https://authjs.dev/reference/utilities/#getsession) |
*
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt)
*/
session: (params: {
session: Session
user: User | AdapterUser
token: JWT
}) => Awaitable<Session>
session: (
params:
| {
session: Session
/** Available when {@link AuthConfig.session} is set to `strategy: "jwt"` */
token: JWT
/** Available when {@link AuthConfig.session} is set to `strategy: "database"`. */
user: AdapterUser
} & {
/**
* Available when using {@link AuthConfig.session} `strategy: "database"` and an update is triggered for the session.
*
* :::note
* You should validate this data before using it.
* :::
*/
newSession: any
trigger: "update"
}
) => Awaitable<Session | DefaultSession>
/**
* 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 inaccessible from the client.
* Anything else will be kept from your front-end.
*
* Returning `null` will invalidate the JWT session by clearing
* the user's cookies. You'll still have to monitor and invalidate
* unexpired tokens from future requests yourself to prevent
* unauthorized access.
* The JWT is encrypted by default.
*
* By default the JWT is encrypted.
*
* [Documentation](https://authjs.dev/guides/basics/callbacks#jwt-callback) |
* [`session` callback](https://authjs.dev/guides/basics/callbacks#session-callback)
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt: (params: {
/**
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT},
* `name`, `email` and `image` will be included.
*
* Otherwise, it will be the full {@link JWT} for subsequent calls.
*/
token: JWT
user?: User | AdapterUser
account?: A | null
/**
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback.
* @note available when `trigger` is `"signIn"` or `"signUp"`.
*
* Resources:
* - [Credentials Provider](https://authjs.dev/reference/core/providers_credentials)
* - [User database model](https://authjs.dev/reference/adapters#user)
*/
user: User | AdapterUser
/**
* Contains information about the provider that was used to sign in.
* Also includes {@link TokenSet}
* @note available when `trigger` is `"signIn"` or `"signUp"`
*/
account: A | null
/**
* The OAuth profile returned from your provider.
* (In case of OIDC it will be the decoded ID Token or /userinfo response)
* @note available when `trigger` is `"signIn"`.
*/
profile?: P
/**
* Check why was the jwt callback invoked. Possible reasons are:
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present.
* - user sign-up: a user is created for the first time in the database (when {@link AuthConfig.session}.strategy is set to `"database"`)
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
* In case of the latter, `trigger` will be `undefined`.
*/
trigger?: "signIn" | "signUp" | "update"
/** @deprecated use `trigger === "signUp"` instead */
isNewUser?: boolean
/**
* When using {@link AuthConfig.session} `strategy: "jwt"`, this is the data
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method.
*
* ⚠ Note, you should validate this data before using it.
*/
session?: any
}) => Awaitable<JWT | null>
}
@@ -262,7 +358,7 @@ export interface EventCallbacks {
/**
* 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.
*/
signOut: (
@@ -280,7 +376,7 @@ export interface EventCallbacks {
/**
* 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.
*/
session: (message: { session: Session; token: JWT }) => Awaitable<void>
@@ -295,7 +391,7 @@ export type ErrorPageParam = "Configuration" | "AccessDenied" | "Verification"
export type SignInPageErrorParam =
| "Signin"
| "OAuthSignin"
| "OAuthCallback"
| "OAuthCallbackError"
| "OAuthCreateAccount"
| "EmailCreateAccount"
| "Callback"
@@ -385,15 +481,42 @@ export type InternalProvider<T = ProviderType> = (T extends "oauth"
callbackUrl: string
}
/**
* Supported actions by Auth.js. Each action map to a REST API endpoint.
* Some actions have a `GET` and `POST` variant, depending on if the action
* changes the state of the server.
*
* - **`"callback"`**:
* - **`GET`**: Handles the callback from an [OAuth provider](https://authjs.dev/reference/core/providers_oauth).
* - **`POST`**: Handles the callback from a [Credentials provider](https://authjs.dev/reference/core/providers_credentials).
* - **`"csrf"`**: Returns the raw CSRF token, which is saved in a cookie (encrypted).
* It is used for CSRF protection, implementing the [double submit cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) technique.
* :::note
* Some frameworks have built-in CSRF protection and can therefore disable this action. In this case, the corresponding endpoint will return a 404 response. Read more at [`skipCSRFCheck`](https://authjs.dev/reference/core#skipcsrfcheck).
* _⚠ We don't recommend manually disabling CSRF protection, unless you know what you're doing._
* :::
* - **`"error"`**: Renders the built-in error page.
* - **`"providers"`**: Returns a client-safe list of all configured providers.
* - **`"session"`**:
* - **`GET**`: Returns the user's session if it exists, otherwise `null`.
* - **`POST**`: Updates the user's session and returns the updated session.
* - **`"signin"`**:
* - **`GET`**: Renders the built-in sign-in page.
* - **`POST`**: Initiates the sign-in flow.
* - **`"signout"`**:
* - **`GET`**: Renders the built-in sign-out page.
* - **`POST`**: Initiates the sign-out flow. This will invalidate the user's session (deleting the cookie, and if there is a session in the database, it will be deleted as well).
* - **`"verify-request"`**: Renders the built-in verification request page.
*/
export type AuthAction =
| "callback"
| "csrf"
| "error"
| "providers"
| "session"
| "csrf"
| "signin"
| "signout"
| "callback"
| "verify-request"
| "error"
/** @internal */
export interface RequestInternal {

View File

@@ -51,7 +51,7 @@ export async function signIn<
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
},
// @ts-expect-error -- ignore
// @ts-ignore
body: new URLSearchParams({
...options,
csrfToken,

View File

@@ -52,7 +52,7 @@ export async function signIn<
"Content-Type": "application/x-www-form-urlencoded",
"X-Auth-Return-Redirect": "1",
},
// @ts-expect-error -- ignore
// @ts-ignore
body: new URLSearchParams({
...options,
csrfToken,

View File

@@ -224,6 +224,13 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
<div>WorkOS</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://www.descope.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/97479186?v=4" alt="Descope Logo" />
</a><br />
<div>Descope</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />

View File

@@ -21,7 +21,7 @@ export interface JWTEncodeParams {
secret: string | Buffer
/**
* The maximum age of the NextAuth.js issued JWT in seconds.
* @default 30 * 24 * 30 * 60 // 30 days
* @default 30 * 24 * 60 * 60 // 30 days
*/
maxAge?: number
}
@@ -42,7 +42,7 @@ export interface JWTOptions {
secret: string
/**
* The maximum age of the NextAuth.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 NextAuth.js issued JWT encoding. */

3636
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**/*"]
"outputs": ["dist/**/*", "*.js", "*.d.ts", "*.d.ts.map"]
},
"next-auth#build": {
"dependsOn": ["^build"],
@@ -53,6 +53,7 @@
"docs#dev": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -62,7 +63,6 @@
"@next-auth/mongodb-adapter#build",
"@next-auth/neo4j-adapter#build",
"@next-auth/pouchdb-adapter#build",
"@next-auth/prisma-adapter#build",
"@next-auth/sequelize-adapter#build",
"@next-auth/supabase-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",
@@ -76,6 +76,7 @@
"docs#build": {
"dependsOn": [
"@auth/core#build",
"@auth/prisma-adapter#build",
"@auth/sveltekit#build",
"@next-auth/dgraph-adapter#build",
"@next-auth/dynamodb-adapter#build",
@@ -85,7 +86,6 @@
"@next-auth/mongodb-adapter#build",
"@next-auth/neo4j-adapter#build",
"@next-auth/pouchdb-adapter#build",
"@next-auth/prisma-adapter#build",
"@next-auth/sequelize-adapter#build",
"@next-auth/supabase-adapter#build",
"@next-auth/typeorm-legacy-adapter#build",