mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
44 Commits
next-auth@
...
v4.0.0-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acc9393560 | ||
|
|
6911dd9267 | ||
|
|
cff153bd80 | ||
|
|
a2e5afa162 | ||
|
|
53e5e37948 | ||
|
|
8ff4b26014 | ||
|
|
2c35aa27f9 | ||
|
|
2833b661bd | ||
|
|
6c1a0ec620 | ||
|
|
988c9912b1 | ||
|
|
a225324d4f | ||
|
|
3a48b8e467 | ||
|
|
fb50b54466 | ||
|
|
fa89431573 | ||
|
|
3383857715 | ||
|
|
bbc2d9b538 | ||
|
|
d10bd9beba | ||
|
|
c1c866f664 | ||
|
|
86ff89e296 | ||
|
|
dd12181378 | ||
|
|
47c17a89ae | ||
|
|
c07fe1b9a7 | ||
|
|
abaa5aed65 | ||
|
|
ca0ed1e2a8 | ||
|
|
ed345346db | ||
|
|
5ac1db741a | ||
|
|
0c17af969e | ||
|
|
ea9b6e37a9 | ||
|
|
960bc1e9c0 | ||
|
|
d29e3e9c9d | ||
|
|
a388b44d0b | ||
|
|
b6a3a72db4 | ||
|
|
edcb10a823 | ||
|
|
2acabe19e0 | ||
|
|
a6f5f4c184 | ||
|
|
9fa93e3b5e | ||
|
|
cb4342fdda | ||
|
|
5f717b3914 | ||
|
|
d09a45ec7c | ||
|
|
930f58eba3 | ||
|
|
c20b7f2930 | ||
|
|
e418cddd96 | ||
|
|
111e7aabdf | ||
|
|
a113ef6fab |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +1,2 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
/__tests__/ @lluia
|
||||
|
||||
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 💚
|
||||
|
||||
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Release Flow
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,37 +11,66 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Tests
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
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: Build
|
||||
run: npm run build
|
||||
- name: Run tests
|
||||
run: npm test -- --coverage --verbose
|
||||
run: npm test -- --coverage --verbose && npm run test:types
|
||||
- name: Coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
directory: ./coverage
|
||||
fail_ci_if_error: false
|
||||
- name: Build
|
||||
run: npm run build
|
||||
release:
|
||||
name: Release
|
||||
needs: test
|
||||
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: Release
|
||||
- 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 }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,6 +40,8 @@ src/providers/index.js
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
/react.js
|
||||
/react.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
@@ -61,4 +63,4 @@ app/yarn.lock
|
||||
/prisma/migrations
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/coverage
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
# npx pretty-quick --staged
|
||||
|
||||
@@ -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,40 +80,27 @@ 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 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
|
||||
|
||||
@@ -121,9 +115,8 @@ Some recommended scopes are:
|
||||
- **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
|
||||
|
||||
|
||||
112
README.md
112
README.md
@@ -38,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.
|
||||
|
||||
@@ -48,96 +48,116 @@ 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 practices for safeguarding user data
|
||||
- Uses Cross-Site Request Forgery (CSRF) Tokens on POST routes (sign in, sign out)
|
||||
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
|
||||
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
|
||||
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
|
||||
- Auto-generates symmetric signing and encryption keys for developer convenience
|
||||
- Features tab/window syncing and session polling to support short lived sessions
|
||||
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org)
|
||||
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
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 documentation.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
## Example
|
||||
|
||||
### Add API Route
|
||||
|
||||
```javascript
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
// pages/api/auth/[...nextauth].js
|
||||
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
|
||||
})
|
||||
```
|
||||
|
||||
### Add React Component
|
||||
### Add React Hook
|
||||
|
||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||
|
||||
```javascript
|
||||
import {
|
||||
useSession, signIn, signOut
|
||||
} from 'next-auth/client'
|
||||
import { useSession, signIn, signOut } from "next-auth/react"
|
||||
|
||||
export default function Component() {
|
||||
const [ session, loading ] = useSession()
|
||||
if(session) {
|
||||
return <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
const { data: session } = 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Acknowledgements
|
||||
### Share/configure session state
|
||||
|
||||
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps }
|
||||
}) {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ 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 publicly.
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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,18 @@
|
||||
import { signIn } from 'next-auth/client'
|
||||
import { signIn } from "next-auth/react"
|
||||
|
||||
export default function AccessDenied () {
|
||||
export default function AccessDenied() {
|
||||
return (
|
||||
<>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
href="/api/auth/signin"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>You must be signed in to view this page
|
||||
>
|
||||
You must be signed in to view this page
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import Link from 'next/link'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import styles from './header.module.css'
|
||||
import Link from "next/link"
|
||||
import { signIn, signOut, useSession } from "next-auth/react"
|
||||
import styles from "./header.module.css"
|
||||
|
||||
// The approach used in this component shows how to built a sign in and sign out
|
||||
// component that works on pages which support both client and server side
|
||||
// rendering, and avoids any flash incorrect content on initial page load.
|
||||
export default function Header () {
|
||||
const [session, loading] = useSession()
|
||||
export default function Header() {
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
return (
|
||||
<header>
|
||||
<noscript>
|
||||
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
|
||||
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
|
||||
</noscript>
|
||||
<div className={styles.signedInStatus}>
|
||||
<p
|
||||
className={`nojs-show ${
|
||||
!session && loading ? styles.loading : styles.loaded
|
||||
!session && status === "loading" ? styles.loading : styles.loaded
|
||||
}`}
|
||||
>
|
||||
{!session && (
|
||||
@@ -25,7 +25,7 @@ export default function Header () {
|
||||
You are not signed in
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
href="/api/auth/signin"
|
||||
className={styles.buttonPrimary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -50,7 +50,7 @@ export default function Header () {
|
||||
<strong>{session.user.email || session.user.name}</strong>
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signout'
|
||||
href="/api/auth/signout"
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -66,42 +66,42 @@ export default function Header () {
|
||||
<nav>
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/'>
|
||||
<Link href="/">
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/client'>
|
||||
<Link href="/client">
|
||||
<a>Client</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/server'>
|
||||
<Link href="/server">
|
||||
<a>Server</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected'>
|
||||
<Link href="/protected">
|
||||
<a>Protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected-ssr'>
|
||||
<Link href="/protected-ssr">
|
||||
<a>Protected(SSR)</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/api-example'>
|
||||
<Link href="/api-example">
|
||||
<a>API</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/credentials'>
|
||||
<Link href="/credentials">
|
||||
<a>Credentials</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/email'>
|
||||
<Link href="/email">
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = {
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
"next-auth$": path.join(process.cwd(), "next-auth/server"),
|
||||
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
|
||||
"next-auth/react$": path.join(process.cwd(), "next-auth/client/react"),
|
||||
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
|
||||
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
|
||||
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"next": "^11.0.1",
|
||||
"nodemailer": "^6.6.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
import { Provider } from "next-auth/client"
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import "./styles.css"
|
||||
|
||||
// Use the <Provider> to improve performance and allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
export default function App({ Component, pageProps }) {
|
||||
export default function App({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) {
|
||||
return (
|
||||
<Provider
|
||||
// Provider options are not required but can be useful in situations where
|
||||
// you have a short session maxAge time. Shown here with default values.
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0,
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import Auth0Provider from "next-auth/providers/auth0"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// cookies: {
|
||||
@@ -45,11 +41,11 @@ export default NextAuth({
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// protection: ["pkce", "state"],
|
||||
// checks: ["pkce", "state"],
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: "pkce",
|
||||
checks: ["pkce"],
|
||||
}),
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
@@ -79,13 +75,4 @@ export default NextAuth({
|
||||
},
|
||||
debug: false,
|
||||
theme: "auto",
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
import jwt from 'next-auth/jwt'
|
||||
import jwt from "next-auth/jwt"
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
// This is an example of to protect an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
import { getSession } from "next-auth/react"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
|
||||
res.send({
|
||||
content:
|
||||
"This is protected content. You can access this content because you are signed in.",
|
||||
})
|
||||
} else {
|
||||
res.send({ error: 'You must be sign in to view the protected content on this page.' })
|
||||
res.send({
|
||||
error: "You must be sign in to view the protected content on this page.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This is an example of how to access a session from an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
import { getSession } from "next-auth/react"
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
import * as React from "react"
|
||||
import { signIn, signOut, useSession } from "next-auth/react"
|
||||
import Layout from "components/layout"
|
||||
|
||||
export default function Page () {
|
||||
export default function Page() {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const handleLogin = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signIn('credentials', options)
|
||||
return signIn("credentials", options)
|
||||
}
|
||||
const response = await signIn('credentials', options)
|
||||
const response = await signIn("credentials", options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
@@ -21,18 +21,22 @@ export default function Page () {
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
const { data: session } = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<span className="spacing">Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button>
|
||||
<br />
|
||||
<span className="spacing">No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button>
|
||||
<br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
<pre style={{ background: "#eee", padding: 16 }}>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -40,14 +44,24 @@ export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials login</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
|
||||
<span className="spacing">Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: "password" })}>
|
||||
Login
|
||||
</button>
|
||||
<br />
|
||||
<span className="spacing">No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: "password" })}>
|
||||
Login
|
||||
</button>
|
||||
<br />
|
||||
<span className="spacing">No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: "" })}>
|
||||
Login
|
||||
</button>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
<pre style={{ background: "#eee", padding: 16 }}>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
import * as React from "react"
|
||||
import { signIn, signOut, useSession } from "next-auth/react"
|
||||
import Layout from "components/layout"
|
||||
|
||||
export default function Page () {
|
||||
export default function Page() {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const [email, setEmail] = React.useState('')
|
||||
const [email, setEmail] = React.useState("")
|
||||
|
||||
const handleChange = (event) => {
|
||||
setEmail(event.target.value)
|
||||
@@ -15,9 +15,9 @@ export default function Page () {
|
||||
event.preventDefault()
|
||||
|
||||
if (options.redirect) {
|
||||
return signIn('email', options)
|
||||
return signIn("email", options)
|
||||
}
|
||||
const response = await signIn('email', options)
|
||||
const response = await signIn("email", options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
@@ -29,18 +29,22 @@ export default function Page () {
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
const { data: session } = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<span className="spacing">Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button>
|
||||
<br />
|
||||
<span className="spacing">No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button>
|
||||
<br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
<pre style={{ background: "#eee", padding: 16 }}>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -48,20 +52,29 @@ export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email login</h1>
|
||||
<label className='spacing'>
|
||||
Email address:{' '}
|
||||
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
|
||||
</label><br />
|
||||
<label className="spacing">
|
||||
Email address:{" "}
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
value={email}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</label>
|
||||
<br />
|
||||
<form onSubmit={handleLogin({ redirect: true, email })}>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
<span className="spacing">Default:</span>
|
||||
<button type="submit">Sign in with Email</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin({ redirect: false, email })}>
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
<span className="spacing">No redirect:</span>
|
||||
<button type="submit">Sign in with Email</button>
|
||||
</form>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
<pre style={{ background: "#eee", padding: 16 }}>
|
||||
{JSON.stringify(response, null, 2)}
|
||||
</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,37 +1,47 @@
|
||||
// This is an example of how to protect content using server rendering
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
import { getSession } from "next-auth/react"
|
||||
import Layout from "../components/layout"
|
||||
import AccessDenied from "../components/access-denied"
|
||||
|
||||
export default function Page ({ content, session }) {
|
||||
export default function Page({ content, session }) {
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
if (!session) {
|
||||
return (
|
||||
<Layout>
|
||||
<AccessDenied />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
<p>
|
||||
<strong>{content}</strong>
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context)
|
||||
let content = null
|
||||
|
||||
if (session) {
|
||||
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
|
||||
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
|
||||
const options = { headers: { cookie: context.req.headers.cookie } }
|
||||
const res = await fetch(`${hostname}/api/examples/protected`, options)
|
||||
const json = await res.json()
|
||||
if (json.content) { content = json.content }
|
||||
if (json.content) {
|
||||
content = json.content
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
content
|
||||
}
|
||||
content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
import { useState, useEffect } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
import Layout from "../components/layout"
|
||||
|
||||
export default function Page () {
|
||||
const [session, loading] = useSession()
|
||||
export default function Page() {
|
||||
const { status } = useSession({
|
||||
required: true,
|
||||
})
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(() => {
|
||||
if (status === "loading") return
|
||||
const fetchData = async () => {
|
||||
const res = await fetch('/api/examples/protected')
|
||||
const res = await fetch("/api/examples/protected")
|
||||
const json = await res.json()
|
||||
if (json.content) { setContent(json.content) }
|
||||
if (json.content) {
|
||||
setContent(json.content)
|
||||
}
|
||||
}
|
||||
fetchData()
|
||||
}, [session])
|
||||
}, [status])
|
||||
|
||||
// When rendering client side don't display anything until loading is complete
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
if (status === "loading") return <Layout>Loading...</Layout>
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
<p>
|
||||
<strong>{content}</strong>
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import { getSession } from "next-auth/react"
|
||||
import Layout from "../components/layout"
|
||||
|
||||
export default function Page () {
|
||||
export default function Page() {
|
||||
// As this page uses Server Side Rendering, the `session` will be already
|
||||
// populated on render without needing to go through a loading stage.
|
||||
// This is possible because of the shared context configured in `_app.js` that
|
||||
@@ -11,27 +11,31 @@ export default function Page () {
|
||||
<Layout>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
|
||||
This page uses the universal <strong>getSession()</strong> method in{" "}
|
||||
<strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
|
||||
support Server Side Rendering with authentication.
|
||||
Using <strong>getSession()</strong> in{" "}
|
||||
<strong>getServerSideProps()</strong> is the recommended approach if you
|
||||
need to support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require client side JavaScript.
|
||||
The advantage of Server Side Rendering is this page does not require
|
||||
client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to render.
|
||||
The disadvantage of Server Side Rendering is that this page is slower to
|
||||
render.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps (context) {
|
||||
export async function getServerSideProps(context) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(context)
|
||||
}
|
||||
session: await getSession(context),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// https://nextjs.org/docs/basic-features/supported-browsers-features
|
||||
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
|
||||
presets: [["@babel/preset-env", { targets: { node: "12" } }]],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-transform-runtime",
|
||||
@@ -12,7 +12,10 @@ module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
test: ["../src/client/**"],
|
||||
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { ie: "11" } }],
|
||||
["@babel/preset-react", { runtime: "automatic" }],
|
||||
],
|
||||
},
|
||||
{
|
||||
test: ["../src/server/pages/**"],
|
||||
@@ -20,14 +23,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: ["../src/**/*.test.js"],
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
],
|
||||
presets: [["@babel/preset-react", { runtime: "automatic" }]],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ const path = require("path")
|
||||
|
||||
const MODULE_ENTRIES = {
|
||||
SERVER: "index",
|
||||
CLIENT: "client",
|
||||
REACT: "react",
|
||||
PROVIDERS: "providers",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
@@ -13,12 +13,16 @@ const MODULE_ENTRIES = {
|
||||
// Building submodule entries
|
||||
|
||||
const BUILD_TARGETS = {
|
||||
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
|
||||
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
|
||||
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
|
||||
[`${MODULE_ENTRIES.SERVER}.js`]:
|
||||
"module.exports = require('./dist/server').default\n",
|
||||
[`${MODULE_ENTRIES.REACT}.js`]:
|
||||
"module.exports = require('./dist/client/react').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]:
|
||||
"module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]:
|
||||
"module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]:
|
||||
"module.exports = require('./dist/lib/errors').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
@@ -32,7 +36,7 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
|
||||
const TYPES_TARGETS = [
|
||||
`${MODULE_ENTRIES.SERVER}.d.ts`,
|
||||
`${MODULE_ENTRIES.CLIENT}.d.ts`,
|
||||
`${MODULE_ENTRIES.REACT}-client.d.ts`,
|
||||
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
@@ -43,7 +47,10 @@ const TYPES_TARGETS = [
|
||||
TYPES_TARGETS.forEach((target) => {
|
||||
fs.copy(
|
||||
path.resolve("types", target),
|
||||
path.join(process.cwd(), target),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
target.startsWith("react-client") ? "react.d.ts" : target
|
||||
),
|
||||
(err) => {
|
||||
if (err) throw err
|
||||
console.log(`[build-types] copying "${target}" to root folder`)
|
||||
|
||||
@@ -8,4 +8,5 @@ module.exports = {
|
||||
collectCoverageFrom: ["!client/__tests__/**"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
coverageDirectory: "../coverage",
|
||||
testEnvironment: "jsdom",
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
34632
package-lock.json
generated
34632
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
113
package.json
113
package.json
@@ -22,8 +22,7 @@
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./react": "./dist/client/react.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
@@ -32,7 +31,7 @@
|
||||
"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",
|
||||
@@ -42,7 +41,9 @@
|
||||
"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",
|
||||
"website": "cd www && npm run start"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -52,8 +53,8 @@
|
||||
"providers.d.ts",
|
||||
"adapters.js",
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"react.js",
|
||||
"react.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"jwt.js",
|
||||
@@ -62,73 +63,67 @@
|
||||
],
|
||||
"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",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"@babel/runtime": "^7.14.6",
|
||||
"futoin-hkdf": "^1.3.3",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"nodemailer": "^6.4.16",
|
||||
"oauth": "^0.9.15",
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0"
|
||||
"pkce-challenge": "^2.2.0",
|
||||
"preact": "^10.5.13",
|
||||
"preact-render-to-string": "^5.1.19"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
"react-dom": "^16.13.1 || ^17"
|
||||
"nodemailer": "^6.6.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
"mysql": "^2.18.1",
|
||||
"mssql": "^6.2.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.16.1"
|
||||
"nodemailer": "^6.6.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@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",
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@babel/cli": "^7.14.5",
|
||||
"@babel/core": "^7.14.6",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
|
||||
"@babel/plugin-transform-runtime": "^7.14.5",
|
||||
"@babel/preset-env": "^7.14.7",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"@types/nodemailer": "^6.4.2",
|
||||
"@types/react": "^17.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"autoprefixer": "^10.2.6",
|
||||
"babel-jest": "^27.0.5",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"dtslint": "^4.0.8",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"conventional-changelog-conventionalcommits": "4.6.0",
|
||||
"cssnano": "^5.0.6",
|
||||
"dtslint": "^4.1.0",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard-with-typescript": "^20.0.0",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"eslint-plugin-jest": "^24.3.6",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"husky": "^6.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"msw": "^0.28.2",
|
||||
"next": "^10.0.5",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"pretty-quick": "^3.1.0",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3",
|
||||
"jest": "^27.0.5",
|
||||
"msw": "^0.30.0",
|
||||
"next": "^11.0.1",
|
||||
"postcss-cli": "^8.3.1",
|
||||
"postcss-nested": "^5.0.5",
|
||||
"prettier": "^2.3.1",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"typescript": "^4.3.4",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"prettier": {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as TypeORM from "./typeorm"
|
||||
import * as Prisma from "./prisma"
|
||||
|
||||
export { TypeORM, Prisma }
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma,
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"
|
||||
@@ -1,9 +0,0 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
export {
|
||||
TypeORMLegacyAdapter as Adapter,
|
||||
Models,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
@@ -1,9 +1,7 @@
|
||||
import { useState } from "react"
|
||||
import { rest } from "msw"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import { Provider, useSession } from ".."
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { SessionProvider, useSession } from "../react"
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
@@ -18,6 +16,22 @@ afterAll(() => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
test("it won't allow to fetch the session in isolation without a session context", () => {
|
||||
function App() {
|
||||
useSession()
|
||||
return null
|
||||
}
|
||||
|
||||
jest.spyOn(console, "error")
|
||||
console.error.mockImplementation(() => {})
|
||||
|
||||
expect(() => render(<App />)).toThrow(
|
||||
"useSession must be wrapped in a SessionProvider"
|
||||
)
|
||||
|
||||
console.error.mockRestore()
|
||||
})
|
||||
|
||||
test("fetches the session once and re-uses it for different consumers", async () => {
|
||||
const sessionRouteCall = jest.fn()
|
||||
|
||||
@@ -30,6 +44,9 @@ test("fetches the session once and re-uses it for different consumers", async ()
|
||||
|
||||
render(<ProviderFlow />)
|
||||
|
||||
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
|
||||
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
|
||||
|
||||
@@ -40,25 +57,44 @@ test("fetches the session once and re-uses it for different consumers", async ()
|
||||
})
|
||||
})
|
||||
|
||||
test("when there's an existing session, it won't initialize as loading", async () => {
|
||||
const sessionRouteCall = jest.fn()
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
sessionRouteCall()
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
})
|
||||
)
|
||||
|
||||
render(<ProviderFlow session={mockSession} />)
|
||||
|
||||
expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent(
|
||||
"loading"
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
|
||||
"loading"
|
||||
)
|
||||
|
||||
expect(sessionRouteCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
function ProviderFlow({ options = {} }) {
|
||||
return (
|
||||
<>
|
||||
<Provider options={options}>
|
||||
<SessionConsumer />
|
||||
<SessionConsumer testId="2" />
|
||||
</Provider>
|
||||
</>
|
||||
<SessionProvider {...options}>
|
||||
<SessionConsumer />
|
||||
<SessionConsumer testId="2" />
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionConsumer({ testId = 1 }) {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
if (loading) return <span>loading</span>
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
return (
|
||||
<div data-testid={`session-consumer-${testId}`}>
|
||||
{JSON.stringify(session)}
|
||||
{status === "loading" ? "loading" : JSON.stringify(session)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockCSRFToken } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { getCsrfToken } from ".."
|
||||
import { getCsrfToken } from "../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
@@ -78,11 +78,10 @@ test("when the fetch fails it'll throw a client fetch error", async () => {
|
||||
|
||||
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")
|
||||
)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
path: "csrf",
|
||||
error: new SyntaxError("Unexpected token s in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockProviders } from "./helpers/mocks"
|
||||
import { getProviders } from ".."
|
||||
import { getProviders } from "../react"
|
||||
import logger from "../../lib/logger"
|
||||
import { rest } from "msw"
|
||||
|
||||
@@ -56,11 +56,10 @@ test("when failing to fetch the providers, it'll log the error", async () => {
|
||||
|
||||
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")
|
||||
)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
path: "providers",
|
||||
error: new SyntaxError("Unexpected token s in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { rest } from "msw"
|
||||
import { server, mockSession } from "./helpers/mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from ".."
|
||||
import { getSession } from "../react"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
@@ -70,11 +70,10 @@ test("if there's an error fetching the session, it should log it", async () => {
|
||||
|
||||
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")
|
||||
)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
path: "session",
|
||||
error: new SyntaxError("Unexpected token S in JSON at position 0"),
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./helpers/mocks"
|
||||
import { signIn } from ".."
|
||||
import { signIn } from "../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
@@ -250,11 +250,10 @@ test("when it fails to fetch the providers, it redirected back to signin page",
|
||||
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
errorMsg
|
||||
)
|
||||
expect(logger.error).toBeCalledWith("CLIENT_FETCH_ERROR", {
|
||||
error: "Error when retrieving providers",
|
||||
path: "providers",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSignOutResponse } from "./helpers/mocks"
|
||||
import { signOut } from ".."
|
||||
import { signOut } from "../react"
|
||||
import { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./helpers/utils"
|
||||
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
// Note about signIn() and signOut() methods:
|
||||
//
|
||||
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
|
||||
// instead of HTTP as redirect URLs on other domains are not returned to
|
||||
// requests made using the fetch API in the browser, and we need to ask the API
|
||||
// to return the response as a JSON object (the end point still defaults to
|
||||
// returning an HTTP response with a redirect for non-JavaScript clients).
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
createContext,
|
||||
createElement,
|
||||
} from "react"
|
||||
import _logger, { proxyLogger } from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
// 1. An empty value is legitimate when the code is being invoked client side as
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
/** @type {import("types/internals/client").NextAuthConfig} */
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ||
|
||||
process.env.NEXTAUTH_URL ||
|
||||
process.env.VERCEL_URL
|
||||
).baseUrl,
|
||||
basePathServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
|
||||
).basePath,
|
||||
keepAlive: 0,
|
||||
clientMaxAge: 0,
|
||||
// Properties starting with _ are used for tracking internal app state
|
||||
_clientLastSync: 0,
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
// Fetch new session data but tell it to not to fire another event to
|
||||
// avoid an infinite loop.
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
|
||||
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
/** @type {import("types/internals/client").SessionContext} */
|
||||
const SessionContext = createContext()
|
||||
|
||||
export function useSession(session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook(session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
useEffect(() => {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === "storage"
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = _now()
|
||||
const clientSession = __NEXTAUTH._clientSession
|
||||
|
||||
// Updates triggered by a storage event *always* trigger an update and we
|
||||
// always update if we don't have any value for the current session state.
|
||||
if (!triggeredByStorageEvent && clientSession !== undefined) {
|
||||
if (clientMaxAge === 0 && triggredByEvent !== true) {
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it.
|
||||
return
|
||||
} else if (clientMaxAge > 0 && clientSession === null) {
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (
|
||||
clientMaxAge > 0 &&
|
||||
currentTime < clientLastSync + clientMaxAge
|
||||
) {
|
||||
// If the session freshness is within clientMaxAge then don't request
|
||||
// it again on this call (avoids too many invokations).
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession === undefined) {
|
||||
__NEXTAUTH._clientSession = null
|
||||
}
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = _now()
|
||||
|
||||
// If this call was invoked via a storage event (i.e. another window) then
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent,
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
// if a session exists at least once.
|
||||
__NEXTAUTH._clientSession = newClientSessionData
|
||||
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_USE_SESSION_ERROR", error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
})
|
||||
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken(ctx) {
|
||||
return (await _fetchData("csrf", ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
|
||||
if (!(provider in providers)) {
|
||||
return window.location.replace(
|
||||
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === "credentials"
|
||||
const isEmail = providers[provider].type === "email"
|
||||
const isSupportingReturn = isCredentials || isEmail
|
||||
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Method to set options. The documented way is to use the provider, but this
|
||||
// method is being left in as an alternative, that will be helpful if/when we
|
||||
// expose a vanilla JavaScript version that doesn't depend on React.
|
||||
export function setOptions({
|
||||
baseUrl,
|
||||
basePath,
|
||||
clientMaxAge,
|
||||
keepAlive,
|
||||
} = {}) {
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
clearTimeout(__NEXTAUTH._clientSyncTimer)
|
||||
}
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: "timer" })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
{ value: useSession(session) },
|
||||
children
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow _fetchData to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
|
||||
try {
|
||||
const baseUrl = await _apiBaseUrl()
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${baseUrl}/${path}`, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_FETCH_ERROR", path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl() {
|
||||
if (typeof window === "undefined") {
|
||||
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive(onReceive) {
|
||||
if (typeof window === "undefined") return
|
||||
window.addEventListener("storage", async (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/client").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post(message) {
|
||||
if (typeof localStorage === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases. These should be removed in a newer release, as it only
|
||||
// creates problems for bundlers and adds confusion to users. TypeScript declarations
|
||||
// will provide sufficient help when importing
|
||||
export {
|
||||
setOptions as options,
|
||||
getSession as session,
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout,
|
||||
}
|
||||
|
||||
export default {
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
getProviders,
|
||||
useSession,
|
||||
signIn,
|
||||
signOut,
|
||||
Provider,
|
||||
/* Deprecated / unsupported features below this line */
|
||||
// Use setOptions() set options globally in the app.
|
||||
setOptions,
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases.
|
||||
options: setOptions,
|
||||
session: getSession,
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut,
|
||||
}
|
||||
385
src/client/react.js
vendored
Normal file
385
src/client/react.js
vendored
Normal file
@@ -0,0 +1,385 @@
|
||||
// Note about signIn() and signOut() methods:
|
||||
//
|
||||
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
|
||||
// instead of HTTP as redirect URLs on other domains are not returned to
|
||||
// requests made using the fetch API in the browser, and we need to ask the API
|
||||
// to return the response as a JSON object (the end point still defaults to
|
||||
// returning an HTTP response with a redirect for non-JavaScript clients).
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
import * as React from "react"
|
||||
import _logger, { proxyLogger } from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
// 1. An empty value is legitimate when the code is being invoked client side as
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
/** @type {import("types/internals/react").NextAuthConfig} */
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ||
|
||||
process.env.NEXTAUTH_URL ||
|
||||
process.env.VERCEL_URL
|
||||
).baseUrl,
|
||||
basePathServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
|
||||
).basePath,
|
||||
_lastSync: 0,
|
||||
_session: undefined,
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
/** @type {import("types/internals/react").SessionContext} */
|
||||
const SessionContext = React.createContext()
|
||||
|
||||
export function useSession(options = {}) {
|
||||
const value = React.useContext(SessionContext)
|
||||
|
||||
if (process.env.NODE_ENV !== "production" && !value) {
|
||||
throw new Error("useSession must be wrapped in a SessionProvider")
|
||||
}
|
||||
|
||||
const { required, onUnauthenticated } = options
|
||||
|
||||
const requiredAndNotLoading = required && value.status === "unauthenticated"
|
||||
|
||||
React.useEffect(() => {
|
||||
if (requiredAndNotLoading) {
|
||||
const url = `/api/auth/signin?${new URLSearchParams({
|
||||
error: "SessionRequired",
|
||||
callbackUrl: window.location.href,
|
||||
})}`
|
||||
if (onUnauthenticated) onUnauthenticated()
|
||||
else window.location.replace(url)
|
||||
}
|
||||
}, [requiredAndNotLoading, onUnauthenticated])
|
||||
|
||||
if (requiredAndNotLoading) {
|
||||
return { data: value.data, status: "loading" }
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.broadcast ?? true) {
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken(ctx) {
|
||||
const response = await _fetchData("csrf", ctx)
|
||||
return response?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
|
||||
if (!(provider in providers)) {
|
||||
return window.location.replace(
|
||||
`${baseUrl}/signin?${new URLSearchParams({ callbackUrl })}`
|
||||
)
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === "credentials"
|
||||
const isEmail = providers[provider].type === "email"
|
||||
const isSupportingReturn = isCredentials || isEmail
|
||||
|
||||
const signInUrl = `${baseUrl}/${
|
||||
isCredentials ? "callback" : "signin"
|
||||
}/${provider}`
|
||||
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/** @param {import("types/react-client").SessionProviderProps} props */
|
||||
export function SessionProvider(props) {
|
||||
const { children, baseUrl, basePath, staleTime = 0 } = props
|
||||
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
|
||||
/**
|
||||
* If session was `null`, there was an attempt to fetch it,
|
||||
* but it failed, but we still treat it as a valid initial value.
|
||||
*/
|
||||
const hasInitialSession = props.session !== undefined
|
||||
|
||||
/** If session was passed, initialize as already synced */
|
||||
__NEXTAUTH._lastSync = hasInitialSession ? _now() : 0
|
||||
|
||||
const [session, setSession] = React.useState(() => {
|
||||
if (hasInitialSession) __NEXTAUTH._session = props.session
|
||||
return props.session
|
||||
})
|
||||
|
||||
/** If session was passed, initialize as not loading */
|
||||
const [loading, setLoading] = React.useState(!hasInitialSession)
|
||||
|
||||
React.useEffect(() => {
|
||||
__NEXTAUTH._getSession = async ({ event } = {}) => {
|
||||
try {
|
||||
const storageEvent = event === "storage"
|
||||
// We should always update if we don't have a client session yet
|
||||
// or if there are events from other tabs/windows
|
||||
if (storageEvent || __NEXTAUTH._session === undefined) {
|
||||
__NEXTAUTH._lastSync = _now()
|
||||
__NEXTAUTH._session = await getSession({
|
||||
broadcast: !storageEvent,
|
||||
})
|
||||
setSession(__NEXTAUTH._session)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it
|
||||
(staleTime === 0 && !event) ||
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a "stroage" event
|
||||
// event anyway)
|
||||
(staleTime > 0 && __NEXTAUTH._session === null) ||
|
||||
// Bail out early if the client session is not stale yet
|
||||
(staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// An event or session staleness occurred, update the client session.
|
||||
__NEXTAUTH._lastSync = _now()
|
||||
__NEXTAUTH._session = await getSession()
|
||||
setSession(__NEXTAUTH._session)
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_SESSION_ERROR", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
}, [staleTime])
|
||||
|
||||
React.useEffect(() => {
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
// Fetch new session data but tell it to not to fire another event to
|
||||
// avoid an infinite loop.
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
const unsubscribe = broadcast.receive(
|
||||
async () => await __NEXTAUTH._getSession({ event: "storage" })
|
||||
)
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
// Set up visibility change
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
const visibilityHandler = () => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
|
||||
}
|
||||
document.addEventListener("visibilitychange", visibilityHandler, false)
|
||||
return () =>
|
||||
document.removeEventListener("visibilitychange", visibilityHandler, false)
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const { refetchInterval } = props
|
||||
// Set up polling
|
||||
if (refetchInterval) {
|
||||
const refetchIntervalTimer = setInterval(async () => {
|
||||
if (__NEXTAUTH._session) {
|
||||
await __NEXTAUTH._getSession({ event: "poll" })
|
||||
}
|
||||
}, refetchInterval * 1000)
|
||||
return () => clearInterval(refetchIntervalTimer)
|
||||
}
|
||||
}, [props.refetchInterval])
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
data: session,
|
||||
status: loading
|
||||
? "loading"
|
||||
: session
|
||||
? "authenticated"
|
||||
: "unauthenticated",
|
||||
}),
|
||||
[session, loading]
|
||||
)
|
||||
|
||||
return (
|
||||
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow _fetchData to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
|
||||
try {
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${_apiBaseUrl()}/${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", {
|
||||
error,
|
||||
path,
|
||||
...(req ? { header: req.headers } : {}),
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl() {
|
||||
if (typeof window === "undefined") {
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import("types/internals/react").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive(onReceive) {
|
||||
const handler = (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/react").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
}
|
||||
window.addEventListener("storage", handler)
|
||||
return () => window.removeEventListener("storage", handler)
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post(message) {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -32,67 +32,3 @@ export class OAuthCallbackError extends UnknownError {
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {
|
||||
name = "GetUserByProviderAccountIdError"
|
||||
}
|
||||
|
||||
export class UpdateUserError extends UnknownError {
|
||||
name = "UpdateUserError"
|
||||
}
|
||||
|
||||
export class DeleteUserError extends UnknownError {
|
||||
name = "DeleteUserError"
|
||||
}
|
||||
|
||||
export class LinkAccountError extends UnknownError {
|
||||
name = "LinkAccountError"
|
||||
}
|
||||
|
||||
export class UnlinkAccountError extends UnknownError {
|
||||
name = "UnlinkAccountError"
|
||||
}
|
||||
|
||||
export class CreateSessionError extends UnknownError {
|
||||
name = "CreateSessionError"
|
||||
}
|
||||
|
||||
export class GetSessionError extends UnknownError {
|
||||
name = "GetSessionError"
|
||||
}
|
||||
|
||||
export class UpdateSessionError extends UnknownError {
|
||||
name = "UpdateSessionError"
|
||||
}
|
||||
|
||||
export class DeleteSessionError extends UnknownError {
|
||||
name = "DeleteSessionError"
|
||||
}
|
||||
|
||||
export class CreateVerificationRequestError extends UnknownError {
|
||||
name = "CreateVerificationRequestError"
|
||||
}
|
||||
|
||||
export class GetVerificationRequestError extends UnknownError {
|
||||
name = "GetVerificationRequestError"
|
||||
}
|
||||
|
||||
export class DeleteVerificationRequestError extends UnknownError {
|
||||
name = "DeleteVerificationRequestError"
|
||||
}
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import { UnknownError } from "./errors"
|
||||
|
||||
/** Makes sure that error is always serializable */
|
||||
function formatError(o) {
|
||||
if (o instanceof Error && !(o instanceof UnknownError)) {
|
||||
return { message: o.message, stack: o.stack, name: o.name }
|
||||
}
|
||||
if (o?.error) {
|
||||
o.error = formatError(o.error)
|
||||
o.message = o.message ?? o.error.message
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
/** @type {import("types").LoggerInstance} */
|
||||
const _logger = {
|
||||
error(code, ...message) {
|
||||
error(code, metadata) {
|
||||
metadata = formatError(metadata)
|
||||
console.error(
|
||||
`[next-auth][error][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
...message
|
||||
metadata.message,
|
||||
metadata
|
||||
)
|
||||
},
|
||||
warn(code, ...message) {
|
||||
warn(code) {
|
||||
console.warn(
|
||||
`[next-auth][warn][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
|
||||
...message
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
debug(code, ...message) {
|
||||
debug(code, metadata) {
|
||||
if (!process?.env?._NEXTAUTH_DEBUG) return
|
||||
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
|
||||
console.log(`[next-auth][debug][${code.toLowerCase()}]`, metadata)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,31 +62,19 @@ export function proxyLogger(logger = _logger, basePath) {
|
||||
|
||||
const clientLogger = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, ...message) => {
|
||||
_logger[level](code, ...message) // Log on client as usual
|
||||
clientLogger[level] = (code, metadata) => {
|
||||
_logger[level](code, metadata) // Logs to console
|
||||
|
||||
if (level === "error") {
|
||||
metadata = formatError(metadata)
|
||||
}
|
||||
metadata.client = true
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({
|
||||
level,
|
||||
code,
|
||||
message: JSON.stringify(
|
||||
message.map((m) => {
|
||||
if (m instanceof Error) {
|
||||
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
return { name: m.name, message: m.message, stack: m.stack }
|
||||
}
|
||||
return m
|
||||
})
|
||||
),
|
||||
})
|
||||
const body = new URLSearchParams({ level, code, ...metadata })
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(url, body)
|
||||
}
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
})
|
||||
return fetch(url, { method: "POST", body, keepalive: true })
|
||||
}
|
||||
}
|
||||
return clientLogger
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function Apple(options) {
|
||||
privateKey: null,
|
||||
keyId: null,
|
||||
},
|
||||
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default function AzureADB2C(options) {
|
||||
const tenant = options.tenantId ? options.tenantId : "common"
|
||||
const { tenantName, primaryUserFlow } = options
|
||||
const authorizeUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`
|
||||
const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`
|
||||
|
||||
return {
|
||||
id: "azure-ad-b2c",
|
||||
@@ -9,14 +11,29 @@ export default function AzureADB2C(options) {
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
|
||||
profileUrl: "https://graph.microsoft.com/v1.0/me/",
|
||||
profile(profile) {
|
||||
accessTokenUrl: tokenUrl,
|
||||
requestTokenUrl: tokenUrl,
|
||||
authorizationUrl: `${authorizeUrl}?response_type=code+id_token&response_mode=query`,
|
||||
profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
|
||||
idToken: true,
|
||||
profile: (profile) => {
|
||||
let name = ''
|
||||
|
||||
if (profile.name) {
|
||||
// B2C "Display Name"
|
||||
name = profile.name
|
||||
} else if (profile.given_name && profile.family_name) {
|
||||
// B2C "Given Name" & "Surname"
|
||||
name = `${profile.given_name} ${profile.family_name}`
|
||||
} else if (profile.given_name) {
|
||||
// B2C "Given Name"
|
||||
name = `${profile.given_name}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.userPrincipalName,
|
||||
name,
|
||||
id: profile.oid,
|
||||
email: profile.emails[0]
|
||||
}
|
||||
},
|
||||
...options,
|
||||
|
||||
24
src/providers/azure-ad.js
Normal file
24
src/providers/azure-ad.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function AzureAD(options) {
|
||||
const tenant = options.tenantId ?? 'common'
|
||||
|
||||
return {
|
||||
id: 'azure-ad',
|
||||
name: 'Azure Active Directory',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: {
|
||||
grant_type: 'authorization_code'
|
||||
},
|
||||
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
|
||||
profileUrl: 'https://graph.microsoft.com/v1.0/me/',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.userPrincipalName
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* import { signIn } from "next-auth/react"
|
||||
* ...
|
||||
* <button onClick={() => signIn("dropbox")}>
|
||||
* Sign in
|
||||
@@ -29,26 +29,26 @@
|
||||
*/
|
||||
export default function Dropbox(options) {
|
||||
return {
|
||||
id: 'dropbox',
|
||||
name: 'Dropbox',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'account_info.read',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
|
||||
id: "dropbox",
|
||||
name: "Dropbox",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "account_info.read",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.dropboxapi.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
|
||||
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||
"https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code",
|
||||
profileUrl: "https://api.dropboxapi.com/2/users/get_current_account",
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
name: profile.name.display_name,
|
||||
email: profile.email,
|
||||
image: profile.profile_photo_url,
|
||||
email_verified: profile.email_verified
|
||||
email_verified: profile.email_verified,
|
||||
}
|
||||
},
|
||||
protection: ["state", "pkce"],
|
||||
...options
|
||||
checks: ["state", "pkce"],
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logger from '../lib/logger'
|
||||
import nodemailer from "nodemailer"
|
||||
import logger from "../lib/logger"
|
||||
|
||||
export default function Email(options) {
|
||||
return {
|
||||
@@ -22,34 +22,24 @@ export default function Email(options) {
|
||||
}
|
||||
}
|
||||
|
||||
const sendVerificationRequest = ({
|
||||
identifier: email,
|
||||
url,
|
||||
baseUrl,
|
||||
provider,
|
||||
}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { server, from } = provider
|
||||
// Strip protocol from URL and use domain as site name
|
||||
const site = baseUrl.replace(/^https?:\/\//, "")
|
||||
|
||||
nodemailer.createTransport(server).sendMail(
|
||||
{
|
||||
async function sendVerificationRequest ({ identifier: email, url, baseUrl, provider }) {
|
||||
const { server, from } = provider
|
||||
// Strip protocol from URL and use domain as site name
|
||||
const site = baseUrl.replace(/^https?:\/\//, '')
|
||||
try {
|
||||
await nodemailer
|
||||
.createTransport(server)
|
||||
.sendMail({
|
||||
to: email,
|
||||
from,
|
||||
subject: `Sign in to ${site}`,
|
||||
text: text({ url, site, email }),
|
||||
html: html({ url, site, email }),
|
||||
},
|
||||
(error) => {
|
||||
if (error) {
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
|
||||
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
|
||||
}
|
||||
return resolve()
|
||||
}
|
||||
)
|
||||
})
|
||||
html: html({ url, site, email })
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
|
||||
throw new Error('SEND_VERIFICATION_EMAIL_ERROR')
|
||||
}
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function GitHub(options) {
|
||||
profileUrl: "https://api.github.com/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
id: profile.id.toString(),
|
||||
name: profile.name || profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* import { signIn } from "next-auth/react"
|
||||
* ...
|
||||
* <button onClick={() => signIn("instagram")}>
|
||||
* Sign in
|
||||
|
||||
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" },
|
||||
checks: ["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,
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default function Salesforce(options) {
|
||||
authorizationUrl:
|
||||
"https://login.salesforce.com/services/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://login.salesforce.com/services/oauth2/userinfo",
|
||||
protection: "none",
|
||||
checks: ["none"],
|
||||
profile(profile) {
|
||||
return {
|
||||
...profile,
|
||||
|
||||
@@ -15,7 +15,10 @@ export default function Twitter(options) {
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
|
||||
image: profile.profile_image_url_https.replace(
|
||||
/_normal\.(jpg|png|gif)$/,
|
||||
".$1"
|
||||
),
|
||||
}
|
||||
},
|
||||
...options,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
export default function WorkOS(options) {
|
||||
const domain = options.domain || 'api.workos.com';
|
||||
const domain = options.domain || "api.workos.com"
|
||||
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
id: "workos",
|
||||
name: "WorkOS",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: {
|
||||
grant_type: 'authorization_code',
|
||||
grant_type: "authorization_code",
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
client_secret: options.clientSecret,
|
||||
},
|
||||
accessTokenUrl: `https://${domain}/sso/token`,
|
||||
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
|
||||
@@ -18,9 +18,9 @@ export default function WorkOS(options) {
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
name: `${profile.first_name} ${profile.last_name}`
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
}
|
||||
},
|
||||
...options
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import adapters from "../adapters"
|
||||
import jwt from "../lib/jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
@@ -18,7 +17,7 @@ import * as state from "./lib/oauth/state-handler"
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
logger.warn("NEXTAUTH_URL")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,11 +43,11 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
extendRes(req, res, resolve)
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error =
|
||||
const message =
|
||||
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
|
||||
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
|
||||
return res.status(500).end(`Error: ${error}`)
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", new Error(message))
|
||||
return res.status(500).end(`Error: ${message}`)
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -78,37 +77,20 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
baseUrl,
|
||||
basePath,
|
||||
})
|
||||
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
// Protection only works on OAuth 2.x providers
|
||||
// TODO:
|
||||
// - rename to `checks` in 4.x, so it is similar to `openid-client`
|
||||
// - stop supporting `protection` as string
|
||||
// - remove `state` property
|
||||
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
|
||||
// Priority: (protection array > protection string) > state > default
|
||||
if (provider.protection) {
|
||||
provider.protection = Array.isArray(provider.protection)
|
||||
? provider.protection
|
||||
: [provider.protection]
|
||||
} else if (provider.state !== undefined) {
|
||||
provider.protection = [provider.state ? "state" : "none"]
|
||||
} else {
|
||||
// Default to state, as we did in 3.1
|
||||
// REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ["state"]
|
||||
}
|
||||
// Checks only work on OAuth 2.x providers
|
||||
if (
|
||||
provider?.type === "oauth" &&
|
||||
provider.version?.startsWith("2") &&
|
||||
!provider.checks
|
||||
) {
|
||||
provider.checks = ["state"]
|
||||
}
|
||||
|
||||
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
|
||||
|
||||
// Parse database / adapter
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
const adapter =
|
||||
userOptions.adapter ??
|
||||
(userOptions.database && adapters.Default(userOptions.database))
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
req.options = {
|
||||
@@ -119,7 +101,6 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
...userOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
// and are request-specific.
|
||||
adapter,
|
||||
baseUrl,
|
||||
basePath,
|
||||
action,
|
||||
@@ -129,7 +110,7 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
providers,
|
||||
// Session options
|
||||
session: {
|
||||
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
...userOptions.session,
|
||||
@@ -219,6 +200,7 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
"OAuthAccountNotLinked",
|
||||
"EmailSignin",
|
||||
"CredentialsSignin",
|
||||
"SessionRequired",
|
||||
].includes(error)
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
|
||||
@@ -262,13 +244,8 @@ async function NextAuthHandler(req, res, userOptions) {
|
||||
case "_log":
|
||||
if (userOptions.logger) {
|
||||
try {
|
||||
const {
|
||||
code = "CLIENT_ERROR",
|
||||
level = "error",
|
||||
message = "[]",
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
const { code, level, ...metadata } = req.body
|
||||
logger[level](code, metadata)
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error("LOGGER_ERROR", error)
|
||||
|
||||
@@ -16,7 +16,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
|
||||
* @param {import("types").Session} sessionToken
|
||||
* @param {import("types").Profile} profile
|
||||
* @param {import("types").Account} account
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
* @param {import("types/internals").InternalOptions} options
|
||||
*/
|
||||
export default async function callbackHandler(
|
||||
sessionToken,
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
// @ts-check
|
||||
import * as cookie from "../lib/cookie"
|
||||
|
||||
/**
|
||||
* Get callback URL based on query param / cookie + validation,
|
||||
* and add it to `req.options.callbackUrl`.
|
||||
* @note: `req.options` must already be defined when called.
|
||||
* @type {import("types/internals").NextAuthApiHandler}
|
||||
*/
|
||||
export default async function callbackUrlHandler (req, res) {
|
||||
export default async function callbackUrlHandler(req, res) {
|
||||
const { query } = req
|
||||
const { body } = req
|
||||
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
|
||||
const { cookies, baseUrl, callbacks } = req.options
|
||||
|
||||
// Handle preserving and validating callback URLs
|
||||
// If no defaultCallbackUrl option specified, default to the homepage for the site
|
||||
let callbackUrl = defaultCallbackUrl || baseUrl
|
||||
let callbackUrl = baseUrl
|
||||
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
|
||||
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
|
||||
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
|
||||
if (callbackUrlParamValue) {
|
||||
// If callbackUrl form field or query parameter is passed try to use it if allowed
|
||||
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
|
||||
callbackUrl = await callbacks.redirect({
|
||||
url: callbackUrlParamValue,
|
||||
baseUrl,
|
||||
})
|
||||
} else if (callbackUrlCookieValue) {
|
||||
// If no callbackUrl specified, try using the value from the cookie if allowed
|
||||
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
|
||||
callbackUrl = await callbacks.redirect({
|
||||
url: callbackUrlCookieValue,
|
||||
baseUrl,
|
||||
})
|
||||
}
|
||||
|
||||
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
|
||||
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
|
||||
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
|
||||
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
|
||||
if (callbackUrl && callbackUrl !== callbackUrlCookieValue) {
|
||||
cookie.set(
|
||||
res,
|
||||
cookies.callbackUrl.name,
|
||||
callbackUrl,
|
||||
cookies.callbackUrl.options
|
||||
)
|
||||
}
|
||||
|
||||
req.options.callbackUrl = callbackUrl
|
||||
|
||||
@@ -1,64 +1,24 @@
|
||||
/**
|
||||
* Use the signIn callback to control if a user is allowed to sign in or not.
|
||||
*
|
||||
* This is triggered before sign in flow completes, so the user profile may be
|
||||
* a user object (with an ID) or it may be just their name and email address,
|
||||
* depending on the sign in flow and if they have an account already.
|
||||
*
|
||||
* When using email sign in, this method is triggered both when the user
|
||||
* requests to sign in and again when they activate the link in the sign in
|
||||
* email.
|
||||
*
|
||||
* @param {object} profile User profile (e.g. user id, name, email)
|
||||
* @param {object} account Account used to sign in (e.g. OAuth account)
|
||||
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
|
||||
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
export async function signIn () {
|
||||
// @ts-check
|
||||
|
||||
/** @type {import("types").CallbacksOptions["signIn"]} */
|
||||
export function signIn() {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect is called anytime the user is redirected on signin or signout.
|
||||
* By default, for security, only Callback URLs on the same URL as the site
|
||||
* are allowed, you can use this callback to customise that behaviour.
|
||||
*
|
||||
* @param {string} url URL provided as callback URL by the client
|
||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||
* @return {Promise<string>} URL the client will be redirect to
|
||||
*/
|
||||
export async function redirect (url, baseUrl) {
|
||||
/** @type {import("types").CallbacksOptions["redirect"]} */
|
||||
export function redirect({ url, baseUrl }) {
|
||||
if (url.startsWith(baseUrl)) {
|
||||
return url
|
||||
}
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* The session callback is called whenever a session is checked.
|
||||
* e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
|
||||
*
|
||||
* @param {object} session Session object
|
||||
* @param {object} token JSON Web Token (if enabled)
|
||||
* @return {Promise<object>} Session that will be returned to the client
|
||||
*/
|
||||
export async function session (session) {
|
||||
/** @type {import("types").CallbacksOptions["session"]} */
|
||||
export function session({ session }) {
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is called whenever a JSON Web Token is created / updated.
|
||||
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
|
||||
*
|
||||
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
|
||||
* with an OAuth provider. It is not avalible on subsequent calls. You can
|
||||
* take advantage of this to persist additional data you need to in the JWT.
|
||||
*
|
||||
* @param {object} token Decrypted JSON Web Token
|
||||
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
||||
* @return {Promise<object>} JSON Web Token that will be saved
|
||||
*/
|
||||
export async function jwt (token) {
|
||||
/** @type {import("types").CallbacksOptions["jwt"]} */
|
||||
export function jwt({ token }) {
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -23,13 +23,12 @@ export default async function oAuthCallback(req) {
|
||||
code = body.code
|
||||
user = body.user != null ? JSON.parse(body.user) : null
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
"OAUTH_CALLBACK_HANDLER_ERROR",
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", {
|
||||
error,
|
||||
req.body,
|
||||
provider.id,
|
||||
code
|
||||
)
|
||||
body: req.body,
|
||||
providerId: provider.id,
|
||||
code,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -62,7 +61,11 @@ 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,
|
||||
providerId: provider.id,
|
||||
code,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -74,7 +77,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,
|
||||
@@ -113,7 +120,7 @@ export default async function oAuthCallback(req) {
|
||||
async function getProfile({ profileData, tokens, provider, user }) {
|
||||
try {
|
||||
// Convert profileData into an object if it's a string
|
||||
if (typeof profileData === "string" || profileData instanceof String) {
|
||||
if (typeof profileData === "string") {
|
||||
profileData = JSON.parse(profileData)
|
||||
}
|
||||
|
||||
@@ -122,7 +129,7 @@ async function getProfile({ profileData, tokens, provider, user }) {
|
||||
profileData.user = user
|
||||
}
|
||||
|
||||
logger.debug("PROFILE_DATA", profileData)
|
||||
logger.debug("PROFILE_DATA", { profile: profileData })
|
||||
|
||||
const profile = await provider.profile(profileData, tokens)
|
||||
// Return profile, raw profile and auth provider details
|
||||
@@ -139,7 +146,7 @@ async function getProfile({ profileData, tokens, provider, user }) {
|
||||
},
|
||||
OAuthProfile: profileData,
|
||||
}
|
||||
} catch (exception) {
|
||||
} catch (error) {
|
||||
// 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.
|
||||
//
|
||||
@@ -147,7 +154,7 @@ async function getProfile({ profileData, tokens, provider, user }) {
|
||||
// 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", { error, profileData })
|
||||
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,52 +112,68 @@ 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.checks.includes("pkce")) {
|
||||
params.code_verifier = codeVerifier
|
||||
}
|
||||
|
||||
@@ -157,14 +181,18 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
'POST',
|
||||
"POST",
|
||||
url,
|
||||
headers,
|
||||
postData,
|
||||
null,
|
||||
(error, data, response) => {
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
|
||||
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", {
|
||||
error,
|
||||
data,
|
||||
response,
|
||||
})
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
@@ -181,7 +209,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
}
|
||||
|
||||
let accessToken
|
||||
if (provider.id === 'slack') {
|
||||
if (provider.id === "slack") {
|
||||
const { ok, error } = raw
|
||||
if (!ok) {
|
||||
return reject(error)
|
||||
@@ -197,7 +225,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
accessTokenExpires: null,
|
||||
refreshToken: raw.refresh_token,
|
||||
idToken: raw.id_token,
|
||||
...raw
|
||||
...raw,
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -213,60 +241,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)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import pkceChallenge from 'pkce-challenge'
|
||||
import * as cookie from '../cookie'
|
||||
import jwt from '../../../lib/jwt'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
import pkceChallenge from "pkce-challenge"
|
||||
import * as cookie from "../cookie"
|
||||
import jwt from "../../../lib/jwt"
|
||||
import logger from "../../../lib/logger"
|
||||
import { OAuthCallbackError } from "../../../lib/errors"
|
||||
|
||||
const PKCE_LENGTH = 64
|
||||
const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
const PKCE_CODE_CHALLENGE_METHOD = "S256" // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/**
|
||||
@@ -13,32 +13,36 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
export async function handleCallback(req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
// Provider does not support PKCE, nothing to do.
|
||||
if (!provider.protection?.includes('pkce')) {
|
||||
if (!provider.checks?.includes("pkce")) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!(cookies.pkceCodeVerifier.name in req.cookies)) {
|
||||
throw new OAuthCallbackError('The code_verifier cookie was not found.')
|
||||
throw new OAuthCallbackError("The code_verifier cookie was not found.")
|
||||
}
|
||||
const pkce = await jwt.decode({
|
||||
...req.options.jwt,
|
||||
token: req.cookies[cookies.pkceCodeVerifier.name],
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
encryption: true
|
||||
encryption: true,
|
||||
})
|
||||
req.options.pkce = pkce
|
||||
logger.debug('OAUTH_CALLBACK_PROTECTION', 'Read PKCE verifier from cookie', {
|
||||
logger.debug("PKCE_VERIFIER_FROM_COOKIE", {
|
||||
code_verifier: pkce.code_verifier,
|
||||
pkceLength: PKCE_LENGTH,
|
||||
method: PKCE_CODE_CHALLENGE_METHOD
|
||||
method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
})
|
||||
// remove PKCE after it has been used
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, "", {
|
||||
...cookies.pkceCodeVerifier.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, null, { maxAge: 0 }) // remove PKCE after it has been used
|
||||
} catch (error) {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
logger.error("CALLBACK_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
}
|
||||
@@ -48,42 +52,43 @@ export async function handleCallback (req, res) {
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
export async function handleSignin(req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
|
||||
if (!provider.checks?.includes("pkce")) {
|
||||
// Provider does not support PKCE, nothing to do.
|
||||
return
|
||||
}
|
||||
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie
|
||||
const pkce = pkceChallenge(PKCE_LENGTH)
|
||||
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE challenge/verifier', {
|
||||
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
|
||||
...pkce,
|
||||
pkceLength: PKCE_LENGTH,
|
||||
method: PKCE_CODE_CHALLENGE_METHOD
|
||||
method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
})
|
||||
|
||||
provider.authorizationParams = {
|
||||
...provider.authorizationParams,
|
||||
code_challenge: pkce.code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
}
|
||||
|
||||
const encryptedCodeVerifier = await jwt.encode({
|
||||
...req.options.jwt,
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
token: { code_verifier: pkce.code_verifier },
|
||||
encryption: true
|
||||
encryption: true,
|
||||
})
|
||||
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (PKCE_MAX_AGE * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + PKCE_MAX_AGE * 1000)
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, encryptedCodeVerifier, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.pkceCodeVerifier.options
|
||||
...cookies.pkceCodeVerifier.options,
|
||||
})
|
||||
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE code_verifier saved in cookie')
|
||||
logger.debug("PKCE_CODE_VERIFIER_SAVED", { encryptedCodeVerifier })
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
logger.error("SIGNIN_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createHash } from 'crypto'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
import { createHash } from "crypto"
|
||||
import logger from "../../../lib/logger"
|
||||
import { OAuthCallbackError } from "../../../lib/errors"
|
||||
|
||||
/**
|
||||
* For OAuth 2.0 flows, if the provider supports state,
|
||||
@@ -9,27 +9,23 @@ import { OAuthCallbackError } from '../../../lib/errors'
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
export async function handleCallback(req, res) {
|
||||
const { csrfToken, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
// Provider does not support state, nothing to do.
|
||||
if (!provider.protection?.includes('state')) {
|
||||
if (!provider.checks?.includes("state")) {
|
||||
return
|
||||
}
|
||||
|
||||
const state = req.query.state || req.body.state
|
||||
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
||||
const expectedState = createHash("sha256").update(csrfToken).digest("hex")
|
||||
|
||||
logger.debug(
|
||||
'OAUTH_CALLBACK_PROTECTION',
|
||||
'Comparing received and expected state',
|
||||
{ state, expectedState }
|
||||
)
|
||||
logger.debug("STATE_CHECK", { state, expectedState })
|
||||
if (state !== expectedState) {
|
||||
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
|
||||
throw new OAuthCallbackError("Invalid state returned from OAuth provider")
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('STATE_ERROR', error)
|
||||
logger.error("STATE_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
}
|
||||
@@ -39,31 +35,21 @@ export async function handleCallback (req, res) {
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
export async function handleSignin(req, res) {
|
||||
const { provider, baseUrl, basePath, csrfToken } = req.options
|
||||
try {
|
||||
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
|
||||
if (!provider.checks?.includes("state")) {
|
||||
// Provider does not support state, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
if ('state' in provider) {
|
||||
logger.warn(
|
||||
'STATE_OPTION_DEPRECATION',
|
||||
'The `state` provider option is being replaced with `protection`. See the docs.'
|
||||
)
|
||||
}
|
||||
|
||||
// A hash of the NextAuth.js CSRF token is used as the state
|
||||
const state = createHash('sha256').update(csrfToken).digest('hex')
|
||||
const state = createHash("sha256").update(csrfToken).digest("hex")
|
||||
|
||||
provider.authorizationParams = { ...provider.authorizationParams, state }
|
||||
logger.debug(
|
||||
'OAUTH_CALLBACK_PROTECTION',
|
||||
'Added state to authorization params',
|
||||
{ state }
|
||||
)
|
||||
logger.debug("STATE_ADDED_TO_PARAMS", { state })
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
logger.error("SIGNIN_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import adapterErrorHandler from "../../../adapters/error-handler"
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {import("types/providers").EmailConfig} provider
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
* @param {import("types/internals").InternalOptions} options
|
||||
* @returns
|
||||
*/
|
||||
export default async function email(email, provider, options) {
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import oAuthClient from '../oauth/client'
|
||||
import logger from '../../../lib/logger'
|
||||
import oAuthClient from "../oauth/client"
|
||||
import logger from "../../../lib/logger"
|
||||
|
||||
/** @param {import("types/internals").NextAuthRequest} req */
|
||||
export default async function getAuthorizationUrl (req) {
|
||||
export default async function getAuthorizationUrl(req) {
|
||||
const { provider } = req.options
|
||||
|
||||
delete req.query?.nextauth
|
||||
const params = {
|
||||
...provider.authorizationParams,
|
||||
...req.query
|
||||
...req.query,
|
||||
}
|
||||
|
||||
const client = oAuthClient(provider)
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
if (provider.version?.startsWith("2.")) {
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
scope: provider.scope,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl
|
||||
redirect_uri: provider.callbackUrl,
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
@@ -27,13 +27,13 @@ export default async function getAuthorizationUrl (req) {
|
||||
// which inadvertantly strips them.
|
||||
//
|
||||
// https://github.com/ciaranj/node-oauth/pull/193
|
||||
if (provider.authorizationUrl.includes('?')) {
|
||||
if (provider.authorizationUrl.includes("?")) {
|
||||
const parseUrl = new URL(provider.authorizationUrl)
|
||||
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
|
||||
url = url.replace(baseUrl, provider.authorizationUrl + '&')
|
||||
url = url.replace(baseUrl, provider.authorizationUrl + "&")
|
||||
}
|
||||
|
||||
logger.debug('GET_AUTHORIZATION_URL', url)
|
||||
logger.debug("GET_AUTHORIZATION_URL", { url, params })
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -42,12 +42,12 @@ export default async function getAuthorizationUrl (req) {
|
||||
const url = `${provider.authorizationUrl}?${new URLSearchParams({
|
||||
oauth_token: tokens.oauth_token,
|
||||
oauth_token_secret: tokens.oauth_token_secret,
|
||||
...tokens.params
|
||||
...tokens.params,
|
||||
})}`
|
||||
logger.debug('GET_AUTHORIZATION_URL', url)
|
||||
logger.debug("GET_AUTHORIZATION_URL", { url, tokens })
|
||||
return url
|
||||
} catch (error) {
|
||||
logger.error('GET_AUTHORIZATION_URL_ERROR', error)
|
||||
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Handle callbacks from login services
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
* @type {import("types/internals").NextAuthApiHandler}
|
||||
*/
|
||||
export default async function callback(req, res) {
|
||||
const {
|
||||
@@ -72,12 +71,12 @@ export default async function callback(req, res) {
|
||||
}
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
userOrProfile,
|
||||
const signInCallbackResponse = await callbacks.signIn({
|
||||
user: userOrProfile,
|
||||
account,
|
||||
OAuthProfile
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
profile: OAuthProfile,
|
||||
})
|
||||
if (!signInCallbackResponse) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=AccessDenied`
|
||||
)
|
||||
@@ -85,16 +84,11 @@ export default async function callback(req, res) {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
@@ -106,22 +100,22 @@ export default async function callback(req, res) {
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
const defaultToken = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
const token = await callbacks.jwt({
|
||||
token: defaultToken,
|
||||
user,
|
||||
account,
|
||||
OAuthProfile,
|
||||
isNewUser
|
||||
)
|
||||
profile: OAuthProfile,
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
@@ -221,27 +215,22 @@ export default async function callback(req, res) {
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
profile,
|
||||
const signInCallbackResponse = await callbacks.signIn({
|
||||
user: profile,
|
||||
account,
|
||||
{ email }
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
email: { email },
|
||||
})
|
||||
if (!signInCallbackResponse) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
@@ -253,22 +242,22 @@ export default async function callback(req, res) {
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
const defaultToken = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
const token = await callbacks.jwt({
|
||||
token: defaultToken,
|
||||
user,
|
||||
account,
|
||||
profile,
|
||||
isNewUser
|
||||
)
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
@@ -314,7 +303,9 @@ export default async function callback(req, res) {
|
||||
if (!useJwtSession) {
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_JWT_ERROR",
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
new Error(
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
)
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
@@ -324,7 +315,9 @@ export default async function callback(req, res) {
|
||||
if (!provider.authorize) {
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
new Error(
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
@@ -336,7 +329,8 @@ export default async function callback(req, res) {
|
||||
let userObjectReturnedFromAuthorizeHandler
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
|
||||
credentials, {...req, options: {}, cookies: {}}
|
||||
credentials,
|
||||
{ ...req, options: {}, cookies: {} }
|
||||
)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return res
|
||||
@@ -346,59 +340,53 @@ export default async function callback(req, res) {
|
||||
provider.id
|
||||
)}`
|
||||
)
|
||||
} else if (typeof userObjectReturnedFromAuthorizeHandler === "string") {
|
||||
return res.redirect(userObjectReturnedFromAuthorizeHandler)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
|
||||
)
|
||||
}
|
||||
|
||||
const user = userObjectReturnedFromAuthorizeHandler
|
||||
const account = { id: provider.id, type: "credentials" }
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
const signInCallbackResponse = await callbacks.signIn({
|
||||
user,
|
||||
account,
|
||||
credentials
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
credentials,
|
||||
})
|
||||
if (!signInCallbackResponse) {
|
||||
return res
|
||||
.status(403)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
|
||||
)
|
||||
}
|
||||
|
||||
const defaultJwtPayload = {
|
||||
const defaultToken = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
const token = await callbacks.jwt({
|
||||
token: defaultToken,
|
||||
user,
|
||||
account,
|
||||
userObjectReturnedFromAuthorizeHandler,
|
||||
false
|
||||
)
|
||||
profile: userObjectReturnedFromAuthorizeHandler,
|
||||
isNewUser: false,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
|
||||
@@ -22,7 +22,7 @@ export default async function session(req, res) {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
// Decrypt and verify token
|
||||
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
|
||||
// Generate new session expiry date
|
||||
const sessionExpiresDate = new Date()
|
||||
@@ -33,38 +33,35 @@ export default async function session(req, res) {
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
// as needed for presentation purposes (e.g. "you are logged in as…").
|
||||
const defaultSessionPayload = {
|
||||
const defaultSession = {
|
||||
user: {
|
||||
name: decodedJwt.name || null,
|
||||
email: decodedJwt.email || null,
|
||||
image: decodedJwt.picture || null,
|
||||
name: decodedToken.name || null,
|
||||
email: decodedToken.email || null,
|
||||
image: decodedToken.picture || null,
|
||||
},
|
||||
expires: sessionExpires,
|
||||
}
|
||||
|
||||
// Pass Session and JSON Web Token through to the session callback
|
||||
const jwtPayload = await callbacks.jwt(decodedJwt)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
jwtPayload
|
||||
)
|
||||
const token = await callbacks.jwt({ token: decodedToken })
|
||||
const session = await callbacks.session({
|
||||
session: defaultSession,
|
||||
token,
|
||||
})
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
response = session
|
||||
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
cookie.set(res, cookies.sessionToken.name, newToken, {
|
||||
expires: sessionExpires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, {
|
||||
session: sessionPayload,
|
||||
jwt: jwtPayload,
|
||||
})
|
||||
await dispatchEvent(events.session, { session, token })
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error("JWT_SESSION_ERROR", error)
|
||||
@@ -88,7 +85,7 @@ export default async function session(req, res) {
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
// as needed for presentation purposes (e.g. "you are logged in as…").
|
||||
const defaultSessionPayload = {
|
||||
const defaultSession = {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
@@ -99,10 +96,10 @@ export default async function session(req, res) {
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
user
|
||||
)
|
||||
const sessionPayload = await callbacks.session({
|
||||
session: defaultSession,
|
||||
user,
|
||||
})
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
|
||||
@@ -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}`)
|
||||
@@ -47,14 +41,15 @@ export default async function signin(req, res) {
|
||||
const email = req.body.email?.toLowerCase() ?? null
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const user = (await getUserByEmail(email)) || { email }
|
||||
const account = { id: provider.id, type: "email", providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, {
|
||||
email,
|
||||
verificationRequest: true,
|
||||
const signInCallbackResponse = await callbacks.signIn({
|
||||
user,
|
||||
account,
|
||||
email: { email, verificationRequest: true },
|
||||
})
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
@@ -62,14 +57,9 @@ export default async function signin(req, res) {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"types": ["./types"],
|
||||
"next-auth": ["./src/server"],
|
||||
"next-auth/adapters": ["./src/adapters"],
|
||||
"next-auth/client": ["./src/client"],
|
||||
"next-auth/react": ["./src/client/react"],
|
||||
"next-auth/jwt": ["./src/lib/jwt"],
|
||||
"next-auth/providers": ["./src/providers"]
|
||||
"next-auth/providers": ["./src/providers"],
|
||||
},
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
|
||||
36
types/adapters.d.ts
vendored
36
types/adapters.d.ts
vendored
@@ -1,39 +1,7 @@
|
||||
import { AppOptions } from "./internals"
|
||||
import { InternalOptions } from "./internals"
|
||||
import { User, Profile, Session } from "."
|
||||
import { EmailConfig } from "./providers"
|
||||
|
||||
/** Legacy */
|
||||
|
||||
export {
|
||||
TypeORMAccountModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import {
|
||||
TypeORMAdapter,
|
||||
TypeORMAdapterModels,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export const TypeORM: {
|
||||
Models: TypeORMAdapterModels
|
||||
Adapter: TypeORMAdapter
|
||||
}
|
||||
|
||||
export const Prisma: {
|
||||
Adapter: PrismaLegacyAdapter
|
||||
}
|
||||
|
||||
declare const Adapters: {
|
||||
Default: TypeORMAdapter
|
||||
TypeORM: typeof TypeORM
|
||||
Prisma: typeof Prisma
|
||||
}
|
||||
export default Adapters
|
||||
|
||||
/**
|
||||
* Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
* Custom adapters created and maintained by our community can be found in the adapters repository.
|
||||
@@ -152,5 +120,5 @@ export type Adapter<
|
||||
client: C,
|
||||
options?: O
|
||||
) => {
|
||||
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
|
||||
getAdapter(appOptions: InternalOptions): Promise<AdapterInstance<U, P, S>>
|
||||
}
|
||||
|
||||
114
types/index.d.ts
vendored
114
types/index.d.ts
vendored
@@ -2,10 +2,9 @@
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import { ConnectionOptions } from "typeorm"
|
||||
import { Adapter } from "./adapters"
|
||||
import { JWTOptions, JWT } from "./jwt"
|
||||
import { AppProviders } from "./providers"
|
||||
import { AppProviders, Credentials } from "./providers"
|
||||
import {
|
||||
Awaitable,
|
||||
NextApiRequest,
|
||||
@@ -29,14 +28,6 @@ export interface NextAuthOptions {
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
|
||||
*/
|
||||
providers: AppProviders
|
||||
/**
|
||||
* A database connection string or configuration object.
|
||||
* * **Default value**: `null`
|
||||
* * **Required**: *No (unless using email provider)*
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#database) | [Databases](https://next-auth.js.org/configuration/databases)
|
||||
*/
|
||||
database?: string | Record<string, any> | ConnectionOptions
|
||||
/**
|
||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
@@ -82,7 +73,7 @@ export interface NextAuthOptions {
|
||||
* signOut: '/auth/signout',
|
||||
* error: '/auth/error',
|
||||
* verifyRequest: '/auth/verify-request',
|
||||
* newUser: null
|
||||
* newUser: '/auth/new-user'
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
@@ -98,7 +89,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
|
||||
*/
|
||||
callbacks?: CallbacksOptions
|
||||
callbacks?: Partial<CallbacksOptions>
|
||||
/**
|
||||
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
|
||||
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
|
||||
@@ -113,18 +104,11 @@ export interface NextAuthOptions {
|
||||
*/
|
||||
events?: Partial<JWTEventCallbacks | SessionEventCallbacks>
|
||||
/**
|
||||
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
|
||||
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
|
||||
* You can use the adapter option to use the Prisma adapter - or pass in your own adapter
|
||||
* if you want to use a database that is not supported by one of the built-in adapters.
|
||||
* * **Default value**: TypeORM adapter
|
||||
* You can use the adapter option to pass in your database adapter.
|
||||
*
|
||||
* * **Required**: *No*
|
||||
*
|
||||
* - ⚠ If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
|
||||
* - ⚠ Adapters are being migrated to their own home in a Community maintained repository.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
|
||||
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters)
|
||||
*/
|
||||
adapter?: ReturnType<Adapter>
|
||||
@@ -212,7 +196,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
|
||||
*/
|
||||
cookies?: CookiesOptions
|
||||
cookies?: Partial<CookiesOptions>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,9 +213,22 @@ export type Theme = "auto" | "dark" | "light"
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger)
|
||||
*/
|
||||
export interface LoggerInstance {
|
||||
warn(code: string, ...message: unknown[]): void
|
||||
error(code: string, ...message: unknown[]): void
|
||||
debug(code: string, ...message: unknown[]): void
|
||||
warn(
|
||||
code:
|
||||
| "JWT_AUTO_GENERATED_SIGNING_KEY"
|
||||
| "JWT_AUTO_GENERATED_ENCRYPTION_KEY"
|
||||
| "NEXTAUTH_URL"
|
||||
): void
|
||||
error(
|
||||
code: string,
|
||||
/**
|
||||
* Either an instance of (JSON serializable) Error
|
||||
* or an object that contains some debug information.
|
||||
* (Error is still available through `metadata.error`)
|
||||
*/
|
||||
metadata: Error | { error: Error; [key: string]: unknown }
|
||||
): void
|
||||
debug(code: string, metadata: unknown): void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +280,29 @@ export interface CallbacksOptions<
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
|
||||
signIn(params: {
|
||||
user: User
|
||||
account: A
|
||||
/**
|
||||
* If OAuth provider is used, it contains the full
|
||||
* OAuth profile returned by your provider.
|
||||
*/
|
||||
profile: P & Record<string, unknown>
|
||||
/**
|
||||
* If Email provider is used, it contains the email, and optionally on the first call a
|
||||
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
|
||||
* When the callback is invoked after a user has clicked on a sign in link,
|
||||
* this property will not be present. You can check for the `verificationRequest` property
|
||||
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
|
||||
* for email address in an allow list.
|
||||
*/
|
||||
email: {
|
||||
email: string | null
|
||||
verificationRequest?: boolean
|
||||
}
|
||||
/** If Credentials provider is used, it contains the user credentials */
|
||||
credentials: Credentials
|
||||
}): Awaitable<string | boolean>
|
||||
/**
|
||||
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
|
||||
* By default only URLs on the same URL as the site are allowed,
|
||||
@@ -291,12 +310,19 @@ export interface CallbacksOptions<
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
|
||||
*/
|
||||
redirect?(url: string, baseUrl: string): Awaitable<string>
|
||||
redirect(params: {
|
||||
/** URL provided as callback URL by the client */
|
||||
url: string
|
||||
/** Default base URL of site (can be used as fallback) */
|
||||
baseUrl: string
|
||||
}): Awaitable<string>
|
||||
/**
|
||||
* This callback is called whenever a session is checked.
|
||||
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
|
||||
*
|
||||
* - ⚠ By default, only a subset of the token is returned for increased security.
|
||||
* ⚠ By default, only a subset (email, name, imgage)
|
||||
* of the token is returned for increased security.
|
||||
*
|
||||
* If you want to make something available you added to the token through the `jwt` callback,
|
||||
* you have to explicitely forward it here to make it available to the client.
|
||||
*
|
||||
@@ -306,7 +332,11 @@ export interface CallbacksOptions<
|
||||
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
|
||||
*
|
||||
*/
|
||||
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
|
||||
session(params: {
|
||||
session: Session
|
||||
user: User
|
||||
token: JWT
|
||||
}): Awaitable<Session>
|
||||
/**
|
||||
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
|
||||
* or updated (i.e whenever a session is accessed in the client).
|
||||
@@ -314,18 +344,18 @@ export interface CallbacksOptions<
|
||||
* where you can control what should be returned to the client.
|
||||
* Anything else will be kept from your front-end.
|
||||
*
|
||||
* - ⚠ By default the JWT is signed, but not encrypted.
|
||||
* ⚠ By default the JWT is signed, but not encrypted.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
|
||||
*/
|
||||
jwt?(
|
||||
token: JWT,
|
||||
user?: User,
|
||||
account?: A,
|
||||
profile?: P,
|
||||
jwt(params: {
|
||||
token: JWT
|
||||
user?: User
|
||||
account?: A
|
||||
profile?: P
|
||||
isNewUser?: boolean
|
||||
): Awaitable<JWT>
|
||||
}): Awaitable<JWT>
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
@@ -343,10 +373,10 @@ export interface CookieOption {
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
export interface CookiesOptions {
|
||||
sessionToken?: CookieOption
|
||||
callbackUrl?: CookieOption
|
||||
csrfToken?: CookieOption
|
||||
pkceCodeVerifier?: CookieOption
|
||||
sessionToken: CookieOption
|
||||
callbackUrl: CookieOption
|
||||
csrfToken: CookieOption
|
||||
pkceCodeVerifier: CookieOption
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
@@ -428,11 +458,11 @@ export interface DefaultSession extends Record<string, unknown> {
|
||||
|
||||
/**
|
||||
* Returned by `useSession`, `getSession`, returned by the `session` callback
|
||||
* and also the shape received as a prop on the `Provider` React Context
|
||||
* and also the shape received as a prop on the `SessionProvider` React Context
|
||||
*
|
||||
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
|
||||
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
|
||||
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
|
||||
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
|
||||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
|
||||
*/
|
||||
export interface Session extends Record<string, unknown>, DefaultSession {}
|
||||
|
||||
34
types/internals/client.d.ts
vendored
34
types/internals/client.d.ts
vendored
@@ -1,34 +0,0 @@
|
||||
import * as React from "react"
|
||||
import { Session } from ".."
|
||||
|
||||
export interface BroadcastMessage {
|
||||
event?: "session"
|
||||
data?: {
|
||||
trigger?: "signout" | "getSession"
|
||||
}
|
||||
clientId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface NextAuthConfig {
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
baseUrlServer: string
|
||||
basePathServer: string
|
||||
/** 0 means disabled (don't send); 60 means send every 60 seconds */
|
||||
keepAlive: number
|
||||
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
|
||||
clientMaxAge: number
|
||||
/** Used for timestamp since last sycned (in seconds) */
|
||||
_clientLastSync: number
|
||||
/** Stores timer for poll interval */
|
||||
_clientSyncTimer: ReturnType<typeof setTimeout>
|
||||
/** Tracks if event listeners have been added */
|
||||
_eventListenersAdded: boolean
|
||||
/** Stores last session response from hook */
|
||||
_clientSession: Session | null | undefined
|
||||
/** Used to store to function export by getSession() hook */
|
||||
_getSession: any
|
||||
}
|
||||
|
||||
export type SessionContext = React.Context<Session>
|
||||
40
types/internals/index.d.ts
vendored
40
types/internals/index.d.ts
vendored
@@ -1,18 +1,18 @@
|
||||
import { NextApiRequest, NextApiResponse } from "./utils"
|
||||
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
|
||||
import { Awaitable, NextApiRequest, NextApiResponse } from "./utils"
|
||||
import {
|
||||
CallbacksOptions,
|
||||
CookiesOptions,
|
||||
EventCallbacks,
|
||||
LoggerInstance,
|
||||
PagesOptions,
|
||||
SessionOptions,
|
||||
Theme,
|
||||
} from ".."
|
||||
import { AppProvider } from "../providers"
|
||||
import { JWTOptions } from "next-auth/jwt"
|
||||
import { Adapter } from "next-auth/adapters"
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
export type NextAuthSharedOptions =
|
||||
| "pages"
|
||||
| "jwt"
|
||||
| "events"
|
||||
| "callbacks"
|
||||
| "cookies"
|
||||
| "adapter"
|
||||
|
||||
export interface AppOptions
|
||||
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
|
||||
export interface InternalOptions {
|
||||
providers: AppProvider[]
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
@@ -42,10 +42,22 @@ export interface AppOptions
|
||||
debug: boolean
|
||||
logger: LoggerInstance
|
||||
session: Required<SessionOptions>
|
||||
pages: PagesOptions
|
||||
jwt: JWTOptions
|
||||
events: EventCallbacks
|
||||
adapter: ReturnType<Adapter>
|
||||
callbacks: CallbacksOptions
|
||||
cookies: CookiesOptions
|
||||
callbackUrl: string
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: AppOptions
|
||||
options: InternalOptions
|
||||
}
|
||||
|
||||
export type NextAuthResponse = NextApiResponse
|
||||
|
||||
export type NextAuthApiHandler = (
|
||||
req: NextAuthRequest,
|
||||
res: NextAuthResponse
|
||||
) => Awaitable<void>
|
||||
|
||||
37
types/internals/react.d.ts
vendored
Normal file
37
types/internals/react.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as React from "react"
|
||||
import { Session } from ".."
|
||||
|
||||
export interface BroadcastMessage {
|
||||
event?: "session"
|
||||
data?: {
|
||||
trigger?: "signout" | "getSession"
|
||||
}
|
||||
clientId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface NextAuthConfig {
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
baseUrlServer: string
|
||||
basePathServer: string
|
||||
/** Stores last session response */
|
||||
_session?: Session | null
|
||||
/** Used for timestamp since last sycned (in seconds) */
|
||||
_lastSync: number
|
||||
/**
|
||||
* Stores the `SessionProvider`'s session update method to be able to
|
||||
* trigger session updates from places like `signIn` or `signOut`
|
||||
*/
|
||||
_getSession: any
|
||||
}
|
||||
|
||||
export type SessionContextValue<R extends boolean = false> = R extends true
|
||||
?
|
||||
| { data: Session; status: "authenticated" }
|
||||
| { data: null; status: "loading" }
|
||||
:
|
||||
| { data: Session; status: "authenticated" }
|
||||
| { data: null; status: "unauthenticated" | "loading" }
|
||||
|
||||
export type SessionContext = React.Context<SessionContextValue>
|
||||
40
types/providers.d.ts
vendored
40
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"
|
||||
|
||||
@@ -13,7 +14,7 @@ export interface CommonProviderOptions {
|
||||
* OAuth Provider
|
||||
*/
|
||||
|
||||
type ProtectionType = "pkce" | "state" | "both" | "none"
|
||||
type ChecksType = "pkce" | "state" | "both" | "none"
|
||||
|
||||
/**
|
||||
* OAuth provider options
|
||||
@@ -33,18 +34,13 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
|
||||
authorizationUrl: string
|
||||
profileUrl: string
|
||||
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
|
||||
protection?: ProtectionType | ProtectionType[]
|
||||
checks?: ChecksType | ChecksType[]
|
||||
clientId: string
|
||||
clientSecret:
|
||||
| string
|
||||
// TODO: only allow for Apple
|
||||
| Record<"appleId" | "teamId" | "privateKey" | "keyId", string>
|
||||
idToken?: boolean
|
||||
/**
|
||||
* @deprecated Will be removed in an upcoming major release. Use `protection: ["state"]` instead.
|
||||
*/
|
||||
state?: boolean
|
||||
|
||||
// TODO: only allow for BattleNet
|
||||
region?: string
|
||||
// TODO: only allow for some
|
||||
@@ -57,6 +53,7 @@ export type OAuthProviderType =
|
||||
| "Apple"
|
||||
| "Atlassian"
|
||||
| "Auth0"
|
||||
| "AzureAD"
|
||||
| "AzureADB2C"
|
||||
| "Basecamp"
|
||||
| "BattleNet"
|
||||
@@ -83,6 +80,7 @@ export type OAuthProviderType =
|
||||
| "Mailchimp"
|
||||
| "MailRu"
|
||||
| "Medium"
|
||||
| "Naver"
|
||||
| "Netlify"
|
||||
| "Okta"
|
||||
| "Osso"
|
||||
@@ -113,30 +111,24 @@ interface CredentialInput {
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
|
||||
export type Credentials = Record<string, CredentialInput>
|
||||
|
||||
interface CredentialsConfig<C extends Credentials = {}>
|
||||
extends CommonProviderOptions {
|
||||
type: "credentials"
|
||||
credentials: C
|
||||
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
|
||||
authorize(
|
||||
credentials: Record<keyof C, string>,
|
||||
req: NextApiRequest
|
||||
): Awaitable<User | null>
|
||||
}
|
||||
|
||||
export type CredentialsProvider = (
|
||||
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
|
||||
@@ -148,7 +140,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
|
||||
/**
|
||||
|
||||
88
types/client.d.ts → types/react-client.d.ts
vendored
88
types/client.d.ts → types/react-client.d.ts
vendored
@@ -2,6 +2,7 @@ import * as React from "react"
|
||||
import { IncomingMessage } from "http"
|
||||
import { Session } from "."
|
||||
import { ProviderType } from "./providers"
|
||||
import { SessionContextValue } from "internals/react"
|
||||
|
||||
export interface CtxOrReq {
|
||||
req?: IncomingMessage
|
||||
@@ -17,29 +18,24 @@ export type GetSessionOptions = CtxOrReq & {
|
||||
triggerEvent?: boolean
|
||||
}
|
||||
|
||||
export interface UseSessionOptions<R extends boolean> {
|
||||
required: R
|
||||
/** Defaults to `signIn` */
|
||||
action?(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* React Hook that gives you access
|
||||
* to the logged in user's session data.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
|
||||
*/
|
||||
export function useSession(): [Session | null, boolean]
|
||||
export function useSession<R extends boolean>(
|
||||
options?: UseSessionOptions<R>
|
||||
): SessionContextValue<R>
|
||||
|
||||
/**
|
||||
* Can be called client or server side to return a session asynchronously.
|
||||
* It calls `/api/auth/session` and returns a promise with a session object,
|
||||
* or null if no session exists.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
|
||||
*/
|
||||
export function getSession(options?: GetSessionOptions): Promise<Session | null>
|
||||
|
||||
/**
|
||||
* Alias for `getSession`
|
||||
* @docs https://next-auth.js.org/getting-started/client#getsession
|
||||
*/
|
||||
export const session: typeof getSession
|
||||
|
||||
/*******************
|
||||
* CSRF Token types
|
||||
******************/
|
||||
@@ -54,12 +50,6 @@ export const session: typeof getSession
|
||||
*/
|
||||
export function getCsrfToken(ctxOrReq?: CtxOrReq): Promise<string | null>
|
||||
|
||||
/**
|
||||
* Alias for `getCsrfToken`
|
||||
* @docs https://next-auth.js.org/getting-started/client#getcsrftoken
|
||||
*/
|
||||
export const csrfToken: typeof getCsrfToken
|
||||
|
||||
/******************
|
||||
* Providers types
|
||||
*****************/
|
||||
@@ -84,12 +74,6 @@ export function getProviders(): Promise<Record<
|
||||
ClientSafeProvider
|
||||
> | null>
|
||||
|
||||
/**
|
||||
* Alias for `getProviders`
|
||||
* @docs https://next-auth.js.org/getting-started/client#getproviders
|
||||
*/
|
||||
export const providers: typeof getProviders
|
||||
|
||||
/****************
|
||||
* Sign in types
|
||||
***************/
|
||||
@@ -137,12 +121,6 @@ export function signIn<P extends SignInProvider = undefined>(
|
||||
P extends RedirectableProvider ? SignInResponse | undefined : undefined
|
||||
>
|
||||
|
||||
/**
|
||||
* Alias for `signIn`
|
||||
* @docs https://next-auth.js.org/getting-started/client#signin
|
||||
*/
|
||||
export const signin: typeof signIn
|
||||
|
||||
/****************
|
||||
* Sign out types
|
||||
****************/
|
||||
@@ -169,21 +147,25 @@ export function signOut<R extends boolean = true>(
|
||||
params?: SignOutParams<R>
|
||||
): Promise<R extends true ? undefined : SignOutResponse>
|
||||
|
||||
/**
|
||||
* @docs https://next-auth.js.org/getting-started/client#signout
|
||||
* Alias for `signOut`
|
||||
*/
|
||||
export const signout: typeof signOut
|
||||
/************************
|
||||
* SessionProvider types
|
||||
***********************/
|
||||
|
||||
/** @docs: https://next-auth.js.org/getting-started/client#options */
|
||||
export interface SessionProviderOptions {
|
||||
export interface SessionProviderProps {
|
||||
session?: Session
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
clientMaxAge?: number
|
||||
keepAlive?: number
|
||||
/**
|
||||
* The amount of time (in seconds) after a session should be considered stale.
|
||||
* If set to `0` (default), the session will never be re-fetched.
|
||||
*/
|
||||
staleTime?: number
|
||||
/**
|
||||
* A time interval (in seconds) after which the session will be re-fetched.
|
||||
* If set to `0` (default), the session is not polled.
|
||||
*/
|
||||
refetchInterval?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,28 +173,6 @@ export interface SessionProviderOptions {
|
||||
* Can also be used to throttle the number of requests to the endpoint
|
||||
* `/api/auth/session`.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#sessionprovider)
|
||||
*/
|
||||
export type SessionProvider = React.FC<{
|
||||
children: React.ReactNode
|
||||
session?: Session
|
||||
options?: SessionProviderOptions
|
||||
}>
|
||||
|
||||
/**
|
||||
* Provider to wrap the app in to make session data available globally.
|
||||
* Can also be used to throttle the number of requests to the endpoint
|
||||
* `/api/auth/session`.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
|
||||
*/
|
||||
export const Provider: SessionProvider
|
||||
|
||||
/** @docs: https://next-auth.js.org/getting-started/client#options */
|
||||
export function setOptions(options: SessionProviderOptions): void
|
||||
|
||||
/**
|
||||
* Alias for `setOptions`
|
||||
* @docs: https://next-auth.js.org/getting-started/client#options
|
||||
*/
|
||||
export const options: typeof setOptions
|
||||
export const SessionProvider: React.FC<SessionProviderProps>
|
||||
@@ -1,26 +0,0 @@
|
||||
import Adapters from "next-auth/adapters"
|
||||
|
||||
// ExpectType TypeORMAdapter["Adapter"]
|
||||
Adapters.Default({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
|
||||
// ExpectType TypeORMAdapter
|
||||
Adapters.TypeORM.Adapter({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
|
||||
// ExpectType PrismaAdapter
|
||||
Adapters.Prisma.Adapter({
|
||||
prisma: {},
|
||||
modelMapping: {
|
||||
User: "foo",
|
||||
Account: "bar",
|
||||
Session: "session",
|
||||
VerificationRequest: "foo",
|
||||
},
|
||||
})
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as client from "next-auth/client"
|
||||
import * as client from "next-auth/react"
|
||||
import { nextReq } from "./test-helpers"
|
||||
|
||||
const clientSession = {
|
||||
@@ -11,87 +11,89 @@ const clientSession = {
|
||||
expires: "1234",
|
||||
}
|
||||
|
||||
// $ExpectType [Session | null, boolean]
|
||||
/**
|
||||
* $ExpectType
|
||||
* | { data: Session; status: "authenticated"; }
|
||||
* | { data: null; status: "unauthenticated" | "loading"; }
|
||||
* | { //// data: Session; status: "authenticated"; }
|
||||
* | { data: null; status: "loading"; }
|
||||
*/
|
||||
client.useSession()
|
||||
|
||||
// $ExpectType { data: Session; status: "authenticated"; } | { data: null; status: "loading"; }
|
||||
const session = client.useSession({ required: true })
|
||||
if (session.status === "loading") {
|
||||
// $ExpectType null
|
||||
session.data
|
||||
} else {
|
||||
// $ExpectType Session
|
||||
session.data
|
||||
}
|
||||
|
||||
// $ExpectType Promise<Session | null>
|
||||
client.getSession({ req: nextReq })
|
||||
|
||||
// $ExpectType Promise<Session | null>
|
||||
client.session({ req: nextReq })
|
||||
|
||||
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
|
||||
client.getProviders()
|
||||
|
||||
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
|
||||
client.providers()
|
||||
|
||||
// $ExpectType Promise<string | null>
|
||||
client.getCsrfToken({ req: nextReq })
|
||||
|
||||
// $ExpectType Promise<string | null>
|
||||
client.csrfToken({ req: nextReq })
|
||||
|
||||
// $ExpectType Promise<string | null>
|
||||
client.csrfToken({ ctx: { req: nextReq } })
|
||||
client.getCsrfToken({ ctx: { req: nextReq } })
|
||||
|
||||
// $ExpectType Promise<undefined>
|
||||
client.signin("github", { callbackUrl: "foo" }, { login: "username" })
|
||||
client.signIn("github", { callbackUrl: "foo" }, { login: "username" })
|
||||
|
||||
// $ExpectType Promise<SignInResponse | undefined>
|
||||
client.signin("credentials", { callbackUrl: "foo", redirect: true })
|
||||
client.signIn("credentials", { callbackUrl: "foo", redirect: true })
|
||||
|
||||
// $ExpectType Promise<SignInResponse | undefined>
|
||||
client.signin("credentials", { redirect: false })
|
||||
client.signIn("credentials", { redirect: false })
|
||||
|
||||
// $ExpectType Promise<SignInResponse | undefined>
|
||||
client.signin("email", { callbackUrl: "foo", redirect: false })
|
||||
client.signIn("email", { callbackUrl: "foo", redirect: false })
|
||||
|
||||
// $ExpectType Promise<SignInResponse | undefined>
|
||||
client.signin("email", { callbackUrl: "foo", redirect: true })
|
||||
client.signIn("email", { callbackUrl: "foo", redirect: true })
|
||||
|
||||
// $ExpectType Promise<undefined>
|
||||
client.signout()
|
||||
client.signOut()
|
||||
|
||||
// $ExpectType Promise<undefined>
|
||||
client.signout({ callbackUrl: "https://foo.com/callback", redirect: true })
|
||||
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: true })
|
||||
|
||||
// $ExpectType Promise<SignOutResponse>
|
||||
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: false })
|
||||
|
||||
// $ExpectType ReactElement<any, any> | null
|
||||
client.Provider({
|
||||
client.SessionProvider({
|
||||
children: null,
|
||||
session: clientSession,
|
||||
options: {
|
||||
baseUrl: "https://foo.com",
|
||||
basePath: "/",
|
||||
clientMaxAge: 1234,
|
||||
},
|
||||
baseUrl: "https://foo.com",
|
||||
basePath: "/",
|
||||
staleTime: 1234,
|
||||
})
|
||||
|
||||
// $ExpectType ReactElement<any, any> | null
|
||||
client.Provider({
|
||||
client.SessionProvider({
|
||||
children: null,
|
||||
session: clientSession,
|
||||
})
|
||||
|
||||
// $ExpectType ReactElement<any, any> | null
|
||||
client.Provider({
|
||||
client.SessionProvider({
|
||||
children: null,
|
||||
options: {},
|
||||
})
|
||||
|
||||
// $ExpectType ReactElement<any, any> | null
|
||||
client.Provider({
|
||||
client.SessionProvider({
|
||||
children: null,
|
||||
session: {
|
||||
expires: "",
|
||||
},
|
||||
options: {
|
||||
baseUrl: "https://foo.com",
|
||||
basePath: "/",
|
||||
clientMaxAge: 1234,
|
||||
keepAlive: 4321,
|
||||
},
|
||||
baseUrl: "https://foo.com",
|
||||
basePath: "/",
|
||||
staleTime: 1234,
|
||||
refetchInterval: 4321,
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import NextAuth, * as NextAuthTypes from "next-auth"
|
||||
import { IncomingMessage, ServerResponse } from "http"
|
||||
import { Socket } from "net"
|
||||
import { NextApiRequest, NextApiResponse } from "internals/utils"
|
||||
import { AppOptions } from "internals"
|
||||
import { InternalOptions } from "internals"
|
||||
|
||||
const req: NextApiRequest = Object.assign(new IncomingMessage(new Socket()), {
|
||||
query: {},
|
||||
@@ -62,7 +62,7 @@ const exampleVerificationRequest = {
|
||||
|
||||
const MyAdapter: Adapter<Record<string, unknown>> = () => {
|
||||
return {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
async getAdapter(appOptions: InternalOptions) {
|
||||
return {
|
||||
async createUser(profile) {
|
||||
return exampleUser
|
||||
@@ -135,7 +135,6 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
|
||||
clientSecret: "123",
|
||||
}),
|
||||
],
|
||||
database: "path/to/db",
|
||||
debug: true,
|
||||
secret: "my secret",
|
||||
session: {
|
||||
@@ -154,16 +153,16 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
|
||||
},
|
||||
pages: pageOptions,
|
||||
callbacks: {
|
||||
async signIn(user, account, profile) {
|
||||
async signIn({ user, account, email, credentials, profile }) {
|
||||
return true
|
||||
},
|
||||
async redirect(url, baseUrl) {
|
||||
async redirect({ url, baseUrl }) {
|
||||
return "path/to/foo"
|
||||
},
|
||||
async session(session, userOrToken) {
|
||||
return { ...session }
|
||||
async session({ session, user, token }) {
|
||||
return session
|
||||
},
|
||||
async jwt(token, user, account, profile, isNewUser) {
|
||||
async jwt({ token, user, account, profile, isNewUser }) {
|
||||
return token
|
||||
},
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"next-auth": ["."],
|
||||
"next-auth/providers": ["./providers"],
|
||||
"next-auth/adapters": ["./adapters"],
|
||||
"next-auth/client": ["./client"],
|
||||
"next-auth/react": ["./react-client"],
|
||||
"next-auth/jwt": ["./jwt"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ You can find the full schema in the table structure section below.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter@canary`
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/dynamodb-adapter@canary
|
||||
npm install next-auth @next-auth/dynamodb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
@@ -11,10 +11,10 @@ You can find the Fauna schema and seed information in the docs at [next-auth.js.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/fauna-adapter@canary`
|
||||
1. Install `next-auth` and `@next-auth/fauna-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/fauna-adapter@canary
|
||||
npm install next-auth @next-auth/fauna-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
@@ -9,10 +9,10 @@ This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This p
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter@canary`
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/firebase-adapter@canary
|
||||
npm install next-auth @next-auth/firebase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
@@ -11,7 +11,6 @@ All table/collection names in the built in models are plural, and all table name
|
||||
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
|
||||
:::
|
||||
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
@@ -30,7 +29,7 @@ If a user first signs in with OAuth then their email address is automatically po
|
||||
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured).
|
||||
:::
|
||||
|
||||
## Account
|
||||
## Account
|
||||
|
||||
Table: `accounts`
|
||||
|
||||
@@ -60,4 +59,4 @@ The Verification Request model is used to store tokens for passwordless sign in
|
||||
|
||||
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
|
||||
|
||||
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
|
||||
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
|
||||
|
||||
@@ -15,6 +15,7 @@ There you can find the following adapters:
|
||||
- [`fauna`](./fauna)
|
||||
- [`dynamodb`](./dynamodb)
|
||||
- [`firebase`](./firebase)
|
||||
- [`pouchdb`](./pouchdb)
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
|
||||
|
||||
> **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@canary`
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/pouchdb-adapter@canary
|
||||
npm install next-auth @next-auth/pouchdb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
|
||||
@@ -28,10 +28,9 @@ 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
|
||||
|
||||
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
|
||||
@@ -49,7 +48,7 @@ export default NextAuth({
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced
|
||||
## Advanced
|
||||
|
||||
### Memory-First Caching Strategy
|
||||
|
||||
@@ -60,4 +59,3 @@ Use an in-memory PouchDB as your main authentication database, and synchronize i
|
||||
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
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Configure your NextAuth.js to use the Prisma Adapter:
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import Adapters from "next-auth/adapters"
|
||||
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
@@ -35,7 +35,7 @@ export default NextAuth({
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||
adapter: PrismaLegacyAdapter({ prisma }),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -161,7 +161,7 @@ You can use custom model names by using the `modelMapping` option (shown here wi
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
adapter: Adapters.Prisma.Adapter({
|
||||
adapter: PrismaLegacyAdapter({
|
||||
prisma,
|
||||
modelMapping: {
|
||||
User: 'user',
|
||||
|
||||
@@ -11,10 +11,10 @@ You can also use NextAuth.js with the new experimental Adapter for [Prisma](http
|
||||
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@canary` package:
|
||||
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@canary
|
||||
npm install @prisma/client @next-auth/prisma-adapter
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
|
||||
@@ -19,4 +19,4 @@ Objects stored in MongoDB use similar datatypes to SQL, with some differences:
|
||||
|
||||
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
|
||||
|
||||
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
|
||||
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
---
|
||||
id: mssql
|
||||
title: Microsoft SQL Server
|
||||
---
|
||||
|
||||
Schema for a Microsoft SQL Server (mssql) database.
|
||||
|
||||
:::note
|
||||
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
|
||||
|
||||
This transform is also applied to any properties of type `timestamp` when using custom models.
|
||||
:::
|
||||
|
||||
```sql
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
compound_id varchar(255) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
provider_type varchar(255) NOT NULL,
|
||||
provider_id varchar(255) NOT NULL,
|
||||
provider_account_id varchar(255) NOT NULL,
|
||||
refresh_token text NULL,
|
||||
access_token text NULL,
|
||||
access_token_expires datetime NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
session_token varchar(255) NOT NULL,
|
||||
access_token varchar(255) NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
name varchar(255) NULL,
|
||||
email varchar(255) NULL,
|
||||
email_verified datetime NULL,
|
||||
image varchar(255) NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
identifier varchar(255) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
|
||||
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
|
||||
:::
|
||||
---
|
||||
id: mssql
|
||||
title: Microsoft SQL Server
|
||||
---
|
||||
|
||||
Schema for a Microsoft SQL Server (mssql) database.
|
||||
|
||||
:::note
|
||||
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
|
||||
|
||||
This transform is also applied to any properties of type `timestamp` when using custom models.
|
||||
:::
|
||||
|
||||
```sql
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
compound_id varchar(255) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
provider_type varchar(255) NOT NULL,
|
||||
provider_id varchar(255) NOT NULL,
|
||||
provider_account_id varchar(255) NOT NULL,
|
||||
refresh_token text NULL,
|
||||
access_token text NULL,
|
||||
access_token_expires datetime NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
session_token varchar(255) NOT NULL,
|
||||
access_token varchar(255) NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
name varchar(255) NULL,
|
||||
email varchar(255) NULL,
|
||||
email_verified datetime NULL,
|
||||
image varchar(255) NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
identifier varchar(255) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
|
||||
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
|
||||
:::
|
||||
|
||||
@@ -84,4 +84,4 @@ CREATE UNIQUE INDEX email
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user