mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
107 Commits
v3.23.2
...
next-auth@
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29db75ad28 | ||
|
|
d348ca1dc1 | ||
|
|
d53e1ea6c4 | ||
|
|
e701342b1a | ||
|
|
8a133bf5fd | ||
|
|
35a3ea6620 | ||
|
|
289800fbb4 | ||
|
|
28eccc3e64 | ||
|
|
e16bf939a9 | ||
|
|
9b078c92b2 | ||
|
|
87f6f576b1 | ||
|
|
50584bdc4c | ||
|
|
b4429235c0 | ||
|
|
e1b297d06d | ||
|
|
ab764e3793 | ||
|
|
c8941e4b3e | ||
|
|
ead715219a | ||
|
|
8faa7553dd | ||
|
|
90a6a0084b | ||
|
|
cb844a2436 | ||
|
|
74558d6cc2 | ||
|
|
d03125a77b | ||
|
|
66d16f8bf4 | ||
|
|
be74dd0e7e | ||
|
|
9bf867ddcf | ||
|
|
0f460c22da | ||
|
|
887cb00877 | ||
|
|
75ca097ff7 | ||
|
|
bcb9383aec | ||
|
|
b953963101 | ||
|
|
4649f1968b | ||
|
|
45f4a69a4e | ||
|
|
2155c93a3c | ||
|
|
d5958571a4 | ||
|
|
ebecaa6a4b | ||
|
|
1c5173a818 | ||
|
|
35ce332cc6 | ||
|
|
ec295287f1 | ||
|
|
46978ac02f | ||
|
|
f546e550dd | ||
|
|
ac5b4db0f2 | ||
|
|
8bbffdd08c | ||
|
|
a22a0a36fd | ||
|
|
797272afe1 | ||
|
|
13e56bcf2f | ||
|
|
b0f7f87c04 | ||
|
|
9c0851c0f9 | ||
|
|
f5b3c29ab1 | ||
|
|
b4f2a0106a | ||
|
|
9c095b0532 | ||
|
|
0475964a0f | ||
|
|
ad6c13cdc9 | ||
|
|
591aa7cc7e | ||
|
|
9abb392b4e | ||
|
|
b89ae87fb1 | ||
|
|
3687d17724 | ||
|
|
b04ff82fb9 | ||
|
|
c11915ba9c | ||
|
|
24ee459f97 | ||
|
|
ac4851d238 | ||
|
|
84094b0ee7 | ||
|
|
f09ab4a04f | ||
|
|
067364381b | ||
|
|
6ee36b6842 | ||
|
|
5a89ab69d3 | ||
|
|
665445818e | ||
|
|
67cf2a11bb | ||
|
|
832d51f10e | ||
|
|
29862ac887 | ||
|
|
5aa2b61b88 | ||
|
|
929c644653 | ||
|
|
2657e72e81 | ||
|
|
8ff7dbb18f | ||
|
|
748d576a5a | ||
|
|
9f16e3f0fb | ||
|
|
1042e9a93d | ||
|
|
aa57f2dd7e | ||
|
|
1817286ce3 | ||
|
|
b942dd34f3 | ||
|
|
4d9622e1cc | ||
|
|
a7eadf80e5 | ||
|
|
75c7dbc3e7 | ||
|
|
d36b89cb12 | ||
|
|
349cd03fbd | ||
|
|
5cd130669b | ||
|
|
638233f4a0 | ||
|
|
37e175195f | ||
|
|
e8a9e8aeb6 | ||
|
|
1fb308a6f4 | ||
|
|
613c303315 | ||
|
|
d24fe1cebb | ||
|
|
885b02ca95 | ||
|
|
f218697fd6 | ||
|
|
dbead0ad85 | ||
|
|
704ded5310 | ||
|
|
25fbcb4648 | ||
|
|
53a439b44b | ||
|
|
16a2e37fd6 | ||
|
|
0392a8df9a | ||
|
|
a459b95c5b | ||
|
|
13df7eb81d | ||
|
|
62f261209c | ||
|
|
da43d0d896 | ||
|
|
4b1271ba75 | ||
|
|
d30da0170f | ||
|
|
887b2985fc | ||
|
|
d2bbac1164 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
||||
# 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
43
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,43 +0,0 @@
|
||||
---
|
||||
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:
|
||||
|
||||
```
|
||||
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
|
||||
```
|
||||
|
||||
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.
|
||||
91
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
91
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
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 💚
|
||||
|
||||
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
39
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,39 +0,0 @@
|
||||
---
|
||||
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.
|
||||
68
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
68
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
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
32
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,32 +0,0 @@
|
||||
---
|
||||
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.
|
||||
62
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/question.yaml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
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
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
@@ -1,36 +0,0 @@
|
||||
---
|
||||
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.
|
||||
58
.github/ISSUE_TEMPLATE/typescript.yaml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/typescript.yaml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
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 💚
|
||||
|
||||
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# Simple check that the build is valid and no linting errors.
|
||||
# Currently is run as a seperate workflow as it's fast to fail.
|
||||
name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
68
.github/workflows/codeql-analysis.yml
vendored
68
.github/workflows/codeql-analysis.yml
vendored
@@ -1,67 +1,27 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: Code Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta, next ]
|
||||
branches: [main, beta, next]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
- cron: "43 17 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
name: Verify
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
language: ["javascript"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
57
.github/workflows/integration.yml
vendored
57
.github/workflows/integration.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Only run tests integration against Pull Requests from branches in
|
||||
# this repository. We do this as integration tests require access to
|
||||
# secrets in GitHub and they are not exposed to tests run against
|
||||
# forks (for security reasons), so integration test against
|
||||
# Pull Requests from external repos just fail and generate noise.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
|
||||
# fail due to IP Address checks done by providers, which enforce
|
||||
# CAPTCHA checks on login request from cloud compute IP addresses to
|
||||
# prevent abuse.
|
||||
runs-on: self-hosted
|
||||
|
||||
# Target time is under 5 minutes to run all tests. If it takes longer than
|
||||
# 10 minutes should look at running tests in parallel. No individual flow
|
||||
# should take longer than 5 minutes to build and run.
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
# TODO Tests should exit out if env vars not set (currently hangs)
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
|
||||
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
|
||||
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
|
||||
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
|
||||
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
|
||||
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
|
||||
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
|
||||
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}
|
||||
12
.github/workflows/labeler.yml
vendored
12
.github/workflows/labeler.yml
vendored
@@ -1,11 +1,13 @@
|
||||
name: "Pull Request Labeler"
|
||||
name: PR Labeler
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: Triage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
68
.github/workflows/release.yml
vendored
68
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,20 +8,69 @@ on:
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npx semantic-release@17
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm test -- --coverage --verbose && npm run test:types
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
directory: ./coverage
|
||||
fail_ci_if_error: false
|
||||
release-branch:
|
||||
name: Publish branch
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
environment: Production
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Publish to npm and GitHub
|
||||
run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
release-pr:
|
||||
name: Publish PR
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
environment: Preview
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
|
||||
npm run version:pr
|
||||
npm publish --access public --tag experimental
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.number }}
|
||||
|
||||
27
.github/workflows/types.yml
vendored
27
.github/workflows/types.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Types
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
run: npm run test:types
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -58,4 +58,12 @@ app/yarn.lock
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
/prisma/migrations
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
|
||||
# v4
|
||||
packages
|
||||
apps
|
||||
docs/providers.json
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
@@ -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. 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 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.
|
||||
|
||||
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
|
||||
|
||||
@@ -11,43 +11,49 @@ Please raise any significant new functionality or breaking change an issue for d
|
||||
## For contributors
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
- Pull Requests should be raised for any change
|
||||
- Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
- We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
- We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
- If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
|
||||
### Setting up local environment
|
||||
|
||||
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
|
||||
|
||||
The dev application requires you to use `npm@7`.
|
||||
|
||||
1. Clone the repo:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
2. Install packages:
|
||||
2. Install packages, set up the dev application:
|
||||
|
||||
```sh
|
||||
npm i && npm run dev:setup
|
||||
npm run dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
Your dev application will be available on `http://localhost:3000`
|
||||
|
||||
That's it! 🎉
|
||||
|
||||
@@ -64,6 +70,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
|
||||
#### Providers
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
|
||||
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
@@ -73,57 +80,43 @@ You can look at the existing built-in providers for inspiration.
|
||||
|
||||
#### Databases
|
||||
|
||||
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
|
||||
|
||||
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||
|
||||
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
||||
|
||||
You will need Docker and Docker Compose installed to be able to start / stop the databases.
|
||||
|
||||
When stopping the databases, it will reset their contents.
|
||||
|
||||
If you would like to contribute to an existing database adapter or help create a new one, head over to the [nextauthjs/adapters](https://www.github.com/nextauthjs/adapters) repository and follow the instructions provided there.
|
||||
#### Testing
|
||||
|
||||
Tests can be run with `npm run test`.
|
||||
|
||||
Automated tests are currently crude and limited in functionality, but improvements are in development.
|
||||
|
||||
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
|
||||
|
||||
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
|
||||
|
||||
## For maintainers
|
||||
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
* Use "Squash and merge"
|
||||
* Make sure you merge contributor PRs into `main`
|
||||
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
- Use "Squash and merge"
|
||||
- Make sure you merge contributor PRs into `main`
|
||||
- Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
- Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
|
||||
### Recommended Scopes
|
||||
|
||||
A typical conventional commit looks like this:
|
||||
|
||||
```
|
||||
type(scope): title
|
||||
|
||||
body
|
||||
```
|
||||
|
||||
Scope is the part that will help groupping the different commit types in the release notes.
|
||||
Scope is the part that will help grouping the different commit types in the release notes.
|
||||
|
||||
Some recommened scopes are:
|
||||
Some recommended scopes are:
|
||||
|
||||
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
|
||||
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
### Skipping a release
|
||||
|
||||
|
||||
135
README.md
135
README.md
@@ -7,11 +7,8 @@
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
@@ -41,7 +38,7 @@ It is designed from the ground up to support Next.js and Serverless.
|
||||
npm install --save next-auth
|
||||
```
|
||||
|
||||
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||
|
||||
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
|
||||
|
||||
@@ -51,40 +48,40 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
|
||||
|
||||
### Flexible and easy to use
|
||||
|
||||
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
* Supports email / passwordless authentication
|
||||
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
* Supports both JSON Web Tokens and database sessions
|
||||
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
- Supports email / passwordless authentication
|
||||
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
- Supports both JSON Web Tokens and database sessions
|
||||
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
|
||||
### Own your own data
|
||||
|
||||
NextAuth.js can be used with or without a database.
|
||||
|
||||
* An open source solution that allows you to keep control of your data
|
||||
* Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
* Works great with databases from popular hosting providers
|
||||
* Can also be used *without a database* (e.g. OAuth + JWT)
|
||||
- An open source solution that allows you to keep control of your data
|
||||
- Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
- Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
- Works great with databases from popular hosting providers
|
||||
- Can also be used _without a database_ (e.g. OAuth + JWT)
|
||||
|
||||
### 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)
|
||||
* 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/)
|
||||
- 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)
|
||||
- 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/)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### TypeScript
|
||||
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
@@ -93,50 +90,52 @@ The package at `@types/next-auth` is now deprecated.
|
||||
### Add API Route
|
||||
|
||||
```javascript
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: process.env.APPLE_SECRET
|
||||
clientSecret: process.env.APPLE_SECRET,
|
||||
}),
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
// Sign in with passwordless email link
|
||||
Providers.Email({
|
||||
server: process.env.MAIL_SERVER,
|
||||
from: '<no-reply@example.com>'
|
||||
from: "<no-reply@example.com>",
|
||||
}),
|
||||
],
|
||||
// SQL or MongoDB database (or leave empty)
|
||||
database: process.env.DATABASE_URL
|
||||
database: process.env.DATABASE_URL,
|
||||
})
|
||||
```
|
||||
|
||||
### Add React Component
|
||||
|
||||
```javascript
|
||||
import {
|
||||
useSession, signIn, signOut
|
||||
} from 'next-auth/client'
|
||||
import { useSession, signIn, signOut } from "next-auth/client"
|
||||
|
||||
export default function Component() {
|
||||
const [ session, loading ] = useSession()
|
||||
if(session) {
|
||||
return <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
const [session, loading] = useSession()
|
||||
if (session) {
|
||||
return (
|
||||
<>
|
||||
Signed in as {session.user.email} <br />
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <>
|
||||
Not signed in <br/>
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
</>
|
||||
return (
|
||||
<>
|
||||
Not signed in <br />
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -148,14 +147,44 @@ 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">
|
||||
<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>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
|
||||
</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
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
|
||||
10
SECURITY.md
10
SECURITY.md
@@ -14,11 +14,11 @@ We request that you contact us directly to report serious issues that might impa
|
||||
|
||||
If you contact us regarding a serious issue:
|
||||
|
||||
* We will endeavor to get back to you within 72 hours.
|
||||
* We will aim to publish a fix within 30 days.
|
||||
* 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 publically.
|
||||
- We will endeavor to get back to you within 72 hours.
|
||||
- We will aim to publish a fix within 30 days.
|
||||
- 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 emailing me@iaincollins.com
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
@@ -23,8 +23,6 @@ TWITTER_SECRET=
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
# You can use any of these as the "DATABASE_URL" for
|
||||
# databases started with Docker using `npm run db:start`.
|
||||
# Note: If using with Prisma adapter, you need to use a `.env`
|
||||
# file rather than a `.env.local` file to configure env vars.
|
||||
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
import styles from './footer.module.css'
|
||||
import { version } from 'package.json'
|
||||
import Link from "next/link"
|
||||
import styles from "./footer.module.css"
|
||||
import packageJSON from "package.json"
|
||||
|
||||
export default function Footer () {
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<hr />
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
|
||||
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
|
||||
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
|
||||
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
|
||||
<li className={styles.navItem}><em>{version}</em></li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://next-auth.js.org">Documentation</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://www.npmjs.com/package/next-auth">NPM</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href="/policy">
|
||||
<a>Policy</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<em>{packageJSON.version}</em>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"next": "^11.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
@@ -18,5 +18,16 @@ module.exports = {
|
||||
test: ["../src/server/pages/**"],
|
||||
presets: ["preact"],
|
||||
},
|
||||
{
|
||||
test: ["../src/**/*.test.js"],
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
2
config/jest-setup.js
Normal file
2
config/jest-setup.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import "whatwg-fetch"
|
||||
11
config/jest.config.js
Normal file
11
config/jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('@jest/types').Config.InitialOptions} */
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
|
||||
},
|
||||
rootDir: "../src",
|
||||
setupFilesAfterEnv: ["../config/jest-setup.js"],
|
||||
collectCoverageFrom: ["!client/__tests__/**"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
coverageDirectory: "../coverage",
|
||||
}
|
||||
17
config/version-pr.js
Normal file
17
config/version-pr.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const fs = require("fs-extra")
|
||||
const path = require("path")
|
||||
|
||||
try {
|
||||
const packageJSONPath = path.join(process.cwd(), "package.json")
|
||||
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
|
||||
|
||||
const sha8 = process.env.GITHUB_SHA.substr(0, 8)
|
||||
const prNumber = process.env.PR_NUMBER
|
||||
|
||||
packageJSON.version = `0.0.0-pr.${prNumber}.${sha8}`
|
||||
|
||||
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))
|
||||
} catch (error) {
|
||||
console.error("Could not set PR version", error)
|
||||
process.exit(1)
|
||||
}
|
||||
38177
package-lock.json
generated
38177
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
58
package.json
58
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "0.0.0-semantically-released",
|
||||
"version": "3.29.9",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
@@ -32,16 +32,18 @@
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev:setup": "npm run build:css && cd app && npm i",
|
||||
"dev:setup": "npm i && npm run build:css && cd app && npm i",
|
||||
"dev": "cd app && npm run dev",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test": "jest --config ./config/jest.config.js",
|
||||
"test:ci": "npm run lint && npm run test:types && npm run test -- --ci",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
"lint:fix": "eslint . --fix",
|
||||
"version:pr": "node ./config/version-pr"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -62,8 +64,8 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
|
||||
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
|
||||
"@next-auth/prisma-legacy-adapter": "0.1.2",
|
||||
"@next-auth/typeorm-legacy-adapter": "0.1.4",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -90,15 +92,17 @@
|
||||
"@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.14.2",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@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",
|
||||
"@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",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
@@ -108,16 +112,22 @@
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.6",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"next": "^10.0.5",
|
||||
"husky": "^6.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"msw": "^0.28.2",
|
||||
"next": "^11.0.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
"pretty-quick": "^3.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"typescript": "^4.1.3",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
@@ -144,7 +154,23 @@
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*test.js"
|
||||
],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
64
src/client/__tests__/client-provider.test.js
Normal file
64
src/client/__tests__/client-provider.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
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"
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("fetches the session once and re-uses it for different consumers", 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 />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
|
||||
|
||||
const session1 = screen.getByTestId("session-consumer-1").textContent
|
||||
const session2 = screen.getByTestId("session-consumer-2").textContent
|
||||
|
||||
expect(session1).toEqual(session2)
|
||||
})
|
||||
})
|
||||
|
||||
function ProviderFlow({ options = {} }) {
|
||||
return (
|
||||
<>
|
||||
<Provider options={options}>
|
||||
<SessionConsumer />
|
||||
<SessionConsumer testId="2" />
|
||||
</Provider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionConsumer({ testId = 1 }) {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
if (loading) return <span>loading</span>
|
||||
|
||||
return (
|
||||
<div data-testid={`session-consumer-${testId}`}>
|
||||
{JSON.stringify(session)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
src/client/__tests__/csrf.test.js
Normal file
105
src/client/__tests__/csrf.test.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { useState } from "react"
|
||||
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 { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toEqual(
|
||||
mockCSRFToken.csrfToken
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when there's no CSRF token returned, it'll reflect that", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockCSRFToken,
|
||||
csrfToken: null,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
|
||||
})
|
||||
})
|
||||
|
||||
test("when the fetch fails it'll throw a client fetch error", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<CSRFFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"csrf",
|
||||
new SyntaxError("Unexpected token s in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function CSRFFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleCSRF() {
|
||||
const result = await getCsrfToken()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="csrf-result">
|
||||
{response === null ? "null-response" : response || "no response"}
|
||||
</p>
|
||||
<button onClick={handleCSRF}>Get CSRF</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
90
src/client/__tests__/helpers/mocks.js
Normal file
90
src/client/__tests__/helpers/mocks.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { setupServer } from "msw/node"
|
||||
import { rest } from "msw"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export const mockSession = {
|
||||
ok: true,
|
||||
user: {
|
||||
image: null,
|
||||
name: "John",
|
||||
email: "john@email.com",
|
||||
},
|
||||
expires: 123213139,
|
||||
}
|
||||
|
||||
export const mockProviders = {
|
||||
ok: true,
|
||||
github: {
|
||||
id: "github",
|
||||
name: "Github",
|
||||
type: "oauth",
|
||||
signinUrl: "path/to/signin",
|
||||
callbackUrl: "path/to/callback",
|
||||
},
|
||||
credentials: {
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
},
|
||||
email: {
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCSRFToken = {
|
||||
ok: true,
|
||||
csrfToken: randomBytes(32).toString("hex"),
|
||||
}
|
||||
|
||||
export const mockGithubResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/github/url",
|
||||
}
|
||||
|
||||
export const mockCredentialsResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/credentials/url",
|
||||
}
|
||||
|
||||
export const mockEmailResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/email/url",
|
||||
}
|
||||
|
||||
export const mockSignOutResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/signout/url",
|
||||
}
|
||||
|
||||
export const server = setupServer(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSignOutResponse))
|
||||
),
|
||||
rest.get("/api/auth/session", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
),
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCSRFToken))
|
||||
),
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockProviders))
|
||||
),
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
),
|
||||
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCredentialsResponse))
|
||||
),
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmailResponse))
|
||||
),
|
||||
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
|
||||
)
|
||||
8
src/client/__tests__/helpers/utils.js
Normal file
8
src/client/__tests__/helpers/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getBroadcastEvents() {
|
||||
return window.localStorage.setItem.mock.calls
|
||||
.filter((call) => call[0] === "nextauth.message")
|
||||
.map(([eventName, value]) => {
|
||||
const { timestamp, ...rest } = JSON.parse(value)
|
||||
return { eventName, value: rest }
|
||||
})
|
||||
}
|
||||
85
src/client/__tests__/providers.test.js
Normal file
85
src/client/__tests__/providers.test.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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 logger from "../../lib/logger"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("when called it'll return the currently configured providers for sign in", async () => {
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("providers-result").textContent).toEqual(
|
||||
JSON.stringify(mockProviders)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("when failing to fetch the providers, it'll log the error", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.text("some error happened"))
|
||||
)
|
||||
)
|
||||
|
||||
render(<ProvidersFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
new SyntaxError("Unexpected token s in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function ProvidersFlow() {
|
||||
const [response, setResponse] = useState()
|
||||
|
||||
async function handleGerProviders() {
|
||||
const result = await getProviders()
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="providers-result">
|
||||
{response === null
|
||||
? "null-response"
|
||||
: JSON.stringify(response) || "no response"}
|
||||
</p>
|
||||
<button onClick={handleGerProviders}>Get Providers</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
98
src/client/__tests__/session.test.js
Normal file
98
src/client/__tests__/session.test.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from ".."
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("if it can fetch the session, it should store it in `localStorage`", async () => {
|
||||
render(<SessionFlow />)
|
||||
|
||||
// In the start, there is no session
|
||||
const noSession = await screen.findByText("No session")
|
||||
expect(noSession).toBeInTheDocument()
|
||||
|
||||
// After we fetched the session, it should have been rendered by `<SessionFlow />`
|
||||
const session = await screen.findByText(new RegExp(mockSession.user.name))
|
||||
expect(session).toBeInTheDocument()
|
||||
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "getSession",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
|
||||
test("if there's an error fetching the session, it should log it", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.body("Server error"))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SessionFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"session",
|
||||
new SyntaxError("Unexpected token S in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SessionFlow() {
|
||||
const [session, setSession] = useState(null)
|
||||
useEffect(() => {
|
||||
async function fetchUserSession() {
|
||||
try {
|
||||
const result = await getSession()
|
||||
setSession(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
fetchUserSession()
|
||||
}, [])
|
||||
|
||||
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
|
||||
return <p>No session</p>
|
||||
}
|
||||
290
src/client/__tests__/sign-in.test.js
Normal file
290
src/client/__tests__/sign-in.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../lib/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./helpers/mocks"
|
||||
import { signIn } from ".."
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider, it redirects to the default sign-in page",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider supplied and no callback URL, redirects using the current location",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | mockUrl
|
||||
${`email`} | ${mockEmailResponse.url}
|
||||
${`credentials`} | ${mockCredentialsResponse.url}
|
||||
`(
|
||||
"$provider provider redirects if `redirect` is `true`",
|
||||
async ({ provider, mockUrl }) => {
|
||||
render(<SignInFlow providerId={provider} redirect={true} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test("redirection can't be stopped using an oauth provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="github"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'credentials' provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="credentials"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockCredentialsResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn`
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/credentials/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'email' provider", async () => {
|
||||
render(
|
||||
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockEmailResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn` oauth
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/email/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockEmailResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test("params are propagated to the signin URL when supplied", async () => {
|
||||
let matchedParams = ""
|
||||
const authParams = "foo=bar&bar=foo"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) => {
|
||||
matchedParams = req.url.search
|
||||
return res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(matchedParams).toEqual(`?${authParams}`)
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
|
||||
const errorMsg = "Error when retrieving providers"
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(errorMsg))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
errorMsg
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SignInFlow({
|
||||
providerId,
|
||||
callbackUrl,
|
||||
redirect = true,
|
||||
authorizationParams = {},
|
||||
}) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignIn() {
|
||||
const result = await signIn(
|
||||
providerId,
|
||||
{
|
||||
callbackUrl,
|
||||
redirect,
|
||||
},
|
||||
authorizationParams
|
||||
)
|
||||
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signin-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignIn}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
129
src/client/__tests__/sign-out.test.js
Normal file
129
src/client/__tests__/sign-out.test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
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 { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
const { location } = window
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test("by default it redirects to the current URL if the server did not provide one", async () => {
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
|
||||
})
|
||||
})
|
||||
|
||||
test("it redirects to the URL allowed by the server", async () => {
|
||||
render(<SignOutFlow callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
mockSignOutResponse.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("if url contains a hash during redirection a page reload happens", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockSignOutResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
})
|
||||
})
|
||||
|
||||
test("will broadcast the signout event to other tabs", async () => {
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "signout",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignOutFlow({ callbackUrl, redirect = true }) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignOut() {
|
||||
const result = await signOut({ callbackUrl, redirect })
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signout-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={handleSignOut}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -8,9 +8,15 @@
|
||||
//
|
||||
// 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'
|
||||
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
|
||||
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
|
||||
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,
|
||||
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
|
||||
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
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)
|
||||
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
// 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' }))
|
||||
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)
|
||||
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) {
|
||||
export function useSession(session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
function _useSessionHook(session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
const triggeredByStorageEvent = event === "storage"
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
} 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 }
|
||||
if (clientSession === undefined) {
|
||||
__NEXTAUTH._clientSession = null
|
||||
}
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
triggerEvent: !triggeredByStorageEvent,
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
logger.error("CLIENT_USE_SESSION_ERROR", error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -137,114 +158,112 @@ function _useSessionHook (session) {
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
export async function getCsrfToken(ctx) {
|
||||
return (await _fetchData("csrf", ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
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}`
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
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')
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
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()
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -252,13 +271,18 @@ export async function signOut (options = {}) {
|
||||
// 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 } = {}) {
|
||||
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
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
@@ -269,12 +293,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
await __NEXTAUTH._getSession({ event: "timer" })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider ({ children, session, options }) {
|
||||
export function Provider({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
@@ -290,24 +314,25 @@ export function Provider ({ children, session, options }) {
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
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)
|
||||
logger.error("CLIENT_FETCH_ERROR", path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
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')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
@@ -318,7 +343,7 @@ function _apiBaseUrl () {
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
@@ -328,35 +353,34 @@ function _now () {
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
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) => {
|
||||
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
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
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
|
||||
@@ -368,7 +392,7 @@ export {
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout
|
||||
signOut as signout,
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -390,5 +414,5 @@ export default {
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
signout: signOut,
|
||||
}
|
||||
|
||||
24
src/providers/coinbase.js
Normal file
24
src/providers/coinbase.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Coinbase(options) {
|
||||
return {
|
||||
id: "coinbase",
|
||||
name: "Coinbase",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "wallet:user:email wallet:user:read",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
requestTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://www.coinbase.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.coinbase.com/v2/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.name,
|
||||
email: profile.data.email,
|
||||
image: profile.data.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ const sendVerificationRequest = ({
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", error)
|
||||
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
|
||||
}
|
||||
return resolve()
|
||||
@@ -53,12 +53,11 @@ const sendVerificationRequest = ({
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
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
|
||||
const html = ({ url, site }) => {
|
||||
// Insert invisible space into domains to prevent the
|
||||
// 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 on their email address to sign in.
|
||||
const escapedEmail = `${email.replace(/\./g, "​.")}`
|
||||
// like they are supposed to click it to sign in.
|
||||
const escapedSite = `${site.replace(/\./g, "​.")}`
|
||||
|
||||
// Some simple styling options
|
||||
@@ -73,17 +72,12 @@ const html = ({ url, site, email }) => {
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedSite}</strong>
|
||||
<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>
|
||||
</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">
|
||||
|
||||
20
src/providers/freshbooks.js
Normal file
20
src/providers/freshbooks.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
};
|
||||
}
|
||||
18
src/providers/naver.js
Normal file
18
src/providers/naver.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export default function Naver(options) {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
protection: ["state"],
|
||||
accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
|
||||
authorizationUrl:
|
||||
"https://nid.naver.com/oauth2.0/authorize?response_type=code",
|
||||
profileUrl: "https://openapi.naver.com/v1/nid/me",
|
||||
profile(profile) {
|
||||
return profile.response
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
19
src/providers/onelogin.js
Normal file
19
src/providers/onelogin.js
Normal file
@@ -0,0 +1,19 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export default function WorkOS(options) {
|
||||
const domain = options.domain || 'api.workos.com';
|
||||
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
@@ -10,9 +12,9 @@ export default function WorkOS(options) {
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: 'https://api.workos.com/sso/token/',
|
||||
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
|
||||
profileUrl: 'https://api.workos.com/sso/profile/',
|
||||
accessTokenUrl: `https://${domain}/sso/token`,
|
||||
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/sso/profile`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
|
||||
@@ -4,7 +4,7 @@ export default function Yandex(options) {
|
||||
name: "Yandex",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "login:email login:info",
|
||||
scope: "login:email login:info login:avatar",
|
||||
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: null,
|
||||
image: profile.is_avatar_empty ? null : `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
|
||||
20
src/providers/zoom.js
Normal file
20
src/providers/zoom.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function Zoom(options) {
|
||||
return {
|
||||
id: "zoom",
|
||||
name: "Zoom",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://zoom.us/oauth/token",
|
||||
authorizationUrl: "https://zoom.us/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.zoom.us/v2/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,16 @@ 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
|
||||
@@ -71,6 +81,23 @@ 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({
|
||||
@@ -280,7 +307,9 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
}
|
||||
return res
|
||||
.status(400)
|
||||
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
.end(
|
||||
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,46 +134,47 @@ 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: {
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
httpOnly: true,
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as cookie from './cookie'
|
||||
|
||||
/**
|
||||
* Ensure CSRF Token cookie is set for any subsequent requests.
|
||||
* Used as part of the strateigy for mitigation for CSRF tokens.
|
||||
* Used as part of the strategy 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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
export async function signIn () {
|
||||
export async function signIn() {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -28,10 +28,9 @@ export async function signIn () {
|
||||
* @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(baseUrl)) {
|
||||
return url
|
||||
}
|
||||
export async function redirect(url, baseUrl) {
|
||||
if (url.startsWith("/")) return `${baseUrl}${url}`
|
||||
else if (new URL(url).origin === baseUrl) return url
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ export async function redirect (url, baseUrl) {
|
||||
* @param {object} token JSON Web Token (if enabled)
|
||||
* @return {Promise<object>} Session that will be returned to the client
|
||||
*/
|
||||
export async function session (session) {
|
||||
export async function session(session) {
|
||||
return session
|
||||
}
|
||||
|
||||
@@ -59,6 +58,6 @@ export async function session (session) {
|
||||
* @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) {
|
||||
export async function jwt(token) {
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export default async function oAuthCallback(req) {
|
||||
provider.id,
|
||||
code
|
||||
)
|
||||
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", req.body)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -62,7 +63,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, code)
|
||||
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -74,7 +75,11 @@ 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,
|
||||
@@ -143,11 +148,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.
|
||||
//
|
||||
// Unfortuately, we can't tell which - at least not in a way that works for
|
||||
// Unfortunately, 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, profileData)
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception)
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { OAuth, OAuth2 } from 'oauth'
|
||||
import querystring from 'querystring'
|
||||
import logger from '../../../lib/logger'
|
||||
import { sign as jwtSign } from 'jsonwebtoken'
|
||||
import { OAuth, OAuth2 } from "oauth"
|
||||
import querystring from "querystring"
|
||||
import logger from "../../../lib/logger"
|
||||
import { sign as jwtSign } from "jsonwebtoken"
|
||||
|
||||
/**
|
||||
* @TODO Refactor to remove dependancy on 'oauth' package
|
||||
@@ -9,8 +9,8 @@ import { sign as jwtSign } from 'jsonwebtoken'
|
||||
* would be easier to maintain if all the code was native to next-auth.
|
||||
* @param {import("types/providers").OAuthConfig} provider
|
||||
*/
|
||||
export default function oAuthClient (provider) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
export default function oAuthClient(provider) {
|
||||
if (provider.version?.startsWith("2.")) {
|
||||
// Handle OAuth v2.x
|
||||
const authorizationUrl = new URL(provider.authorizationUrl)
|
||||
const basePath = authorizationUrl.origin
|
||||
@@ -34,9 +34,9 @@ export default function oAuthClient (provider) {
|
||||
provider.accessTokenUrl,
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
provider.version || '1.0',
|
||||
provider.version || "1.0",
|
||||
provider.callbackUrl,
|
||||
provider.encoding || 'HMAC-SHA1'
|
||||
provider.encoding || "HMAC-SHA1"
|
||||
)
|
||||
|
||||
// Promisify get() and getOAuth2AccessToken() for OAuth1
|
||||
@@ -51,40 +51,48 @@ export default function oAuthClient (provider) {
|
||||
})
|
||||
})
|
||||
}
|
||||
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
|
||||
const originalGetOAuth1AccessToken =
|
||||
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthAccessToken = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
originalGetOAuth1AccessToken(
|
||||
...args,
|
||||
(error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
resolve({
|
||||
// TODO: Remove, this is only kept for backward compativility
|
||||
// These are not in the OAuth 1.x spec
|
||||
accessToken: oauth_token,
|
||||
refreshToken: oauth_token_secret,
|
||||
results: params,
|
||||
|
||||
oauth_token,
|
||||
oauth_token_secret,
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
resolve({
|
||||
// TODO: Remove, this is only kept for backward compativility
|
||||
// These are not in the OAuth 1.x spec
|
||||
accessToken: oauth_token,
|
||||
refreshToken: oauth_token_secret,
|
||||
results: params,
|
||||
|
||||
oauth_token,
|
||||
oauth_token_secret,
|
||||
params
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
|
||||
const originalGetOAuthRequestToken =
|
||||
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthRequestToken = (params = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
originalGetOAuthRequestToken(
|
||||
params,
|
||||
(error, oauth_token, oauth_token_secret, params) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve({ oauth_token, oauth_token_secret, params })
|
||||
}
|
||||
resolve({ oauth_token, oauth_token_secret, params })
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
return oauth1Client
|
||||
@@ -104,103 +112,112 @@ export default function oAuthClient (provider) {
|
||||
* @param {import("types/providers").OAuthConfig} provider
|
||||
* @param {string | undefined} codeVerifier
|
||||
*/
|
||||
async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
async function getOAuth2AccessToken(code, provider, codeVerifier) {
|
||||
const url = provider.accessTokenUrl
|
||||
const params = { ...provider.params }
|
||||
const headers = { ...provider.headers }
|
||||
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
|
||||
const codeParam =
|
||||
params.grant_type === "refresh_token" ? "refresh_token" : "code"
|
||||
|
||||
if (!params[codeParam]) { params[codeParam] = code }
|
||||
if (!params[codeParam]) {
|
||||
params[codeParam] = code
|
||||
}
|
||||
|
||||
if (!params.client_id) { params.client_id = provider.clientId }
|
||||
if (!params.client_id) {
|
||||
params.client_id = provider.clientId
|
||||
}
|
||||
|
||||
// For Apple the client secret must be generated on-the-fly.
|
||||
// Using the properties in clientSecret to create a JWT.
|
||||
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') {
|
||||
if (provider.id === "apple" && typeof provider.clientSecret === "object") {
|
||||
const { keyId, teamId, privateKey } = provider.clientSecret
|
||||
const clientSecret = jwtSign({
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: provider.clientId
|
||||
},
|
||||
// Automatically convert \\n into \n if found in private key. If the key
|
||||
// is passed in an environment variable \n can get escaped as \\n
|
||||
privateKey.replace(/\\n/g, '\n'),
|
||||
{ algorithm: 'ES256', keyid: keyId }
|
||||
const clientSecret = jwtSign(
|
||||
{
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
|
||||
aud: "https://appleid.apple.com",
|
||||
sub: provider.clientId,
|
||||
},
|
||||
// Automatically convert \\n into \n if found in private key. If the key
|
||||
// is passed in an environment variable \n can get escaped as \\n
|
||||
privateKey.replace(/\\n/g, "\n"),
|
||||
{ algorithm: "ES256", keyid: keyId }
|
||||
)
|
||||
params.client_secret = clientSecret
|
||||
} else {
|
||||
params.client_secret = provider.clientSecret
|
||||
}
|
||||
|
||||
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
|
||||
|
||||
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
|
||||
// Added as a fix to accomodate change in Twitch OAuth API
|
||||
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
|
||||
// Added as a fix for Reddit Authentication
|
||||
if (provider.id === 'reddit') {
|
||||
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
|
||||
if (!params.redirect_uri) {
|
||||
params.redirect_uri = provider.callbackUrl
|
||||
}
|
||||
|
||||
if (provider.id === 'identity-server4' && !headers.Authorization) {
|
||||
if (!headers["Content-Type"]) {
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
}
|
||||
// Added as a fix to accomodate change in Twitch OAuth API
|
||||
if (!headers["Client-ID"]) {
|
||||
headers["Client-ID"] = provider.clientId
|
||||
}
|
||||
// Added as a fix for Reddit Authentication
|
||||
if (provider.id === "reddit") {
|
||||
headers.Authorization =
|
||||
"Basic " +
|
||||
Buffer.from(provider.clientId + ":" + provider.clientSecret).toString(
|
||||
"base64"
|
||||
)
|
||||
}
|
||||
|
||||
if (provider.id === "identity-server4" && !headers.Authorization) {
|
||||
headers.Authorization = `Bearer ${code}`
|
||||
}
|
||||
|
||||
if (provider.protection.includes('pkce')) {
|
||||
if (provider.protection.includes("pkce")) {
|
||||
params.code_verifier = codeVerifier
|
||||
}
|
||||
|
||||
const postData = querystring.stringify(params)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
'POST',
|
||||
url,
|
||||
headers,
|
||||
postData,
|
||||
null,
|
||||
(error, data, response) => {
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
})
|
||||
accessToken = raw.authed_user.access_token
|
||||
} else {
|
||||
accessToken = raw.access_token
|
||||
}
|
||||
)
|
||||
|
||||
resolve({
|
||||
accessToken,
|
||||
accessTokenExpires: null,
|
||||
refreshToken: raw.refresh_token,
|
||||
idToken: raw.id_token,
|
||||
...raw,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,60 +230,69 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
* @param {string} accessToken
|
||||
* @param {any} results
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
async function getOAuth2(provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
let httpMethod = 'GET'
|
||||
let httpMethod = "GET"
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
headers.Authorization = this.buildAuthHeader(accessToken)
|
||||
|
||||
// Mail.ru & vk.com require 'access_token' as URL request parameter
|
||||
if (['mailru', 'vk'].includes(provider.id)) {
|
||||
if (["mailru", "vk"].includes(provider.id)) {
|
||||
const safeAccessTokenURL = new URL(url)
|
||||
safeAccessTokenURL.searchParams.append('access_token', accessToken)
|
||||
safeAccessTokenURL.searchParams.append("access_token", accessToken)
|
||||
url = safeAccessTokenURL.href
|
||||
}
|
||||
|
||||
// This line is required for Twitch
|
||||
if (provider.id === 'twitch') {
|
||||
headers['Client-ID'] = provider.clientId
|
||||
if (provider.id === "twitch") {
|
||||
headers["Client-ID"] = provider.clientId
|
||||
}
|
||||
accessToken = null
|
||||
}
|
||||
|
||||
if (provider.id === 'bungie') {
|
||||
if (provider.id === "bungie") {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
/** Dropbox requires POST instead of GET
|
||||
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
|
||||
*/
|
||||
if (provider.id === 'dropbox') {
|
||||
httpMethod = 'POST'
|
||||
if (provider.id === "dropbox") {
|
||||
httpMethod = "POST"
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
this._request(
|
||||
httpMethod,
|
||||
url,
|
||||
headers,
|
||||
null,
|
||||
accessToken,
|
||||
(error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(profileData)
|
||||
}
|
||||
resolve(profileData)
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/** Bungie needs special handling */
|
||||
function prepareProfileUrl ({ provider, url, results }) {
|
||||
function prepareProfileUrl({ provider, url, results }) {
|
||||
if (!results.membership_id) {
|
||||
// internal error
|
||||
// @TODO: handle better
|
||||
throw new Error('Expected membership_id to be passed.')
|
||||
throw new Error("Expected membership_id to be passed.")
|
||||
}
|
||||
|
||||
if (!provider.headers?.['X-API-Key']) {
|
||||
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
|
||||
if (!provider.headers?.["X-API-Key"]) {
|
||||
throw new Error(
|
||||
'The Bungie provider requires the X-API-Key option to be present in "headers".'
|
||||
)
|
||||
}
|
||||
|
||||
return url.replace('{membershipId}', results.membership_id)
|
||||
return url.replace("{membershipId}", results.membership_id)
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ export async function handleCallback (req, res) {
|
||||
pkceLength: PKCE_LENGTH,
|
||||
method: PKCE_CODE_CHALLENGE_METHOD
|
||||
})
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, null, { maxAge: 0 }) // remove PKCE after it has been used
|
||||
// remove PKCE after it has been used
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, "", {
|
||||
...cookies.pkceCodeVerifier.options,
|
||||
maxAge: 0
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
|
||||
@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
scope: provider.scope,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope
|
||||
redirect_uri: provider.callbackUrl
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import { h } from "preact" // eslint-disable-line no-unused-vars
|
||||
|
||||
export default function signin ({ csrfToken, providers, callbackUrl, email, error: errorType }) {
|
||||
export default function signin({
|
||||
csrfToken,
|
||||
providers,
|
||||
callbackUrl,
|
||||
email,
|
||||
error: errorType,
|
||||
}) {
|
||||
// We only want to render providers
|
||||
const providersToRender = providers.filter(provider => {
|
||||
if (provider.type === 'oauth' || provider.type === 'email') {
|
||||
const providersToRender = providers.filter((provider) => {
|
||||
if (provider.type === "oauth" || provider.type === "email") {
|
||||
// Always render oauth and email type providers
|
||||
return true
|
||||
} else if (provider.type === 'credentials' && provider.credentials) {
|
||||
} else if (provider.type === "credentials" && provider.credentials) {
|
||||
// Only render credentials type provider if credentials are defined
|
||||
return true
|
||||
}
|
||||
@@ -15,70 +21,93 @@ export default function signin ({ csrfToken, providers, callbackUrl, email, erro
|
||||
})
|
||||
|
||||
const errors = {
|
||||
Signin: 'Try signing with a different account.',
|
||||
OAuthSignin: 'Try signing with a different account.',
|
||||
OAuthCallback: 'Try signing with a different account.',
|
||||
OAuthCreateAccount: 'Try signing with a different account.',
|
||||
EmailCreateAccount: 'Try signing with a different account.',
|
||||
Callback: 'Try signing with a different account.',
|
||||
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
|
||||
EmailSignin: 'Check your email address.',
|
||||
CredentialsSignin: 'Sign in failed. Check the details you provided are correct.',
|
||||
default: 'Unable to sign in.'
|
||||
Signin: "Try signing in with a different account.",
|
||||
OAuthSignin: "Try signing in with a different account.",
|
||||
OAuthCallback: "Try signing in with a different account.",
|
||||
OAuthCreateAccount: "Try signing in with a different account.",
|
||||
EmailCreateAccount: "Try signing in with a different account.",
|
||||
Callback: "Try signing in with a different account.",
|
||||
OAuthAccountNotLinked:
|
||||
"To confirm your identity, sign in with the same account you used originally.",
|
||||
EmailSignin: "Check your email inbox.",
|
||||
CredentialsSignin:
|
||||
"Sign in failed. Check the details you provided are correct.",
|
||||
default: "Unable to sign in.",
|
||||
}
|
||||
|
||||
const error = errorType && (errors[errorType] ?? errors.default)
|
||||
|
||||
return (
|
||||
<div className='signin'>
|
||||
{error &&
|
||||
<div className='error'>
|
||||
<div className="signin">
|
||||
{error && (
|
||||
<div className="error">
|
||||
<p>{error}</p>
|
||||
</div>}
|
||||
{providersToRender.map((provider, i) =>
|
||||
<div key={provider.id} className='provider'>
|
||||
{provider.type === 'oauth' &&
|
||||
<form action={provider.signinUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
|
||||
<button type='submit' className='button'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
|
||||
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
|
||||
<hr />}
|
||||
{provider.type === 'email' &&
|
||||
<form action={provider.signinUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
<label for={`input-email-for-${provider.id}-provider`}>Email</label>
|
||||
<input id={`input-email-for-${provider.id}-provider`} autoFocus type='text' name='email' value={email} placeholder='email@example.com' />
|
||||
<button type='submit'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{provider.type === 'credentials' &&
|
||||
<form action={provider.callbackUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
{Object.keys(provider.credentials).map(credential => {
|
||||
</div>
|
||||
)}
|
||||
{providersToRender.map((provider, i) => (
|
||||
<div key={provider.id} className="provider">
|
||||
{provider.type === "oauth" && (
|
||||
<form action={provider.signinUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
{callbackUrl && (
|
||||
<input type="hidden" name="callbackUrl" value={callbackUrl} />
|
||||
)}
|
||||
<button type="submit" className="button">
|
||||
Sign in with {provider.name}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
{(provider.type === "email" || provider.type === "credentials") &&
|
||||
i > 0 &&
|
||||
providersToRender[i - 1].type !== "email" &&
|
||||
providersToRender[i - 1].type !== "credentials" && <hr />}
|
||||
{provider.type === "email" && (
|
||||
<form action={provider.signinUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
<label for={`input-email-for-${provider.id}-provider`}>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id={`input-email-for-${provider.id}-provider`}
|
||||
autoFocus
|
||||
type="text"
|
||||
name="email"
|
||||
value={email}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<button type="submit">Sign in with {provider.name}</button>
|
||||
</form>
|
||||
)}
|
||||
{provider.type === "credentials" && (
|
||||
<form action={provider.callbackUrl} method="POST">
|
||||
<input type="hidden" name="csrfToken" value={csrfToken} />
|
||||
{Object.keys(provider.credentials).map((credential) => {
|
||||
return (
|
||||
<div key={`input-group-${provider.id}`}>
|
||||
<label
|
||||
for={`input-${credential}-for-${provider.id}-provider`}
|
||||
>{provider.credentials[credential].label || credential}
|
||||
>
|
||||
{provider.credentials[credential].label || credential}
|
||||
</label>
|
||||
<input
|
||||
name={credential}
|
||||
id={`input-${credential}-for-${provider.id}-provider`}
|
||||
type={provider.credentials[credential].type || 'text'}
|
||||
value={provider.credentials[credential].value || ''}
|
||||
placeholder={provider.credentials[credential].placeholder || ''}
|
||||
type={provider.credentials[credential].type || "text"}
|
||||
value={provider.credentials[credential].value || ""}
|
||||
placeholder={
|
||||
provider.credentials[credential].placeholder || ""
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<button type='submit'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{(provider.type === 'email' || provider.type === 'credentials') && ((i + 1) < providersToRender.length) &&
|
||||
<hr />}
|
||||
<button type="submit">Sign in with {provider.name}</button>
|
||||
</form>
|
||||
)}
|
||||
{(provider.type === "email" || provider.type === "credentials") &&
|
||||
i + 1 < providersToRender.length && <hr />}
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default function providers (req, res) {
|
||||
export default function providers(req, res) {
|
||||
const { providers } = req.options
|
||||
|
||||
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
}, {})
|
||||
const result = providers.reduce(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
@@ -8,14 +8,8 @@ import adapterErrorHandler from "../../adapters/error-handler"
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signin(req, res) {
|
||||
const {
|
||||
provider,
|
||||
baseUrl,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks,
|
||||
logger,
|
||||
} = req.options
|
||||
const { provider, baseUrl, basePath, adapter, callbacks, logger } =
|
||||
req.options
|
||||
|
||||
if (!provider.type) {
|
||||
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
|
||||
@@ -46,6 +40,10 @@ 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 account = { id: provider.id, type: "email", providerAccountId: email }
|
||||
|
||||
@@ -3,31 +3,15 @@
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": [
|
||||
"./types"
|
||||
],
|
||||
"next-auth": [
|
||||
"./src/server"
|
||||
],
|
||||
"next-auth/adapters": [
|
||||
"./src/adapters"
|
||||
],
|
||||
"next-auth/client": [
|
||||
"./src/client"
|
||||
],
|
||||
"next-auth/jwt": [
|
||||
"./src/lib/jwt"
|
||||
],
|
||||
"next-auth/providers": [
|
||||
"./src/providers"
|
||||
]
|
||||
"types": ["./types"],
|
||||
"next-auth": ["./src/server"],
|
||||
"next-auth/adapters": ["./src/adapters"],
|
||||
"next-auth/client": ["./src/client"],
|
||||
"next-auth/jwt": ["./src/lib/jwt"],
|
||||
"next-auth/providers": ["./src/providers"]
|
||||
},
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
@@ -44,9 +28,8 @@
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js"
|
||||
"**/*.js",
|
||||
".eslintrc.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
@@ -82,7 +82,7 @@ export interface NextAuthOptions {
|
||||
* signOut: '/auth/signout',
|
||||
* error: '/auth/error',
|
||||
* verifyRequest: '/auth/verify-request',
|
||||
* newUser: null
|
||||
* newUser: '/auth/new-user'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
|
||||
27
types/providers.d.ts
vendored
27
types/providers.d.ts
vendored
@@ -1,5 +1,6 @@
|
||||
import { Profile, TokenSet, User } from "."
|
||||
import { Awaitable, NextApiRequest } from "./internals/utils"
|
||||
import { Options as SMTPConnectionOptions } from 'nodemailer/lib/smtp-connection'
|
||||
|
||||
export type ProviderType = "oauth" | "email" | "credentials"
|
||||
|
||||
@@ -26,7 +27,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
|
||||
headers?: Record<string, any>
|
||||
type: "oauth"
|
||||
version: string
|
||||
scope: string
|
||||
scope: string | string[]
|
||||
params: { grant_type: string }
|
||||
accessTokenUrl: string
|
||||
requestTokenUrl?: string
|
||||
@@ -63,6 +64,7 @@ export type OAuthProviderType =
|
||||
| "Box"
|
||||
| "Bungie"
|
||||
| "Cognito"
|
||||
| "Coinbase"
|
||||
| "Discord"
|
||||
| "Dropbox"
|
||||
| "EVEOnline"
|
||||
@@ -70,6 +72,7 @@ export type OAuthProviderType =
|
||||
| "FACEIT"
|
||||
| "FortyTwo"
|
||||
| "Foursquare"
|
||||
| "Freshbooks"
|
||||
| "FusionAuth"
|
||||
| "GitHub"
|
||||
| "GitLab"
|
||||
@@ -82,8 +85,10 @@ export type OAuthProviderType =
|
||||
| "Mailchimp"
|
||||
| "MailRu"
|
||||
| "Medium"
|
||||
| "Naver"
|
||||
| "Netlify"
|
||||
| "Okta"
|
||||
| "OneLogin"
|
||||
| "Osso"
|
||||
| "Reddit"
|
||||
| "Salesforce"
|
||||
@@ -97,6 +102,7 @@ export type OAuthProviderType =
|
||||
| "WorkOS"
|
||||
| "Yandex"
|
||||
| "Zoho"
|
||||
| "Zoom"
|
||||
|
||||
export type OAuthProvider = (options: Partial<OAuthConfig>) => OAuthConfig
|
||||
|
||||
@@ -118,23 +124,12 @@ interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
|
||||
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
|
||||
}
|
||||
|
||||
export type CredentialsProvider = (
|
||||
options: Partial<CredentialsConfig>
|
||||
) => CredentialsConfig
|
||||
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(
|
||||
options: Partial<CredentialsConfig<C>>
|
||||
) => CredentialsConfig<C>
|
||||
|
||||
export type CredentialsProviderType = "Credentials"
|
||||
|
||||
/** Email Provider */
|
||||
|
||||
export interface EmailConfigServerOptions {
|
||||
host: string
|
||||
port: number
|
||||
auth: {
|
||||
user: string
|
||||
pass: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SendVerificationRequest = (params: {
|
||||
identifier: string
|
||||
url: string
|
||||
@@ -146,7 +141,7 @@ export type SendVerificationRequest = (params: {
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
||||
server: string | EmailConfigServerOptions
|
||||
server: string | SMTPConnectionOptions
|
||||
/** @default "NextAuth <no-reply@example.com>" */
|
||||
from?: string
|
||||
/**
|
||||
|
||||
@@ -19,12 +19,12 @@ Providers.Email({
|
||||
from: "path/from",
|
||||
})
|
||||
|
||||
// $ExpectType CredentialsConfig<{}>
|
||||
// $ExpectType CredentialsConfig<{ username: { label: string; type: string; }; password: { label: string; type: string; }; }>
|
||||
Providers.Credentials({
|
||||
id: "login",
|
||||
name: "account",
|
||||
credentials: {
|
||||
user: {
|
||||
username: {
|
||||
label: "Password",
|
||||
type: "password",
|
||||
},
|
||||
@@ -33,7 +33,7 @@ Providers.Credentials({
|
||||
type: "password",
|
||||
},
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
authorize: async ({ username, password }) => {
|
||||
const user = {
|
||||
/* fetched user */
|
||||
}
|
||||
@@ -152,6 +152,13 @@ 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",
|
||||
@@ -257,3 +264,9 @@ Providers.Zoho({
|
||||
clientId: "foo123",
|
||||
clientSecret: "bar123",
|
||||
})
|
||||
|
||||
// $ExpectType OAuthConfig<Profile>
|
||||
Providers.Freshbooks({
|
||||
clientId: "foo123",
|
||||
clientSecret: "bar123",
|
||||
})
|
||||
|
||||
71
www/docs/adapters/dynamodb.md
Normal file
71
www/docs/adapters/dynamodb.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: dynamodb
|
||||
title: DynamoDB Adapter
|
||||
---
|
||||
|
||||
# DynamoDB
|
||||
|
||||
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
|
||||
|
||||
You need a table with a partition key `pk` and a sort key `sk`. Your table also needs a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. You can set whatever you want as the table name and the billing method.
|
||||
|
||||
You can find the full schema in the table structure section below.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/dynamodb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
You need to pass `DocumentClient` instance from `aws-sdk` to the adapter.
|
||||
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import AWS from "aws-sdk";
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
|
||||
|
||||
AWS.config.update({
|
||||
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY,
|
||||
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY,
|
||||
region: process.env.NEXT_AUTH_AWS_REGION,
|
||||
});
|
||||
|
||||
export default NextAuth({
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
adapter: DynamoDBAdapter(
|
||||
new AWS.DynamoDB.DocumentClient()
|
||||
),
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
|
||||
|
||||
## Schema
|
||||
|
||||
The table respects the single table design pattern. This has many advantages:
|
||||
|
||||
- Only one table to manage, monitor and provision.
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
|
||||
- Only one table needs to be replicated, if you want to go multi-region.
|
||||
|
||||
Here is a schema of the table :
|
||||
|
||||

|
||||
86
www/docs/adapters/fauna.md
Normal file
86
www/docs/adapters/fauna.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
id: fauna
|
||||
title: FaunaDB Adapter
|
||||
---
|
||||
|
||||
# FaunaDB
|
||||
|
||||
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/fauna-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/fauna-adapter
|
||||
```
|
||||
|
||||
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"
|
||||
import Providers from "next-auth/providers"
|
||||
import * as Fauna from "faunadb"
|
||||
import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
|
||||
const client = new Fauna.Client({
|
||||
secret: "secret",
|
||||
scheme: "http",
|
||||
domain: "localhost",
|
||||
port: 8443,
|
||||
})
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FaunaAdapter({ faunaClient: client})
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## 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" })
|
||||
CreateCollection({ name: "users" })
|
||||
CreateCollection({ name: "verification_requests" })
|
||||
CreateIndex({
|
||||
name: "account_by_provider_account_id",
|
||||
source: Collection("accounts"),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ["data", "providerId"] },
|
||||
{ field: ["data", "providerAccountId"] },
|
||||
],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "session_by_token",
|
||||
source: Collection("sessions"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "sessionToken"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "user_by_email",
|
||||
source: Collection("users"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "email"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "verification_request_by_token_and_identifier",
|
||||
source: Collection("verification_requests"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],
|
||||
})
|
||||
```
|
||||
73
www/docs/adapters/firebase.md
Normal file
73
www/docs/adapters/firebase.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: firebase
|
||||
title: Firebase Adapter
|
||||
---
|
||||
|
||||
# Firebase
|
||||
|
||||
This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/firebase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
import firebase from "firebase/app"
|
||||
import "firebase/firestore"
|
||||
|
||||
const firestore = (
|
||||
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
|
||||
).firestore()
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirebaseAdapter(firestore),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
:::tip **From Firebase**
|
||||
|
||||
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
|
||||
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
:::
|
||||
42
www/docs/adapters/overview.md
Normal file
42
www/docs/adapters/overview.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
id: overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
|
||||
The adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters).
|
||||
|
||||
There you can find the following adapters:
|
||||
|
||||
- [`typeorm-legacy`](./typeorm/typeorm-overview)
|
||||
- [`prisma`](./prisma)
|
||||
- [`prisma-legacy`](./prisma-legacy)
|
||||
- [`fauna`](./fauna)
|
||||
- [`dynamodb`](./dynamodb)
|
||||
- [`firebase`](./firebase)
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
|
||||
|
||||
### Editor integration
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @type { import("next-auth/adapters").Adapter } */
|
||||
const MyAdapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
61
www/docs/adapters/pouchdb.md
Normal file
61
www/docs/adapters/pouchdb.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
id: pouchdb
|
||||
title: PouchDB Adapter
|
||||
---
|
||||
|
||||
# PouchDB
|
||||
|
||||
This is the PouchDB Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
Depending on your architecture you can use PouchDB's http adapter to reach any database compliant with the CouchDB protocol (CouchDB, Cloudant, ...) or use any other PouchDB compatible adapter (leveldb, in-memory, ...)
|
||||
|
||||
## Getting Started
|
||||
|
||||
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
|
||||
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/pouchdb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { PouchDBAdapter } from "@next-auth/pouchdb-adapter"
|
||||
import PouchDB from "pouchdb"
|
||||
|
||||
// Setup your PouchDB instance and database
|
||||
PouchDB.plugin(require("pouchdb-adapter-leveldb")) // Any other adapter
|
||||
.plugin(require("pouchdb-find")) // Don't forget the `pouchdb-find` plugin
|
||||
|
||||
const pouchdb = new PouchDB("auth_db", { adapter: "leveldb" })
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: PouchDBAdapter(pouchdb),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
### Memory-First Caching Strategy
|
||||
|
||||
If you need to boost your authentication layer performance, you may use PouchDB's powerful sync features and various adapters, to build a memory-first caching strategy.
|
||||
|
||||
Use an in-memory PouchDB as your main authentication database, and synchronize it with any other persisted PouchDB. You may do a one way, one-off replication at startup from the persisted PouchDB into the in-memory PouchDB, then two-way, continuous, retriable sync.
|
||||
|
||||
This will most likely not increase performance much in a serverless environment due to various reasons such as concurrency, function startup time increases, etc.
|
||||
|
||||
For more details, please see https://pouchdb.com/api.html#sync
|
||||
@@ -1,62 +1,16 @@
|
||||
---
|
||||
id: adapters
|
||||
title: Database Adapters
|
||||
id: prisma-legacy
|
||||
title: Prisma Adapter (Legacy)
|
||||
---
|
||||
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
# Prisma (Legacy)
|
||||
|
||||
You do not need to specify an Adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma Adapter instead of the default TypeORM Adapter, or if you are creating a custom Adapter to connect to a database that is not one of the supported databases.
|
||||
You can also use NextAuth.js with the built-in Adapter for [Prisma](https://www.prisma.io/docs/). This is included in the core `next-auth` package at the moment. The other adapter needs to be installed from its own additional package.
|
||||
|
||||
### Database Schemas
|
||||
|
||||
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
|
||||
|
||||
- [MySQL Schema](/schemas/mysql)
|
||||
- [Postgres Schema](/schemas/postgres)
|
||||
- [Microsoft SQL Server Schema](/schemas/mssql)
|
||||
|
||||
## TypeORM Adapter
|
||||
|
||||
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
|
||||
|
||||
The default Adapter is the TypeORM Adapter, the following configuration options are exactly equivalent.
|
||||
|
||||
```javascript
|
||||
database: {
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.Default({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.TypeORM.Adapter({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
|
||||
|
||||
:::tip
|
||||
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
|
||||
|
||||
However, it should not be enabled against production databases as it may cause data loss if the configured schema does not match the expected schema!
|
||||
:::info
|
||||
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
|
||||
:::
|
||||
|
||||
## Prisma Adapter
|
||||
|
||||
You can also use NextAuth.js with the experimental Adapter for [Prisma 2](https://www.prisma.io/docs/).
|
||||
|
||||
To use this Adapter, you need to install Prisma Client and Prisma CLI:
|
||||
|
||||
```
|
||||
@@ -88,8 +42,9 @@ export default NextAuth({
|
||||
:::tip
|
||||
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
|
||||
:::
|
||||
Schema for the Prisma Adapter
|
||||
|
||||
### Prisma Schema
|
||||
## Setup
|
||||
|
||||
Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
@@ -104,7 +59,7 @@ datasource db {
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
@@ -119,12 +74,11 @@ model Account {
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
@@ -136,7 +90,7 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
@@ -148,36 +102,19 @@ model User {
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
:::note
|
||||
Set the `datasource` option appropriately for your database:
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
@@ -192,10 +129,28 @@ To configure you database to use the new schema (i.e. create tables and columns)
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
To generate a schema in this way with the above example code, you will need to specify your datbase connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
|
||||
|
||||
:::tip
|
||||
If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
let prisma
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient()
|
||||
}
|
||||
prisma = global.prisma
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Custom Models
|
||||
|
||||
You can add properties to the schema and map them to any database column names you wish, but you should not change the base properties or types defined in the example schema.
|
||||
@@ -217,46 +172,3 @@ adapter: Adapters.Prisma.Adapter({
|
||||
})
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
let prisma
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient()
|
||||
}
|
||||
prisma = global.prisma
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
|
||||
|
||||
### Editor integration
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @type { import("next-auth/adapters").Adapter } */
|
||||
const MyAdapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
218
www/docs/adapters/prisma.md
Normal file
218
www/docs/adapters/prisma.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
id: prisma
|
||||
title: Prisma Adapter
|
||||
---
|
||||
|
||||
# Prisma
|
||||
|
||||
You can also use NextAuth.js with the new experimental Adapter for [Prisma](https://www.prisma.io/docs/). This version of the Prisma Adapter is not included in the core `next-auth` package, and must be installed separately.
|
||||
|
||||
:::info
|
||||
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
|
||||
:::
|
||||
|
||||
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
|
||||
|
||||
```
|
||||
npm install @prisma/client @next-auth/prisma-adapter
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
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 { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: PrismaAdapter(prisma),
|
||||
})
|
||||
```
|
||||
|
||||
:::tip
|
||||
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
|
||||
:::
|
||||
Schema for the Prisma Adapter (`@next-auth/prisma-adapter`)
|
||||
|
||||
## Setup
|
||||
|
||||
Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
```json title="schema.prisma"
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
providerType String
|
||||
providerId String
|
||||
providerAccountId String
|
||||
refreshToken String?
|
||||
accessToken String?
|
||||
accessTokenExpires DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([providerId, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
expires DateTime
|
||||
sessionToken String @unique
|
||||
accessToken String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id String @id @default(cuid())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
|
||||
```
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
|
||||
|
||||
## Schema History
|
||||
|
||||
Changes from the original Prisma Adapter
|
||||
|
||||
```diff
|
||||
model Account {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
- compoundId String @unique @map(name: "compound_id")
|
||||
- userId Int @map(name: "user_id")
|
||||
+ userId String
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
- providerType String @map(name: "provider_type")
|
||||
+ providerType String
|
||||
- providerId String @map(name: "provider_id")
|
||||
+ providerId String
|
||||
- providerAccountId String @map(name: "provider_account_id")
|
||||
+ providerAccountId String
|
||||
- refreshToken String? @map(name: "refresh_token")
|
||||
+ refreshToken String?
|
||||
- accessToken String? @map(name: "access_token")
|
||||
+ accessToken String?
|
||||
- accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
+ accessTokenExpires DateTime?
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@index([providerAccountId], name: "providerAccountId")
|
||||
- @@index([providerId], name: "providerId")
|
||||
- @@index([userId], name: "userId")
|
||||
- @@map(name: "accounts")
|
||||
+ @@unique([providerId, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
- userId Int @map(name: "user_id")
|
||||
+ userId String
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
expires DateTime
|
||||
- sessionToken String @unique @map(name: "session_token")
|
||||
+ sessionToken String @unique
|
||||
- accessToken String @unique @map(name: "access_token")
|
||||
+ accessToken String @unique
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
-
|
||||
- @@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
- emailVerified DateTime? @map(name: "email_verified")
|
||||
+ emailVerified DateTime?
|
||||
image String?
|
||||
+ accounts Account[]
|
||||
+ sessions Session[]
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@map(name: "verification_requests")
|
||||
+ @@unique([identifier, token])
|
||||
}
|
||||
```
|
||||
49
www/docs/adapters/typeorm/overview.md
Normal file
49
www/docs/adapters/typeorm/overview.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: typeorm-overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
## TypeORM Adapter
|
||||
|
||||
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use in your project and pass a database connection string to NextAuth.js.
|
||||
|
||||
### Database Schemas
|
||||
|
||||
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
|
||||
|
||||
- [MySQL Schema](./mysql)
|
||||
- [Postgres Schema](./postgres)
|
||||
- [Microsoft SQL Server Schema](./mssql)
|
||||
- [MongoDB](./mongodb)
|
||||
|
||||
The default Adapter is the TypeORM Adapter and the default database type for TypeORM is SQLite, the following configuration options are exactly equivalent.
|
||||
|
||||
```javascript
|
||||
database: {
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.Default({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.TypeORM.Adapter({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
|
||||
|
||||
:::tip
|
||||
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres. This will automatically apply any changes it finds in the entity model, therefore it **should not be enabled against production databases** as it may cause data loss if the configured schema does not match the expected schema!
|
||||
:::
|
||||
@@ -66,11 +66,11 @@ 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.
|
||||
|
||||
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 an 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 `authorization` 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 `authorize` callback and the `profile` 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.
|
||||
@@ -78,10 +78,10 @@ When using NextAuth.js with a database, the User object will be either a user ob
|
||||
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you only want to allow users who already have accounts in the database to sign in, you can check for the existence of a `user.id` property and reject any sign in attempts from accounts that do not have one.
|
||||
:::note
|
||||
Redirects returned by this callback cancel the authentication flow. Only redirect to error pages that, for example, tell the user why they're not allowed to sign in.
|
||||
|
||||
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
|
||||
To redirect to a page after a successful sign in, please use [the `callbackUrl` option](/getting-started/client#specifying-a-callbackurl) or [the redirect callback](/configuration/callbacks#redirect-callback).
|
||||
:::
|
||||
|
||||
## Redirect callback
|
||||
@@ -163,7 +163,7 @@ If you need to persist a large amount of data, you will need to persist it elsew
|
||||
|
||||
## Session callback
|
||||
|
||||
The session callback is called whenever a session is checked. By default, only a subset 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.
|
||||
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
@@ -188,6 +188,8 @@ 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.
|
||||
|
||||
@@ -5,16 +5,20 @@ title: Databases
|
||||
|
||||
NextAuth.js comes with multiple ways of connecting to a database:
|
||||
|
||||
* **TypeORM** (default)<br/>
|
||||
_The TypeORM adapter supports MySQL, Postgres, MsSql, SQLite and MongoDB databases._
|
||||
* **Prisma**<br/>
|
||||
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
|
||||
* **Custom Adapter**<br/>
|
||||
- **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._
|
||||
|
||||
> 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](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
|
||||
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
|
||||
|
||||
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
|
||||
|
||||
@@ -27,7 +31,7 @@ You can specify database credentials as as a connection string or a [TypeORM con
|
||||
The following approaches are exactly equivalent:
|
||||
|
||||
```js
|
||||
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name'
|
||||
database: "mysql://nextauth:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -44,13 +48,14 @@ database: {
|
||||
:::tip
|
||||
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
|
||||
|
||||
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
|
||||
_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_'
|
||||
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
|
||||
|
||||
```
|
||||
|
||||
*…or as a database configuration object:*
|
||||
_…or as a database configuration object:_
|
||||
|
||||
```js
|
||||
database: {
|
||||
@@ -63,6 +68,7 @@ database: {
|
||||
entityPrefix: 'nextauth_'
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -73,15 +79,15 @@ Using SQL to create tables and columns is the recommended way to set up an SQL d
|
||||
|
||||
Check out the links below for SQL you can run to set up a database for NextAuth.js.
|
||||
|
||||
* [MySQL Schema](/schemas/mysql)
|
||||
* [Postgres Schema](/schemas/postgres)
|
||||
- [MySQL Schema](/adapters/typeorm/mysql)
|
||||
- [Postgres Schema](/adapters/typeorm/postgres)
|
||||
|
||||
_If you are running SQLite, MongoDB or a Document database you can skip this step._
|
||||
|
||||
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'
|
||||
database: "mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -122,7 +128,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mysql://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
@@ -133,7 +139,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mariadb://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### Postgres / CockroachDB
|
||||
@@ -144,13 +150,15 @@ Install module:
|
||||
#### Example
|
||||
|
||||
PostgresDB
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:5432/database_name'
|
||||
database: "postgres://username:password@127.0.0.1:5432/database_name"
|
||||
```
|
||||
|
||||
CockroachDB
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:26257/database_name'
|
||||
database: "postgres://username:password@127.0.0.1:26257/database_name"
|
||||
```
|
||||
|
||||
If the node is using Self-signed cert
|
||||
@@ -182,7 +190,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mssql://sa:password@localhost:1433/database_name'
|
||||
database: "mssql://sa:password@localhost:1433/database_name"
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
@@ -193,12 +201,12 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mongodb://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
*SQLite is intended only for development / testing and not for production use.*
|
||||
_SQLite is intended only for development / testing and not for production use._
|
||||
|
||||
Install module:
|
||||
`npm i sqlite3`
|
||||
@@ -206,9 +214,9 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'sqlite://localhost/:memory:'
|
||||
database: "sqlite://localhost/:memory:"
|
||||
```
|
||||
|
||||
## Other databases
|
||||
|
||||
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
|
||||
@@ -66,7 +66,7 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
|
||||
#### Description
|
||||
|
||||
A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
|
||||
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
|
||||
|
||||
@@ -123,27 +123,34 @@ 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.
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
// You can generate a secret be using `openssl rand -base64 64`
|
||||
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,
|
||||
// encryptionKey: "",
|
||||
// decryptionKey = encryptionKey,
|
||||
// decryptionOptions = {
|
||||
// algorithms: ['A256GCM']
|
||||
// },
|
||||
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']
|
||||
},
|
||||
// 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 }) {},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -229,7 +236,7 @@ pages: {
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error', // Error code passed in query string as ?error=
|
||||
verifyRequest: '/auth/verify-request', // (used for check email message)
|
||||
newUser: null // If set, new users will be directed here on first sign in
|
||||
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -309,7 +316,7 @@ By default NextAuth.js uses a database adapter that uses TypeORM and supports My
|
||||
|
||||
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](/schemas/adapters) for more information.
|
||||
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.
|
||||
|
||||
@@ -16,7 +16,7 @@ To add a custom login page, you can use the `pages` option:
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error', // Error code passed in query string as ?error=
|
||||
verifyRequest: '/auth/verify-request', // (used for check email message)
|
||||
newUser: null // If set, new users will be directed here on first sign in
|
||||
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -28,7 +28,7 @@ We purposefully restrict the returned error codes for increased security.
|
||||
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.
|
||||
- **AccessDenied**: Usually occurs, when you restriected access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
|
||||
- **AccessDenied**: Usually occurs, when you restricted access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
|
||||
- **Verification**: Related to the Email provider. The token has expired or has already been used
|
||||
- **Default**: Catch all, will apply, if none of the above matched
|
||||
|
||||
@@ -46,7 +46,7 @@ The following errors are passed as error query parameters to the default or over
|
||||
- **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.
|
||||
- **Default**: Catch all, will apply, if none of the above matched
|
||||
|
||||
|
||||
Example: `/auth/error?error=Default`
|
||||
|
||||
## Theming
|
||||
@@ -186,5 +186,5 @@ signIn('credentials', { username: 'jsmith', password: '1234' })
|
||||
```
|
||||
|
||||
:::tip
|
||||
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
|
||||
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
|
||||
:::
|
||||
|
||||
@@ -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 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)
|
||||
• 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)
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
@@ -254,22 +254,26 @@ providers: [
|
||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const user = (credentials, req) => {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// 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 request object to obtain additional parameters
|
||||
// (i.e., the request IP address)
|
||||
return null
|
||||
}
|
||||
if (user) {
|
||||
// Any user object returned here will be saved in the JSON Web Token
|
||||
async authorize(credentials, req) {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// 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)
|
||||
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
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
// Return null if user data could not be retrieved
|
||||
return null
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ This error occurs when the `useSession()` React Hook has a problem fetching sess
|
||||
|
||||
#### CLIENT_FETCH_ERROR
|
||||
|
||||
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` envionment variable.
|
||||
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` environment variable.
|
||||
|
||||
---
|
||||
|
||||
@@ -63,9 +63,9 @@ The Email authentication provider can only be used if a database is configured.
|
||||
|
||||
The Credentials Provider can only be used if JSON Web Tokens are used for sessions.
|
||||
|
||||
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explictly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
|
||||
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
|
||||
|
||||
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and manged outside of NextAuth.js.
|
||||
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and managed outside of NextAuth.js.
|
||||
|
||||
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
|
||||
|
||||
|
||||
@@ -23,13 +23,11 @@ You can use also NextAuth.js with any database using a custom database adapter,
|
||||
|
||||
### What authentication services does NextAuth.js support?
|
||||
|
||||
|
||||
<p>NextAuth.js includes built-in support for signing in with
|
||||
{Object.values(require("../providers.json")).sort().join(", ")}.
|
||||
(See also: <a href="/configuration/providers">Providers</a>)
|
||||
</p>
|
||||
|
||||
|
||||
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
|
||||
|
||||
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.
|
||||
@@ -58,7 +56,6 @@ NextAuth.js is designed as a secure, confidential client and implements a server
|
||||
|
||||
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?
|
||||
|
||||
Yes! Check out the [TypeScript docs](/getting-started/typescript)
|
||||
@@ -83,9 +80,9 @@ If you are using a database with NextAuth.js, you can still explicitly enable JS
|
||||
|
||||
### Should I use a database?
|
||||
|
||||
* 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 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).
|
||||
- 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?
|
||||
|
||||
@@ -93,16 +90,15 @@ Managed database solutions for MySQL, Postgres and MongoDB (and compatible datab
|
||||
|
||||
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/)).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
## Security
|
||||
|
||||
### I think I've found a security problem, what should I do?
|
||||
|
||||
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 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 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.
|
||||
|
||||
### What is the disclosure policy for NextAuth.js?
|
||||
|
||||
@@ -165,14 +161,14 @@ Ultimately if your request is not accepted or is not actively in development, yo
|
||||
|
||||
---
|
||||
|
||||
## JSON Web Tokens
|
||||
## JSON Web Tokens
|
||||
|
||||
### Does NextAuth.js use JSON Web Tokens?
|
||||
|
||||
NextAuth.js supports both database session tokens and JWT session tokens.
|
||||
|
||||
* If a database is specified, database session tokens will be used by default.
|
||||
* If no database is specified, JWT session tokens will be used by default.
|
||||
- If a database is specified, database session tokens will be used by default.
|
||||
- If no database is specified, JWT session tokens will be used by default.
|
||||
|
||||
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
|
||||
|
||||
@@ -180,33 +176,33 @@ You can also choose to use JSON Web Tokens as session tokens with using a databa
|
||||
|
||||
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.
|
||||
|
||||
* Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
|
||||
- Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
|
||||
|
||||
* JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
|
||||
- JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
|
||||
|
||||
* You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
|
||||
- You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
|
||||
|
||||
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in an server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
|
||||
- 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?
|
||||
|
||||
* 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.
|
||||
- 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.
|
||||
|
||||
Shorter session expiry times are used when using JSON Web Tokens as session tokens to allow sessions to be invalidated sooner and simplify this problem.
|
||||
|
||||
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
|
||||
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
|
||||
- As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
|
||||
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
|
||||
|
||||
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
|
||||
- Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
|
||||
|
||||
Even if appropriately configured, information stored in an encrypted JWT should not be assumed to be impossible to decrypt at some point - e.g. due to the discovery of a defect or advances in technology.
|
||||
|
||||
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 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 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.
|
||||
|
||||
@@ -214,11 +210,11 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
|
||||
|
||||
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).
|
||||
|
||||
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
|
||||
- JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
|
||||
|
||||
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
|
||||
- If encryption is enabled by setting `jwt: { encryption: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
|
||||
|
||||
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
|
||||
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
|
||||
|
||||
NextAuth.js will generate keys for you, but this will generate a warning at start up.
|
||||
|
||||
@@ -228,14 +224,14 @@ Using explicit public/private keys for signing is strongly recommended.
|
||||
|
||||
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
|
||||
|
||||
* [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
|
||||
* [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
|
||||
* [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
|
||||
* [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
|
||||
* [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
|
||||
- [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
|
||||
- [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
|
||||
- [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
|
||||
- [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
|
||||
- [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
|
||||
|
||||
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)
|
||||
- [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)
|
||||
|
||||
@@ -22,15 +22,15 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
|
||||
:::tip
|
||||
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
|
||||
|
||||
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
|
||||
You can use the [session callback](/configuration/callbacks#session-callback) to customize the session object returned to the client if you need to return additional data in the session object.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## useSession()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
- Client Side: **Yes**
|
||||
- Server Side: No
|
||||
|
||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||
|
||||
@@ -39,12 +39,12 @@ It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { useSession } from 'next-auth/client'
|
||||
import { useSession } from "next-auth/client"
|
||||
|
||||
export default function Component() {
|
||||
const [ session, loading ] = useSession()
|
||||
const [session, loading] = useSession()
|
||||
|
||||
if(session) {
|
||||
if (session) {
|
||||
return <p>Signed in as {session.user.email}</p>
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function Component() {
|
||||
|
||||
## getSession()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
- Client Side: **Yes**
|
||||
- Server Side: **Yes**
|
||||
|
||||
NextAuth.js provides a `getSession()` method which can be called client or server side to return a session.
|
||||
|
||||
@@ -75,7 +75,7 @@ async function myFunction() {
|
||||
#### Server Side Example
|
||||
|
||||
```js
|
||||
import { getSession } from 'next-auth/client'
|
||||
import { getSession } from "next-auth/client"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
@@ -94,8 +94,8 @@ The tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-r
|
||||
|
||||
## getCsrfToken()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
- Client Side: **Yes**
|
||||
- Server Side: **Yes**
|
||||
|
||||
The `getCsrfToken()` method returns the current Cross Site Request Forgery Token (CSRF Token) required to make POST requests (e.g. for signing in and signing out).
|
||||
|
||||
@@ -113,7 +113,7 @@ async function myFunction() {
|
||||
#### Server Side Example
|
||||
|
||||
```js
|
||||
import { getCsrfToken } from 'next-auth/client'
|
||||
import { getCsrfToken } from "next-auth/client"
|
||||
|
||||
export default async (req, res) => {
|
||||
const csrfToken = await getCsrfToken({ req })
|
||||
@@ -126,8 +126,8 @@ export default async (req, res) => {
|
||||
|
||||
## getProviders()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
- Client Side: **Yes**
|
||||
- Server Side: **Yes**
|
||||
|
||||
The `getProviders()` method returns the list of providers currently configured for sign in.
|
||||
|
||||
@@ -140,11 +140,11 @@ 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/client"
|
||||
|
||||
export default async (req, res) => {
|
||||
const providers = await getProviders()
|
||||
console.log('Providers', providers)
|
||||
console.log("Providers", providers)
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
@@ -157,8 +157,8 @@ Unlike `getSession()` and `getCsrfToken()`, when calling `getProviders()` server
|
||||
|
||||
## signIn()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
- Client Side: **Yes**
|
||||
- Server Side: No
|
||||
|
||||
Using the `signIn()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF Tokens for you automatically when signing in with email.
|
||||
|
||||
@@ -167,20 +167,18 @@ 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/client"
|
||||
|
||||
export default () => (
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
)
|
||||
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/client"
|
||||
|
||||
export default () => (
|
||||
<button onClick={() => signIn('google')}>Sign in with Google</button>
|
||||
<button onClick={() => signIn("google")}>Sign in with Google</button>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -189,10 +187,10 @@ 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/client"
|
||||
|
||||
export default ({ email }) => (
|
||||
<button onClick={() => signIn('email', { email })}>Sign in with Email</button>
|
||||
<button onClick={() => signIn("email", { email })}>Sign in with Email</button>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -204,11 +202,11 @@ You can specify a different `callbackUrl` by specifying it as the second argumen
|
||||
|
||||
e.g.
|
||||
|
||||
* `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
|
||||
- `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
- `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). 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 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 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.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
@@ -234,8 +232,8 @@ e.g.
|
||||
error: string | undefined
|
||||
/**
|
||||
* HTTP status code,
|
||||
* hints the kind of error that happened.
|
||||
*/
|
||||
* hints the kind of error that happened.
|
||||
*/
|
||||
status: number
|
||||
/**
|
||||
* `true` if the signin was successful
|
||||
@@ -258,34 +256,32 @@ 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("auth0", null, { login_hint: "info@example.com" })` *hints the e-mail address to the provider*
|
||||
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to reauthenticate_
|
||||
- `signIn("auth0", null, { login_hint: "info@example.com" })` _hints the e-mail address to the provider_
|
||||
|
||||
:::note
|
||||
You can also set these parameters through [`provider.authorizationParams`](/configuration/providers#oauth-provider-options).
|
||||
:::
|
||||
|
||||
:::note
|
||||
The following parameters are always overridden server-side: `redirect_uri`, `scope`, `state`
|
||||
The following parameters are always overridden server-side: `redirect_uri`, `state`
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## signOut()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
- Client Side: **Yes**
|
||||
- Server Side: No
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
It reloads the page in the browser when complete.
|
||||
|
||||
```js
|
||||
import { signOut } from 'next-auth/client'
|
||||
import { signOut } from "next-auth/client"
|
||||
|
||||
export default () => (
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
)
|
||||
export default () => <button onClick={() => signOut()}>Sign out</button>
|
||||
```
|
||||
|
||||
#### Specifying a callbackUrl
|
||||
@@ -294,7 +290,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). 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 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 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.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
@@ -315,9 +311,9 @@ Using the supplied React `<Provider>` allows instances of `useSession()` to shar
|
||||
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`.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
import { Provider } from "next-auth/client"
|
||||
|
||||
export default function App ({ Component, pageProps }) {
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider session={pageProps.session}>
|
||||
<Component {...pageProps} />
|
||||
@@ -328,25 +324,44 @@ export default function App ({ Component, pageProps }) {
|
||||
|
||||
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.
|
||||
|
||||
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"
|
||||
|
||||
...
|
||||
|
||||
export async function getServerSideProps(ctx) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(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).
|
||||
|
||||
### 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 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 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.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
import { Provider } from "next-auth/client"
|
||||
|
||||
export default function App ({ Component, pageProps }) {
|
||||
export default function App({ Component, 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
|
||||
<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
|
||||
}}
|
||||
>
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
@@ -358,7 +373,7 @@ 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 `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
|
||||
:::
|
||||
|
||||
#### Client Max Age
|
||||
@@ -367,7 +382,7 @@ The `clientMaxAge` option is the maximum age a session data can be on the client
|
||||
|
||||
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.
|
||||
|
||||
If set to any value other than zero, it specifies in seconds the maxium age of session data on the client before the `useSession()` hook will call the server again to sync 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).
|
||||
|
||||
@@ -384,5 +399,76 @@ If set to any value other than zero, it specifies in seconds how often the clien
|
||||
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
|
||||
|
||||
:::note
|
||||
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
|
||||
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **\_app.js** in Next.js applications.
|
||||
:::
|
||||
|
||||
## Alternatives
|
||||
|
||||
### 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.
|
||||
|
||||
```js title="pages/admin.jsx"
|
||||
export default function AdminDashboard() {
|
||||
const [session] = useSession()
|
||||
// session is always non-null inside this page, all the way down the React tree.
|
||||
return "Some super secret dashboard"
|
||||
}
|
||||
|
||||
AdminDashboard.auth = true
|
||||
```
|
||||
|
||||
```jsx title="pages/_app.jsx"
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider session={pageProps.session}>
|
||||
{Component.auth ? (
|
||||
<Auth>
|
||||
<Component {...pageProps} />
|
||||
</Auth>
|
||||
) : (
|
||||
<Component {...pageProps} />
|
||||
)}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Auth({ children }) {
|
||||
const [session, loading] = useSession()
|
||||
const isUser = !!session?.user
|
||||
React.useEffect(() => {
|
||||
if (loading) return // Do nothing while loading
|
||||
if (!isUser) signIn() // If not authenticated, force log in
|
||||
}, [isUser, loading])
|
||||
|
||||
if (isUser) {
|
||||
return children
|
||||
}
|
||||
|
||||
// Session is being fetched, or no user.
|
||||
// If no user, useEffect() will redirect.
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
```
|
||||
|
||||
It can be easily be extended/modified to support something like an options object for role based authentication on pages. An example:
|
||||
|
||||
```jsx title="pages/admin.jsx"
|
||||
AdminDashboard.auth = {
|
||||
role: "admin",
|
||||
loading: <AdminLoadingSkeleton />,
|
||||
unauthorized: "/login-with-different-user", // redirect to this url
|
||||
}
|
||||
```
|
||||
|
||||
Because of how \_app is done, it won't unnecessarily contact the /api/auth/session endpoint for pages that do not require auth.
|
||||
|
||||
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`.
|
||||
|
||||
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.
|
||||
|
||||
@@ -5,10 +5,10 @@ title: Example Code
|
||||
|
||||
## Get started with NextAuth.js
|
||||
|
||||
The example code below describes to add authentication to a Next.js app.
|
||||
The example code below describes how to add authentication to a Next.js app.
|
||||
|
||||
:::tip
|
||||
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.now.sh](https://next-auth-example.now.sh)
|
||||
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.vercel.app](https://next-auth-example.vercel.app)
|
||||
:::
|
||||
|
||||
### Add API route
|
||||
|
||||
@@ -45,7 +45,7 @@ providers: [
|
||||
|
||||
:::tip
|
||||
|
||||
You can convert your Apple key to a single line to use it in a environment variable.
|
||||
You can convert your Apple key to a single line to use it in an environment variable.
|
||||
|
||||
**Mac**
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ title: Azure Active Directory B2C
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
|
||||
https://docs.microsoft.com/azure/active-directory-b2c/tutorial-create-tenant
|
||||
|
||||
## Options
|
||||
|
||||
@@ -28,7 +28,7 @@ You can override any of the options to suit your own use case.
|
||||
- When asked for a redirection URL, use http://localhost:3000/api/auth/callback/azure-ad-b2c
|
||||
- Create a new secret and remember / copy its value immediately, it will disappear.
|
||||
|
||||
In `.env.local` create the follwing entries:
|
||||
In `.env.local` create the following entries:
|
||||
|
||||
```
|
||||
AZURE_CLIENT_ID=<copy Application (client) ID here>
|
||||
|
||||
38
www/docs/providers/coinbase.md
Normal file
38
www/docs/providers/coinbase.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: coinbase
|
||||
title: Coinbase
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developers.coinbase.com/api/v2
|
||||
|
||||
## Configuration
|
||||
|
||||
https://www.coinbase.com/settings/api
|
||||
|
||||
## Options
|
||||
|
||||
The **Coinbase Provider** comes with a set of default options:
|
||||
|
||||
- [Coinbase Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/coinbase.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Coinbase({
|
||||
clientId: process.env.COINBASE_CLIENT_ID,
|
||||
clientSecret: process.env.COINBASE_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
This Provider template has a 2 hour access token to it. A refresh token is also returned.
|
||||
:::
|
||||
@@ -83,7 +83,7 @@ See the [callbacks documentation](/configuration/callbacks) for more information
|
||||
|
||||
You can specify more than one credentials provider by specifying a unique `id` for each one.
|
||||
|
||||
You can also use them in conjuction with other provider options.
|
||||
You can also use them in conjunction with other provider options.
|
||||
|
||||
As with all providers, the order you specify them is the order they are displayed on the sign in page.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ The Email provider can be used in conjunction with (or instead of) one or more O
|
||||
|
||||
### How it works
|
||||
|
||||
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
|
||||
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used within that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
|
||||
|
||||
If someone provides the email address of an _existing account_ when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
|
||||
|
||||
@@ -187,7 +187,7 @@ const html = ({ url, site, email }) => {
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
||||
33
www/docs/providers/freshbooks.md
Normal file
33
www/docs/providers/freshbooks.md
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
id: freshbooks
|
||||
title: Freshbooks
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://www.freshbooks.com/api/authenticating-with-oauth-2-0-on-the-new-freshbooks-api
|
||||
|
||||
## Configuration
|
||||
|
||||
https://my.freshbooks.com/#/developer
|
||||
|
||||
## Options
|
||||
|
||||
The Freshbooks Provider comes with a set of default options:
|
||||
|
||||
https://www.freshbooks.com/api/start
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Freshbooks({
|
||||
clientId: process.env.FRESHBOOKS_CLIENT_ID,
|
||||
clientSecret: process.env.FRESHBOOKS_CLIENT_SECRET,
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
@@ -34,7 +34,7 @@ providers: [
|
||||
```
|
||||
|
||||
:::warning
|
||||
Google only provide the Refresh Token to an application the first time a user signs in.
|
||||
Google only provides Refresh Token to an application the first time a user signs in.
|
||||
|
||||
To force Google to re-issue a Refresh Token, the user needs to remove the application from their account and sign in again:
|
||||
https://myaccount.google.com/permissions
|
||||
|
||||
@@ -46,5 +46,5 @@ Email address is not returned by the Instagram API.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrock](https://ngrok.com/docs).
|
||||
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrok](https://ngrok.com/docs).
|
||||
:::
|
||||
|
||||
@@ -19,7 +19,7 @@ From the Auth tab get the client ID and client secret. On the same tab, add redi
|
||||
|
||||
The **LinkedIn Provider** comes with a set of default options:
|
||||
|
||||
- [LinkedIn Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/linked-in.js)
|
||||
- [LinkedIn Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/linkedin.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
|
||||
34
www/docs/providers/naver.md
Normal file
34
www/docs/providers/naver.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: naver
|
||||
title: Naver
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developers.naver.com/docs/login/overview/overview.md
|
||||
|
||||
## Configuration
|
||||
|
||||
https://developers.naver.com/docs/login/api/api.md
|
||||
|
||||
## Options
|
||||
|
||||
The **Naver Provider** comes with a set of default options:
|
||||
|
||||
- [Naver Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/naver.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Naver({
|
||||
clientId: process.env.NAVER_CLIENT_ID,
|
||||
clientSecret: process.env.NAVER_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
31
www/docs/providers/onelogin.md
Normal file
31
www/docs/providers/onelogin.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
id: onelogin
|
||||
title: OneLogin
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developers.onelogin.com/openid-connect
|
||||
|
||||
## Options
|
||||
|
||||
The **OneLogin Provider** comes with a set of default options:
|
||||
|
||||
- [OneLogin Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/onelogin.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.OneLogin({
|
||||
clientId: process.env.ONELOGIN_CLIENT_ID,
|
||||
clientSecret: process.env.ONELOGIN_CLIENT_SECRET,
|
||||
domain: process.env.ONELOGIN_DOMAIN
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
@@ -7,6 +7,10 @@ title: WorkOS
|
||||
|
||||
https://workos.com/docs/sso/guide
|
||||
|
||||
## Configuration
|
||||
|
||||
https://dashboard.workos.com
|
||||
|
||||
## Options
|
||||
|
||||
The **WorkOS Provider** comes with a set of default options:
|
||||
@@ -22,10 +26,87 @@ import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.WorkOS({
|
||||
clientId: process.env.WORKOS_ID,
|
||||
clientSecret: process.env.WORKOS_SECRET,
|
||||
domain: process.env.WORKOS_DOMAIN
|
||||
clientId: process.env.WORKOS_CLIENT_ID,
|
||||
clientSecret: process.env.WORKOS_API_KEY,
|
||||
}),
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
WorkOS is not an identity provider itself, but, rather, a bridge to multiple single sign-on (SSO) providers. As a result, we need to make some additional changes to authenticate users using WorkOS.
|
||||
|
||||
In order to sign a user in using WorkOS, we need to specify which WorkOS Connection to use. A common way to do this is to collect the user's email address and extract the domain.
|
||||
|
||||
This can be done using a custom login page.
|
||||
|
||||
To add a custom login page, you can use the `pages` option:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
}
|
||||
```
|
||||
|
||||
We can then add a custom login page that displays an input where the user can enter their email address. We then extract the domain from the user's email address and pass it to the `authorizationParams` parameter on the `signIn` function:
|
||||
|
||||
```jsx title="pages/auth/signin.js"
|
||||
import { getProviders, signIn } from 'next-auth/client'
|
||||
|
||||
export default function SignIn({ providers }) {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(providers).map((provider) => {
|
||||
if (provider.id === 'workos') {
|
||||
return (
|
||||
<div key={provider.id}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder="Email"
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
signIn(provider.id, undefined, {
|
||||
domain: email.split('@')[1],
|
||||
})
|
||||
}
|
||||
>
|
||||
Sign in with SSO
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={provider.id}>
|
||||
<button onClick={() => signIn(provider.id)}>
|
||||
Sign in with {provider.name}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// This is the recommended way for Next.js 9.3 or newer
|
||||
export async function getServerSideProps(context){
|
||||
const providers = await getProviders()
|
||||
return {
|
||||
props: { providers }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// If older than Next.js 9.3
|
||||
SignIn.getInitialProps = async () => {
|
||||
return {
|
||||
providers: await getProviders()
|
||||
}
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
34
www/docs/providers/zoom.md
Normal file
34
www/docs/providers/zoom.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: zoom
|
||||
title: Zoom
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://marketplace.zoom.us/docs/guides/auth/oauth
|
||||
|
||||
## Configuration
|
||||
|
||||
https://marketplace.zoom.us
|
||||
|
||||
## Options
|
||||
|
||||
The **Zoom Provider** comes with a set of default options:
|
||||
|
||||
- [Zoom Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/zoom.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Zoom({
|
||||
clientId: process.env.ZOOM_CLIENT_ID,
|
||||
clientSecret: process.env.ZOOM_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -83,6 +83,18 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
|
||||
|
||||
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and API routes behind that authentication, etc.
|
||||
|
||||
### [Introduction to NextAuth.js](https://www.youtube.com/watch?v=npZsJxWntJM)
|
||||
|
||||
This is an introductory video to NextAuth.js for beginners. In this video, it is explained how to set up authentication in a few easy steps and add different configurations to make it more robust and secure.
|
||||
|
||||
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
|
||||
|
||||
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.
|
||||
|
||||
### [How to Authenticate Next.js Apps with Twitter & NextAuth.js](https://spacejelly.dev/posts/how-to-authenticate-next-js-apps-with-twitter-nextauth-js/)
|
||||
|
||||
Learn how to add Twitter authentication and login to a Next.js app both clientside and serverside with NextAuth.js.
|
||||
|
||||
### [Using NextAuth.js with Magic links](https://dev.to/narciero/using-nextauth-js-with-magic-links-df4)
|
||||
|
||||
Learn how to use [Magic](https://magic.link) link authentication with [NextAuth.js](https://next-auth.js.org) to enable passwordless authentication without a database.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user