mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
63 Commits
next-auth@
...
v4.0.0-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb8ba69d3b | ||
|
|
a9f699fd54 | ||
|
|
e15bf9b2f5 | ||
|
|
e06ced5b66 | ||
|
|
2c43fbd867 | ||
|
|
78d8f28968 | ||
|
|
8914f88cd7 | ||
|
|
55132e5da2 | ||
|
|
65040dcc83 | ||
|
|
92b9d22309 | ||
|
|
b50a2eb845 | ||
|
|
e5fe470792 | ||
|
|
7c65bda6f1 | ||
|
|
f06e4d286b | ||
|
|
bececbc200 | ||
|
|
6d74da1f65 | ||
|
|
3312e53279 | ||
|
|
ebf420c84a | ||
|
|
111d5fc572 | ||
|
|
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 }}
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -26,8 +26,13 @@ node_modules
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
www/providers.json
|
||||
src/providers/index.js
|
||||
/internals
|
||||
/providers
|
||||
/types/providers/*
|
||||
!types/providers/index.d.ts
|
||||
!types/providers/email.d.ts
|
||||
!types/providers/credentials.d.ts
|
||||
!types/providers/oauth.d.ts
|
||||
/adapters.d.ts
|
||||
/adapters.js
|
||||
/client.d.ts
|
||||
@@ -36,16 +41,18 @@ src/providers/index.js
|
||||
/index.js
|
||||
/jwt.d.ts
|
||||
/jwt.js
|
||||
/providers.d.ts
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
/react.js
|
||||
/react.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
app/dist/css
|
||||
app/package-lock.json
|
||||
app/yarn.lock
|
||||
app/prisma/migrations
|
||||
app/prisma/dev.db*
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
@@ -61,4 +68,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
|
||||
|
||||
@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
@@ -55,11 +55,11 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting me@iaincollins.com. All complaints will be reviewed and
|
||||
investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. The project team is obligated to maintain
|
||||
confidentiality with regard to the reporter of an incident. Further details of
|
||||
specific enforcement policies may be posted separately.
|
||||
reported by contacting me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
|
||||
All complaints will be reviewed and investigated and will result in a response
|
||||
that is deemed necessary and appropriate to the circumstances. The project team
|
||||
is obligated to maintain confidentiality with regard to the reporter of an
|
||||
incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
|
||||
@@ -11,43 +11,49 @@ Please raise any significant new functionality or breaking change an issue for d
|
||||
## For contributors
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
- The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
- Pull Requests should be raised for any change
|
||||
- Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
- We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
|
||||
- We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
- If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
|
||||
### Setting up local environment
|
||||
|
||||
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
|
||||
|
||||
The dev application requires you to use `npm@7`.
|
||||
|
||||
1. Clone the repo:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
2. Install packages:
|
||||
2. Install packages, set up the dev application:
|
||||
|
||||
```sh
|
||||
npm i && npm run dev:setup
|
||||
npm run dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
Your dev application will be available on `http://localhost:3000`
|
||||
|
||||
That's it! 🎉
|
||||
|
||||
@@ -64,6 +70,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
|
||||
#### Providers
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
|
||||
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
@@ -73,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
|
||||
|
||||
|
||||
154
README.md
154
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)
|
||||
|
||||
@@ -145,14 +165,44 @@ export default function Component() {
|
||||
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
|
||||
</a>
|
||||
<div>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
|
||||
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
|
||||
</div>
|
||||
|
||||
### Support
|
||||
|
||||
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
|
||||
|
||||
<!--sponsors start-->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://vercel.com" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
|
||||
</a><br />
|
||||
<div>Vercel</div><br />
|
||||
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://prisma.io" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
|
||||
</a><br />
|
||||
<div>Prisma</div><br />
|
||||
<sub>🥉 Bronze Financial Sponsor</sub>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://checklyhq.com" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
|
||||
</a><br />
|
||||
<div>Checkly</div><br />
|
||||
<sub>☁️ Infrastructure Support</sub>
|
||||
</td>
|
||||
</tr><tr></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br />
|
||||
<!--sponsors end-->
|
||||
|
||||
## Contributing
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
|
||||
10
SECURITY.md
10
SECURITY.md
@@ -14,11 +14,11 @@ We request that you contact us directly to report serious issues that might impa
|
||||
|
||||
If you contact us regarding a serious issue:
|
||||
|
||||
* We will endeavor to get back to you within 72 hours.
|
||||
* We will aim to publish a fix within 30 days.
|
||||
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
|
||||
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue 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
|
||||
Currently, the best way to report an issue is by contacting us via email at me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
|
||||
|
||||
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.
|
||||
|
||||
@@ -6,28 +6,26 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
# and/or verification tokens.
|
||||
SECRET=secret
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_DOMAIN=
|
||||
AUTH0_SECRET=
|
||||
AUTH0_ISSUER=
|
||||
|
||||
KEYCLOAK_ID=
|
||||
KEYCLOAK_SECRET=
|
||||
KEYCLOAK_ISSUER=
|
||||
|
||||
IDS4_ID=
|
||||
IDS4_SECRET=
|
||||
IDS4_ISSUER=
|
||||
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
TWITCH_ID=
|
||||
TWITCH_SECRET=
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
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
|
||||
# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true
|
||||
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
|
||||
DATABASE_URL=
|
||||
TWITTER_SECRET=
|
||||
@@ -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>
|
||||
|
||||
4
app/next-env.d.ts
vendored
4
app/next-env.d.ts
vendored
@@ -1,2 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -10,16 +10,24 @@
|
||||
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
|
||||
"watch:css": "cd .. && npm run watch:css",
|
||||
"dev:css": "npm-run-all --parallel watch:css copy:css",
|
||||
"start": "next start"
|
||||
"start": "next start",
|
||||
"start:email": "npx fake-smtp-server"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"@next-auth/fauna-adapter": "0.2.2-next.4",
|
||||
"@next-auth/prisma-adapter": "0.5.2-next.5",
|
||||
"@prisma/client": "^2.29.1",
|
||||
"fake-smtp-server": "^0.8.0",
|
||||
"faunadb": "^4.3.0",
|
||||
"next": "^11.1.0",
|
||||
"nodemailer": "^6.6.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "^2.29.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import NextAuth from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
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: {
|
||||
// csrfToken: {
|
||||
// name: 'next-auth.csrf-token',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// },
|
||||
// pkceCodeVerifier: {
|
||||
// name: 'next-auth.pkce.code_verifier',
|
||||
// options: {
|
||||
// httpOnly: true,
|
||||
// sameSite: 'none',
|
||||
// path: '/',
|
||||
// secure: true
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
EmailProvider({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
|
||||
// protection: ["pkce", "state"],
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: "pkce",
|
||||
}),
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
if (credentials.password === "password") {
|
||||
return {
|
||||
id: 1,
|
||||
name: "Fill Murray",
|
||||
email: "bill@fillmurray.com",
|
||||
image: "https://www.fillmurray.com/64/64",
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
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 })
|
||||
})
|
||||
142
app/pages/api/auth/[...nextauth].ts
Normal file
142
app/pages/api/auth/[...nextauth].ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import NextAuth from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import Auth0Provider from "next-auth/providers/auth0"
|
||||
import KeycloakProvider from "next-auth/providers/keycloak"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import IDS4Provider from "next-auth/providers/identity-server4"
|
||||
import Twitch from "next-auth/providers/twitch"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import FacebookProvider from "next-auth/providers/facebook"
|
||||
import FoursquareProvider from "next-auth/providers/foursquare"
|
||||
// import FreshbooksProvider from "next-auth/providers/freshbooks"
|
||||
import GitlabProvider from "next-auth/providers/gitlab"
|
||||
import InstagramProvider from "next-auth/providers/instagram"
|
||||
import LineProvider from "next-auth/providers/line"
|
||||
import LinkedInProvider from "next-auth/providers/linkedin"
|
||||
import MailchimpProvider from "next-auth/providers/mailchimp"
|
||||
import DiscordProvider from "next-auth/providers/discord"
|
||||
|
||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
// const prisma = new PrismaClient()
|
||||
// const adapter = PrismaAdapter(prisma)
|
||||
|
||||
// import { Client as FaunaClient } from "faunadb"
|
||||
// import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
|
||||
// const client = new FaunaClient({
|
||||
// secret: process.env.FAUNA_SECRET,
|
||||
// domain: process.env.FAUNA_DOMAIN,
|
||||
// })
|
||||
// const adapter = FaunaAdapter(client)
|
||||
|
||||
export default NextAuth({
|
||||
// adapter,
|
||||
providers: [
|
||||
// E-mail
|
||||
// Start fake e-mail server with `npm run start:email`
|
||||
EmailProvider({
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
auth: null,
|
||||
secure: false,
|
||||
port: 1025,
|
||||
tls: { rejectUnauthorized: false },
|
||||
},
|
||||
}),
|
||||
// Credentials
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
if (credentials.password === "password") {
|
||||
return {
|
||||
name: "Fill Murray",
|
||||
email: "bill@fillmurray.com",
|
||||
image: "https://www.fillmurray.com/64/64",
|
||||
}
|
||||
}
|
||||
return null
|
||||
},
|
||||
}),
|
||||
// OAuth 1
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
// OAuth 2 / OIDC
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
issuer: process.env.AUTH0_ISSUER,
|
||||
}),
|
||||
KeycloakProvider({
|
||||
clientId: process.env.KEYCLOAK_ID,
|
||||
clientSecret: process.env.KEYCLOAK_SECRET,
|
||||
issuer: process.env.KEYCLOAK_ISSUER,
|
||||
}),
|
||||
Twitch({
|
||||
clientId: process.env.TWITCH_ID,
|
||||
clientSecret: process.env.TWITCH_SECRET,
|
||||
}),
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
FacebookProvider({
|
||||
clientId: process.env.FACEBOOK_ID,
|
||||
clientSecret: process.env.FACEBOOK_SECRET,
|
||||
}),
|
||||
FoursquareProvider({
|
||||
clientId: process.env.FOURSQUARE_ID,
|
||||
clientSecret: process.env.FOURSQUARE_SECRET,
|
||||
}),
|
||||
// FreshbooksProvider({
|
||||
// clientId: process.env.FRESHBOOKS_ID,
|
||||
// clientSecret: process.env.FRESHBOOKS_SECRET,
|
||||
// }),
|
||||
GitlabProvider({
|
||||
clientId: process.env.GITLAB_ID,
|
||||
clientSecret: process.env.GITLAB_SECRET,
|
||||
}),
|
||||
InstagramProvider({
|
||||
clientId: process.env.INSTAGRAM_ID,
|
||||
clientSecret: process.env.INSTAGRAM_SECRET,
|
||||
}),
|
||||
LineProvider({
|
||||
clientId: process.env.LINE_ID,
|
||||
clientSecret: process.env.LINE_SECRET,
|
||||
}),
|
||||
LinkedInProvider({
|
||||
clientId: process.env.LINKEDIN_ID,
|
||||
clientSecret: process.env.LINKEDIN_SECRET,
|
||||
}),
|
||||
MailchimpProvider({
|
||||
clientId: process.env.MAILCHIMP_ID,
|
||||
clientSecret: process.env.MAILCHIMP_SECRET,
|
||||
}),
|
||||
IDS4Provider({
|
||||
clientId: process.env.IDS4_ID,
|
||||
clientSecret: process.env.IDS4_SECRET,
|
||||
issuer: process.env.IDS4_ISSUER,
|
||||
}),
|
||||
DiscordProvider({
|
||||
clientId: process.env.DISCORD_ID,
|
||||
clientSecret: process.env.DISCORD_SECRET,
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
debug: true,
|
||||
theme: "auto",
|
||||
})
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,57 @@
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
providerId String @map(name: "provider_id")
|
||||
providerAccountId String @map(name: "provider_account_id")
|
||||
refreshToken String? @map(name: "refresh_token")
|
||||
accessToken String? @map(name: "access_token")
|
||||
accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String?
|
||||
session_state String?
|
||||
oauth_token_secret String?
|
||||
oauth_token String?
|
||||
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map(name: "accounts")
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
userId Int @map(name: "user_id")
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
accessToken String @unique @map(name: "access_token")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "sessions")
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "users")
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
30
app/tsconfig.json
Normal file
30
app/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -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,22 +3,20 @@ const path = require("path")
|
||||
|
||||
const MODULE_ENTRIES = {
|
||||
SERVER: "index",
|
||||
CLIENT: "client",
|
||||
PROVIDERS: "providers",
|
||||
REACT: "react",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
ERRORS: "errors",
|
||||
}
|
||||
|
||||
// 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.JWT}.js`]:
|
||||
"module.exports = require('./dist/lib/jwt').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
@@ -29,40 +27,44 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
})
|
||||
|
||||
// Building types
|
||||
fs.mkdirSync("providers", { recursive: true })
|
||||
|
||||
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`,
|
||||
"providers",
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ERRORS}.d.ts`,
|
||||
"internals",
|
||||
]
|
||||
|
||||
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`)
|
||||
console.log(`[types] copying "${target}" to root folder`)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Building providers
|
||||
// Generate provider types
|
||||
|
||||
const providersDir = path.join(process.cwd(), "/src/providers")
|
||||
const providersDirPath = path.join(process.cwd(), "/src/providers")
|
||||
|
||||
const files = fs
|
||||
.readdirSync(providersDir, "utf8")
|
||||
.filter((file) => file !== "index.js")
|
||||
const oauthProviderFiles = fs
|
||||
.readdirSync(providersDirPath, "utf8")
|
||||
.filter((f) => f !== "credentials.js" && f !== "email.js")
|
||||
|
||||
let importLines = ""
|
||||
let exportLines = `export default {\n`
|
||||
files.forEach((file) => {
|
||||
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
|
||||
let type = `export type OAuthProviderType =\n`
|
||||
|
||||
oauthProviderFiles.forEach((file) => {
|
||||
const provider = fs.readFileSync(path.join(providersDirPath, file), "utf8")
|
||||
const fileName = file.split(".")[0]
|
||||
try {
|
||||
// NOTE: If this fails, the default export probably wasn't a named function.
|
||||
// Always use a named function as default export.
|
||||
@@ -71,8 +73,24 @@ files.forEach((file) => {
|
||||
/export default function (?<functionName>.+)\s?\(/
|
||||
).groups
|
||||
|
||||
importLines += `import ${functionName} from "./${file}"\n`
|
||||
exportLines += ` ${functionName},\n`
|
||||
type += ` | "${functionName}"\n`
|
||||
const providerModule = `import { OAuthConfig } from "."
|
||||
|
||||
declare module "next-auth/providers/${fileName}" {
|
||||
export default function ${functionName}Provider(
|
||||
options: Partial<OAuthConfig>
|
||||
): OAuthConfig
|
||||
}
|
||||
`
|
||||
|
||||
fs.writeFile(
|
||||
path.join("types/providers", `${fileName}.d.ts`),
|
||||
providerModule
|
||||
)
|
||||
|
||||
console.log(
|
||||
`[types] created module declaration for "${functionName}" provider`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
[
|
||||
@@ -83,9 +101,8 @@ files.forEach((file) => {
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
exportLines += `}\n`
|
||||
|
||||
fs.writeFile(
|
||||
path.join(process.cwd(), "src/providers/index.js"),
|
||||
[importLines, exportLines].join("\n")
|
||||
path.join(process.cwd(), "types/providers/oauth-providers.d.ts"),
|
||||
type
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
33169
package-lock.json
generated
33169
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
129
package.json
129
package.json
@@ -22,17 +22,14 @@
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
"./react": "./dist/client/react.js",
|
||||
"./providers/*": "./dist/providers/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"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,93 +39,82 @@
|
||||
"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",
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"providers.js",
|
||||
"providers.d.ts",
|
||||
"adapters.js",
|
||||
"providers",
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"react.js",
|
||||
"react.d.ts",
|
||||
"jwt.js",
|
||||
"jwt.d.ts",
|
||||
"internals"
|
||||
],
|
||||
"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"
|
||||
"openid-client": "^4.7.4",
|
||||
"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"
|
||||
"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/oauth": "^0.9.1",
|
||||
"@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": {
|
||||
@@ -157,6 +143,11 @@
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/strict-boolean-expressions": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { UnknownError } from "../lib/errors"
|
||||
|
||||
/**
|
||||
* Handles adapter induced errors.
|
||||
* @param {import("types/adapters").AdapterInstance} adapter
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @return {import("types/adapters").AdapterInstance}
|
||||
*/
|
||||
export default function adapterErrorHandler(adapter, logger) {
|
||||
return Object.keys(adapter).reduce((acc, method) => {
|
||||
const name = capitalize(method)
|
||||
const code = upperSnake(name, adapter.displayName)
|
||||
|
||||
const adapterMethod = adapter[method]
|
||||
acc[method] = async (...args) => {
|
||||
try {
|
||||
logger.debug(code, ...args)
|
||||
return await adapterMethod(...args)
|
||||
} catch (error) {
|
||||
logger.error(`${code}_ERROR`, error)
|
||||
const e = new UnknownError(error)
|
||||
e.name = `${name}Error`
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
function upperSnake(s, prefix = "ADAPTER") {
|
||||
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
|
||||
}
|
||||
@@ -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() })
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -33,66 +33,55 @@ export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
export function upperSnake(s) {
|
||||
return s.replace(/([A-Z])/g, "_$1").toUpperCase()
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
export function capitalize(s) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
/**
|
||||
* Wraps an object of methods and adds error handling.
|
||||
* @param {import("types").EventCallbacks} methods
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @return {import("types").EventCallbacks}
|
||||
*/
|
||||
export function eventsErrorHandler(methods, logger) {
|
||||
return Object.entries(methods).reduce((acc, [name, method]) => {
|
||||
acc[name] = async (...args) => {
|
||||
try {
|
||||
return await method(...args)
|
||||
} catch (e) {
|
||||
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
/**
|
||||
* Handles adapter induced errors.
|
||||
* @param {import("types/adapters").Adapter} [adapter]
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @return {import("types/adapters").Adapter}
|
||||
*/
|
||||
export function adapterErrorHandler(adapter, logger) {
|
||||
if (!adapter) return
|
||||
|
||||
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"
|
||||
return Object.keys(adapter).reduce((acc, method) => {
|
||||
acc[method] = async (...args) => {
|
||||
try {
|
||||
logger.debug(`adapter_${method}`, ...args)
|
||||
const adapterMethod = adapter[method]
|
||||
return await adapterMethod(...args)
|
||||
} catch (error) {
|
||||
logger.error(`adapter_error_${method}`, error)
|
||||
const e = new UnknownError(error)
|
||||
e.name = `${capitalize(method)}Error`
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
@@ -200,9 +200,3 @@ function getDerivedEncryptionKey(secret) {
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getToken,
|
||||
}
|
||||
|
||||
@@ -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()}]`,
|
||||
`[next-auth][error][${code}]`,
|
||||
`\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
|
||||
`[next-auth][warn][${code}]`,
|
||||
`\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}]`, 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
|
||||
|
||||
33
src/lib/merge.js
Normal file
33
src/lib/merge.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Source: https://stackoverflow.com/a/34749873/5364135
|
||||
|
||||
/**
|
||||
* Simple object check.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isObject(item) {
|
||||
return item && typeof item === "object" && !Array.isArray(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects.
|
||||
* @param target
|
||||
* @param ...sources
|
||||
*/
|
||||
export function merge(target, ...sources) {
|
||||
if (!sources.length) return target
|
||||
const source = sources.shift()
|
||||
|
||||
if (isObject(target) && isObject(source)) {
|
||||
for (const key in source) {
|
||||
if (isObject(source[key])) {
|
||||
if (!target[key]) Object.assign(target, { [key]: {} })
|
||||
merge(target[key], source[key])
|
||||
} else {
|
||||
Object.assign(target, { [key]: source[key] })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merge(target, ...sources)
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: '42-school',
|
||||
name: '42 School',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://api.intra.42.fr/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.intra.42.fr/v2/me',
|
||||
profile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
name: profile.usual_full_name,
|
||||
}),
|
||||
...options,
|
||||
id: "42-school",
|
||||
name: "42 School",
|
||||
type: "oauth",
|
||||
authorization: "https://api.intra.42.fr/oauth/authorize",
|
||||
token: "https://api.intra.42.fr/oauth/token",
|
||||
userinfo: "https://api.intra.42.fr/v2/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.usual_full_name,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,32 +3,34 @@ export default function Apple(options) {
|
||||
id: "apple",
|
||||
name: "Apple",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "name email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://appleid.apple.com/auth/token",
|
||||
authorizationUrl:
|
||||
"https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post",
|
||||
profileUrl: null,
|
||||
idToken: true,
|
||||
authorization: {
|
||||
url: "https://appleid.apple.com/auth/authorize",
|
||||
params: {
|
||||
scope: "name email",
|
||||
response_type: "code",
|
||||
id_token: "",
|
||||
response_mode: "form_post",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: "https://appleid.apple.com/auth/token",
|
||||
idToken: true,
|
||||
},
|
||||
jwks_endpoint: "https://appleid.apple.com/auth/keys",
|
||||
profile(profile) {
|
||||
// The name of the user will only return on first login
|
||||
// The name of the user will only be returned on first login
|
||||
const name = profile.user
|
||||
? profile.user.name.firstName + " " + profile.user.name.lastName
|
||||
: null
|
||||
|
||||
return {
|
||||
id: profile.sub,
|
||||
name:
|
||||
profile.user != null
|
||||
? profile.user.name.firstName + " " + profile.user.name.lastName
|
||||
: null,
|
||||
name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: {
|
||||
teamId: null,
|
||||
privateKey: null,
|
||||
keyId: null,
|
||||
},
|
||||
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
...options,
|
||||
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,15 @@ export default function Atlassian(options) {
|
||||
id: "atlassian",
|
||||
name: "Atlassian",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
authorization: {
|
||||
url: "https://auth.atlassian.com/oauth/authorize",
|
||||
params: {
|
||||
audience: "api.atlassian.com",
|
||||
prompt: "consent",
|
||||
},
|
||||
},
|
||||
accessTokenUrl: "https://auth.atlassian.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent",
|
||||
profileUrl: "https://api.atlassian.com/me",
|
||||
token: "https://auth.atlassian.com/oauth/token",
|
||||
userinfo: "https://api.atlassian.com/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
@@ -19,6 +20,6 @@ export default function Atlassian(options) {
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/** @type {import("types/providers").OAuthProvider} */
|
||||
export default function Auth0(options) {
|
||||
return {
|
||||
id: "auth0",
|
||||
name: "Auth0",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
scope: "openid email profile",
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/userinfo`,
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
@@ -17,6 +16,6 @@ export default function Auth0(options) {
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
export default function AzureADB2C(options) {
|
||||
const tenant = options.tenantId ? options.tenantId : "common"
|
||||
const { tenantName, primaryUserFlow } = options
|
||||
|
||||
return {
|
||||
id: "azure-ad-b2c",
|
||||
name: "Azure Active Directory B2C",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
authorization: {
|
||||
url: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`,
|
||||
params: {
|
||||
response_type: "code id_token",
|
||||
response_mode: "query",
|
||||
},
|
||||
},
|
||||
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/",
|
||||
token: {
|
||||
url: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`,
|
||||
idToken: true,
|
||||
},
|
||||
jwks_uri: `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}}/discovery/v2.0/keys`,
|
||||
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,
|
||||
id: profile.oid,
|
||||
name,
|
||||
email: profile.emails[0],
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
21
src/providers/azure-ad.js
Normal file
21
src/providers/azure-ad.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function AzureAD(options) {
|
||||
const tenant = options.tenantId ?? "common"
|
||||
|
||||
return {
|
||||
id: "azure-ad",
|
||||
name: "Azure Active Directory",
|
||||
type: "oauth",
|
||||
authorization: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_mode=query`,
|
||||
token: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||
userinfo: "https://graph.microsoft.com/v1.0/me/",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.displayName,
|
||||
email: profile.userPrincipalName,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export default function Basecamp(options) {
|
||||
return {
|
||||
id: "basecamp",
|
||||
name: "Basecamp",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
accessTokenUrl:
|
||||
"https://launchpad.37signals.com/authorization/token?type=web_server",
|
||||
authorizationUrl:
|
||||
"https://launchpad.37signals.com/authorization/new?type=web_server",
|
||||
profileUrl: "https://launchpad.37signals.com/authorization.json",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.identity.id,
|
||||
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
|
||||
email: profile.identity.email_address,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,17 @@
|
||||
export default function BattleNet(options) {
|
||||
const { region } = options
|
||||
const base =
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth"
|
||||
: `https://${region}.battle.net/oauth`
|
||||
|
||||
return {
|
||||
id: "battlenet",
|
||||
name: "Battle.net",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl:
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/token"
|
||||
: `https://${region}.battle.net/oauth/token`,
|
||||
authorizationUrl:
|
||||
region === "CN"
|
||||
? "https://www.battlenet.com.cn/oauth/authorize?response_type=code"
|
||||
: `https://${region}.battle.net/oauth/authorize?response_type=code`,
|
||||
profileUrl: "https://us.battle.net/oauth/userinfo",
|
||||
authorization: `${base}/authorize`,
|
||||
token: `${base}/token`,
|
||||
userinfo: "https://us.battle.net/oauth/userinfo",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -24,6 +20,6 @@ export default function BattleNet(options) {
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@ export default function Box(options) {
|
||||
id: "box",
|
||||
name: "Box",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.box.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://account.box.com/api/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://api.box.com/2.0/users/me",
|
||||
authorization: "https://account.box.com/api/oauth2/authorize",
|
||||
token: "https://api.box.com/oauth2/token",
|
||||
userinfo: "https://api.box.com/2.0/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -18,6 +14,6 @@ export default function Box(options) {
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,9 @@ export default function Bungie(options) {
|
||||
id: "bungie",
|
||||
name: "Bungie",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "",
|
||||
params: { reauth: "true", grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
|
||||
requestTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
|
||||
authorizationUrl:
|
||||
"https://www.bungie.net/en/OAuth/Authorize?response_type=code",
|
||||
profileUrl:
|
||||
authorization: "https://www.bungie.net/en/OAuth/Authorize?reauth=true",
|
||||
token: "https://www.bungie.net/platform/app/oauth/token/",
|
||||
userinfo:
|
||||
"https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/",
|
||||
profile(profile) {
|
||||
const { bungieNetUser: user } = profile.Response
|
||||
@@ -18,17 +13,12 @@ export default function Bungie(options) {
|
||||
return {
|
||||
id: user.membershipId,
|
||||
name: user.displayName,
|
||||
email: null,
|
||||
image: `https://www.bungie.net${
|
||||
user.profilePicturePath.startsWith("/") ? "" : "/"
|
||||
}${user.profilePicturePath}`,
|
||||
email: null,
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
"X-API-Key": null,
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
export default function Cognito(options) {
|
||||
const { domain } = options
|
||||
return {
|
||||
id: "cognito",
|
||||
name: "Cognito",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid profile email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${domain}/oauth2/token`,
|
||||
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/oauth2/userInfo`,
|
||||
authorization: `${options.issuer}oauth2/authorize?scope=openid+profile+email`,
|
||||
token: `${options.issuer}oauth2/token`,
|
||||
userinfo: `${options.issuer}oauth2/userInfo`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
@@ -18,6 +14,6 @@ export default function Cognito(options) {
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,18 @@ export default function Coinbase(options) {
|
||||
id: "coinbase",
|
||||
name: "Coinbase",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "wallet:user:email wallet:user:read",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
requestTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://www.coinbase.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.coinbase.com/v2/user",
|
||||
authorization:
|
||||
"https://www.coinbase.com/oauth/authorize?scope=wallet:user:email+wallet:user:read",
|
||||
token: "https://api.coinbase.com/oauth/token",
|
||||
userinfo: "https://api.coinbase.com/v2/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.name,
|
||||
email: profile.data.email,
|
||||
email: profile.data.email,
|
||||
image: profile.data.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ export default function Credentials(options) {
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,10 @@ export default function Discord(options) {
|
||||
id: "discord",
|
||||
name: "Discord",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "identify email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://discord.com/api/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://discord.com/api/oauth2/authorize?response_type=code&prompt=none",
|
||||
profileUrl: "https://discord.com/api/users/@me",
|
||||
authorization:
|
||||
"https://discord.com/api/oauth2/authorize?scope=identify+email",
|
||||
token: "https://discord.com/api/oauth2/token",
|
||||
userinfo: "https://discord.com/api/users/@me",
|
||||
profile(profile) {
|
||||
if (profile.avatar === null) {
|
||||
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
|
||||
@@ -21,10 +18,10 @@ export default function Discord(options) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
image: profile.image_url,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
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,22 @@
|
||||
*/
|
||||
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',
|
||||
authorizationUrl:
|
||||
'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) => {
|
||||
id: "dropbox",
|
||||
name: "Dropbox",
|
||||
type: "oauth",
|
||||
authorization:
|
||||
"https://www.dropbox.com/oauth2/authorize?token_access_type=offline&scope=account_info.read",
|
||||
token: "https://api.dropboxapi.com/oauth2/token",
|
||||
userinfo: "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
|
||||
}
|
||||
},
|
||||
protection: ["state", "pkce"],
|
||||
...options
|
||||
checks: ["state", "pkce"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
import nodemailer from "nodemailer"
|
||||
import logger from "../lib/logger"
|
||||
|
||||
/** @type {import("providers").EmailProvider} */
|
||||
export default function Email(options) {
|
||||
return {
|
||||
id: "email",
|
||||
@@ -17,49 +18,34 @@ export default function Email(options) {
|
||||
},
|
||||
from: "NextAuth <no-reply@example.com>",
|
||||
maxAge: 24 * 60 * 60,
|
||||
sendVerificationRequest,
|
||||
...options,
|
||||
async sendVerificationRequest({
|
||||
identifier: email,
|
||||
url,
|
||||
provider: { server, from },
|
||||
}) {
|
||||
const { host } = new URL(url)
|
||||
console.log(server)
|
||||
const transport = nodemailer.createTransport(server)
|
||||
await transport.sendMail({
|
||||
to: email,
|
||||
from,
|
||||
subject: `Sign in to ${host}`,
|
||||
text: text({ url, host }),
|
||||
html: html({ url, host, 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(
|
||||
{
|
||||
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()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
const html = ({ url, site, email }) => {
|
||||
function html({ url, host, email }) {
|
||||
// Insert invisible space into domains and email address to prevent both the
|
||||
// email address and the domain from being turned into a hyperlink by email
|
||||
// clients like Outlook and Apple mail, as this is confusing because it seems
|
||||
// like they are supposed to click on their email address to sign in.
|
||||
const escapedEmail = `${email.replace(/\./g, "​.")}`
|
||||
const escapedSite = `${site.replace(/\./g, "​.")}`
|
||||
const escapedHost = `${host.replace(/\./g, "​.")}`
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = "#f9f9f9"
|
||||
@@ -74,7 +60,7 @@ const html = ({ url, site, email }) => {
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedSite}</strong>
|
||||
<strong>${escapedHost}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -88,7 +74,7 @@ const html = ({ url, site, email }) => {
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
@@ -104,4 +90,6 @@ const html = ({ url, site, email }) => {
|
||||
}
|
||||
|
||||
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
|
||||
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
|
||||
function text({ url, host }) {
|
||||
return `Sign in to ${host}\n${url}\n\n`
|
||||
}
|
||||
|
||||
@@ -3,20 +3,17 @@ export default function EVEOnline(options) {
|
||||
id: "eveonline",
|
||||
name: "EVE Online",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://login.eveonline.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://login.eveonline.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://login.eveonline.com/oauth/verify",
|
||||
authorization: "https://login.eveonline.com/oauth/authorize",
|
||||
token: "https://login.eveonline.com/oauth/token",
|
||||
userinfo: "https://login.eveonline.com/oauth/verify",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
email: null,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
/** @type {import("types/providers").OAuthProvider} */
|
||||
export default function Facebook(options) {
|
||||
return {
|
||||
id: "facebook",
|
||||
name: "Facebook",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "email",
|
||||
accessTokenUrl: "https://graph.facebook.com/oauth/access_token",
|
||||
authorizationUrl:
|
||||
"https://www.facebook.com/v7.0/dialog/oauth?response_type=code",
|
||||
profileUrl: "https://graph.facebook.com/me?fields=email,name,picture",
|
||||
authorization: "https://www.facebook.com/v11.0/dialog/oauth?scope=email",
|
||||
token: "https://graph.facebook.com/oauth/access_token",
|
||||
userinfo: {
|
||||
url: "https://graph.facebook.com/me",
|
||||
// https://developers.facebook.com/docs/graph-api/reference/user/#fields
|
||||
params: { fields: "id,name,email,picture" },
|
||||
request({ tokens, client, provider }) {
|
||||
return client.userinfo(tokens.access_token, {
|
||||
params: provider.userinfo.params,
|
||||
})
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -17,6 +24,6 @@ export default function Facebook(options) {
|
||||
image: profile.picture.data.url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,22 @@ export default function FACEIT(options) {
|
||||
id: "faceit",
|
||||
name: "FACEIT",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
authorization: "https://accounts.faceit.com/accounts?redirect_popup=true",
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
`${options.clientId}:${options.clientSecret}`
|
||||
).toString("base64")}`,
|
||||
},
|
||||
accessTokenUrl: "https://api.faceit.com/auth/v1/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code",
|
||||
profileUrl: "https://api.faceit.com/auth/v1/resources/userinfo",
|
||||
token: "https://api.faceit.com/auth/v1/oauth/token",
|
||||
userinfo: "https://api.faceit.com/auth/v1/resources/userinfo",
|
||||
profile(profile) {
|
||||
const { guid: id, nickname: name, email, picture: image } = profile
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
id: profile.guid,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
/** @type {import("types/providers").OAuthProvider} */
|
||||
export default function Foursquare(options) {
|
||||
const { apiVersion } = options
|
||||
const { apiVersion = "20210801" } = options
|
||||
return {
|
||||
id: "foursquare",
|
||||
name: "Foursquare",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://foursquare.com/oauth2/access_token",
|
||||
authorizationUrl:
|
||||
"https://foursquare.com/oauth2/authenticate?response_type=code",
|
||||
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
|
||||
profile(profile) {
|
||||
authorization: "https://foursquare.com/oauth2/authenticate",
|
||||
token: "https://foursquare.com/oauth2/access_token",
|
||||
userinfo: {
|
||||
url: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
|
||||
request({ tokens, client }) {
|
||||
return client.userinfo(undefined, {
|
||||
params: { oauth_token: tokens.access_token },
|
||||
})
|
||||
},
|
||||
},
|
||||
profile({ response: { profile } }) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.firstName} ${profile.lastName}`,
|
||||
image: `${profile.prefix}original${profile.suffix}`,
|
||||
email: profile.contact.email,
|
||||
image: profile.photo
|
||||
? `${profile.photo.prefix}original${profile.photo.suffix}`
|
||||
: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
20
src/providers/freshbooks.js
Normal file
20
src/providers/freshbooks.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function Freshbooks(options) {
|
||||
return {
|
||||
id: 'freshbooks',
|
||||
name: 'Freshbooks',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.freshbooks.com/auth/oauth/token',
|
||||
authorizationUrl: 'https://auth.freshbooks.com/service/auth/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.freshbooks.com/auth/api/v1/users/me',
|
||||
async profile(profile) {
|
||||
return {
|
||||
id: profile.response.id,
|
||||
name: `${profile.response.first_name} ${profile.response.last_name}`,
|
||||
email: profile.response.email,
|
||||
};
|
||||
},
|
||||
...options
|
||||
};
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
export default function FusionAuth(options) {
|
||||
let authorizationUrl = `https://${options.domain}/oauth2/authorize?response_type=code`
|
||||
if (options.tenantId) {
|
||||
authorizationUrl += `&tenantId=${options.tenantId}`
|
||||
}
|
||||
|
||||
return {
|
||||
id: "fusionauth",
|
||||
name: "FusionAuth",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${options.domain}/oauth2/token`,
|
||||
authorizationUrl,
|
||||
profileUrl: `https://${options.domain}/oauth2/userinfo`,
|
||||
authorization: `${options.issuer}oauth2/authorize`,
|
||||
token: `${options.issuer}oauth2/token`,
|
||||
userinfo: `${options.issuer}oauth2/userinfo`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
@@ -22,6 +14,6 @@ export default function FusionAuth(options) {
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@ export default function GitHub(options) {
|
||||
id: "github",
|
||||
name: "GitHub",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "user",
|
||||
accessTokenUrl: "https://github.com/login/oauth/access_token",
|
||||
authorizationUrl: "https://github.com/login/oauth/authorize",
|
||||
profileUrl: "https://api.github.com/user",
|
||||
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email",
|
||||
token: "https://github.com/login/oauth/access_token",
|
||||
userinfo: "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,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
/** @type {import("types/providers").OAuthProvider} */
|
||||
export default function GitLab(options) {
|
||||
return {
|
||||
id: "gitlab",
|
||||
name: "GitLab",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "read_user",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://gitlab.com/oauth/token",
|
||||
authorizationUrl: "https://gitlab.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://gitlab.com/api/v4/user",
|
||||
authorization: "https://gitlab.com/oauth/authorize?scope=read_user",
|
||||
token: "https://gitlab.com/oauth/token",
|
||||
userinfo: "https://gitlab.com/api/v4/user",
|
||||
checks: ["pkce", "state"],
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -17,6 +16,6 @@ export default function GitLab(options) {
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,18 @@ export default function Google(options) {
|
||||
id: "google",
|
||||
name: "Google",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope:
|
||||
"https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://accounts.google.com/o/oauth2/token",
|
||||
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
|
||||
authorizationUrl:
|
||||
"https://accounts.google.com/o/oauth2/auth?response_type=code",
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
wellKnown: "https://accounts.google.com/.well-known/openid-configuration",
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
idToken: true,
|
||||
checks: ["pkce", "state"],
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
/** @return {import("types/providers").OAuthConfig} */
|
||||
export default function IdentityServer4(options) {
|
||||
return {
|
||||
id: "identity-server4",
|
||||
name: "IdentityServer4",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid profile email",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${options.domain}/connect/token`,
|
||||
authorizationUrl: `https://${options.domain}/connect/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/connect/userinfo`,
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid profile email" } },
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return { ...profile, id: profile.sub }
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* import { signIn } from "next-auth/react"
|
||||
* ...
|
||||
* <button onClick={() => signIn("instagram")}>
|
||||
* Sign in
|
||||
@@ -29,13 +29,10 @@ export default function Instagram(options) {
|
||||
id: "instagram",
|
||||
name: "Instagram",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "user_profile",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.instagram.com/oauth/access_token",
|
||||
authorizationUrl:
|
||||
"https://api.instagram.com/oauth/authorize?response_type=code",
|
||||
profileUrl:
|
||||
authorization:
|
||||
"https://api.instagram.com/oauth/authorize?scope=user_profile",
|
||||
token: "https://api.instagram.com/oauth/access_token",
|
||||
userinfo:
|
||||
"https://graph.instagram.com/me?fields=id,username,account_type,name",
|
||||
async profile(profile) {
|
||||
return {
|
||||
@@ -45,6 +42,6 @@ export default function Instagram(options) {
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@ export default function Kakao(options) {
|
||||
id: "kakao",
|
||||
name: "Kakao",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://kauth.kakao.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://kauth.kakao.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://kapi.kakao.com/v2/user/me",
|
||||
authorization: "https://kauth.kakao.com/oauth/authorize",
|
||||
token: "https://kauth.kakao.com/oauth/token",
|
||||
userinfo: "https://kapi.kakao.com/v2/user/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -17,6 +14,6 @@ export default function Kakao(options) {
|
||||
image: profile.kakao_account?.profile.profile_image_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
21
src/providers/keycloak.js
Normal file
21
src/providers/keycloak.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @type {import("types/providers").OAuthProvider} */
|
||||
export default function Keycloak(options) {
|
||||
return {
|
||||
id: "keycloak",
|
||||
name: "Keycloak",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
type: "oauth",
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
checks: ["pkce", "state"],
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,10 @@ export default function LINE(options) {
|
||||
id: "line",
|
||||
name: "LINE",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "profile openid",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.line.me/oauth2/v2.1/token",
|
||||
authorizationUrl:
|
||||
"https://access.line.me/oauth2/v2.1/authorize?response_type=code",
|
||||
profileUrl: "https://api.line.me/v2/profile",
|
||||
authorization:
|
||||
"https://access.line.me/oauth2/v2.1/authorize?scope=openid+profile",
|
||||
token: "https://api.line.me/oauth2/v2.1/token",
|
||||
userinfo: "https://api.line.me/v2/profile",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.userId,
|
||||
@@ -18,6 +15,6 @@ export default function LINE(options) {
|
||||
image: profile.pictureUrl,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,26 +3,19 @@ export default function LinkedIn(options) {
|
||||
id: "linkedin",
|
||||
name: "LinkedIn",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "r_liteprofile",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
},
|
||||
accessTokenUrl: "https://www.linkedin.com/oauth/v2/accessToken",
|
||||
authorizationUrl:
|
||||
"https://www.linkedin.com/oauth/v2/authorization?response_type=code",
|
||||
profileUrl:
|
||||
authorization:
|
||||
"https://www.linkedin.com/oauth/v2/authorization?scope=r_liteprofile",
|
||||
token: "https://www.linkedin.com/oauth/v2/accessToken",
|
||||
userinfo:
|
||||
"https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName)",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.localizedFirstName + " " + profile.localizedLastName,
|
||||
name: `${profile.localizedFirstName} ${profile.localizedLastName}`,
|
||||
email: null,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
export default function Mailchimp(options) {
|
||||
return {
|
||||
id: 'mailchimp',
|
||||
name: 'Mailchimp',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.mailchimp.com/oauth2/token',
|
||||
authorizationUrl: 'https://login.mailchimp.com/oauth2/authorize?response_type=code',
|
||||
profileUrl: 'https://login.mailchimp.com/oauth2/metadata',
|
||||
profile: (profile) => {
|
||||
id: "mailchimp",
|
||||
name: "Mailchimp",
|
||||
type: "oauth",
|
||||
authorization: "https://login.mailchimp.com/oauth2/authorize",
|
||||
token: "https://login.mailchimp.com/oauth2/token",
|
||||
userinfo: "https://login.mailchimp.com/oauth2/metadata",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.login.login_id,
|
||||
name: profile.accountname,
|
||||
email: profile.login.email,
|
||||
image: null
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,9 @@ export default function MailRu(options) {
|
||||
id: "mailru",
|
||||
name: "Mail.ru",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "userinfo",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
},
|
||||
accessTokenUrl: "https://oauth.mail.ru/token",
|
||||
requestTokenUrl: "https://oauth.mail.ru/token",
|
||||
authorizationUrl: "https://oauth.mail.ru/login?response_type=code",
|
||||
profileUrl: "https://oauth.mail.ru/userinfo",
|
||||
authorization: "https://oauth.mail.ru/login?scope=userinfo",
|
||||
token: "https://oauth.mail.ru/token",
|
||||
userinfo: "https://oauth.mail.ru/userinfo",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -20,6 +14,6 @@ export default function MailRu(options) {
|
||||
image: profile.image,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,9 @@ export default function Medium(options) {
|
||||
id: "medium",
|
||||
name: "Medium",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "basicProfile",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.medium.com/v1/tokens",
|
||||
authorizationUrl: "https://medium.com/m/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.medium.com/v1/me",
|
||||
authorization: "https://medium.com/m/oauth/authorize?scope=basicProfile",
|
||||
token: "https://api.medium.com/v1/tokens",
|
||||
userinfo: "https://api.medium.com/v1/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
@@ -17,6 +14,6 @@ export default function Medium(options) {
|
||||
image: profile.data.imageUrl,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
17
src/providers/naver.js
Normal file
17
src/providers/naver.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export default function Naver(options) {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
type: "oauth",
|
||||
authorization: "https://nid.naver.com/oauth2.0/authorize",
|
||||
token: "https://nid.naver.com/oauth2.0/token",
|
||||
userinfo: "https://openapi.naver.com/v1/nid/me",
|
||||
profile(profile) {
|
||||
// REVIEW: By default, we only want to expose the
|
||||
// "id", "name", "email" and "image" fields.
|
||||
return profile.response
|
||||
},
|
||||
checks: ["state"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,9 @@ export default function Netlify(options) {
|
||||
id: "netlify",
|
||||
name: "Netlify",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.netlify.com/oauth/token",
|
||||
authorizationUrl: "https://app.netlify.com/authorize?response_type=code",
|
||||
profileUrl: "https://api.netlify.com/api/v1/user",
|
||||
authorization: "https://app.netlify.com/authorize",
|
||||
token: "https://api.netlify.com/oauth/token",
|
||||
userinfo: "https://api.netlify.com/api/v1/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -16,6 +14,6 @@ export default function Netlify(options) {
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,17 @@ export default function Okta(options) {
|
||||
id: "okta",
|
||||
name: "Okta",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "openid profile email",
|
||||
params: {
|
||||
grant_type: "authorization_code",
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret,
|
||||
},
|
||||
// These will be different depending on the Org.
|
||||
accessTokenUrl: `https://${options.domain}/v1/token`,
|
||||
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/v1/userinfo/`,
|
||||
authorization: `${options.issuer}v1/authorize?scope=openid+profile+email`,
|
||||
token: `${options.issuer}v1/token`,
|
||||
userinfo: `${options.issuer}v1/userinfo`,
|
||||
profile(profile) {
|
||||
return { ...profile, id: profile.sub }
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
20
src/providers/onelogin.js
Normal file
20
src/providers/onelogin.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/** @returns {import("types/providers").OAuthConfig} */
|
||||
export default function OneLogin(options) {
|
||||
return {
|
||||
id: "onelogin",
|
||||
name: "OneLogin",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/oidc/2/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid profile email" } },
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.nickname,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
export default function Osso(options) {
|
||||
return {
|
||||
id: "osso",
|
||||
name: "SAML SSO",
|
||||
name: "Osso",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/oauth/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/oauth/me`,
|
||||
authorization: `${options.issuer}oauth/authorize`,
|
||||
token: `${options.issuer}oauth/token`,
|
||||
userinfo: `${options.issuer}oauth/me`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name || profile.email,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,17 @@ export default function Reddit(options) {
|
||||
id: "reddit",
|
||||
name: "Reddit",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "identity",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: " https://www.reddit.com/api/v1/access_token",
|
||||
authorizationUrl:
|
||||
"https://www.reddit.com/api/v1/authorize?response_type=code",
|
||||
profileUrl: "https://oauth.reddit.com/api/v1/me",
|
||||
authorization: "https://www.reddit.com/api/v1/authorize?scope=identity",
|
||||
token: " https://www.reddit.com/api/v1/access_token",
|
||||
userinfo: "https://oauth.reddit.com/api/v1/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
image: null,
|
||||
email: null,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,19 @@ export default function Salesforce(options) {
|
||||
id: "salesforce",
|
||||
name: "Salesforce",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { display: "page", grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://login.salesforce.com/services/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://login.salesforce.com/services/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://login.salesforce.com/services/oauth2/userinfo",
|
||||
protection: "none",
|
||||
authorization:
|
||||
"https://login.salesforce.com/services/oauth2/authorize?display=page",
|
||||
token: "https://login.salesforce.com/services/oauth2/token",
|
||||
userinfo: "https://login.salesforce.com/services/oauth2/userinfo",
|
||||
profile(profile) {
|
||||
return {
|
||||
...profile,
|
||||
id: profile.user_id,
|
||||
name: null,
|
||||
email: null,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
checks: ["none"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user