Compare commits

..

41 Commits

Author SHA1 Message Date
Balázs Orbán
a2e5afa162 feat(react): make session requireable in useSession (#2236)
A living session could be a requirement for specific pages (like dashboards). If it doesn’t exist, the user should be redirected to a page asking them to sign in again.

Sometimes, a user might log out by accident, or by deleting cookies on purpose. If that happens (e.g. on a separate tab), then `useSession({ required: true })` should detect the absence of a session cookie and always return a non-nullable Session object type.

When `required: true` is set, the default behavior will be to redirect the user to the sign-in page. This can be overridden by an `action()` callback:

```js
const session = useSession({
  required: true,
  action() {
    // ....
  }
})
if (session.status === "Loading") return "Loading or not authenticated..."

// session.data is always defined here.
```

Co-authored-by: Kristóf Poduszló <kripod@protonmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>

BREAKING CHANGE:

The `useSession` hook now returns an object. Here is how to accommodate for this change:

```diff
- const [ session, loading ] = useSession()
+ const { data: session, status } = useSession()
+ const loading = status === "loading"
```

With the new `status` option, you can test states much more clearly.
2021-07-05 16:03:55 +02:00
Nico Domino
53e5e37948 docs: update tutorials/faq structure (#2256)
* docs: cleanup css

* docs: upgrade deps

* docs: cleanup docusaurus config

* docs: reorganise tutorials page

* docs: fix github-counter css

* docs: update faq page structure

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(tutorials): copy

Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(style): reorg css

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-07-05 11:15:45 +02:00
Balázs Orbán
8ff4b26014 Merge main into next 2021-07-02 13:10:34 +02:00
Balázs Orbán
2c35aa27f9 Merge main into next 2021-06-29 22:55:33 +02:00
Balázs Orbán
2833b661bd feat(core): use named params in callbacks (#2173)
Some of our user-facing callbacks come with a bunch of parameters, and it is not always the case that a user needs all of them. Picking out certain parameters from the end of the list would require the user to define params that they wouldn't even need.

Therefore this PR changes such callbacks so the user can only pick the necessary parameters.

This comes with the bonus of better TS support on the `session` and `signIn` callbacks, where some parameters historically could have been different types.

In the `session` callback, the second param could have been `token` (when using JWT sessions) or `user` (when using DB persisted sessions). Now they are separate parameters.

In the `signIn` callback, we now separate `profile` (OAuth), `email` (Email) and `credentials` (Credentials) provider params.

BREAKING CHANGE:

The `callbacks` method signatures are changing the following way:

```diff
- signIn(user, account, profileOrEmailOrCredentials)
+ signIn({ user, account, profile, email, credentials })
```
```diff
- redirect(url, baseUrl)
+ redirect({ url, baseUrl })
```
```diff
- session(session, tokenOrUser)
+ session({ session, token, user })
```
```diff
- jwt(token, user, account, OAuthProfile, isNewUser)
+ jwt({ token, user, account, profile, isNewUser })
```

> NOTE: You only need to define the params that you actually need (no more need  for `_` params.)

This way, if you only need `token` and `account` in the `jwt` callback, you can write:

```js
jwt({ token, account }) {
  if(account) {
    token.accessToken = account.access_token
  }
  return token
}
```
2021-06-26 14:54:13 +02:00
Balázs Orbán
6c1a0ec620 feat: bump dependencies (#2254)
Node 10 has reached end-of-life, Next.js stopped supporting it in Next 11. Since we are a Next.js library, it doesn't make sense for us to support anything lower than that either.

I also upgraded a bunch of dependencies and dropped some that weren't necessary anymore.

BREAKING CHANGE:

The lowest supported Node version is 12. (We still support IE11 in browsers, until that is not dropped by Next.js itself)
2021-06-26 11:39:18 +02:00
Balázs Orbán
988c9912b1 test(ts): fix ts tests 2021-06-26 00:32:35 +02:00
Balázs Orbán
a225324d4f Merge main into next 2021-06-25 22:10:09 +02:00
Nico Domino
3a48b8e467 docs: update errors page with more details (#2196) 2021-06-22 20:15:17 +02:00
Lluis Agusti
fb50b54466 test(client-provider): fix flaky test (#2216) 2021-06-20 20:04:34 +02:00
Sheldon Vaughn
fa89431573 docs(provider): import useState in WorkOS example (#2198)
Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Add useState dependency

I went to execute the signin.js file and received an error that useState was undefined. Was able to reconcile this by adding the dependency.

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Sangwon Park <dev.psw@gmail.com>
2021-06-17 01:23:50 +02:00
Balázs Orbán
3383857715 Merge branch 'main' into next 2021-06-17 01:21:49 +02:00
David Peherstorfer
bbc2d9b538 docs(provider): scope expects space separated string (#2188)
* fix(docs): scope expects space separated string

Currently the docs list string[] as possible type for scope. 
However, It only accepts a string (with space as separator).

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-16 07:38:39 +02:00
Hugh Boylan
d10bd9beba fix(react): publish react scripts with npm build (#2192) 2021-06-15 23:27:06 +02:00
Vikrant Bhat
c1c866f664 docs(provider): English language sentence fix (#2175)
* English language sentence fix

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-14 23:55:10 +02:00
Balázs Orbán
86ff89e296 fix(react): initialize session without loading state (#2180)
See commit dd12181378
2021-06-14 21:16:42 +02:00
Hugh Boylan
dd12181378 initialize session without loading state (#2180) 2021-06-14 20:11:56 +02:00
Balázs Orbán
47c17a89ae Merge branch 'main' into next
commit 665445818e
Author: Balázs Orbán <info@balazsorban.com>
Date:   Sat Jun 12 17:11:53 2021 +0200

    docs(config): link to next documentation instead of canary

commit 67cf2a11bb
Author: ndom91 <yo@ndo.dev>
Date:   Sat Jun 12 16:42:48 2021 +0200

    docs: fix alt client provider example
2021-06-12 17:15:35 +02:00
Nico Domino
c07fe1b9a7 docs: add versioned docs for unreleased v4.0.0-next.* (#2167)
* docs: add versioned docs for unreleased canary 4.x

* docs(fix): prettier broke custom image component without .mdx extension

* docs(feat): automatically fetch latest stable version label

* docs: cleanup script

* docs: cleanup next version name

* docs(chore): cleanup version leftovers

* docs: fix .gitignore

* docs: v4 default

* docs: remove auto version generation

* docs: fix missing import

* docs: rm node-fetch
2021-06-12 17:04:32 +02:00
Balázs Orbán
abaa5aed65 fix(react): don't use localStorage on server side (#2166) 2021-06-11 22:11:50 +02:00
Balázs Orbán
ca0ed1e2a8 feat(react): create client tailored to React (#1473)
**What**:

These changes ensure that we work more tightly with React that can also result in unforeseen performance boosts. In case we would decide on expanding to other libraries/frameworks, a new file per framework could be added.

**Why**:

Some performance issues (https://github.com/nextauthjs/next-auth/issues/844) could only be fixed by moving more of the client code into the `Provider`.

**How**:

Refactoring `next-auth/client`

Related: #1461, #1084, #1462

BREAKING CHANGE:
**1.** `next-auth/client` is renamed to `next-auth/react`.

**2.** In the past, we exposed most of the functions with different names for convenience. To simplify our source code, the new React specific client code exports only the following functions, listed with the necessary changes:

- `setOptions`: Not exposed anymore, use `SessionProvider` props
- `options`: Not exposed anymore, use `SessionProvider` props
- `session`: Rename to `getSession`
- `providers`: Rename to `getProviders`
- `csrfToken`: Rename to `getCsrfToken`
- `signin`: Rename to `signIn`
- `signout`: Rename to `signOut`
- `Provider`: Rename to `SessionProvider`

**3.** `Provider` changes.
- `Provider` is renamed to `SessionProvider`
- The `options` prop is now flattened as the props of `SessionProvider`.
- `clientMaxAge` has been renamed to `staleTime`.
- `keepAlive` has been renamed to `refetchInterval`.
An example of the changes:
```diff
- <Provider options={{clientMaxAge: 0, keepAlive: 0}}>{children}</Provider>
+ <SessionProvider staleTime={0} refetchInterval={0}>{children}</SessionProvider> 
```

**4.** It is now **required** to wrap the part of your application that uses `useSession` into a `SessionProvider`.

Usually, the best place for this is in your `pages/_app.jsx` file:

```jsx
import { SessionProvider } from "next-auth/react"

export default function App({
  Component,
  pageProps: { session, ...pageProps }
}) {
  return (
    // `session` comes from `getServerSideProps` or `getInitialProps`.
    // Avoids flickering/session loading on first load.
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}
```
2021-06-11 21:59:36 +02:00
Balázs Orbán
ed345346db fix(ts): add AzureAD to OAuthProviderType 2021-06-10 20:11:12 +02:00
Ben
5ac1db741a feat(provider): refactor Azure AD, B2C providers (#1591)
BREAKING CHANGE: 

If you currently use `AzureADB2C`, you will need to update it to to `AzureAD` There should be no other changes needed.
2021-06-10 20:09:34 +02:00
Balázs Orbán
0c17af969e Merge branch 'main' into next 2021-06-10 14:49:36 +02:00
Tom Richter
ea9b6e37a9 fix(provider): convert github profile id from int to string (#2108) 2021-06-09 17:02:52 +02:00
Balázs Orbán
960bc1e9c0 feat(adapter): remove adapters from core (#1919)
* feat(adapter): remove built-in adapters and database

BREAKING CHANGE:

From now on, you will have to import your own adapter

Check out https://github.com/nextauthjs/adapters

The migration is super easy and has HUGE advantages for those not using TypeORM.

```diff
// [...nextauth].js
+ import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"

...
export default NextAuth({
-  database: "yourconnectionstring",
+ adapter: TypeORMAdapter("yourconnectionstring")
})
```


Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Giovanni Carnel <479046+g10@users.noreply.github.com>
2021-06-09 14:45:13 +02:00
Balázs Orbán
d29e3e9c9d Merge branch 'main'
Conflicts:
	config/babel.config.json
	package-lock.json
	package.json
	src/server/index.js
	src/server/routes/callback.js
	src/server/routes/signin.js
2021-06-09 02:16:11 +02:00
Balázs Orbán
a388b44d0b Merge branch 'main' into next 2021-05-03 21:11:04 +02:00
Balázs Orbán
b6a3a72db4 Merge branch 'main' into next 2021-04-24 23:20:41 +02:00
Balázs Orbán
edcb10a823 Merge branch 'main' into next 2021-04-23 15:43:20 +02:00
Balázs Orbán
2acabe19e0 Merge main into next 2021-04-23 15:28:26 +02:00
Balázs Orbán
a6f5f4c184 fix: use upgraded require optional (#1743)
* chore(deps): switch back to (updated) require_optional

* fix: use @balazsorban/require-optional
2021-04-16 16:05:44 +02:00
Balázs Orbán
9fa93e3b5e fix(build): use optional-require dependency (#1736)
* chore(deps): add optional-require

* refactor: use optional-require
2021-04-16 00:23:29 +02:00
Balázs Orbán
cb4342fdda feat(build): modernize how we bundle next-auth (#1682)
* feat(build): optionally include TypeORM

If the user doesn't use databases,
it shouldn't be necessary to iclude it in the bundle.
This can more than half the package size!

* feat(build): clean up in dependencies

Remove unused dependencies, move optional ones to be optional

* feat(build): add exports field

* fix: use peerDependenciesMeta instead of non-standard peerOptionalDependecns field

* fix: ts-standard string quotes

* fix: ts-standard string quotes

* refactor: use asnyc/await for sendVerificationRequest

* chore(deps): upgrade mongodb, remove require_optional

Co-authored-by: ndom91 <yo@ndo.dev>

BREAKING CHANGE:
`typeorm`, and `nodemailer` are no longer dependencies added by default.
If you need any of them, you will have to install them yourself in your project directory.
TypeOrm is the default adapter, so if you only provide an `adapter` configuration or a `database`, you will need `typeorm`. You could also check out `@next-auth/typeorm-adapter`. In case you are using the Email provider, you will have to install `nodemailer` (or you can use the choice of your library in the `sendVerificationRequest` callback to send out the e-mail.)
2021-04-15 23:40:33 +02:00
Balázs Orbán
5f717b3914 chore: merge main into next 2021-04-12 00:46:27 +02:00
Balázs Orbán
d09a45ec7c chore: merge main into next 2021-03-26 16:23:35 +01:00
Balázs Orbán
930f58eba3 chore: merge main into next 2021-03-08 01:05:54 +01:00
Balázs Orbán
c20b7f2930 feat: use IE11 as client code bundle target (#1402) 2021-03-03 20:25:42 +01:00
Balázs Orbán
e418cddd96 chore: merge main into next 2021-03-03 20:25:42 +01:00
Balázs Orbán
111e7aabdf feat(provider): remove state property
BREAKING CHANGE: adding `state: true` is already redundant
as `protection: "state` is the default value. `state: false`
can be substituted with `protection: "state"`
2021-02-15 21:47:47 +01:00
Balázs Orbán
a113ef6fab feat: encourage returning strings instead of throwing
BREAKING CHANGE: We have supported throwing strings
for redirections, while we were showing a waring.
From now on, it is not possible. The user MUST return a string,
rather than throw it.
2021-02-15 21:47:35 +01:00
215 changed files with 18892 additions and 9836 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +1,2 @@
/types/ @balazsorban44 @lluia
/__tests__/ @lluia

1
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,3 @@
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
open_collective: nextauth
github: [balazsorban44]

43
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,43 @@
---
name: Bug report
about: Report a defect with NextAuth.js
labels: bug
assignees: ""
---
## Description 🐜
Please provide a clear and concise description of the bug in NextAuth.js.
🚧 _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
## How to reproduce ☕️
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
## Screenshots / Logs 📽
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
## Environment 🖥
Please run this command in your project's root folder:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,next-auth,react"
```
and paste the output here.
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -1,91 +0,0 @@
name: Bug Report
description: File a bug report
labels: bug
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please provide the following information:
- type: textarea
id: description
attributes:
label: Description 🐜
description: Please provide a clear and concise description of the bug in NextAuth.js
validations:
required: true
- type: dropdown
id: ownproject
attributes:
label: Is this a bug in your own project?
description: 🚧 _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) or use the [Discussions tab](https://github.com/nextauthjs/next-auth/discussions) - this helps us reduce the maintenance overhead._
multiple: false
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: Please provide a link or code snippets to a minimal reproduction of the bug
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
- type: textarea
id: logs
attributes:
label: Screenshots / Logs 📽
description: We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
- type: markdown
attributes:
value: |
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
validations:
required: false
- type: textarea
id: environment
attributes:
label: Environment 🖥
validations:
required: true
- type: markdown
attributes:
value: |
Please run this command in your project's root folder:
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,next-auth,react"
```
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help solve this bug in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

View File

@@ -0,0 +1,39 @@
---
name: Feature request
about: Suggest an idea for NextAuth.js
labels: enhancement
assignees: ""
---
## Summary 💭
A clear and concise summary of the feature being proposed.
## Description 📓
Please provide a more in-depth description of the feature proposed.
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
- **purpose of the feature**
- **potential problems**
- **potential alternatives**
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -1,68 +0,0 @@
name: Feature Request
description: Suggest an idea for NextAuth.js
labels: enhancement
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library. Please provide the following information:
- type: textarea
id: description
attributes:
label: Description 📓
description: Please provide a more in-depth description of the feature proposed.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
- **purpose of the feature**
- **potential problems**
- **potential alternatives**
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: If you have a CodeSandbox playground or some code snippets to help us visualize your idea better, please provide it here.
validations:
required: true
- type: markdown
attributes:
value: |
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help implement this feature in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

32
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Question
about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ""
---
## Question 💬
Please provide an in-depth description of the question you have.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -1,62 +0,0 @@
name: Question
description: Ask a question about NextAuth.js or for help using it
labels: question
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: markdown
attributes:
value: |
We are glad that you have a question about this library. Please provide the following information:
- type: textarea
id: question
attributes:
label: Question 💬
description: Please provide an in-depth description of the question you have.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
- type: textarea
id: reproduction
attributes:
label: How to reproduce ☕️
description: Please provide a link to a minimal reproduction or code snippets that represents your question
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help answer this question in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

36
.github/ISSUE_TEMPLATE/typescript.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: TypeScript
about: Ask a question about NextAuth.js TypeScript integration
labels:
- question
- TypeScript
assignees:
- lluia
- balazsorban44
---
## Question 💬
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -1,58 +0,0 @@
name: TypeScript
description: Ask a question about NextAuth.js TypeScript integration
labels: [question, TypeScript]
assignees: [lluia, balazsorban44]
# note: markdown sections will NOT appear as part of the issue as per documentation, rather they provide context to the user
# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema#markdown
body:
- type: textarea
id: question
attributes:
label: Question 💬
description: Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
validations:
required: true
- type: markdown
attributes:
value: |
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
- type: textarea
id: codesandbox
attributes:
label: How to reproduce ☕️
description: Please provide a link to a minimal reproduction or code snippets that represents your question
validations:
required: true
- type: markdown
attributes:
value: |
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
- type: dropdown
id: pr
attributes:
label: Contributing 🙌🏽
multiple: false
options:
- "Yes, I am willing to help answer this question in a PR"
- "No, I am afraid I cannot help regarding this"
validations:
required: true
- type: markdown
attributes:
value: |
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚

7
.gitignore vendored
View File

@@ -40,6 +40,8 @@ src/providers/index.js
/providers.js
/errors.js
/errors.d.ts
/react.js
/react.d.ts
# Development app
app/next-auth
@@ -62,8 +64,3 @@ app/yarn.lock
# Tests
/coverage
# v4
packages
apps
docs/providers.json

View File

@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx pretty-quick --staged
# npx pretty-quick --staged

View File

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -55,11 +55,11 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted separately.
reported by contacting me@iaincollins.com. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other

View File

@@ -67,15 +67,15 @@ NextAuth.js can be used with or without a database.
### Secure by default
- Promotes the use of passwordless sign in mechanisms
- Designed to be secure by default and encourage best practice for safeguarding user data
- Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
- Promotes the use of passwordless sign-in mechanisms
- Designed to be secure by default and encourage best practices for safeguarding user data
- Uses Cross-Site Request Forgery (CSRF) Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and keepalive messages to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
- Features tab/window syncing and session polling to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
@@ -90,6 +90,7 @@ The package at `@types/next-auth` is now deprecated.
### Add API Route
```javascript
// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
@@ -110,18 +111,18 @@ export default NextAuth({
from: "<no-reply@example.com>",
}),
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL,
})
```
### Add React Component
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```javascript
import { useSession, signIn, signOut } from "next-auth/client"
import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
const [session, loading] = useSession()
const { data: session } = useSession()
if (session) {
return (
<>
@@ -139,7 +140,26 @@ export default function Component() {
}
```
## Acknowledgements
### Share/configure session state
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```
## Acknowledgments
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
@@ -147,43 +167,13 @@ export default function Component() {
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
</div>
<div>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
</div>
### Support
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://vercel.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
</a><br />
<div>Vercel</div><br />
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://prisma.io" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
</a><br />
<div>Prisma</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" />
</a><br />
<div>Checkly</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
</tr><tr></tr>
</tbody>
</table>
<br />
<!--sponsors end-->
## Contributing

View File

@@ -19,6 +19,6 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
Currently, the best way to report an issue is by contacting us via email at me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
Currently, the best way to report an issue is by emailing me@iaincollins.com
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -1,17 +1,18 @@
import { signIn } from 'next-auth/client'
import { signIn } from "next-auth/react"
export default function AccessDenied () {
export default function AccessDenied() {
return (
<>
<h1>Access Denied</h1>
<p>
<a
href='/api/auth/signin'
href="/api/auth/signin"
onClick={(e) => {
e.preventDefault()
signIn()
}}
>You must be signed in to view this page
>
You must be signed in to view this page
</a>
</p>
</>

View File

@@ -1,22 +1,22 @@
import Link from 'next/link'
import { signIn, signOut, useSession } from 'next-auth/client'
import styles from './header.module.css'
import Link from "next/link"
import { signIn, signOut, useSession } from "next-auth/react"
import styles from "./header.module.css"
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header () {
const [session, loading] = useSession()
export default function Header() {
const { data: session, status } = useSession()
return (
<header>
<noscript>
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
!session && status === "loading" ? styles.loading : styles.loaded
}`}
>
{!session && (
@@ -25,7 +25,7 @@ export default function Header () {
You are not signed in
</span>
<a
href='/api/auth/signin'
href="/api/auth/signin"
className={styles.buttonPrimary}
onClick={(e) => {
e.preventDefault()
@@ -50,7 +50,7 @@ export default function Header () {
<strong>{session.user.email || session.user.name}</strong>
</span>
<a
href='/api/auth/signout'
href="/api/auth/signout"
className={styles.button}
onClick={(e) => {
e.preventDefault()
@@ -66,42 +66,42 @@ export default function Header () {
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href='/'>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/client'>
<Link href="/client">
<a>Client</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/server'>
<Link href="/server">
<a>Server</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected'>
<Link href="/protected">
<a>Protected</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected-ssr'>
<Link href="/protected-ssr">
<a>Protected(SSR)</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/api-example'>
<Link href="/api-example">
<a>API</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/credentials'>
<Link href="/credentials">
<a>Credentials</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/email'>
<Link href="/email">
<a>Email</a>
</Link>
</li>

View File

@@ -7,7 +7,7 @@ module.exports = {
alias: {
...config.resolve.alias,
"next-auth$": path.join(process.cwd(), "next-auth/server"),
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
"next-auth/react$": path.join(process.cwd(), "next-auth/client/react"),
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),

View File

@@ -15,6 +15,7 @@
"license": "ISC",
"dependencies": {
"next": "^11.0.1",
"nodemailer": "^6.6.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},

View File

@@ -1,31 +1,13 @@
import { Provider } from "next-auth/client"
import { SessionProvider } from "next-auth/react"
import "./styles.css"
// Use the <Provider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<Provider
// Provider options are not required but can be useful in situations where
// you have a short session maxAge time. Shown here with default values.
options={{
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
clientMaxAge: 0,
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0,
}}
session={pageProps.session}
>
<SessionProvider session={session}>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}

View File

@@ -5,10 +5,6 @@ import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
@@ -79,13 +75,4 @@ export default NextAuth({
},
debug: false,
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
})

View File

@@ -1,5 +1,5 @@
// This is an example of how to read a JSON Web Token from an API route
import jwt from 'next-auth/jwt'
import jwt from "next-auth/jwt"
const secret = process.env.SECRET

View File

@@ -1,12 +1,17 @@
// This is an example of to protect an API route
import { getSession } from 'next-auth/client'
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
res.send({
content:
"This is protected content. You can access this content because you are signed in.",
})
} else {
res.send({ error: 'You must be sign in to view the protected content on this page.' })
res.send({
error: "You must be sign in to view the protected content on this page.",
})
}
}

View File

@@ -1,5 +1,5 @@
// This is an example of how to access a session from an API route
import { getSession } from 'next-auth/client'
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })

View File

@@ -1,15 +1,15 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page () {
export default function Page() {
const [response, setResponse] = React.useState(null)
const handleLogin = (options) => async () => {
if (options.redirect) {
return signIn('credentials', options)
return signIn("credentials", options)
}
const response = await signIn('credentials', options)
const response = await signIn("credentials", options)
setResponse(response)
}
@@ -21,18 +21,22 @@ export default function Page () {
setResponse(response)
}
const [session] = useSession()
const { data: session } = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Credentials logout</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
@@ -40,14 +44,24 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Credentials login</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
<span className="spacing">Default:</span>
<button onClick={handleLogin({ redirect: true, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: "" })}>
Login
</button>
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -1,11 +1,11 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page () {
export default function Page() {
const [response, setResponse] = React.useState(null)
const [email, setEmail] = React.useState('')
const [email, setEmail] = React.useState("")
const handleChange = (event) => {
setEmail(event.target.value)
@@ -15,9 +15,9 @@ export default function Page () {
event.preventDefault()
if (options.redirect) {
return signIn('email', options)
return signIn("email", options)
}
const response = await signIn('email', options)
const response = await signIn("email", options)
setResponse(response)
}
@@ -29,18 +29,22 @@ export default function Page () {
setResponse(response)
}
const [session] = useSession()
const { data: session } = useSession()
if (session) {
return (
<Layout>
<h1>Test different flows for Email logout</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
@@ -48,20 +52,29 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Email login</h1>
<label className='spacing'>
Email address:{' '}
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
</label><br />
<label className="spacing">
Email address:{" "}
<input
type="text"
id="email"
name="email"
value={email}
onChange={handleChange}
/>
</label>
<br />
<form onSubmit={handleLogin({ redirect: true, email })}>
<span className='spacing'>Default:</span>
<button type='submit'>Sign in with Email</button>
<span className="spacing">Default:</span>
<button type="submit">Sign in with Email</button>
</form>
<form onSubmit={handleLogin({ redirect: false, email })}>
<span className='spacing'>No redirect:</span>
<button type='submit'>Sign in with Email</button>
<span className="spacing">No redirect:</span>
<button type="submit">Sign in with Email</button>
</form>
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -1,37 +1,47 @@
// This is an example of how to protect content using server rendering
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page ({ content, session }) {
export default function Page({ content, session }) {
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
<p>
<strong>{content}</strong>
</p>
</Layout>
)
}
export async function getServerSideProps (context) {
export async function getServerSideProps(context) {
const session = await getSession(context)
let content = null
if (session) {
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) { content = json.content }
if (json.content) {
content = json.content
}
}
return {
props: {
session,
content
}
content,
},
}
}

View File

@@ -1,33 +1,35 @@
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
export default function Page () {
const [session, loading] = useSession()
export default function Page() {
const { status } = useSession({
required: true,
})
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
if (status === "loading") return
const fetchData = async () => {
const res = await fetch('/api/examples/protected')
const res = await fetch("/api/examples/protected")
const json = await res.json()
if (json.content) { setContent(json.content) }
if (json.content) {
setContent(json.content)
}
}
fetchData()
}, [session])
}, [status])
// When rendering client side don't display anything until loading is complete
if (typeof window !== 'undefined' && loading) return null
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
if (status === "loading") return <Layout>Loading...</Layout>
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
<p>
<strong>{content}</strong>
</p>
</Layout>
)
}

View File

@@ -1,7 +1,7 @@
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
export default function Page () {
export default function Page() {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
@@ -11,27 +11,31 @@ export default function Page () {
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
This page uses the universal <strong>getSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
support Server Side Rendering with authentication.
Using <strong>getSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require client side JavaScript.
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to render.
The disadvantage of Server Side Rendering is that this page is slower to
render.
</p>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps (context) {
export async function getServerSideProps(context) {
return {
props: {
session: await getSession(context)
}
session: await getSession(context),
},
}
}

View File

@@ -3,7 +3,7 @@
// https://nextjs.org/docs/basic-features/supported-browsers-features
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
presets: [["@babel/preset-env", { targets: { node: "12" } }]],
plugins: [
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
@@ -12,7 +12,10 @@ module.exports = {
overrides: [
{
test: ["../src/client/**"],
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
presets: [
["@babel/preset-env", { targets: { ie: "11" } }],
["@babel/preset-react", { runtime: "automatic" }],
],
},
{
test: ["../src/server/pages/**"],
@@ -20,14 +23,7 @@ module.exports = {
},
{
test: ["../src/**/*.test.js"],
presets: [
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
presets: [["@babel/preset-react", { runtime: "automatic" }]],
},
],
}

View File

@@ -3,7 +3,7 @@ const path = require("path")
const MODULE_ENTRIES = {
SERVER: "index",
CLIENT: "client",
REACT: "react",
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
@@ -13,12 +13,16 @@ const MODULE_ENTRIES = {
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
[`${MODULE_ENTRIES.SERVER}.js`]:
"module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.REACT}.js`]:
"module.exports = require('./dist/client/react').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]:
"module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]:
"module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]:
"module.exports = require('./dist/lib/errors').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
@@ -32,7 +36,7 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
`${MODULE_ENTRIES.REACT}-client.d.ts`,
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
@@ -43,7 +47,10 @@ const TYPES_TARGETS = [
TYPES_TARGETS.forEach((target) => {
fs.copy(
path.resolve("types", target),
path.join(process.cwd(), target),
path.join(
process.cwd(),
target.startsWith("react-client") ? "react.d.ts" : target
),
(err) => {
if (err) throw err
console.log(`[build-types] copying "${target}" to root folder`)

View File

@@ -8,4 +8,5 @@ module.exports = {
collectCoverageFrom: ["!client/__tests__/**"],
testMatch: ["**/*.test.js"],
coverageDirectory: "../coverage",
testEnvironment: "jsdom",
}

11549
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "3.29.9",
"version": "0.0.0-semantically-released",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -22,8 +22,7 @@
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./react": "./dist/client/react.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
@@ -43,7 +42,8 @@
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr"
"version:pr": "node ./config/version-pr",
"website": "cd www && npm run start"
},
"files": [
"dist",
@@ -53,8 +53,8 @@
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"client.js",
"client.d.ts",
"react.js",
"react.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
@@ -63,70 +63,67 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.1.2",
"@next-auth/typeorm-legacy-adapter": "0.1.4",
"futoin-hkdf": "^1.3.2",
"@babel/runtime": "^7.14.6",
"futoin-hkdf": "^1.3.3",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.4.16",
"oauth": "^0.9.15",
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0"
"pkce-challenge": "^2.2.0",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.19"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
"nodemailer": "^6.6.2",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
"nodemailer": "^6.6.2"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@babel/preset-react": "^7.13.13",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@babel/cli": "^7.14.5",
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.1.9",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"@types/react": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6",
"babel-jest": "^27.0.5",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"dtslint": "^4.0.8",
"eslint": "^7.19.0",
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"conventional-changelog-conventionalcommits": "4.6.0",
"cssnano": "^5.0.6",
"dtslint": "^4.1.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-promise": "^5.1.0",
"fs-extra": "^10.0.0",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"jest": "^27.0.5",
"msw": "^0.30.0",
"next": "^11.0.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"postcss-cli": "^8.3.1",
"postcss-nested": "^5.0.5",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.1.3",
"typescript": "^4.3.4",
"whatwg-fetch": "^3.6.2"
},
"prettier": {

View File

@@ -1,10 +0,0 @@
import * as TypeORM from "./typeorm"
import * as Prisma from "./prisma"
export { TypeORM, Prisma }
export default {
Default: TypeORM.Adapter,
TypeORM,
Prisma,
}

View File

@@ -1,6 +0,0 @@
/*
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
*/
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"

View File

@@ -1,9 +0,0 @@
/*
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
*/
export {
TypeORMLegacyAdapter as Adapter,
Models,
} from "@next-auth/typeorm-legacy-adapter"

View File

@@ -1,9 +1,7 @@
import { useState } from "react"
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { Provider, useSession } from ".."
import userEvent from "@testing-library/user-event"
import { SessionProvider, useSession } from "../react"
beforeAll(() => {
server.listen()
@@ -18,6 +16,22 @@ afterAll(() => {
server.close()
})
test("it won't allow to fetch the session in isolation without a session context", () => {
function App() {
useSession()
return null
}
jest.spyOn(console, "error")
console.error.mockImplementation(() => {})
expect(() => render(<App />)).toThrow(
"useSession must be wrapped in a SessionProvider"
)
console.error.mockRestore()
})
test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
@@ -30,6 +44,9 @@ test("fetches the session once and re-uses it for different consumers", async ()
render(<ProviderFlow />)
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
@@ -40,25 +57,44 @@ test("fetches the session once and re-uses it for different consumers", async ()
})
})
test("when there's an existing session, it won't initialize as loading", async () => {
const sessionRouteCall = jest.fn()
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
res(ctx.status(200), ctx.json(mockSession))
})
)
render(<ProviderFlow session={mockSession} />)
expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent(
"loading"
)
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
"loading"
)
expect(sessionRouteCall).not.toHaveBeenCalled()
})
function ProviderFlow({ options = {} }) {
return (
<>
<Provider options={options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</Provider>
</>
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
)
}
function SessionConsumer({ testId = 1 }) {
const [session, loading] = useSession()
if (loading) return <span>loading</span>
const { data: session, status } = useSession()
return (
<div data-testid={`session-consumer-${testId}`}>
{JSON.stringify(session)}
{status === "loading" ? "loading" : JSON.stringify(session)}
</div>
)
}

View File

@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockCSRFToken } from "./helpers/mocks"
import logger from "../../lib/logger"
import { getCsrfToken } from ".."
import { getCsrfToken } from "../react"
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({

View File

@@ -2,7 +2,7 @@ import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockProviders } from "./helpers/mocks"
import { getProviders } from ".."
import { getProviders } from "../react"
import logger from "../../lib/logger"
import { rest } from "msw"

View File

@@ -3,7 +3,7 @@ import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getSession } from "../react"
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({

View File

@@ -8,7 +8,7 @@ import {
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from ".."
import { signIn } from "../react"
import { rest } from "msw"
const { location } = window

View File

@@ -2,7 +2,7 @@ import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from ".."
import { signOut } from "../react"
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"

View File

@@ -1,418 +0,0 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import {
useState,
useEffect,
useContext,
createContext,
createElement,
} from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/client").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0,
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {},
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener(
"visibilitychange",
() => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
},
false
)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
export function useSession(session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
function _useSessionHook(session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(!data)
useEffect(() => {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === "storage"
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = _now()
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (!triggeredByStorageEvent && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (
clientMaxAge > 0 &&
currentTime < clientLastSync + clientMaxAge
) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) {
__NEXTAUTH._clientSession = null
}
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = _now()
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent,
})
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error("CLIENT_USE_SESSION_ERROR", error)
setLoading(false)
}
}
__NEXTAUTH._getSession()
})
return [data, loading]
}
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken(ctx) {
return (await _fetchData("csrf", ctx))?.csrfToken
}
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
}
}
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions({
baseUrl,
basePath,
clientMaxAge,
keepAlive,
} = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === "undefined") return
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
clearTimeout(__NEXTAUTH._clientSyncTimer)
}
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: "timer" })
}, keepAlive * 1000)
}
}
export function Provider({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
{ value: useSession(session) },
children
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now() {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive(onReceive) {
if (typeof window === "undefined") return
window.addEventListener("storage", async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== "session" || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post(message) {
if (typeof localStorage === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
},
}
}
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases. These should be removed in a newer release, as it only
// creates problems for bundlers and adds confusion to users. TypeScript declarations
// will provide sufficient help when importing
export {
setOptions as options,
getSession as session,
getProviders as providers,
getCsrfToken as csrfToken,
signIn as signin,
signOut as signout,
}
export default {
getSession,
getCsrfToken,
getProviders,
useSession,
signIn,
signOut,
Provider,
/* Deprecated / unsupported features below this line */
// Use setOptions() set options globally in the app.
setOptions,
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases.
options: setOptions,
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut,
}

387
src/client/react.js vendored Normal file
View File

@@ -0,0 +1,387 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/react").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
_lastSync: 0,
_session: undefined,
_getSession: () => {},
}
const broadcast = BroadcastChannel()
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
/** @type {import("types/internals/react").SessionContext} */
const SessionContext = React.createContext()
export function useSession(options = {}) {
const value = React.useContext(SessionContext)
if (process.env.NODE_ENV !== "production" && !value) {
throw new Error("useSession must be wrapped in a SessionProvider")
}
const { required, onUnauthenticated } = options
const requiredAndNotLoading = required && value.status === "unauthenticated"
React.useEffect(() => {
if (requiredAndNotLoading) {
const url = `/api/auth/signin?${new URLSearchParams({
error: "SessionRequired",
callbackUrl: window.location.href,
})}`
if (onUnauthenticated) onUnauthenticated()
else window.location.replace(url)
}
}, [requiredAndNotLoading, onUnauthenticated])
if (requiredAndNotLoading) {
return { data: value.data, status: "loading" }
}
return value
}
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.broadcast ?? true) {
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken(ctx) {
const response = await _fetchData("csrf", ctx)
return response?.csrfToken
}
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?${new URLSearchParams({ callbackUrl })}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = `${baseUrl}/${
isCredentials ? "callback" : "signin"
}/${provider}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
}
}
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
/** @param {import("types/react-client").SessionProviderProps} props */
export function SessionProvider(props) {
const { children, baseUrl, basePath, staleTime = 0 } = props
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
/**
* If session was `null`, there was an attempt to fetch it,
* but it failed, but we still treat it as a valid initial value.
*/
const hasInitialSession = props.session !== undefined
/** If session was passed, initialize as already synced */
__NEXTAUTH._lastSync = hasInitialSession ? _now() : 0
const [session, setSession] = React.useState(() => {
if (hasInitialSession) __NEXTAUTH._session = props.session
return props.session
})
/** If session was passed, initialize as not loading */
const [loading, setLoading] = React.useState(!hasInitialSession)
React.useEffect(() => {
__NEXTAUTH._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === "storage"
// We should always update if we don't have a client session yet
// or if there are events from other tabs/windows
if (storageEvent || __NEXTAUTH._session === undefined) {
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession({
broadcast: !storageEvent,
})
setSession(__NEXTAUTH._session)
return
}
if (
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it
(staleTime === 0 && !event) ||
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a "stroage" event
// event anyway)
(staleTime > 0 && __NEXTAUTH._session === null) ||
// Bail out early if the client session is not stale yet
(staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime)
) {
return
}
// An event or session staleness occurred, update the client session.
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession()
setSession(__NEXTAUTH._session)
} catch (error) {
logger.error("CLIENT_SESSION_ERROR", error)
} finally {
setLoading(false)
}
}
__NEXTAUTH._getSession()
}, [staleTime])
React.useEffect(() => {
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
const unsubscribe = broadcast.receive(
async () => await __NEXTAUTH._getSession({ event: "storage" })
)
return () => unsubscribe()
}, [])
React.useEffect(() => {
// Set up visibility change
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
const visibilityHandler = () => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
}
document.addEventListener("visibilitychange", visibilityHandler, false)
return () =>
document.removeEventListener("visibilitychange", visibilityHandler, false)
}, [])
React.useEffect(() => {
const { refetchInterval } = props
// Set up polling
if (refetchInterval) {
const refetchIntervalTimer = setInterval(async () => {
if (__NEXTAUTH._session) {
await __NEXTAUTH._getSession({ event: "poll" })
}
}, refetchInterval * 1000)
return () => clearInterval(refetchIntervalTimer)
}
}, [props.refetchInterval])
const value = React.useMemo(
() => ({
data: session,
status: loading
? "loading"
: session
? "authenticated"
: "unauthenticated",
}),
[session, loading]
)
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now() {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/react").BroadcastMessage) => void} onReceive
*/
receive(onReceive) {
const handler = (event) => {
if (event.key !== name) return
/** @type {import("types/internals/react").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== "session" || !message?.data) return
onReceive(message)
}
window.addEventListener("storage", handler)
return () => window.removeEventListener("storage", handler)
},
/** Notify other tabs/windows. */
post(message) {
if (typeof window === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
},
}
}

View File

@@ -1,5 +1,7 @@
export default function AzureADB2C(options) {
const tenant = options.tenantId ? options.tenantId : "common"
const { tenantName, primaryUserFlow } = options
const authorizeUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`
const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`
return {
id: "azure-ad-b2c",
@@ -9,14 +11,29 @@ export default function AzureADB2C(options) {
params: {
grant_type: "authorization_code",
},
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: "https://graph.microsoft.com/v1.0/me/",
profile(profile) {
accessTokenUrl: tokenUrl,
requestTokenUrl: tokenUrl,
authorizationUrl: `${authorizeUrl}?response_type=code+id_token&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
idToken: true,
profile: (profile) => {
let name = ''
if (profile.name) {
// B2C "Display Name"
name = profile.name
} else if (profile.given_name && profile.family_name) {
// B2C "Given Name" & "Surname"
name = `${profile.given_name} ${profile.family_name}`
} else if (profile.given_name) {
// B2C "Given Name"
name = `${profile.given_name}`
}
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName,
name,
id: profile.oid,
email: profile.emails[0]
}
},
...options,

24
src/providers/azure-ad.js Normal file
View File

@@ -0,0 +1,24 @@
export default function AzureAD(options) {
const tenant = options.tenantId ?? 'common'
return {
id: 'azure-ad',
name: 'Azure Active Directory',
type: 'oauth',
version: '2.0',
params: {
grant_type: 'authorization_code'
},
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/v1.0/me/',
profile: (profile) => {
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName
}
},
...options
}
}

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* import { signIn } from "next-auth/react"
* ...
* <button onClick={() => signIn("dropbox")}>
* Sign in
@@ -29,26 +29,26 @@
*/
export default function Dropbox(options) {
return {
id: 'dropbox',
name: 'Dropbox',
type: 'oauth',
version: '2.0',
scope: 'account_info.read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
id: "dropbox",
name: "Dropbox",
type: "oauth",
version: "2.0",
scope: "account_info.read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.dropboxapi.com/oauth2/token",
authorizationUrl:
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
"https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code",
profileUrl: "https://api.dropboxapi.com/2/users/get_current_account",
profile: (profile) => {
return {
id: profile.account_id,
name: profile.name.display_name,
email: profile.email,
image: profile.profile_photo_url,
email_verified: profile.email_verified
email_verified: profile.email_verified,
}
},
protection: ["state", "pkce"],
...options
...options,
}
}

View File

@@ -1,5 +1,5 @@
import logger from '../lib/logger'
import nodemailer from "nodemailer"
import logger from "../lib/logger"
export default function Email(options) {
return {
@@ -22,42 +22,33 @@ export default function Email(options) {
}
}
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer.createTransport(server).sendMail(
{
async function sendVerificationRequest ({ identifier: email, url, baseUrl, provider }) {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
try {
await nodemailer
.createTransport(server)
.sendMail({
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email }),
},
(error) => {
if (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
}
)
})
html: html({ url, site, email })
})
} catch (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
throw new Error('SEND_VERIFICATION_EMAIL_ERROR')
}
}
// Email HTML body
const html = ({ url, site }) => {
// Insert invisible space into domains to prevent the
// the domain from being turned into a hyperlink by email
const html = ({ url, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click it to sign in.
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, "&#8203;.")}`
const escapedSite = `${site.replace(/\./g, "&#8203;.")}`
// Some simple styling options
@@ -72,12 +63,17 @@ const html = ({ url, site }) => {
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in to <strong>${escapedSite}</strong>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong>
</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">

View File

@@ -1,20 +0,0 @@
export default function Freshbooks(options) {
return {
id: 'freshbooks',
name: 'Freshbooks',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.freshbooks.com/auth/oauth/token',
authorizationUrl: 'https://auth.freshbooks.com/service/auth/oauth/authorize?response_type=code',
profileUrl: 'https://api.freshbooks.com/auth/api/v1/users/me',
async profile(profile) {
return {
id: profile.response.id,
name: `${profile.response.first_name} ${profile.response.last_name}`,
email: profile.response.email,
};
},
...options
};
}

View File

@@ -10,7 +10,7 @@ export default function GitHub(options) {
profileUrl: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id,
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* import { signIn } from "next-auth/react"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in

View File

@@ -1,19 +0,0 @@
export default function OneLogin(options) {
return {
id: "onelogin",
name: "OneLogin",
type: "oauth",
version: "2.0",
scope: "openid profile name email",
params: { grant_type: "authorization_code" },
// These will be different depending on the Org.
accessTokenUrl: `https://${options.domain}/oidc/2/token`,
requestTokenUrl: `https://${options.domain}/oidc/2/auth`,
authorizationUrl: `https://${options.domain}/oidc/2/auth?response_type=code`,
profileUrl: `https://${options.domain}/oidc/2/me`,
profile(profile) {
return { ...profile, id: profile.sub }
},
...options,
}
}

View File

@@ -15,7 +15,10 @@ export default function Twitter(options) {
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
}
},
...options,

View File

@@ -1,16 +1,16 @@
export default function WorkOS(options) {
const domain = options.domain || 'api.workos.com';
const domain = options.domain || "api.workos.com"
return {
id: 'workos',
name: 'WorkOS',
type: 'oauth',
version: '2.0',
scope: '',
id: "workos",
name: "WorkOS",
type: "oauth",
version: "2.0",
scope: "",
params: {
grant_type: 'authorization_code',
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret
client_secret: options.clientSecret,
},
accessTokenUrl: `https://${domain}/sso/token`,
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
@@ -18,9 +18,9 @@ export default function WorkOS(options) {
profile: (profile) => {
return {
...profile,
name: `${profile.first_name} ${profile.last_name}`
name: `${profile.first_name} ${profile.last_name}`,
}
},
...options
...options,
}
}

View File

@@ -4,7 +4,7 @@ export default function Yandex(options) {
name: "Yandex",
type: "oauth",
version: "2.0",
scope: "login:email login:info login:avatar",
scope: "login:email login:info",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://oauth.yandex.ru/token",
requestTokenUrl: "https://oauth.yandex.ru/token",
@@ -15,7 +15,7 @@ export default function Yandex(options) {
id: profile.id,
name: profile.real_name,
email: profile.default_email,
image: profile.is_avatar_empty ? null : `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
image: null,
}
},
...options,

View File

@@ -1,4 +1,3 @@
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
@@ -21,16 +20,6 @@ if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
function isValidHttpUrl(url, baseUrl) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
@@ -81,23 +70,6 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions.cookies,
}
const errorPage = userOptions.pages?.error ?? `${baseUrl}${basePath}/error`
const callbackUrlParam = req.query?.callbackUrl
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const { callbackUrl: defaultCallbackUrl } = cookie.defaultCookies(
userOptions.useSecureCookies ?? baseUrl.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const secret = createSecret({ userOptions, basePath, baseUrl })
const providers = parseProviders({
@@ -129,13 +101,6 @@ async function NextAuthHandler(req, res, userOptions) {
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
// Parse database / adapter
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter =
userOptions.adapter ??
(userOptions.database && adapters.Default(userOptions.database))
// User provided options are overriden by other options,
// except for the options with special handling above
req.options = {
@@ -146,7 +111,6 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions,
// These computed settings can have values in userOptions but we override them
// and are request-specific.
adapter,
baseUrl,
basePath,
action,
@@ -156,7 +120,7 @@ async function NextAuthHandler(req, res, userOptions) {
providers,
// Session options
session: {
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
maxAge,
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userOptions.session,
@@ -246,6 +210,7 @@ async function NextAuthHandler(req, res, userOptions) {
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
@@ -307,9 +272,7 @@ async function NextAuthHandler(req, res, userOptions) {
}
return res
.status(400)
.end(
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
)
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
})
}

View File

@@ -16,7 +16,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
* @param {import("types").Session} sessionToken
* @param {import("types").Profile} profile
* @param {import("types").Account} account
* @param {import("types/internals").AppOptions} options
* @param {import("types/internals").InternalOptions} options
*/
export default async function callbackHandler(
sessionToken,

View File

@@ -1,32 +1,42 @@
import * as cookie from '../lib/cookie'
// @ts-check
import * as cookie from "../lib/cookie"
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
* @note: `req.options` must already be defined when called.
* @type {import("types/internals").NextAuthApiHandler}
*/
export default async function callbackUrlHandler (req, res) {
export default async function callbackUrlHandler(req, res) {
const { query } = req
const { body } = req
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
const { cookies, baseUrl, callbacks } = req.options
// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || baseUrl
let callbackUrl = baseUrl
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
callbackUrl = await callbacks.redirect({
url: callbackUrlParamValue,
baseUrl,
})
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
callbackUrl = await callbacks.redirect({
url: callbackUrlCookieValue,
baseUrl,
})
}
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && callbackUrl !== callbackUrlCookieValue) {
cookie.set(
res,
cookies.callbackUrl.name,
callbackUrl,
cookies.callbackUrl.options
)
}
req.options.callbackUrl = callbackUrl

View File

@@ -8,115 +8,115 @@
* As only partial functionlity is required, only the code we need has been incorporated here
* (with fixes for specific issues) to keep dependancy size down.
*/
export function set(res, name, value, options = {}) {
export function set (res, name, value, options = {}) {
const stringValue =
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value)
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
if ("maxAge" in options) {
if ('maxAge' in options) {
options.expires = new Date(Date.now() + options.maxAge)
options.maxAge /= 1000
}
// Preserve any existing cookies that have already been set in the same session
let setCookieHeader = res.getHeader("Set-Cookie") || []
let setCookieHeader = res.getHeader('Set-Cookie') || []
// If not an array (i.e. a string with a single cookie) convert it into an array
if (!Array.isArray(setCookieHeader)) {
setCookieHeader = [setCookieHeader]
}
setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader("Set-Cookie", setCookieHeader)
res.setHeader('Set-Cookie', setCookieHeader)
}
function _serialize(name, val, options) {
function _serialize (name, val, options) {
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
const opt = options || {}
const enc = opt.encode || encodeURIComponent
if (typeof enc !== "function") {
throw new TypeError("option encode is invalid")
if (typeof enc !== 'function') {
throw new TypeError('option encode is invalid')
}
if (!fieldContentRegExp.test(name)) {
throw new TypeError("argument name is invalid")
throw new TypeError('argument name is invalid')
}
const value = enc(val)
if (value && !fieldContentRegExp.test(value)) {
throw new TypeError("argument val is invalid")
throw new TypeError('argument val is invalid')
}
let str = name + "=" + value
let str = name + '=' + value
if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0
if (isNaN(maxAge) || !isFinite(maxAge)) {
throw new TypeError("option maxAge is invalid")
throw new TypeError('option maxAge is invalid')
}
str += "; Max-Age=" + Math.floor(maxAge)
str += '; Max-Age=' + Math.floor(maxAge)
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
throw new TypeError("option domain is invalid")
throw new TypeError('option domain is invalid')
}
str += "; Domain=" + opt.domain
str += '; Domain=' + opt.domain
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
throw new TypeError("option path is invalid")
throw new TypeError('option path is invalid')
}
str += "; Path=" + opt.path
str += '; Path=' + opt.path
} else {
str += "; Path=/"
str += '; Path=/'
}
if (opt.expires) {
let expires = opt.expires
if (typeof opt.expires.toUTCString === "function") {
if (typeof opt.expires.toUTCString === 'function') {
expires = opt.expires.toUTCString()
} else {
const dateExpires = new Date(opt.expires)
expires = dateExpires.toUTCString()
}
str += "; Expires=" + expires
str += '; Expires=' + expires
}
if (opt.httpOnly) {
str += "; HttpOnly"
str += '; HttpOnly'
}
if (opt.secure) {
str += "; Secure"
str += '; Secure'
}
if (opt.sameSite) {
const sameSite =
typeof opt.sameSite === "string"
typeof opt.sameSite === 'string'
? opt.sameSite.toLowerCase()
: opt.sameSite
switch (sameSite) {
case true:
str += "; SameSite=Strict"
str += '; SameSite=Strict'
break
case "lax":
str += "; SameSite=Lax"
case 'lax':
str += '; SameSite=Lax'
break
case "strict":
str += "; SameSite=Strict"
case 'strict':
str += '; SameSite=Strict'
break
case "none":
str += "; SameSite=None"
case 'none':
str += '; SameSite=None'
break
default:
throw new TypeError("option sameSite is invalid")
throw new TypeError('option sameSite is invalid')
}
}
@@ -134,47 +134,46 @@ function _serialize(name, val, options) {
* @TODO Review cookie settings (names, options)
* @return {import("types").CookiesOptions}
*/
export function defaultCookies(useSecureCookies) {
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
export function defaultCookies (useSecureCookies) {
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
return {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? "__Host-" : ""}next-auth.csrf-token`,
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: useSecureCookies,
},
},
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
}
}
}

View File

@@ -3,7 +3,7 @@ import * as cookie from './cookie'
/**
* Ensure CSRF Token cookie is set for any subsequent requests.
* Used as part of the strategy for mitigation for CSRF tokens.
* Used as part of the strateigy for mitigation for CSRF tokens.
*
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
* where 'token' is the CSRF token and 'hash' is a hash made of the token and

View File

@@ -1,63 +1,24 @@
/**
* Use the signIn callback to control if a user is allowed to sign in or not.
*
* This is triggered before sign in flow completes, so the user profile may be
* a user object (with an ID) or it may be just their name and email address,
* depending on the sign in flow and if they have an account already.
*
* When using email sign in, this method is triggered both when the user
* requests to sign in and again when they activate the link in the sign in
* email.
*
* @param {object} profile User profile (e.g. user id, name, email)
* @param {object} account Account used to sign in (e.g. OAuth account)
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
export async function signIn() {
// @ts-check
/** @type {import("types").CallbacksOptions["signIn"]} */
export function signIn() {
return true
}
/**
* Redirect is called anytime the user is redirected on signin or signout.
* By default, for security, only Callback URLs on the same URL as the site
* are allowed, you can use this callback to customise that behaviour.
*
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {Promise<string>} URL the client will be redirect to
*/
export async function redirect(url, baseUrl) {
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
/** @type {import("types").CallbacksOptions["redirect"]} */
export function redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) {
return url
}
return baseUrl
}
/**
* The session callback is called whenever a session is checked.
* e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* @param {object} session Session object
* @param {object} token JSON Web Token (if enabled)
* @return {Promise<object>} Session that will be returned to the client
*/
export async function session(session) {
/** @type {import("types").CallbacksOptions["session"]} */
export function session({ session }) {
return session
}
/**
* This callback is called whenever a JSON Web Token is created / updated.
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
* with an OAuth provider. It is not avalible on subsequent calls. You can
* take advantage of this to persist additional data you need to in the JWT.
*
* @param {object} token Decrypted JSON Web Token
* @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {Promise<object>} JSON Web Token that will be saved
*/
export async function jwt(token) {
/** @type {import("types").CallbacksOptions["jwt"]} */
export function jwt({ token }) {
return token
}

View File

@@ -30,7 +30,6 @@ export default async function oAuthCallback(req) {
provider.id,
code
)
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", req.body)
throw error
}
}
@@ -63,7 +62,7 @@ export default async function oAuthCallback(req) {
return getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id)
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code)
throw error
}
}
@@ -75,11 +74,7 @@ export default async function oAuthCallback(req) {
// eslint-disable-next-line camelcase
const { token_secret } = await client.getOAuthRequestToken(provider.params)
const tokens = await client.getOAuthAccessToken(
oauth_token,
token_secret,
oauth_verifier
)
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier)
const profileData = await client.get(
provider.profileUrl,
tokens.oauth_token,
@@ -148,11 +143,11 @@ async function getProfile({ profileData, tokens, provider, user }) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortunately, we can't tell which - at least not in a way that works for
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception)
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData)
return {
profile: null,
account: null,

View File

@@ -180,44 +180,51 @@ async function getOAuth2AccessToken(code, provider, codeVerifier) {
const postData = querystring.stringify(params)
return new Promise((resolve, reject) => {
this._request("POST", url, headers, postData, null, (error, data) => {
if (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error)
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
this._request(
"POST",
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, data, response)
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
})
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
}
)
})
}

View File

@@ -36,11 +36,7 @@ export async function handleCallback (req, res) {
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD
})
// remove PKCE after it has been used
cookie.set(res, cookies.pkceCodeVerifier.name, "", {
...cookies.pkceCodeVerifier.options,
maxAge: 0
})
cookie.set(res, cookies.pkceCodeVerifier.name, null, { maxAge: 0 }) // remove PKCE after it has been used
} catch (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)

View File

@@ -5,7 +5,7 @@ import adapterErrorHandler from "../../../adapters/error-handler"
*
* @param {string} email
* @param {import("types/providers").EmailConfig} provider
* @param {import("types/internals").AppOptions} options
* @param {import("types/internals").InternalOptions} options
* @returns
*/
export default async function email(email, provider, options) {

View File

@@ -1,23 +1,23 @@
import oAuthClient from '../oauth/client'
import logger from '../../../lib/logger'
import oAuthClient from "../oauth/client"
import logger from "../../../lib/logger"
/** @param {import("types/internals").NextAuthRequest} req */
export default async function getAuthorizationUrl (req) {
export default async function getAuthorizationUrl(req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query
...req.query,
}
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
if (provider.version?.startsWith("2.")) {
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
scope: provider.scope,
...params,
redirect_uri: provider.callbackUrl
redirect_uri: provider.callbackUrl,
})
// If the authorizationUrl specified in the config has query parameters on it
@@ -27,13 +27,13 @@ export default async function getAuthorizationUrl (req) {
// which inadvertantly strips them.
//
// https://github.com/ciaranj/node-oauth/pull/193
if (provider.authorizationUrl.includes('?')) {
if (provider.authorizationUrl.includes("?")) {
const parseUrl = new URL(provider.authorizationUrl)
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
url = url.replace(baseUrl, provider.authorizationUrl + '&')
url = url.replace(baseUrl, provider.authorizationUrl + "&")
}
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
}
@@ -42,12 +42,12 @@ export default async function getAuthorizationUrl (req) {
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
...tokens.params,
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
} catch (error) {
logger.error('GET_AUTHORIZATION_URL_ERROR', error)
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
throw error
}
}

View File

@@ -32,6 +32,7 @@ export default function signin({
EmailSignin: "Check your email inbox.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}

View File

@@ -6,8 +6,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
/**
* Handle callbacks from login services
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
* @type {import("types/internals").NextAuthApiHandler}
*/
export default async function callback(req, res) {
const {
@@ -72,12 +71,12 @@ export default async function callback(req, res) {
}
try {
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
const signInCallbackResponse = await callbacks.signIn({
user: userOrProfile,
account,
OAuthProfile
)
if (signInCallbackResponse === false) {
profile: OAuthProfile,
})
if (!signInCallbackResponse) {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
@@ -85,16 +84,11 @@ export default async function callback(req, res) {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// Sign user in
@@ -106,22 +100,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
OAuthProfile,
isNewUser
)
profile: OAuthProfile,
isNewUser,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -221,27 +215,22 @@ export default async function callback(req, res) {
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(
profile,
const signInCallbackResponse = await callbacks.signIn({
user: profile,
account,
{ email }
)
if (signInCallbackResponse === false) {
email: { email },
})
if (!signInCallbackResponse) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// Sign user in
@@ -253,22 +242,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
profile,
isNewUser
)
isNewUser,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -336,7 +325,8 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials, {...req, options: {}, cookies: {}}
credentials,
{ ...req, options: {}, cookies: {} }
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res
@@ -346,59 +336,53 @@ export default async function callback(req, res) {
provider.id
)}`
)
} else if (typeof userObjectReturnedFromAuthorizeHandler === "string") {
return res.redirect(userObjectReturnedFromAuthorizeHandler)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
}
const user = userObjectReturnedFromAuthorizeHandler
const account = { id: provider.id, type: "credentials" }
try {
const signInCallbackResponse = await callbacks.signIn(
const signInCallbackResponse = await callbacks.signIn({
user,
account,
credentials
)
if (signInCallbackResponse === false) {
credentials,
})
if (!signInCallbackResponse) {
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
}
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
userObjectReturnedFromAuthorizeHandler,
false
)
profile: userObjectReturnedFromAuthorizeHandler,
isNewUser: false,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()

View File

@@ -22,7 +22,7 @@ export default async function session(req, res) {
if (useJwtSession) {
try {
// Decrypt and verify token
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
// Generate new session expiry date
const sessionExpiresDate = new Date()
@@ -33,38 +33,35 @@ export default async function session(req, res) {
// 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 defaultSessionPayload = {
const defaultSession = {
user: {
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null,
name: decodedToken.name || null,
email: decodedToken.email || null,
image: decodedToken.picture || null,
},
expires: sessionExpires,
}
// Pass Session and JSON Web Token through to the session callback
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
jwtPayload
)
const token = await callbacks.jwt({ token: decodedToken })
const session = await callbacks.session({
session: defaultSession,
token,
})
// Return session payload as response
response = sessionPayload
response = session
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
cookie.set(res, cookies.sessionToken.name, newToken, {
expires: sessionExpires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, {
session: sessionPayload,
jwt: jwtPayload,
})
await dispatchEvent(events.session, { session, token })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error)
@@ -88,7 +85,7 @@ export default async function session(req, res) {
// 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 defaultSessionPayload = {
const defaultSession = {
user: {
name: user.name,
email: user.email,
@@ -99,10 +96,10 @@ export default async function session(req, res) {
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(
defaultSessionPayload,
user
)
const sessionPayload = await callbacks.session({
session: defaultSession,
user,
})
// Return session payload as response
response = sessionPayload

View File

@@ -40,19 +40,16 @@ export default async function signin(req, res) {
// complains about this we can make strict RFC 2821 compliance an option.
const email = req.body.email?.toLowerCase() ?? null
if (!email) {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
// If is an existing user return a user object (otherwise use placeholder)
const profile = (await getUserByEmail(email)) || { email }
const user = (await getUserByEmail(email)) || { email }
const account = { id: provider.id, type: "email", providerAccountId: email }
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, {
email,
verificationRequest: true,
const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { email, verificationRequest: true },
})
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
@@ -60,14 +57,9 @@ export default async function signin(req, res) {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
}
try {

View File

@@ -6,9 +6,9 @@
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/client": ["./src/client"],
"next-auth/react": ["./src/client/react"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"]
"next-auth/providers": ["./src/providers"],
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],

36
types/adapters.d.ts vendored
View File

@@ -1,39 +1,7 @@
import { AppOptions } from "./internals"
import { InternalOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig } from "./providers"
/** Legacy */
export {
TypeORMAccountModel,
TypeORMSessionModel,
TypeORMUserModel,
TypeORMVerificationRequestModel,
} from "@next-auth/typeorm-legacy-adapter"
import {
TypeORMAdapter,
TypeORMAdapterModels,
} from "@next-auth/typeorm-legacy-adapter"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
export const TypeORM: {
Models: TypeORMAdapterModels
Adapter: TypeORMAdapter
}
export const Prisma: {
Adapter: PrismaLegacyAdapter
}
declare const Adapters: {
Default: TypeORMAdapter
TypeORM: typeof TypeORM
Prisma: typeof Prisma
}
export default Adapters
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
* Custom adapters created and maintained by our community can be found in the adapters repository.
@@ -152,5 +120,5 @@ export type Adapter<
client: C,
options?: O
) => {
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
getAdapter(appOptions: InternalOptions): Promise<AdapterInstance<U, P, S>>
}

93
types/index.d.ts vendored
View File

@@ -2,10 +2,9 @@
/// <reference types="node" />
import { ConnectionOptions } from "typeorm"
import { Adapter } from "./adapters"
import { JWTOptions, JWT } from "./jwt"
import { AppProviders } from "./providers"
import { AppProviders, Credentials } from "./providers"
import {
Awaitable,
NextApiRequest,
@@ -29,14 +28,6 @@ export interface NextAuthOptions {
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: AppProviders
/**
* A database connection string or configuration object.
* * **Default value**: `null`
* * **Required**: *No (unless using email provider)*
*
* [Documentation](https://next-auth.js.org/configuration/options#database) | [Databases](https://next-auth.js.org/configuration/databases)
*/
database?: string | Record<string, any> | ConnectionOptions
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
@@ -98,7 +89,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: CallbacksOptions
callbacks?: Partial<CallbacksOptions>
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
@@ -113,18 +104,11 @@ export interface NextAuthOptions {
*/
events?: Partial<JWTEventCallbacks | SessionEventCallbacks>
/**
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
* You can use the adapter option to use the Prisma adapter - or pass in your own adapter
* if you want to use a database that is not supported by one of the built-in adapters.
* * **Default value**: TypeORM adapter
* You can use the adapter option to pass in your database adapter.
*
* * **Required**: *No*
*
* - ⚠ If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
* - ⚠ Adapters are being migrated to their own home in a Community maintained repository.
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
*/
adapter?: ReturnType<Adapter>
@@ -212,7 +196,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: CookiesOptions
cookies?: Partial<CookiesOptions>
}
/**
@@ -283,7 +267,29 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
signIn(params: {
user: User
account: A
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile: P & Record<string, unknown>
/**
* If Email provider is used, it contains the email, and optionally on the first call a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
* When the callback is invoked after a user has clicked on a sign in link,
* this property will not be present. You can check for the `verificationRequest` property
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email: {
email: string | null
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials: Credentials
}): Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
@@ -291,12 +297,19 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect?(url: string, baseUrl: string): Awaitable<string>
redirect(params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
baseUrl: string
}): Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* - ⚠ By default, only a subset of the token is returned for increased security.
* ⚠ By default, only a subset (email, name, imgage)
* of the token is returned for increased security.
*
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitely forward it here to make it available to the client.
*
@@ -306,7 +319,11 @@ export interface CallbacksOptions<
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
session(params: {
session: Session
user: User
token: JWT
}): Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
@@ -314,18 +331,18 @@ export interface CallbacksOptions<
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* - ⚠ By default the JWT is signed, but not encrypted.
* ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt?(
token: JWT,
user?: User,
account?: A,
profile?: P,
jwt(params: {
token: JWT
user?: User
account?: A
profile?: P
isNewUser?: boolean
): Awaitable<JWT>
}): Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
@@ -343,10 +360,10 @@ export interface CookieOption {
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken?: CookieOption
callbackUrl?: CookieOption
csrfToken?: CookieOption
pkceCodeVerifier?: CookieOption
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
@@ -428,11 +445,11 @@ export interface DefaultSession extends Record<string, unknown> {
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `Provider` React Context
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown>, DefaultSession {}

View File

@@ -1,34 +0,0 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: Session | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type SessionContext = React.Context<Session>

View File

@@ -1,18 +1,18 @@
import { NextApiRequest, NextApiResponse } from "./utils"
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
import { Awaitable, NextApiRequest, NextApiResponse } from "./utils"
import {
CallbacksOptions,
CookiesOptions,
EventCallbacks,
LoggerInstance,
PagesOptions,
SessionOptions,
Theme,
} from ".."
import { AppProvider } from "../providers"
import { JWTOptions } from "next-auth/jwt"
import { Adapter } from "next-auth/adapters"
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions =
| "pages"
| "jwt"
| "events"
| "callbacks"
| "cookies"
| "adapter"
export interface AppOptions
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
export interface InternalOptions {
providers: AppProvider[]
baseUrl: string
basePath: string
@@ -42,10 +42,22 @@ export interface AppOptions
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: PagesOptions
jwt: JWTOptions
events: EventCallbacks
adapter: ReturnType<Adapter>
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}
export interface NextAuthRequest extends NextApiRequest {
options: AppOptions
options: InternalOptions
}
export type NextAuthResponse = NextApiResponse
export type NextAuthApiHandler = (
req: NextAuthRequest,
res: NextAuthResponse
) => Awaitable<void>

37
types/internals/react.d.ts vendored Normal file
View File

@@ -0,0 +1,37 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** Stores last session response */
_session?: Session | null
/** Used for timestamp since last sycned (in seconds) */
_lastSync: number
/**
* Stores the `SessionProvider`'s session update method to be able to
* trigger session updates from places like `signIn` or `signOut`
*/
_getSession: any
}
export type SessionContextValue<R extends boolean = false> = R extends true
?
| { data: Session; status: "authenticated" }
| { data: null; status: "loading" }
:
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }
export type SessionContext = React.Context<SessionContextValue>

14
types/providers.d.ts vendored
View File

@@ -27,7 +27,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
headers?: Record<string, any>
type: "oauth"
version: string
scope: string | string[]
scope: string
params: { grant_type: string }
accessTokenUrl: string
requestTokenUrl?: string
@@ -58,6 +58,7 @@ export type OAuthProviderType =
| "Apple"
| "Atlassian"
| "Auth0"
| "AzureAD"
| "AzureADB2C"
| "Basecamp"
| "BattleNet"
@@ -72,7 +73,6 @@ export type OAuthProviderType =
| "FACEIT"
| "FortyTwo"
| "Foursquare"
| "Freshbooks"
| "FusionAuth"
| "GitHub"
| "GitLab"
@@ -88,7 +88,6 @@ export type OAuthProviderType =
| "Naver"
| "Netlify"
| "Okta"
| "OneLogin"
| "Osso"
| "Reddit"
| "Salesforce"
@@ -117,11 +116,16 @@ interface CredentialInput {
placeholder?: string
}
interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
export type Credentials = Record<string, CredentialInput>
interface CredentialsConfig<C extends Credentials = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
authorize(
credentials: Record<keyof C, string>,
req: NextApiRequest
): Awaitable<User | null>
}
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import { IncomingMessage } from "http"
import { Session } from "."
import { ProviderType } from "./providers"
import { SessionContextValue } from "internals/react"
export interface CtxOrReq {
req?: IncomingMessage
@@ -17,29 +18,24 @@ export type GetSessionOptions = CtxOrReq & {
triggerEvent?: boolean
}
export interface UseSessionOptions<R extends boolean> {
required: R
/** Defaults to `signIn` */
action?(): void
}
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
*/
export function useSession(): [Session | null, boolean]
export function useSession<R extends boolean>(
options?: UseSessionOptions<R>
): SessionContextValue<R>
/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
*/
export function getSession(options?: GetSessionOptions): Promise<Session | null>
/**
* Alias for `getSession`
* @docs https://next-auth.js.org/getting-started/client#getsession
*/
export const session: typeof getSession
/*******************
* CSRF Token types
******************/
@@ -54,12 +50,6 @@ export const session: typeof getSession
*/
export function getCsrfToken(ctxOrReq?: CtxOrReq): Promise<string | null>
/**
* Alias for `getCsrfToken`
* @docs https://next-auth.js.org/getting-started/client#getcsrftoken
*/
export const csrfToken: typeof getCsrfToken
/******************
* Providers types
*****************/
@@ -84,12 +74,6 @@ export function getProviders(): Promise<Record<
ClientSafeProvider
> | null>
/**
* Alias for `getProviders`
* @docs https://next-auth.js.org/getting-started/client#getproviders
*/
export const providers: typeof getProviders
/****************
* Sign in types
***************/
@@ -137,12 +121,6 @@ export function signIn<P extends SignInProvider = undefined>(
P extends RedirectableProvider ? SignInResponse | undefined : undefined
>
/**
* Alias for `signIn`
* @docs https://next-auth.js.org/getting-started/client#signin
*/
export const signin: typeof signIn
/****************
* Sign out types
****************/
@@ -169,21 +147,25 @@ export function signOut<R extends boolean = true>(
params?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse>
/**
* @docs https://next-auth.js.org/getting-started/client#signout
* Alias for `signOut`
*/
export const signout: typeof signOut
/************************
* SessionProvider types
***********************/
/** @docs: https://next-auth.js.org/getting-started/client#options */
export interface SessionProviderOptions {
export interface SessionProviderProps {
session?: Session
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
/**
* The amount of time (in seconds) after a session should be considered stale.
* If set to `0` (default), the session will never be re-fetched.
*/
staleTime?: number
/**
* A time interval (in seconds) after which the session will be re-fetched.
* If set to `0` (default), the session is not polled.
*/
refetchInterval?: number
}
/**
@@ -191,28 +173,6 @@ export interface SessionProviderOptions {
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
* [Documentation](https://next-auth.js.org/getting-started/client#sessionprovider)
*/
export type SessionProvider = React.FC<{
children: React.ReactNode
session?: Session
options?: SessionProviderOptions
}>
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export const Provider: SessionProvider
/** @docs: https://next-auth.js.org/getting-started/client#options */
export function setOptions(options: SessionProviderOptions): void
/**
* Alias for `setOptions`
* @docs: https://next-auth.js.org/getting-started/client#options
*/
export const options: typeof setOptions
export const SessionProvider: React.FC<SessionProviderProps>

View File

@@ -1,26 +0,0 @@
import Adapters from "next-auth/adapters"
// ExpectType TypeORMAdapter["Adapter"]
Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType TypeORMAdapter
Adapters.TypeORM.Adapter({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType PrismaAdapter
Adapters.Prisma.Adapter({
prisma: {},
modelMapping: {
User: "foo",
Account: "bar",
Session: "session",
VerificationRequest: "foo",
},
})

View File

@@ -33,7 +33,7 @@ Providers.Credentials({
type: "password",
},
},
authorize: async ({ username, password }) => {
authorize: async ({username, password}) => {
const user = {
/* fetched user */
}
@@ -152,13 +152,6 @@ Providers.Okta({
domain: "https://foo.auth0.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.OneLogin({
clientId: "foo123",
clientSecret: "bar123",
domain: "foo.onelogin.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.BattleNet({
clientId: "foo123",
@@ -264,9 +257,3 @@ Providers.Zoho({
clientId: "foo123",
clientSecret: "bar123",
})
// $ExpectType OAuthConfig<Profile>
Providers.Freshbooks({
clientId: "foo123",
clientSecret: "bar123",
})

View File

@@ -1,4 +1,4 @@
import * as client from "next-auth/client"
import * as client from "next-auth/react"
import { nextReq } from "./test-helpers"
const clientSession = {
@@ -11,87 +11,89 @@ const clientSession = {
expires: "1234",
}
// $ExpectType [Session | null, boolean]
/**
* $ExpectType
* | { data: Session; status: "authenticated"; }
* | { data: null; status: "unauthenticated" | "loading"; }
* | { //// data: Session; status: "authenticated"; }
* | { data: null; status: "loading"; }
*/
client.useSession()
// $ExpectType { data: Session; status: "authenticated"; } | { data: null; status: "loading"; }
const session = client.useSession({ required: true })
if (session.status === "loading") {
// $ExpectType null
session.data
} else {
// $ExpectType Session
session.data
}
// $ExpectType Promise<Session | null>
client.getSession({ req: nextReq })
// $ExpectType Promise<Session | null>
client.session({ req: nextReq })
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.getProviders()
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.providers()
// $ExpectType Promise<string | null>
client.getCsrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.csrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.csrfToken({ ctx: { req: nextReq } })
client.getCsrfToken({ ctx: { req: nextReq } })
// $ExpectType Promise<undefined>
client.signin("github", { callbackUrl: "foo" }, { login: "username" })
client.signIn("github", { callbackUrl: "foo" }, { login: "username" })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("credentials", { callbackUrl: "foo", redirect: true })
client.signIn("credentials", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("credentials", { redirect: false })
client.signIn("credentials", { redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("email", { callbackUrl: "foo", redirect: false })
client.signIn("email", { callbackUrl: "foo", redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("email", { callbackUrl: "foo", redirect: true })
client.signIn("email", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<undefined>
client.signout()
client.signOut()
// $ExpectType Promise<undefined>
client.signout({ callbackUrl: "https://foo.com/callback", redirect: true })
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: true })
// $ExpectType Promise<SignOutResponse>
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: false })
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: clientSession,
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
},
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: clientSession,
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
options: {},
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: {
expires: "",
},
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
keepAlive: 4321,
},
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
refetchInterval: 4321,
})

View File

@@ -4,7 +4,7 @@ import NextAuth, * as NextAuthTypes from "next-auth"
import { IncomingMessage, ServerResponse } from "http"
import { Socket } from "net"
import { NextApiRequest, NextApiResponse } from "internals/utils"
import { AppOptions } from "internals"
import { InternalOptions } from "internals"
const req: NextApiRequest = Object.assign(new IncomingMessage(new Socket()), {
query: {},
@@ -62,7 +62,7 @@ const exampleVerificationRequest = {
const MyAdapter: Adapter<Record<string, unknown>> = () => {
return {
async getAdapter(appOptions: AppOptions) {
async getAdapter(appOptions: InternalOptions) {
return {
async createUser(profile) {
return exampleUser
@@ -135,7 +135,6 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
clientSecret: "123",
}),
],
database: "path/to/db",
debug: true,
secret: "my secret",
session: {
@@ -154,16 +153,16 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
},
pages: pageOptions,
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, email, credentials, profile }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return "path/to/foo"
},
async session(session, userOrToken) {
return { ...session }
async session({ session, user, token }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
},
},

View File

@@ -15,7 +15,7 @@
"next-auth": ["."],
"next-auth/providers": ["./providers"],
"next-auth/adapters": ["./adapters"],
"next-auth/client": ["./client"],
"next-auth/react": ["./react-client"],
"next-auth/jwt": ["./jwt"]
}
}

View File

@@ -49,8 +49,6 @@ export default NextAuth({
## Schema
Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
```javascript
CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
@@ -78,7 +76,7 @@ CreateIndex({
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_request_by_token_and_identifier",
name: "verification_request_by_token",
source: Collection("verification_requests"),
unique: true,
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],

View File

@@ -15,7 +15,7 @@ This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This p
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"

View File

@@ -11,7 +11,6 @@ All table/collection names in the built in models are plural, and all table name
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
:::
---
## User
@@ -30,7 +29,7 @@ If a user first signs in with OAuth then their email address is automatically po
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 email sign in is configured).
:::
## Account
## Account
Table: `accounts`
@@ -60,4 +59,4 @@ The Verification Request model is used to store tokens for passwordless sign in
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).

View File

@@ -15,6 +15,7 @@ There you can find the following adapters:
- [`fauna`](./fauna)
- [`dynamodb`](./dynamodb)
- [`firebase`](./firebase)
- [`pouchdb`](./pouchdb)
## Custom Adapter

View File

@@ -23,7 +23,7 @@ Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import Adapters from "next-auth/adapters"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
@@ -35,7 +35,7 @@ export default NextAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: Adapters.Prisma.Adapter({ prisma }),
adapter: PrismaLegacyAdapter({ prisma }),
})
```
@@ -161,7 +161,7 @@ You can use custom model names by using the `modelMapping` option (shown here wi
```javascript title="pages/api/auth/[...nextauth].js"
...
adapter: Adapters.Prisma.Adapter({
adapter: PrismaLegacyAdapter({
prisma,
modelMapping: {
User: 'user',

View File

@@ -19,4 +19,4 @@ Objects stored in MongoDB use similar datatypes to SQL, with some differences:
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.

View File

@@ -1,88 +1,88 @@
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::

View File

@@ -84,4 +84,4 @@ CREATE UNIQUE INDEX email
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
```

View File

@@ -16,16 +16,16 @@ You can specify a handler for any of the callbacks below.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return baseUrl
},
async session(session, user) {
async session({ session, user, token }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
}
...
@@ -41,15 +41,7 @@ Use the `signIn()` callback to control if a user is allowed to sign in.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @return {boolean|string} Return `true` to allow sign in
* Return `false` to deny access
* Return `string` to redirect to (eg.: "/unauthorized")
*/
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return true
@@ -64,18 +56,18 @@ callbacks: {
...
```
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
- When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again _after_ they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `email` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked _after_ a user has clicked on a sign in link, this property will not be present.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
* When using the **Credentials Provider** the `user` object is the response returned from the `authorize` callback and the `profile` object is the raw body of the `HTTP POST` submission.
- When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `credentials` object is the raw body of the `HTTP POST` submission.
:::note
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
When using NextAuth.js without a database, the user object will always be a prototype user object, with information extracted from the profile.
:::
:::note
@@ -93,52 +85,36 @@ By default only URLs on the same URL as the site are allowed, you can use the re
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {string} URL the client will be redirect to
*/
async redirect(url, baseUrl) {
return url.startsWith(baseUrl)
? url
: baseUrl
redirect({ url, baseUrl }) {
return url.startsWith(baseUrl) ? url : baseUrl
}
}
...
```
:::note
The redirect callback may be invoked more than once in the same flow.
:::
## JWT callback
This JSON Web Token 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).
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). The returned value will be [signed and optionally encrypted](/configuration/options#jwt), and it is stored in a cookie.
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`, `useSession()` will invoke this function, but only if you are using a [JWT session](/configuration/options#session). This method is not invoked when you persist sessions in a database.
* As with database session expiry times, token expiry time is extended whenever a session is active.
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
- As with database persisted session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in. In subsequent calls, only `token` will be available.
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. You can persist data such as User ID, OAuth Access Token in this token. To make it available in the browser, check out the [`session()` callback](#session-callback) as well.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} token Decrypted JSON Web Token
* @param {object} user User object (only available on sign in)
* @param {object} account Provider account (only available on sign in)
* @param {object} profile Provider profile (only available on sign in)
* @param {boolean} isNewUser True if new user (only available on sign in)
* @return {object} JSON Web Token that will be saved
*/
async jwt(token, user, account, profile, isNewUser) {
// Add access_token to the token right after signin
if (account?.accessToken) {
token.accessToken = account.accessToken
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token
}
return token
}
@@ -147,18 +123,13 @@ callbacks: {
```
:::tip
Use an if branch in jwt with checking for existence of any other params than token. If any of those exist, you call jwt for the first time.
This is a good place to add for example an `access_token` to your jwt, if you want to.
:::
:::tip
Check out the content of all the params in addition `token`, to see what info you have available on signin.
Use an if branch to check for the existence of parameters (apart from `token`). If they exist, this means that the callback is being invoked for the first time (i.e. the user is being signed in). This is a good place to persist additional data like an `access_token` in the JWT. Subsequent invocations will only contain the `token` parameter.
:::
:::warning
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback. Opt into database persisted sessions by setting [`session: {jwt: false}`](/configuration/options#session).
:::
## Session callback
@@ -167,20 +138,14 @@ The session callback is called whenever a session is checked. By default, only a
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
- When using database sessions, the User object is passed as an argument.
- When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
@@ -188,17 +153,11 @@ callbacks: {
...
```
If you're using TypeScript, you will want to [augment the session type](/getting-started/typescript#module-augmentation).
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.

View File

@@ -3,47 +3,51 @@ id: databases
title: Databases
---
NextAuth.js comes with multiple ways of connecting to a database:
NextAuth.js offers multiple database adapters:
- **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, PostgreSQL, MSSQL, SQLite and MongoDB databases._
- **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, PostgreSQL and SQLite databases._
- **Fauna**<br/>
_The FaunaDB adapter only supports FaunaDB._
- **Custom Adapter**<br/>
_A custom Adapter can be used to connect to any database._
- [`typeorm-legacy`](./../adapters/typeorm/typeorm-overview)
- [`prisma`](./../adapters/prisma)
- [`prisma-legacy`](./../adapters/prisma-legacy)
- [`fauna`](./../adapters/fauna)
- [`dynamodb`](./../adapters/dynamodb)
- [`firebase`](./../adapters/firebase)
- [`pouchdb`](./../adapters/pouchdb)
> There are currently efforts in the [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters) repository to get community-based DynamoDB, Sanity, PouchDB and Sequelize Adapters merged. If you are interested in any of the above, feel free to check out the PRs in the `nextauthjs/adapters` repository!
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
> As of **v4.0.0** NextAuth.js no longer ships with an adapter included by default. If you would like to persist any information, you need to install one of the many available adapters yourself. See the individual adapter documentation pages for more details.
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
---
**The rest of this document covers the old default adapter (TypeORM).**
## How to use a database
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
## How to use a database
The following approaches are exactly equivalent:
You can specify database credentials as a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object or connection string:
```js
database: "mysql://nextauth:password@127.0.0.1:3306/database_name"
```js title="pages/api/auth/[...nextauth].js"
import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"
export default NextAuth({
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name"
),
// or...
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "nextauth",
password: "password",
database: "database_name",
}),
})
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name'
}
```
Both approaches are exactly equivalent:
:::tip
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
@@ -51,22 +55,23 @@ You can pass in any valid [TypeORM configuration option](https://github.com/type
_e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:_
```js
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
)
```
_…or as a database configuration object:_
```js
database: {
type: 'mysql',
host: '127.0.0.1',
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name',
entityPrefix: 'nextauth_'
}
username: "nextauth",
password: "password",
database: "database_name",
entityPrefix: "nextauth_",
})
```
:::
@@ -87,19 +92,21 @@ _If you are running SQLite, MongoDB or a Document database you can skip this ste
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
```js
database: "mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
)
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name',
synchronize: true
}
username: "nextauth",
password: "password",
database: "database_name",
synchronize: true,
})
```
:::warning
@@ -128,7 +135,9 @@ Install module:
#### Example
```js
database: "mysql://username:password@127.0.0.1:3306/database_name"
adapter: TypeORMAdapter(
"mysql://username:password@127.0.0.1:3306/database_name"
)
```
### MariaDB
@@ -139,7 +148,9 @@ Install module:
#### Example
```js
database: "mariadb://username:password@127.0.0.1:3306/database_name"
adapter: TypeORMAdapter(
"mariadb://username:password@127.0.0.1:3306/database_name"
)
```
### Postgres / CockroachDB
@@ -152,30 +163,34 @@ Install module:
PostgresDB
```js
database: "postgres://username:password@127.0.0.1:5432/database_name"
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:5432/database_name"
)
```
CockroachDB
```js
database: "postgres://username:password@127.0.0.1:26257/database_name"
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:26257/database_name"
)
```
If the node is using Self-signed cert
```js
database: {
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync('/path/to/server-certificates/root.crt').toString()
},
adapter: TypeORMAdapter({
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync("/path/to/server-certificates/root.crt").toString(),
},
})
```
Read more: [https://node-postgres.com/features/ssl](https://node-postgres.com/features/ssl)
@@ -190,7 +205,7 @@ Install module:
#### Example
```js
database: "mssql://sa:password@localhost:1433/database_name"
adapter: TypeORMAdapter("mssql://sa:password@localhost:1433/database_name")
```
### MongoDB
@@ -201,7 +216,9 @@ Install module:
#### Example
```js
database: "mongodb://username:password@127.0.0.1:3306/database_name"
adapter: TypeORMAdapter(
"mongodb://username:password@127.0.0.1:3306/database_name"
)
```
### SQLite
@@ -214,7 +231,7 @@ Install module:
#### Example
```js
database: "sqlite://localhost/:memory:"
adapter: TypeORMAdapter("sqlite://localhost/:memory:")
```
## Other databases

View File

@@ -123,34 +123,27 @@ jwt: {
// Defaults to NextAuth.js secret if not explicitly specified.
// This is used to generate the actual signingKey and produces a warning
// message if not defined explicitly.
// You can generate a secret be using `openssl rand -base64 64`
secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
// This gives you direct knowledge of the key used to sign the token so you can use it
// to authenticate indirectly (eg. to a database driver)
signingKey: {
kty: "oct",
kid: "Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q",
alg: "HS512",
k: "K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"
},
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
// If you chose something other than the default algorithm for the signingKey (HS512)
// you also need to configure the algorithm
verificationOptions: {
algorithms: ['HS256']
},
// verificationOptions: {
// algorithms: ['HS256']
// },
// Set to true to use encryption. Defaults to false (signing only).
encryption: true,
// You can generate an encryption key by using `npx node-jose-tools newkey -s 256 -t oct -a A256GCM -u enc`
encryptionKey: "",
// decryptionKey: encryptionKey,
decryptionOptions: {
algorithms: ['A256GCM']
},
// encryption: true,
// encryptionKey: "",
// decryptionKey = encryptionKey,
// decryptionOptions = {
// algorithms: ['A256GCM']
// },
// You can define your own encode/decode functions for signing and encryption
// if you want to override the default behaviour.
async encode({ secret, token, maxAge }) {},
async decode({ secret, token, maxAge }) {},
// async encode({ secret, token, maxAge }) {},
// async decode({ secret, token, maxAge }) {},
}
```
@@ -259,16 +252,16 @@ You can specify a handler for any of the callbacks below.
```js
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return baseUrl
},
async session(session, user) {
async session({ session, token, user }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
}
}
@@ -307,20 +300,12 @@ events: {
### adapter
- **Default value**: _Adapter.Default()_
- **Default value**: none
- **Required**: _No_
#### Description
By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases. An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
See the [adapter documentation](/adapters/overview) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
:::
By default NextAuth.js does not include an adapter any longer. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the [adapter documentation](/adapters/overview).
---

View File

@@ -22,9 +22,11 @@ To add a custom login page, you can use the `pages` option:
```
## Error codes
We purposefully restrict the returned error codes for increased security.
### Error page
The following errors are passed as error query parameters to the default or overriden error page:
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
@@ -35,6 +37,7 @@ The following errors are passed as error query parameters to the default or over
Example: `/auth/error?error=Configuration`
### Sign-in page
The following errors are passed as error query parameters to the default or overriden sign-in page:
- **OAuthSignin**: Error in constructing an authorization URL ([1](https://github.com/nextauthjs/next-auth/blob/457952bb5abf08b09861b0e5da403080cd5525be/src/server/lib/signin/oauth.js), [2](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/pkce-handler.js), [3](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/state-handler.js)),
@@ -45,6 +48,7 @@ The following errors are passed as error query parameters to the default or over
- **OAuthAccountNotLinked**: If the email on the account is already linked, but not with this OAuth account
- **EmailSignin**: Sending the e-mail with the verification token failed
- **CredentialsSignin**: The `authorize` callback returned `null` in the [Credentials provider](/providers/credentials). We don't recommend providing information about which part of the credentials were wrong, as it might be abused by malicious hackers.
- **SessionRequired**: The content of this page requires you to be signed in at all times. See [useSession](/getting-started/client#require-session) for configuration.
- **Default**: Catch all, will apply, if none of the above matched
Example: `/auth/error?error=Default`
@@ -60,14 +64,16 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from 'next-auth/client'
import { getProviders, signIn } from "next-auth/react"
export default function SignIn({ providers }) {
export default function SignIn({ Providers }) {
return (
<>
{Object.values(providers).map(provider => (
{Object.values(providers).map((provider) => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</>
@@ -75,10 +81,10 @@ export default function SignIn({ providers }) {
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
export async function getServerSideProps(context) {
const providers = await getProviders()
return {
props: { providers }
props: { providers },
}
}
@@ -97,26 +103,26 @@ SignIn.getInitialProps = async () => {
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
```jsx title="pages/auth/email-signin.js"
import { getCsrfToken } from 'next-auth/client'
import { getCsrfToken } from "next-auth/react"
export default function SignIn({ csrfToken }) {
return (
<form method='post' action='/api/auth/signin/email'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<form method="post" action="/api/auth/signin/email">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
Email address
<input type='email' id='email' name='email'/>
<input type="email" id="email" name="email" />
</label>
<button type='submit'>Sign in with Email</button>
<button type="submit">Sign in with Email</button>
</form>
)
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
export async function getServerSideProps(context) {
const csrfToken = await getCsrfToken(context)
return {
props: { csrfToken }
props: { csrfToken },
}
}
@@ -133,7 +139,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('email', { email: 'jsmith@example.com' })
signIn("email", { email: "jsmith@example.com" })
```
### Credentials Sign in
@@ -141,21 +147,21 @@ signIn('email', { email: 'jsmith@example.com' })
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
```jsx title="pages/auth/credentials-signin.js"
import { getCsrfToken } from 'next-auth/client'
import { getCsrfToken } from "next-auth/react"
export default function SignIn({ csrfToken }) {
return (
<form method='post' action='/api/auth/callback/credentials'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<form method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
Username
<input name='username' type='text'/>
<input name="username" type="text" />
</label>
<label>
Password
<input name='password' type='password'/>
<input name="password" type="password" />
</label>
<button type='submit'>Sign in</button>
<button type="submit">Sign in</button>
</form>
)
}
@@ -164,8 +170,8 @@ export default function SignIn({ csrfToken }) {
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context)
}
csrfToken: await getCsrfToken(context),
},
}
}
@@ -182,7 +188,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('credentials', { username: 'jsmith', password: '1234' })
signIn("credentials", { username: "jsmith", password: "1234" })
```
:::tip

View File

@@ -83,7 +83,7 @@ providers: [
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
| scope | OAuth access scopes (expects string with space as separator) | `string` | Yes |
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
@@ -187,7 +187,7 @@ You only need to add two changes:
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
3. Add it to our [provider types](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts) (for TS projects)<br />
• you just need to add your new provider name to [this list](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L56-L97)<br />
• in case your new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
• in case you new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
That's it! 🎉 Others will be able to discover this provider much more easily now!
@@ -260,14 +260,14 @@ providers: [
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
// You can also use the `req` object to obtain additional parameters
// (i.e., the request IP address)
// (i.e., the request IP address)
const res = await fetch("/your/endpoint", {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
return user
@@ -288,10 +288,10 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
### Options
| Name | Description | Type | Required |
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :-----------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials, req) => Promise<User>` | Yes |

View File

@@ -15,9 +15,9 @@ If you are seeing any of these errors in the console, something is wrong.
These errors are returned from the client. As the client is [Universal JavaScript (or "Isomorphic JavaScript")](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) it can be run on the client or server, so these errors can occur in both in the terminal and in the browser console.
#### CLIENT_USE_SESSION_ERROR
#### CLIENT_SESSION_ERROR
This error occurs when the `useSession()` React Hook has a problem fetching session data.
This error occurs when the `SessionProvider` Context has a problem fetching session data.
#### CLIENT_FETCH_ERROR
@@ -33,28 +33,62 @@ These errors are displayed on the terminal.
#### OAUTH_GET_ACCESS_TOKEN_ERROR
This occurs when there was an error in the POST request to the OAuth provider and we were not able to retrieve the access token.
Please double check your provider settings.
#### OAUTH_V1_GET_ACCESS_TOKEN_ERROR
This error is explicitly related to older OAuth v1.x providers, if you are using one of these, please double check all available settings.
#### OAUTH_GET_PROFILE_ERROR
N/A
#### OAUTH_PARSE_PROFILE_ERROR
This error is a result of either a problem with the provider response or the user cancelling the action with the provider, unfortunately we can't discern which with the information we have.
This error should also log the exception and available `profileData` to further aid debugging.
#### OAUTH_CALLBACK_HANDLER_ERROR
This error will occur when there was an issue parsing the json request body, for example.
There should also be further details logged when this occurs, such as the error thrown, and the request body itself to aid in debugging.
---
### Signin / Callback
#### GET_AUTHORIZATION_URL_ERROR
This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL.
Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### SIGNIN_OAUTH_ERROR
This error can occur in one of a few places, first during the redirect to the authorization URL of the provider. Next, in the signin flow while creating the PKCE code verifier. Finally, during the generation of the CSRF Token hash in internal state during signin.
Please check your OAuth provider settings and make sure your URLs and other options are correctly set on the provider side.
#### CALLBACK_OAUTH_ERROR
This can occur during handling of the callback if the `code_verifier` cookie was not found or an invalid state was returned from the OAuth provider.
#### SIGNIN_EMAIL_ERROR
This error can occur when a user tries to sign in via an email link; for example, if the email token could not be generated or the verification request failed.
Please double check your email settings.
#### CALLBACK_EMAIL_ERROR
This can occur during the email callback process. Specifically, if there was an error signing the user in via email, encoding the jwt, etc.
Please double check your Email settings.
#### EMAIL_REQUIRES_ADAPTER_ERROR
The Email authentication provider can only be used if a database is configured.
@@ -71,6 +105,8 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
This error occurs when there was no `authorize()` handler defined on the credential authentication provider.
#### PKCE_ERROR
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
@@ -87,20 +123,20 @@ https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does no
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
````
```
jose newkey -s 512 -t oct -a HS512
````
```
If you are unable to use an HS512 key (for example to interoperate with other services) you can define what is supported using
````
```
jwt: {
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
verificationOptions: {
algorithms: ["HS256"]
}
}
````
```
#### SESSION_ERROR
@@ -110,6 +146,8 @@ If you are unable to use an HS512 key (for example to interoperate with other se
#### SIGNOUT_ERROR
This error occurs when there was an issue deleting the session from the database, for example.
---
### Database
@@ -120,30 +158,56 @@ They all indicate a problem interacting with the database.
#### ADAPTER_CONNECTION_ERROR
This error can occur during the `createConnection()` function. Make sure your database connection string / settings are correct and the database is up and ready to receive connections.
#### CREATE_USER_ERROR
N/A
#### GET_USER_BY_ID_ERROR
N/A
#### GET_USER_BY_EMAIL_ERROR
N/A
#### GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR
N/A
#### LINK_ACCOUNT_ERROR
N/A
#### CREATE_SESSION_ERROR
N/A
#### GET_SESSION_ERROR
N/A
#### UPDATE_SESSION_ERROR
N/A
#### DELETE_SESSION_ERROR
N/A
#### CREATE_VERIFICATION_REQUEST_ERROR
N/A
#### GET_VERIFICATION_REQUEST_ERROR
N/A
#### DELETE_VERIFICATION_REQUEST_ERROR
N/A
---
### Other

View File

@@ -15,13 +15,24 @@ It is not commercial software and is not associated with a commercial organizati
## Compatibility
### What databases does NextAuth.js support?
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases does NextAuth.js support?</h3>
</summary>
<p>
You can use NextAuth.js with MySQL, MariaDB, Postgres, MongoDB and SQLite or without a database. (See also: [Databases](/configuration/databases))
You can use also NextAuth.js with any database using a custom database adapter, or by using a custom credentials authentication provider - e.g. to support signing in with a username and password stored in an existing database.
### What authentication services does NextAuth.js support?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What authentication services does NextAuth.js support?</h3>
</summary>
<p>
<p>NextAuth.js includes built-in support for signing in with&nbsp;
{Object.values(require("../providers.json")).sort().join(", ")}.
@@ -32,7 +43,14 @@ NextAuth.js also supports email for passwordless sign in, which is useful for ac
You can also use a custom based provider to support signing in with a username and password stored in an external database and/or using two factor authentication.
### Does NextAuth.js support signing in with a username and password?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js support signing in with a username and password?</h3>
</summary>
<p>
NextAuth.js is designed to avoid the need to store passwords for user accounts.
@@ -40,7 +58,14 @@ If you have an existing database of usernames and passwords, you can use a custo
_If you use a custom credentials provider user accounts will not be persisted in a database by NextAuth.js (even if one is configured). The option to use JSON Web Tokens for session tokens (which allow sign in without using a session database) must be enabled to use a custom credentials provider._
### Can I use NextAuth.js with a website that does not use Next.js?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with a website that does not use Next.js?</h3>
</summary>
<p>
NextAuth.js is designed for use with Next.js and Serverless.
@@ -50,27 +75,55 @@ If you use NextAuth.js on a website with a different subdomain then the rest of
NextAuth.js does not currently support automatically signing into sites on different top level domains (e.g. `www.example.com` vs `www.example.org`) using a single session.
### Can I use NextAuth.js with React Native?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with React Native?</h3>
</summary>
<p>
NextAuth.js is designed as a secure, confidential client and implements a server side authentication flow.
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
### Is NextAuth.js supporting TypeScript?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Is NextAuth.js supporting TypeScript?</h3>
</summary>
<p>
Yes! Check out the [TypeScript docs](/getting-started/typescript)
</p>
</details>
---
## Databases
### What databases are supported by NextAuth.js?
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases are supported by NextAuth.js?</h3>
</summary>
<p>
NextAuth.js can be used with MySQL, Postgres, MongoDB, SQLite and compatible databases (e.g. MariaDB, Amazon Aurora, Amazon DocumentDB…) or with no database.
It also provides an Adapter API which allows you to connect it to any database.
### What does NextAuth.js use databases for?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What does NextAuth.js use databases for?</h3>
</summary>
<p>
Databases in NextAuth.js are used for persisting users, OAuth accounts, email sign in tokens and sessions.
@@ -78,35 +131,70 @@ Specifying a database is optional if you don't need to persist user data or supp
If you are using a database with NextAuth.js, you can still explicitly enable JSON Web Tokens for sessions (instead of using database sessions).
### Should I use a database?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Should I use a database?</h3>
</summary>
<p>
- Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
- Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
### What database should I use?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What database should I use?</h3>
</summary>
<p>
Managed database solutions for MySQL, Postgres and MongoDB (and compatible databases) are well supported from cloud providers such as Amazon, Google, Microsoft and Atlas.
If you are deploying directly to a particular cloud platform you may also want to consider serverless database offerings they have (e.g. [Amazon Aurora Serverless on AWS](https://aws.amazon.com/rds/aurora/serverless/)).
</p>
</details>
---
## Security
### I think I've found a security problem, what should I do?
<details>
<summary>
<h3 style={{display:"inline-block"}}>I think I've found a security problem, what should I do?</h3>
</summary>
<p>
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com or info@balazsorban.com and yo@ndo.dev) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
### What is the disclosure policy for NextAuth.js?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What is the disclosure policy for NextAuth.js?</h3>
</summary>
<p>
We practice responsible disclosure.
If you contact us regarding a potentially serious issue, we will endeavor to get back to you within 72 hours and to publish a fix within 30 days. We will responsibly disclose the issue (and credit you with your consent) once a fix to resolve the issue has been released - or after 90 days, which ever is sooner.
### How do I get Refresh Tokens and Access Tokens for an OAuth account?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>How do I get Refresh Tokens and Access Tokens for an OAuth account?</h3>
</summary>
<p>
NextAuth.js provides a solution for authentication, session management and user account creation.
@@ -119,7 +207,14 @@ You can then look them up from the database or persist them to the JSON Web Toke
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
### When I sign in with another account with the same email address, why are accounts not linked automatically?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>When I sign in with another account with the same email address, why are accounts not linked automatically?</h3>
</summary>
<p>
Automatic account linking on sign in is not secure between arbitrary providers - with the exception of allowing users to sign in via an email addresses as a fallback (as they must verify their email address as part of the flow).
@@ -137,11 +232,18 @@ Automatic account linking is not a planned feature of NextAuth.js, however there
Providing support for secure account linking and unlinking of additional providers - which can only be done if a user is already signed in already - was originally a feature in v1.x but has not been present since v2.0, is planned to return in a future release.
</p>
</details>
---
## Feature Requests
### Why doesn't NextAuth.js support [a particular feature]?
<details>
<summary>
<h3 style={{display:"inline-block"}}>Why doesn't NextAuth.js support [a particular feature]?</h3>
</summary>
<p>
NextAuth.js is an open source project built by individual contributors who are volunteers writing code and providing support in their spare time.
@@ -149,7 +251,14 @@ If you would like NextAuth.js to support a particular feature, the best way to h
If you are not able to develop a feature yourself, you can offer to sponsor someone to work on it.
### I disagree with a design decision, how can I change your mind?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>I disagree with a design decision, how can I change your mind?</h3>
</summary>
<p>
Product design decisions on NextAuth.js are made by core team members.
@@ -159,11 +268,18 @@ Requests that provide the detail requested in the template and follow the format
Ultimately if your request is not accepted or is not actively in development, you are always free to fork the project under the terms of the ISC License.
</p>
</details>
---
## JSON Web Tokens
### Does NextAuth.js use JSON Web Tokens?
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js use JSON Web Tokens?</h3>
</summary>
<p>
NextAuth.js supports both database session tokens and JWT session tokens.
@@ -172,7 +288,14 @@ NextAuth.js supports both database session tokens and JWT session tokens.
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
### What are the advantages of JSON Web Tokens?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the advantages of JSON Web Tokens?</h3>
</summary>
<p>
JSON Web Tokens can be used for session tokens, but are also used for lots of other things, such as sending signed objects between services in authentication flows.
@@ -184,7 +307,14 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
- You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in a server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
### What are the disadvantages of JSON Web Tokens?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the disadvantages of JSON Web Tokens?</h3>
</summary>
<p>
- You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
@@ -202,11 +332,18 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
- If you do not explicitly specify a secret for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
- If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
### Are JSON Web Tokens secure?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Are JSON Web Tokens secure?</h3>
</summary>
<p>
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space available to store data (total cookie size for a domain is limited to 4KB).
@@ -220,7 +357,14 @@ NextAuth.js will generate keys for you, but this will generate a warning at star
Using explicit public/private keys for signing is strongly recommended.
### What signing and encryption standards does NextAuth.js support?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What signing and encryption standards does NextAuth.js support?</h3>
</summary>
<p>
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
@@ -235,3 +379,6 @@ This incorporates support for:
- [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
- [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
- [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)
</p>
</details>

View File

@@ -34,17 +34,17 @@ You can use the [session callback](/configuration/callbacks#session-callback) to
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
Make sure that [`<SessionProvider>`](#sessionprovider) is added to `pages/_app.js`.
#### Example
```jsx
import { useSession } from "next-auth/client"
import { useSession } from "next-auth/react"
export default function Component() {
const [session, loading] = useSession()
const { data: session, status } = useSession()
if (session) {
if (status === "authenticated") {
return <p>Signed in as {session.user.email}</p>
}
@@ -52,6 +52,41 @@ export default function Component() {
}
```
`useSession()` returns an object containing two values: `data` and `status`:
- **`data`**: This can be three values: [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444) / `undefined` / `null`.
- when the session hasn't been fetched yet, `data` will `undefined`
- in case it failed to retrieve the session, `data` will be `null`
- in case of success, `data` will be [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444).
- **`status`**: enum mapping to three possible session states: `"loading" | "authenticated" | "unauthenticated"`
### Require session
Due to the way how Next.js handles `getServerSideProps` and `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page (SSR). This increases server load, and if you are good with making the requests from the client, there is an alternative. You can use `useSession` in a way that makes sure you always have a valid session. If after the initial loading state there was no session found, you can define the appropriate action to respond.
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onFail()` callback, if you would like to do something else:
#### Example
```jsx title="pages/protected.jsx"
import { useSession } from "next-auth/react"
export default function Admin() {
const { status } = useSession({
required: true,
onUnauthenticated() {
// The user is not authenticated, handle it here.
}
})
const if (status === "loading") {
return "Loading or not authenticated..."
}
return "User is logged in"
}
```
---
## getSession()
@@ -75,7 +110,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getSession } from "next-auth/client"
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })
@@ -113,7 +148,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getCsrfToken } from "next-auth/client"
import { getCsrfToken } from "next-auth/react"
export default async (req, res) => {
const csrfToken = await getCsrfToken({ req })
@@ -140,7 +175,7 @@ It can be useful if you are creating a dynamic custom sign in page.
#### API Route
```jsx title="pages/api/example.js"
import { getProviders } from "next-auth/client"
import { getProviders } from "next-auth/react"
export default async (req, res) => {
const providers = await getProviders()
@@ -167,7 +202,7 @@ The `signIn()` method can be called from the client in different ways, as shown
#### Redirects to sign in page when clicked
```js
import { signIn } from "next-auth/client"
import { signIn } from "next-auth/react"
export default () => <button onClick={() => signIn()}>Sign in</button>
```
@@ -175,7 +210,7 @@ export default () => <button onClick={() => signIn()}>Sign in</button>
#### Starts Google OAuth sign-in flow when clicked
```js
import { signIn } from "next-auth/client"
import { signIn } from "next-auth/react"
export default () => (
<button onClick={() => signIn("google")}>Sign in with Google</button>
@@ -187,7 +222,7 @@ export default () => (
When using it with the email flow, pass the target `email` as an option.
```js
import { signIn } from "next-auth/client"
import { signIn } from "next-auth/react"
export default ({ email }) => (
<button onClick={() => signIn("email", { email })}>Sign in with Email</button>
@@ -206,7 +241,7 @@ e.g.
- `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
- `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same host name, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -248,7 +283,7 @@ e.g.
}
```
#### Additional params
#### Additional parameters
It is also possible to pass additional parameters to the `/authorize` endpoint through the third argument of `signIn()`.
@@ -256,7 +291,7 @@ See the [Authorization Request OIDC spec](https://openid.net/specs/openid-connec
e.g.
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to reauthenticate_
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to re-authenticate_
- `signIn("auth0", null, { login_hint: "info@example.com" })` _hints the e-mail address to the provider_
:::note
@@ -274,12 +309,12 @@ The following parameters are always overridden server-side: `redirect_uri`, `sta
- Client Side: **Yes**
- Server Side: No
In order to logout, use the `signOut()` method to ensure the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
```js
import { signOut } from "next-auth/client"
import { signOut } from "next-auth/react"
export default () => <button onClick={() => signOut()}>Sign out</button>
```
@@ -290,7 +325,7 @@ As with the `signIn()` function, you can specify a `callbackUrl` parameter by pa
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same host name (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -299,35 +334,36 @@ If you pass `redirect: false` to `signOut`, the page will not reload. The sessio
:::tip
If you need to redirect to another page but you want to avoid a page reload, you can try:
`const data = await signOut({redirect: false, callbackUrl: "/foo"})`
where `data.url` is the validated url you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
where `data.url` is the validated URL you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
:::
---
## Provider
## SessionProvider
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
Using the supplied `<SessionProvider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { Provider } from "next-auth/client"
import { SessionProvider } from "next-auth/react"
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<Provider session={pageProps.session}>
<SessionProvider session={session}>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}
```
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
If you pass the `session` page prop to the `<SessionProvider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
This only works on pages where you provide the correct `pageProps`, however. This is normally done in `getInitialProps` or `getServerSideProps` like so:
```js title="pages/index.js"
import { getSession } from "next-auth/client"
import { getSession } from "next-auth/react"
...
@@ -340,30 +376,33 @@ export async function getServerSideProps(ctx) {
}
```
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each auth check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each authentication check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
### Options
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
However, if you need to customize the session behavior and/or are using short session expiry times, you can pass options to the provider to customize the behavior of the `useSession()` hook.
```jsx title="pages/_app.js"
import { Provider } from "next-auth/client"
import { SessionProvider } from "next-auth/react"
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<Provider
session={pageProps.session}
options={{
clientMaxAge: 60, // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60, // Send keepAlive message every 5 minutes
}}
<SessionProvider
session={session}
// Re-fetch session if cache is older than 60 seconds
staleTime={60}
// Re-fetch session every 5 minutes
refetchInterval={5 * 60}
>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}
```
@@ -373,30 +412,30 @@ export default function App({ Component, pageProps }) {
Every tab/window maintains its own copy of the local session state; the session is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
Using low values for `staleTime` or `refetchInterval` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
:::
#### Client Max Age
#### Stale time
The `clientMaxAge` option is the maximum age a session data can be on the client before it is considered stale.
The `staleTime` option is the maximum age a session data can be on the client before it is considered stale.
When `clientMaxAge` is set to `0` (the default) the cache will always be used when useSession is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
When `staleTime` is set to `0` (the default) the cache will always be used when `useSession` is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
If set to any value other than zero, it specifies in seconds the maximum age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
The value for `clientMaxAge` should always be lower than the value of the session `maxAge` option.
The value for `staleTime` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
#### Keep Alive
#### Refetch interval
The `keepAlive` option is how often the client should contact the server to avoid a session expiring.
The `refetchInterval` option can be used to contact the server to avoid a session expiring.
When `keepAlive` is set to `0` (the default) it will not send a keep alive message.
When `refetchInterval` is set to `0` (the default) there will be no session polling.
If set to any value other than zero, it specifies in seconds how often the client should contact the server to update the session state. If the session state has expired when it is triggered, all open tabs/windows will be updated to reflect this.
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
The value for `refetchInterval` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **\_app.js** in Next.js applications.
@@ -406,11 +445,11 @@ See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/cu
### Custom Client Session Handling
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side query to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
```js title="pages/admin.jsx"
export default function AdminDashboard() {
const [session] = useSession()
const { data: session } = useSession()
// session is always non-null inside this page, all the way down the React tree.
return "Some super secret dashboard"
}
@@ -419,9 +458,12 @@ AdminDashboard.auth = true
```
```jsx title="pages/_app.jsx"
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<Provider session={pageProps.session}>
<SessionProvider session={session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
@@ -429,12 +471,12 @@ export default function App({ Component, pageProps }) {
) : (
<Component {...pageProps} />
)}
</Provider>
</SessionProvider>
)
}
function Auth({ children }) {
const [session, loading] = useSession()
const { data: session, loading } = useSession()
const isUser = !!session?.user
React.useEffect(() => {
if (loading) return // Do nothing while loading
@@ -461,14 +503,14 @@ AdminDashboard.auth = {
}
```
Because of how \_app is done, it won't unnecessarily contact the /api/auth/session endpoint for pages that do not require auth.
Because of how `_app` is written, it won't unnecessarily contact the `/api/auth/session` endpoint for pages that do not require authentication.
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
More information can be found in the following [GitHub Issue](https://github.com/nextauthjs/next-auth/issues/1210).
### NextAuth.js + React-Query
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `Provider` from `next-auth/client`.
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `SessionProvider` from `next-auth/react`.
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.

Some files were not shown because too many files have changed in this diff Show More