Compare commits

..

74 Commits

Author SHA1 Message Date
Balázs Orbán
29db75ad28 chore(release): bump version [skip ci] 2022-07-25 12:31:40 +02:00
Balázs Orbán
d348ca1dc1 fix: reduce logger.error context 2022-07-25 11:14:49 +02:00
Balázs Orbán
d53e1ea6c4 chore: gitignore v4 files 2022-07-25 11:13:44 +02:00
Balázs Orbán
e701342b1a update package-lock.json 2022-07-05 13:49:23 +02:00
Balázs Orbán
8a133bf5fd fix: don't render email in email's HTML body 2022-07-05 13:47:28 +02:00
Balázs Orbán
35a3ea6620 fix: handle invalid email 2022-07-01 12:42:20 +02:00
Balázs Orbán
289800fbb4 chore: bump version 2022-06-23 12:05:39 +02:00
Sylvain Bellone
28eccc3e64 fix: ReferenceError: defaultCookies is not defined (#4711) 2022-06-23 10:50:28 +02:00
Balázs Orbán
e16bf939a9 chore: bump version 2022-06-20 10:07:04 +02:00
Balázs Orbán
9b078c92b2 fix: don't show error on relative callbackUrl 2022-06-20 10:05:36 +02:00
Balázs Orbán
87f6f576b1 fix: handle invalid callbackUrl 2022-06-10 15:11:41 +02:00
Balázs Orbán
50584bdc4c chore: bump version 2022-04-26 12:22:45 +02:00
Balázs Orbán
b4429235c0 fix: more strict default callback url handling 2022-04-26 12:22:11 +02:00
Balázs Orbán
e1b297d06d fix: update default callbacks.redirect 2022-04-14 11:32:16 +02:00
Balázs Orbán
ab764e3793 chore: bump release 2022-03-15 22:50:13 +01:00
Balázs Orbán
c8941e4b3e fix: remove action from bad request response 2022-03-15 22:45:10 +01:00
Nico Domino
ead715219a fix(deps): update built-in adapter dependencies (#2589)
* fix(deps): update prisma-legacy-adapter and typeorm-legacy-adapter dependencies

* chore: add missing package-lock update
2021-08-23 21:55:33 +02:00
Ashutosh Kumar
8faa7553dd docs: add suggestions for secret and encryption key generation (#2578) 2021-08-21 23:08:56 +02:00
Eduard Babinyan
90a6a0084b feat(provider): return image for Yandex by default (#2563)
Uploading an user image.
2021-08-20 09:37:30 +02:00
Aaron Powell
cb844a2436 docs(provider): remove en-us from Azure urls (#2554)
MS Docs has a lot of local language translations, so it's best to remove locale information from the URLs so that when someone follows them, they land on the right language version of the content.
2021-08-18 09:46:32 +02:00
Sercan Altundas
74558d6cc2 docs(email): remove duplicate CSS property from html (#2546)
- The CSS property 'text-decoration: none;' was duplicated in the example html code and is removed.
2021-08-17 12:17:54 +02:00
Jaye Hackett
d03125a77b docs(ts): mention module augmentation on callbacks (#2541) 2021-08-17 01:01:19 +02:00
Liam Tait
66d16f8bf4 fix(ts): allow scope as string array type (#2511) 2021-08-12 17:51:31 +02:00
Nico Domino
be74dd0e7e docs(security): email contact update (#2467)
* chore(docs): email contact update

* chore(docs): add me@iaincollins.com back
2021-08-02 17:18:17 +02:00
Aryan Beezadhur
9bf867ddcf docs: Update faq.md (#2458) 2021-07-30 22:34:32 +02:00
Nico Domino
0f460c22da docs(client): add text regarding 'logout' (#2432) 2021-07-28 20:10:08 +02:00
Sigurd Heggemsnes
887cb00877 docs(adapter): Typo in filepath for firebase auth in docs. (#2436) 2021-07-28 12:48:47 +02:00
Douglas
75ca097ff7 docs: Fix link to code (#2405) 2021-07-19 15:36:37 +02:00
Nicolas Azari
bcb9383aec docs: fix typos in options.md (#2393)
* Update options.md

* Update www/docs/configuration/options.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-07-17 22:21:45 +02:00
John Michael Kuhn Jr
b953963101 chore(core): fix typo in csrf-token-handler.js where 'strategy' is misspelled (#2391) 2021-07-17 12:02:38 +02:00
Nico Domino
4649f1968b docs(readme): add opencollective details to readme (#2388)
* docs(readme): add opencollective details to readme

* docs(www): add sponsors to docs footer

* docs(readme): move support under ack

* docs(www): dropped docusaurus link in footer
2021-07-16 18:05:15 +02:00
Angelo Annunziata
45f4a69a4e docs(configuration): remove comments in JWT example (#2378) 2021-07-16 09:28:19 +02:00
Prabhdeep Singh
2155c93a3c feat(providers): add OneLogin (#2345)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-07-14 11:07:56 +02:00
Angelo Annunziata
d5958571a4 docs(provider): fix typo (#2369) 2021-07-13 21:36:00 +02:00
James Q Quick
ebecaa6a4b docs(adapter): match Fauna index name with implementation(#2360)
* Update Fauna Adapter 

- added one-liner to explain how to use the setup scripts inside of the Fauna dashboard
- updated the `verification_request_by_token` index name to match what is expected inside of the SDK which is `verification_request_by_token_and_identifier`

* Update Typo

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-07-13 17:58:58 +02:00
Vincent Grafé
1c5173a818 docs(callbacks): fix typo (#2363) 2021-07-13 10:24:05 +02:00
Ben Goshow
35ce332cc6 feat(providers): add Freshbooks (#2322)
Contains the following squashed commits: 

* Create freshbooks.js
* Create freshbooks.md
* Update providers.d.ts
* Update freshbooks.md
* Update src/providers/freshbooks.js
* Update providers.test.ts
* Update freshbooks.md
2021-07-11 20:25:26 +02:00
Imamuzzaki Abu Salam
ec295287f1 docs: delete can word in "can can" (#2348) 2021-07-11 15:08:05 +02:00
Nick Arciero
46978ac02f docs(tutorial): Add link to blog post about integrating with Magic (#2340) 2021-07-10 09:56:13 +02:00
Pol
f546e550dd fix(oauth): correctly remove code_verifier cookie when used (#2325)
Co-authored-by: Pol Bonastre <pbonastre@plainconcepts.com>
2021-07-08 17:24:56 +02:00
Balázs Orbán
ac5b4db0f2 chore: add OpenCollective link to FUNDING.yml 2021-07-05 17:54:34 +02:00
Mahieyin Rahmun
8bbffdd08c docs(github): remove title property (#2308) 2021-07-04 13:23:44 +02:00
Mahieyin Rahmun
a22a0a36fd docs(github): remove title prefix and make reproductions required (#2306) 2021-07-04 11:19:13 +02:00
Mahieyin Rahmun
797272afe1 docs: use issue template forms (#2274)
* (docs) initial issue template forms as per #2271

* (typo) fix grammar and typo

* (forms) make the requested changes

* (chore) delete the old .md files

* (forms) fix type key
2021-07-02 21:13:03 +02:00
Mahieyin Rahmun
13e56bcf2f docs(adapters): update outdated documentation (#2296) 2021-07-02 12:50:27 +02:00
yokinist
b0f7f87c04 docs: update 'pages' option in example code (#2270) 2021-07-01 17:12:01 +02:00
Balázs Orbán
9c0851c0f9 chore(ci): shorten names in release.yml workflow 2021-06-30 21:36:28 +02:00
Andriy Komm
f5b3c29ab1 fix(ts): improve authorize typing on Credentials provider (#2227)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-30 15:49:38 +02:00
Nico Domino
b4f2a0106a chore(ci): add environment approval (#2214)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-30 15:28:12 +02:00
Balázs Orbán
9c095b0532 chore(dev): fix dev app when running locally (#2280)
* fix: fix console warning in dev app

* chore: add `npm i` to `dev:setup` script

* chore(deps): update dev dependencies (react+next)

* chore: update package-lock.json

* chore: use node 16 in actions
2021-06-29 22:11:55 +02:00
Nico Domino
0475964a0f chore(pages): typo in error messages (#2265) 2021-06-28 02:57:35 +02:00
Justin Forlenza
ad6c13cdc9 fix(ts): extend server type in Email provider from nodemailer (#2259)
* Added optional secure & TLS settings for SMTP

* Replaced custom interface with nodemailers

* Fix lockfile version

* Apply suggestions from code review

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-27 18:51:34 +02:00
Nico Domino
591aa7cc7e docs(adapter): rm @canary from adapters' install instructions (#2260) 2021-06-27 18:28:58 +02:00
ndom91
9abb392b4e chore: fix gh action typo 2021-06-27 03:39:38 +02:00
ndom91
b89ae87fb1 docs: respect color mode 2021-06-27 03:38:04 +02:00
ndom91
3687d17724 Merge branch 'main' of ssh://github.com/nextauthjs/next-auth 2021-06-27 03:11:07 +02:00
Balázs Orbán
b04ff82fb9 chore: clarify where to run envinfo in bug report template 2021-06-24 01:46:02 +02:00
Balázs Orbán
c11915ba9c chore: update bug report template 2021-06-24 01:44:33 +02:00
Balázs Orbán
24ee459f97 chore(ci): run tests and typechecks only 2021-06-24 00:38:17 +02:00
Balázs Orbán
ac4851d238 chore(ci): run test:ci (linting+test+typecheck) 2021-06-24 00:33:32 +02:00
can-mihci
84094b0ee7 docs(client): fix code block typo (#2217) 2021-06-22 20:11:18 +02:00
Vikrant Bhat
f09ab4a04f docs(providers): fix typo (#2220) 2021-06-22 20:08:43 +02:00
Vikrant Bhat
067364381b docs(providers): fix english sentence in Email provider section (#2222) 2021-06-22 09:28:47 +02:00
ndom91
6ee36b6842 ci: test release environment approval 2021-06-18 20:03:07 +02:00
Sangwon Park
5a89ab69d3 feat(provider): add Naver provider (#2172)
* add Naver provider

* fix typo

* Update src/providers/naver.js

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-16 00:46:41 +02:00
Balázs Orbán
665445818e docs(config): link to next documentation instead of canary 2021-06-12 17:11:53 +02:00
ndom91
67cf2a11bb docs: fix alt client provider example 2021-06-12 16:42:48 +02:00
Lluis Agusti
832d51f10e test(client): add more tests (#2135)
Contains the following squashed commits:

* test(client): verify CSRF Token fetch
* test(client): verify `getProviders` logic
* test(client): verify `useSession` happy path
* test(coverage): initial coverage setup (trial)
* chore(test): fix coverage reporting
* chore(test): define report directory for codecov

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-10 11:42:58 +02:00
Balázs Orbán
29862ac887 fix(build): do not run husky on postinstall (#2158) 2021-06-10 00:24:06 +02:00
Christopher Betz
5aa2b61b88 feat(provider): add Coinbase provider (#2153)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-09 22:46:12 +02:00
Nicholas Chiang
929c644653 docs(client): fix callback anchor links (#2151) 2021-06-09 22:19:26 +02:00
Nicholas Chiang
2657e72e81 docs(callbacks): don't use signIn for redirects (#2150)
Specifies that you shouldn't use the `signIn` callback for arbitrary redirects. Instead, use the `callbackUrl` option or the redirect callback.
2021-06-09 22:17:45 +02:00
Apoorv Taneja
8ff7dbb18f docs(tutorial): Adding a YouTube link for NextAuth.js introduction (#2047)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-06-09 17:04:41 +02:00
Manish Kumar
748d576a5a docs(adapter): align DynamoDB docs with source code (#2125)
* Updated DynamoDB Adaptor documentation

* Update dynamodb.md

* Update dynamodb.md

* Update dynamodb.md
2021-06-09 17:01:00 +02:00
89 changed files with 7671 additions and 16079 deletions

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -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
View 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 💚

View File

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

View 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 💚

View File

@@ -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
View 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 💚

View File

@@ -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
View 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 💚

View File

@@ -1,4 +1,4 @@
name: Release Flow
name: Release
on:
push:
@@ -11,32 +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: Run tests
run: npm test
- name: Build
run: npm run build
release:
name: Release
needs: test
- name: Run tests
run: npm test -- --coverage --verbose && npm run test:types
- name: Coverage
uses: codecov/codecov-action@v1
with:
directory: ./coverage
fail_ci_if_error: false
release-branch:
name: Publish branch
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'push' }}
environment: Production
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: 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 }}

8
.gitignore vendored
View File

@@ -59,3 +59,11 @@ app/yarn.lock
# Prisma migrations
/prisma/migrations
# Tests
/coverage
# v4
packages
apps
docs/providers.json

View File

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -55,11 +55,11 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting me@iaincollins.com. 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

View File

@@ -25,6 +25,8 @@ Anyone can be a contributor. Either you found a typo, or you have an awesome fea
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
@@ -32,10 +34,10 @@ 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`:
@@ -78,26 +80,12 @@ 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.

View File

@@ -147,14 +147,44 @@ export default function Component() {
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
</div>
<div>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
</div>
### Support
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://vercel.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
</a><br />
<div>Vercel</div><br />
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://prisma.io" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
</a><br />
<div>Prisma</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
</a><br />
<div>Checkly</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
</tr><tr></tr>
</tbody>
</table>
<br />
<!--sponsors end-->
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).

View File

@@ -19,6 +19,6 @@ If you contact us regarding a serious issue:
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
Currently, the best way to report an issue is by 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.

View File

@@ -23,8 +23,6 @@ TWITTER_SECRET=
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
EMAIL_FROM=user@gmail.com
# You can use any of these as the "DATABASE_URL" for
# databases started with Docker using `npm run db:start`.
# Note: If using with Prisma adapter, you need to use a `.env`
# file rather than a `.env.local` file to configure env vars.
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true

View File

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

View File

@@ -14,7 +14,7 @@
},
"license": "ISC",
"dependencies": {
"next": "^10.1.3",
"next": "^11.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},

View File

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

View File

@@ -1,8 +1,11 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
transform: {
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
},
roots: ["../src"],
setupFilesAfterEnv: ["./jest-setup.js"],
rootDir: "../src",
setupFilesAfterEnv: ["../config/jest-setup.js"],
collectCoverageFrom: ["!client/__tests__/**"],
testMatch: ["**/*.test.js"],
coverageDirectory: "../coverage",
}

17
config/version-pr.js Normal file
View 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)
}

21111
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "0.0.0-semantically-released",
"version": "3.29.9",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -29,11 +29,10 @@
"./errors": "./dist/lib/errors.js"
},
"scripts": {
"postinstall": "npx husky install",
"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",
@@ -43,7 +42,8 @@
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr"
},
"files": [
"dist",
@@ -64,25 +64,28 @@
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.1.2",
"@next-auth/typeorm-legacy-adapter": "0.1.4",
"futoin-hkdf": "^1.3.2",
"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"
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
},
"peerOptionalDependencies": {
"nodemailer": "^6.4.16"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
@@ -91,13 +94,10 @@
"@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",
"@testing-library/user-event": "^13.1.9",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
@@ -119,13 +119,13 @@
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^10.0.5",
"next": "^11.0.1",
"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",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},

View File

@@ -0,0 +1,64 @@
import { useState } from "react"
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { Provider, useSession } from ".."
import userEvent from "@testing-library/user-event"
beforeAll(() => {
server.listen()
})
afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
server.close()
})
test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
res(ctx.status(200), ctx.json(mockSession))
})
)
render(<ProviderFlow />)
await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
const session1 = screen.getByTestId("session-consumer-1").textContent
const session2 = screen.getByTestId("session-consumer-2").textContent
expect(session1).toEqual(session2)
})
})
function ProviderFlow({ options = {} }) {
return (
<>
<Provider options={options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</Provider>
</>
)
}
function SessionConsumer({ testId = 1 }) {
const [session, loading] = useSession()
if (loading) return <span>loading</span>
return (
<div data-testid={`session-consumer-${testId}`}>
{JSON.stringify(session)}
</div>
)
}

View File

@@ -0,0 +1,105 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockCSRFToken } from "./helpers/mocks"
import logger from "../../lib/logger"
import { getCsrfToken } from ".."
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toEqual(
mockCSRFToken.csrfToken
)
})
})
test("when there's no CSRF token returned, it'll reflect that", async () => {
server.use(
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
...mockCSRFToken,
csrfToken: null,
})
)
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
})
})
test("when the fetch fails it'll throw a client fetch error", async () => {
server.use(
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"csrf",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})
function CSRFFlow() {
const [response, setResponse] = useState()
async function handleCSRF() {
const result = await getCsrfToken()
setResponse(result)
}
return (
<>
<p data-testid="csrf-result">
{response === null ? "null-response" : response || "no response"}
</p>
<button onClick={handleCSRF}>Get CSRF</button>
</>
)
}

View File

@@ -3,6 +3,7 @@ import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
ok: true,
user: {
image: null,
name: "John",
@@ -12,6 +13,7 @@ export const mockSession = {
}
export const mockProviders = {
ok: true,
github: {
id: "github",
name: "Github",
@@ -34,6 +36,7 @@ export const mockProviders = {
}
export const mockCSRFToken = {
ok: true,
csrfToken: randomBytes(32).toString("hex"),
}

View File

@@ -0,0 +1,85 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockProviders } from "./helpers/mocks"
import { getProviders } from ".."
import logger from "../../lib/logger"
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("when called it'll return the currently configured providers for sign in", async () => {
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("providers-result").textContent).toEqual(
JSON.stringify(mockProviders)
)
})
})
test("when failing to fetch the providers, it'll log the error", async () => {
server.use(
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})
function ProvidersFlow() {
const [response, setResponse] = useState()
async function handleGerProviders() {
const result = await getProviders()
setResponse(result)
}
return (
<>
<p data-testid="providers-result">
{response === null
? "null-response"
: JSON.stringify(response) || "no response"}
</p>
<button onClick={handleGerProviders}>Get Providers</button>
</>
)
}

View File

@@ -1,10 +1,10 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./mocks"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getBroadcastEvents } from "./utils"
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({
__esModule: true,
@@ -27,10 +27,12 @@ beforeEach(() => {
afterEach(() => {
server.resetHandlers()
jest.restoreAllMocks()
jest.clearAllMocks()
})
afterAll(() => server.close())
afterAll(() => {
server.close()
})
test("if it can fetch the session, it should store it in `localStorage`", async () => {
render(<SessionFlow />)
@@ -81,7 +83,7 @@ function SessionFlow() {
useEffect(() => {
async function fetchUserSession() {
try {
const result = await getSession({})
const result = await getSession()
setSession(result)
} catch (e) {
console.error(e)
@@ -90,8 +92,7 @@ function SessionFlow() {
fetchUserSession()
}, [])
if (session) {
return <pre>{JSON.stringify(session, null, 2)}</pre>
}
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
return <p>No session</p>
}

View File

@@ -7,7 +7,7 @@ import {
mockCredentialsResponse,
mockEmailResponse,
mockGithubResponse,
} from "./mocks"
} from "./helpers/mocks"
import { signIn } from ".."
import { rest } from "msw"
@@ -36,7 +36,7 @@ beforeAll(() => {
})
beforeEach(() => {
jest.resetAllMocks()
jest.clearAllMocks()
server.resetHandlers()
})
@@ -284,7 +284,7 @@ function SignInFlow({
<p data-testid="signin-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={() => handleSignIn()}>Sign in</button>
<button onClick={handleSignIn}>Sign in</button>
</>
)
}

View File

@@ -1,10 +1,10 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./mocks"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from ".."
import { rest } from "msw"
import { getBroadcastEvents } from "./utils"
import { getBroadcastEvents } from "./helpers/utils"
const { location } = window
@@ -24,7 +24,7 @@ beforeEach(() => {
})
afterEach(() => {
jest.resetAllMocks()
jest.clearAllMocks()
server.resetHandlers()
})
@@ -113,7 +113,7 @@ test("will broadcast the signout event to other tabs", async () => {
function SignOutFlow({ callbackUrl, redirect = true }) {
const [response, setResponse] = useState(null)
async function setSignOutRes() {
async function handleSignOut() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
@@ -123,7 +123,7 @@ function SignOutFlow({ callbackUrl, redirect = true }) {
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={() => setSignOutRes()}>Sign out</button>
<button onClick={handleSignOut}>Sign out</button>
</>
)
}

103
src/client/index.d.ts vendored
View File

@@ -1,103 +0,0 @@
import * as React from 'react'
import { GetServerSidePropsContext } from 'next'
interface DefaultSession {
user: {
name: string | null
email: string | null
image: string | null
}
expires: Date | string
}
interface BroadcastMessage {
event?: 'session'
data?: {
trigger?: 'signout' | 'getSession'
}
clientId: string
timestamp: number
}
type GetSession<S extends Record<string, unknown> = DefaultSession> = (options: {
ctx?: GetServerSidePropsContext
req?: GetServerSidePropsContext['req']
event?: 'storage' | 'timer' | 'hidden' | string
triggerEvent?: boolean
}) => Promise<S>
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: DefaultSession | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type GetCsrfToken = (
ctxOrReq: GetServerSidePropsContext & GetServerSidePropsContext['req']
) => Promise<string | null>
export interface SessionOptions {
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
}
export type Provider<S extends Record<string, unknown> = DefaultSession > = (options: {
children: React.ReactNode
session: S
options: SessionOptions
}) => React.ReactNode
export type SetOptions = (options: SessionOptions) => void
export type SessionContext = React.createContext<[DefaultSession | null, boolean]>
export type UseSession = () => [any, boolean]
export type GetProviders = () => Promise<any[]>
// Sign in types
export interface SignInOptions {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: boolean
}
export interface SignInResponse {
error: string | null
status: number
ok: boolean
url: string | null
}
export type SignIn<AuthorizationParams = Record<string, string>> = (
provider?: string,
options?: SignInOptions,
authorizationParams?: AuthorizationParams
) => SignInResponse
// Sign out types
interface SignOutResponse<RedirectType extends boolean=true> {
/** Defaults to the current URL. */
callbackUrl?: string
redirect?: RedirectType
}
export type SignOut<RedirectType extends boolean = true> = (params: SignOutResponse<RedirectType>) => RedirectType extends true ? Promise<{url?: string} | undefined> : undefined

24
src/providers/coinbase.js Normal file
View File

@@ -0,0 +1,24 @@
export default function Coinbase(options) {
return {
id: "coinbase",
name: "Coinbase",
type: "oauth",
version: "2.0",
scope: "wallet:user:email wallet:user:read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.coinbase.com/oauth/token",
requestTokenUrl: "https://api.coinbase.com/oauth/token",
authorizationUrl:
"https://www.coinbase.com/oauth/authorize?response_type=code",
profileUrl: "https://api.coinbase.com/v2/user",
profile(profile) {
return {
id: profile.data.id,
name: profile.data.name,
email: profile.data.email,
image: profile.data.avatar_url,
}
},
...options,
}
}

View File

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

View File

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

View File

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

18
src/providers/naver.js Normal file
View File

@@ -0,0 +1,18 @@
export default function Naver(options) {
return {
id: "naver",
name: "Naver",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
protection: ["state"],
accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
authorizationUrl:
"https://nid.naver.com/oauth2.0/authorize?response_type=code",
profileUrl: "https://openapi.naver.com/v1/nid/me",
profile(profile) {
return profile.response
},
...options,
}
}

19
src/providers/onelogin.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
@@ -14,14 +15,22 @@ import csrfTokenHandler from "./lib/csrf-token-handler"
import * as pkce from "./lib/oauth/pkce-handler"
import * as state from "./lib/oauth/state-handler"
import optionalRequire from "@balazsorban/require-optional"
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
function isValidHttpUrl(url, baseUrl) {
try {
return /^https?:/.test(
new URL(url, url.startsWith("/") ? baseUrl : undefined).protocol
)
} catch {
return false
}
}
/**
* @param {import("next").NextApiRequest} req
* @param {import("next").NextApiResponse} res
@@ -72,6 +81,23 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions.cookies,
}
const errorPage = userOptions.pages?.error ?? `${baseUrl}${basePath}/error`
const callbackUrlParam = req.query?.callbackUrl
if (callbackUrlParam && !isValidHttpUrl(callbackUrlParam, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const { callbackUrl: defaultCallbackUrl } = cookie.defaultCookies(
userOptions.useSecureCookies ?? baseUrl.startsWith("https://")
)
const callbackUrlCookie =
req.cookies?.[cookies?.callbackUrl?.name ?? defaultCallbackUrl.name]
if (callbackUrlCookie && !isValidHttpUrl(callbackUrlCookie, baseUrl)) {
return res.redirect(`${errorPage}?error=Configuration`)
}
const secret = createSecret({ userOptions, basePath, baseUrl })
const providers = parseProviders({
@@ -103,6 +129,13 @@ async function NextAuthHandler(req, res, userOptions) {
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
// Parse database / adapter
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter =
userOptions.adapter ??
(userOptions.database && adapters.Default(userOptions.database))
// User provided options are overriden by other options,
// except for the options with special handling above
req.options = {
@@ -123,7 +156,7 @@ async function NextAuthHandler(req, res, userOptions) {
providers,
// Session options
session: {
jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
maxAge,
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userOptions.session,
@@ -274,7 +307,9 @@ async function NextAuthHandler(req, res, userOptions) {
}
return res
.status(400)
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
.end(
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
)
})
}

View File

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

View File

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

View File

@@ -15,7 +15,7 @@
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
export async function signIn () {
export async function signIn() {
return true
}
@@ -28,10 +28,9 @@ export async function signIn () {
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {Promise<string>} URL the client will be redirect to
*/
export async function redirect (url, baseUrl) {
if (url.startsWith(baseUrl)) {
return url
}
export async function redirect(url, baseUrl) {
if (url.startsWith("/")) return `${baseUrl}${url}`
else if (new URL(url).origin === baseUrl) return url
return baseUrl
}
@@ -43,7 +42,7 @@ export async function redirect (url, baseUrl) {
* @param {object} token JSON Web Token (if enabled)
* @return {Promise<object>} Session that will be returned to the client
*/
export async function session (session) {
export async function session(session) {
return session
}
@@ -59,6 +58,6 @@ export async function session (session) {
* @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {Promise<object>} JSON Web Token that will be saved
*/
export async function jwt (token) {
export async function jwt(token) {
return token
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,18 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import { h } from "preact" // eslint-disable-line no-unused-vars
export default function signin ({ csrfToken, providers, callbackUrl, email, error: errorType }) {
export default function signin({
csrfToken,
providers,
callbackUrl,
email,
error: errorType,
}) {
// We only want to render providers
const providersToRender = providers.filter(provider => {
if (provider.type === 'oauth' || provider.type === 'email') {
const providersToRender = providers.filter((provider) => {
if (provider.type === "oauth" || provider.type === "email") {
// Always render oauth and email type providers
return true
} else if (provider.type === 'credentials' && provider.credentials) {
} else if (provider.type === "credentials" && provider.credentials) {
// Only render credentials type provider if credentials are defined
return true
}
@@ -15,70 +21,93 @@ export default function signin ({ csrfToken, providers, callbackUrl, email, erro
})
const errors = {
Signin: 'Try signing with a different account.',
OAuthSignin: 'Try signing with a different account.',
OAuthCallback: 'Try signing with a different account.',
OAuthCreateAccount: 'Try signing with a different account.',
EmailCreateAccount: 'Try signing with a different account.',
Callback: 'Try signing with a different account.',
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
EmailSignin: 'Check your email address.',
CredentialsSignin: 'Sign in failed. Check the details you provided are correct.',
default: 'Unable to sign in.'
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
OAuthCreateAccount: "Try signing in with a different account.",
EmailCreateAccount: "Try signing in with a different account.",
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "Check your email inbox.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
default: "Unable to sign in.",
}
const error = errorType && (errors[errorType] ?? errors.default)
return (
<div className='signin'>
{error &&
<div className='error'>
<div className="signin">
{error && (
<div className="error">
<p>{error}</p>
</div>}
{providersToRender.map((provider, i) =>
<div key={provider.id} className='provider'>
{provider.type === 'oauth' &&
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
<button type='submit' className='button'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
<hr />}
{provider.type === 'email' &&
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
<label for={`input-email-for-${provider.id}-provider`}>Email</label>
<input id={`input-email-for-${provider.id}-provider`} autoFocus type='text' name='email' value={email} placeholder='email@example.com' />
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{provider.type === 'credentials' &&
<form action={provider.callbackUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{Object.keys(provider.credentials).map(credential => {
</div>
)}
{providersToRender.map((provider, i) => (
<div key={provider.id} className="provider">
{provider.type === "oauth" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{callbackUrl && (
<input type="hidden" name="callbackUrl" value={callbackUrl} />
)}
<button type="submit" className="button">
Sign in with {provider.name}
</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i > 0 &&
providersToRender[i - 1].type !== "email" &&
providersToRender[i - 1].type !== "credentials" && <hr />}
{provider.type === "email" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label for={`input-email-for-${provider.id}-provider`}>
Email
</label>
<input
id={`input-email-for-${provider.id}-provider`}
autoFocus
type="text"
name="email"
value={email}
placeholder="email@example.com"
/>
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{provider.type === "credentials" && (
<form action={provider.callbackUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{Object.keys(provider.credentials).map((credential) => {
return (
<div key={`input-group-${provider.id}`}>
<label
for={`input-${credential}-for-${provider.id}-provider`}
>{provider.credentials[credential].label || credential}
>
{provider.credentials[credential].label || credential}
</label>
<input
name={credential}
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type || 'text'}
value={provider.credentials[credential].value || ''}
placeholder={provider.credentials[credential].placeholder || ''}
type={provider.credentials[credential].type || "text"}
value={provider.credentials[credential].value || ""}
placeholder={
provider.credentials[credential].placeholder || ""
}
/>
</div>
)
})}
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && ((i + 1) < providersToRender.length) &&
<hr />}
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i + 1 < providersToRender.length && <hr />}
</div>
)}
))}
</div>
)
}

View File

@@ -77,7 +77,7 @@ export default async function callback(req, res) {
account,
OAuthProfile
)
if (!signInCallbackResponse) {
if (signInCallbackResponse === false) {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
@@ -85,11 +85,16 @@ export default async function callback(req, res) {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
@@ -221,17 +226,22 @@ export default async function callback(req, res) {
account,
{ email }
)
if (!signInCallbackResponse) {
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
@@ -326,8 +336,7 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials,
{ ...req, options: {}, cookies: {} }
credentials, {...req, options: {}, cookies: {}}
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res
@@ -337,13 +346,16 @@ export default async function callback(req, res) {
provider.id
)}`
)
} else if (typeof userObjectReturnedFromAuthorizeHandler === "string") {
return res.redirect(userObjectReturnedFromAuthorizeHandler)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const user = userObjectReturnedFromAuthorizeHandler
@@ -355,17 +367,20 @@ export default async function callback(req, res) {
account,
credentials
)
if (!signInCallbackResponse) {
if (signInCallbackResponse === false) {
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const defaultJwtPayload = {

View File

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

2
types/index.d.ts vendored
View File

@@ -82,7 +82,7 @@ export interface NextAuthOptions {
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: null
* newUser: '/auth/new-user'
* }
* ```
*

31
types/providers.d.ts vendored
View File

@@ -1,5 +1,6 @@
import { Profile, TokenSet, User } from "."
import { Awaitable, NextApiRequest } from "./internals/utils"
import { Options as SMTPConnectionOptions } from 'nodemailer/lib/smtp-connection'
export type ProviderType = "oauth" | "email" | "credentials"
@@ -26,7 +27,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
headers?: Record<string, any>
type: "oauth"
version: string
scope: string
scope: string | string[]
params: { grant_type: string }
accessTokenUrl: string
requestTokenUrl?: string
@@ -63,6 +64,7 @@ export type OAuthProviderType =
| "Box"
| "Bungie"
| "Cognito"
| "Coinbase"
| "Discord"
| "Dropbox"
| "EVEOnline"
@@ -70,6 +72,7 @@ export type OAuthProviderType =
| "FACEIT"
| "FortyTwo"
| "Foursquare"
| "Freshbooks"
| "FusionAuth"
| "GitHub"
| "GitLab"
@@ -82,8 +85,10 @@ export type OAuthProviderType =
| "Mailchimp"
| "MailRu"
| "Medium"
| "Naver"
| "Netlify"
| "Okta"
| "OneLogin"
| "Osso"
| "Reddit"
| "Salesforce"
@@ -116,29 +121,15 @@ interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(
credentials: Record<keyof C, string>,
req: NextApiRequest
): Awaitable<User | null>
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
}
export type CredentialsProvider = (
options: Partial<CredentialsConfig>
) => CredentialsConfig
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(
options: Partial<CredentialsConfig<C>>
) => CredentialsConfig<C>
export type CredentialsProviderType = "Credentials"
/** Email Provider */
export interface EmailConfigServerOptions {
host: string
port: number
auth: {
user: string
pass: string
}
}
export type SendVerificationRequest = (params: {
identifier: string
url: string
@@ -150,7 +141,7 @@ export type SendVerificationRequest = (params: {
export interface EmailConfig extends CommonProviderOptions {
type: "email"
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
server: string | EmailConfigServerOptions
server: string | SMTPConnectionOptions
/** @default "NextAuth <no-reply@example.com>" */
from?: string
/**

View File

@@ -19,12 +19,12 @@ Providers.Email({
from: "path/from",
})
// $ExpectType CredentialsConfig<{}>
// $ExpectType CredentialsConfig<{ username: { label: string; type: string; }; password: { label: string; type: string; }; }>
Providers.Credentials({
id: "login",
name: "account",
credentials: {
user: {
username: {
label: "Password",
type: "password",
},
@@ -33,7 +33,7 @@ Providers.Credentials({
type: "password",
},
},
authorize: async (credentials) => {
authorize: async ({ username, password }) => {
const user = {
/* fetched user */
}
@@ -152,6 +152,13 @@ Providers.Okta({
domain: "https://foo.auth0.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.OneLogin({
clientId: "foo123",
clientSecret: "bar123",
domain: "foo.onelogin.com",
})
// $ExpectType OAuthConfig<Profile>
Providers.BattleNet({
clientId: "foo123",
@@ -257,3 +264,9 @@ Providers.Zoho({
clientId: "foo123",
clientSecret: "bar123",
})
// $ExpectType OAuthConfig<Profile>
Providers.Freshbooks({
clientId: "foo123",
clientSecret: "bar123",
})

View File

@@ -13,15 +13,16 @@ You can find the full schema in the table structure section below.
## Getting Started
1. Install `next-auth` and `@next-auth/dynamodb-adapter@canary`
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
```js
npm install next-auth @next-auth/dynamodb-adapter@canary
npm install next-auth @next-auth/dynamodb-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
You need to pass `aws-sdk` to the adapter in addition to the table name.
You need to pass `DocumentClient` instance from `aws-sdk` to the adapter.
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
```javascript title="pages/api/auth/[...nextauth].js"
import AWS from "aws-sdk";
@@ -48,10 +49,9 @@ export default NextAuth({
}),
// ...add more providers here
],
adapter: DynamoDBAdapter({
AWS,
tableName: "next-auth-test",
}),
adapter: DynamoDBAdapter(
new AWS.DynamoDB.DocumentClient()
),
...
});
```

View File

@@ -11,10 +11,10 @@ You can find the Fauna schema and seed information in the docs at [next-auth.js.
## Getting Started
1. Install `next-auth` and `@next-auth/fauna-adapter@canary`
1. Install `next-auth` and `@next-auth/fauna-adapter`
```js
npm install next-auth @next-auth/fauna-adapter@canary
npm install next-auth @next-auth/fauna-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
@@ -49,6 +49,8 @@ export default NextAuth({
## Schema
Run the following commands inside of the `Shell` tab in the Fauna dashboard to setup the appropriate collections and indexes.
```javascript
CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
@@ -76,7 +78,7 @@ CreateIndex({
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_request_by_token",
name: "verification_request_by_token_and_identifier",
source: Collection("verification_requests"),
unique: true,
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],

View File

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

View File

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

View File

@@ -13,10 +13,10 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
1. Install `next-auth` and `@next-auth/pouchdb-adapter@canary`
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
```js
npm install next-auth @next-auth/pouchdb-adapter@canary
npm install next-auth @next-auth/pouchdb-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object

View File

@@ -11,10 +11,10 @@ You can also use NextAuth.js with the new experimental Adapter for [Prisma](http
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
:::
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter@canary` package:
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
```
npm install @prisma/client @next-auth/prisma-adapter@canary
npm install @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev
```

View File

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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @param {object} profile Provider profile
* @return {boolean|string} Return `true` to allow sign in
* Return `false` to deny access
* Return `string` to redirect to (eg.: "/unauthorized")
@@ -64,13 +64,13 @@ callbacks: {
...
```
- When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again _after_ they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked _after_ a user has clicked on a sign in link, this property will not be present.
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
- When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `profile` object is the raw body of the `HTTP POST` submission.
* When using the **Credentials Provider** the `user` object is the response returned from the `authorize` callback and the `profile` object is the raw body of the `HTTP POST` submission.
:::note
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
@@ -78,6 +78,12 @@ When using NextAuth.js with a database, the User object will be either a user ob
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
:::
:::note
Redirects returned by this callback cancel the authentication flow. Only redirect to error pages that, for example, tell the user why they're not allowed to sign in.
To redirect to a page after a successful sign in, please use [the `callbackUrl` option](/getting-started/client#specifying-a-callbackurl) or [the redirect callback](/configuration/callbacks#redirect-callback).
:::
## Redirect callback
The redirect callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
@@ -101,21 +107,22 @@ callbacks: {
...
```
:::note
The redirect callback may be invoked more than once in the same flow.
:::
## JWT callback
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client).
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
- As with database session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in.
* As with database session expiry times, token expiry time is extended whenever a session is active.
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
```js title="pages/api/auth/[...nextauth].js"
...
@@ -160,8 +167,8 @@ The session callback is called whenever a session is checked. By default, only a
e.g. `getSession()`, `useSession()`, `/api/auth/session`
- When using database sessions, the User object is passed as an argument.
- When using JSON Web Tokens for sessions, the JWT payload is provided instead.
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
@@ -170,7 +177,7 @@ callbacks: {
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
@@ -181,6 +188,8 @@ callbacks: {
...
```
If you're using TypeScript, you will want to [augment the session type](/getting-started/typescript#module-augmentation).
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.

View File

@@ -52,6 +52,7 @@ _e.g. To set a prefix for all table names you can use the **entityPrefix** optio
```js
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
```
_…or as a database configuration object:_

View File

@@ -123,27 +123,34 @@ jwt: {
// Defaults to NextAuth.js secret if not explicitly specified.
// This is used to generate the actual signingKey and produces a warning
// message if not defined explicitly.
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a secret be using `openssl rand -base64 64`
secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
// This gives you direct knowledge of the key used to sign the token so you can use it
// to authenticate indirectly (eg. to a database driver)
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
signingKey: {
kty: "oct",
kid: "Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q",
alg: "HS512",
k: "K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"
},
// If you chose something other than the default algorithm for the signingKey (HS512)
// you also need to configure the algorithm
// verificationOptions: {
// algorithms: ['HS256']
// },
verificationOptions: {
algorithms: ['HS256']
},
// Set to true to use encryption. Defaults to false (signing only).
// encryption: true,
// encryptionKey: "",
// decryptionKey = encryptionKey,
// decryptionOptions = {
// algorithms: ['A256GCM']
// },
encryption: true,
// You can generate an encryption key by using `npx node-jose-tools newkey -s 256 -t oct -a A256GCM -u enc`
encryptionKey: "",
// decryptionKey: encryptionKey,
decryptionOptions: {
algorithms: ['A256GCM']
},
// You can define your own encode/decode functions for signing and encryption
// if you want to override the default behaviour.
// async encode({ secret, token, maxAge }) {},
// async decode({ secret, token, maxAge }) {},
async encode({ secret, token, maxAge }) {},
async decode({ secret, token, maxAge }) {},
}
```
@@ -229,7 +236,7 @@ pages: {
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
}
```

View File

@@ -16,17 +16,15 @@ To add a custom login page, you can use the `pages` option:
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
}
...
```
## Error codes
We purposefully restrict the returned error codes for increased security.
### Error page
The following errors are passed as error query parameters to the default or overriden error page:
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
@@ -37,7 +35,6 @@ The following errors are passed as error query parameters to the default or over
Example: `/auth/error?error=Configuration`
### Sign-in page
The following errors are passed as error query parameters to the default or overriden sign-in page:
- **OAuthSignin**: Error in constructing an authorization URL ([1](https://github.com/nextauthjs/next-auth/blob/457952bb5abf08b09861b0e5da403080cd5525be/src/server/lib/signin/oauth.js), [2](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/pkce-handler.js), [3](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/state-handler.js)),
@@ -63,16 +60,14 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from "next-auth/client"
import { getProviders, signIn } from 'next-auth/client'
export default function SignIn({ providers }) {
return (
<>
{Object.values(providers).map((provider) => (
{Object.values(providers).map(provider => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
</div>
))}
</>
@@ -80,10 +75,10 @@ export default function SignIn({ providers }) {
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
export async function getServerSideProps(context){
const providers = await getProviders()
return {
props: { providers },
props: { providers }
}
}
@@ -102,26 +97,26 @@ SignIn.getInitialProps = async () => {
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
```jsx title="pages/auth/email-signin.js"
import { getCsrfToken } from "next-auth/client"
import { getCsrfToken } from 'next-auth/client'
export default function SignIn({ csrfToken }) {
return (
<form method="post" action="/api/auth/signin/email">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<form method='post' action='/api/auth/signin/email'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Email address
<input type="email" id="email" name="email" />
<input type='email' id='email' name='email'/>
</label>
<button type="submit">Sign in with Email</button>
<button type='submit'>Sign in with Email</button>
</form>
)
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
export async function getServerSideProps(context){
const csrfToken = await getCsrfToken(context)
return {
props: { csrfToken },
props: { csrfToken }
}
}
@@ -138,7 +133,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn("email", { email: "jsmith@example.com" })
signIn('email', { email: 'jsmith@example.com' })
```
### Credentials Sign in
@@ -146,21 +141,21 @@ signIn("email", { email: "jsmith@example.com" })
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
```jsx title="pages/auth/credentials-signin.js"
import { getCsrfToken } from "next-auth/client"
import { getCsrfToken } from 'next-auth/client'
export default function SignIn({ csrfToken }) {
return (
<form method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<form method='post' action='/api/auth/callback/credentials'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Username
<input name="username" type="text" />
<input name='username' type='text'/>
</label>
<label>
Password
<input name="password" type="password" />
<input name='password' type='password'/>
</label>
<button type="submit">Sign in</button>
<button type='submit'>Sign in</button>
</form>
)
}
@@ -169,8 +164,8 @@ export default function SignIn({ csrfToken }) {
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context),
},
csrfToken: await getCsrfToken(context)
}
}
}
@@ -187,7 +182,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn("credentials", { username: "jsmith", password: "1234" })
signIn('credentials', { username: 'jsmith', password: '1234' })
```
:::tip

View File

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

View File

@@ -87,20 +87,20 @@ https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does no
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
```
````
jose newkey -s 512 -t oct -a HS512
```
````
If you are unable to use an HS512 key (for example to interoperate with other services) you can define what is supported using
```
````
jwt: {
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
verificationOptions: {
algorithms: ["HS256"]
}
}
```
````
#### SESSION_ERROR

View File

@@ -98,7 +98,7 @@ If you are deploying directly to a particular cloud platform you may also want t
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com or info@balazsorban.com and yo@ndo.dev) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
### What is the disclosure policy for NextAuth.js?
@@ -202,7 +202,7 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
- If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
- If you do not explicitly specify a secret for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.

View File

@@ -22,7 +22,7 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
:::tip
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
You can use the [session callback](/configuration/callbacks#session-callback) to customize the session object returned to the client if you need to return additional data in the session object.
:::
---
@@ -206,7 +206,7 @@ e.g.
- `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
- `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -274,7 +274,7 @@ The following parameters are always overridden server-side: `redirect_uri`, `sta
- Client Side: **Yes**
- Server Side: No
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
In order to logout, use the `signOut()` method to ensure the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
@@ -290,7 +290,7 @@ As with the `signIn()` function, you can specify a `callbackUrl` parameter by pa
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -346,21 +346,22 @@ If every one of your pages needs to be protected, you can do this in `_app`, oth
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
import { Provider } from "next-auth/client"
export default function App ({ Component, pageProps }) {
export default function App({ Component, pageProps }) {
return (
<Provider session={pageProps.session}
<Provider
session={pageProps.session}
options={{
clientMaxAge: 60 // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60 // Send keepAlive message every 5 minutes
clientMaxAge: 60, // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60, // Send keepAlive message every 5 minutes
}}
>
>
<Component {...pageProps} />
</Provider>
)
@@ -420,7 +421,7 @@ AdminDashboard.auth = true
```jsx title="pages/_app.jsx"
export default function App({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Provider session={pageProps.session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
@@ -428,7 +429,7 @@ export default function App({ Component, pageProps }) {
) : (
<Component {...pageProps} />
)}
</SessionProvider>
</Provider>
)
}

View File

@@ -18,15 +18,15 @@ To add NextAuth.js to a project create a file called `[...nextauth].js` in `page
[Read more about how to add authentication providers.](/configuration/providers)
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
export default NextAuth({
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
clientSecret: process.env.GITHUB_SECRET
}),
// ...add more providers here
],
@@ -47,27 +47,21 @@ See the [options documentation](/configuration/options) for how to configure pro
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```jsx title="pages/index.js"
import { signIn, signOut, useSession } from "next-auth/client"
import { signIn, signOut, useSession } from 'next-auth/client'
export default function Page() {
const [session, loading] = useSession()
const [ session, loading ] = useSession()
return (
<>
{!session && (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)}
{session && (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)}
</>
)
return <>
{!session && <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>}
</>
}
```
@@ -80,9 +74,9 @@ You can use the `useSession` hook from anywhere in your application (e.g. in a h
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `pages/_app.js`.
```jsx title="pages/_app.js"
import { Provider } from "next-auth/client"
import { Provider } from 'next-auth/client'
export default function App({ Component, pageProps }) {
export default function App ({ Component, pageProps }) {
return (
<Provider session={pageProps.session}>
<Component {...pageProps} />

View File

@@ -5,11 +5,11 @@ title: Azure Active Directory B2C
## Documentation
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow
## Configuration
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
https://docs.microsoft.com/azure/active-directory-b2c/tutorial-create-tenant
## Options

View File

@@ -0,0 +1,38 @@
---
id: coinbase
title: Coinbase
---
## Documentation
https://developers.coinbase.com/api/v2
## Configuration
https://www.coinbase.com/settings/api
## Options
The **Coinbase Provider** comes with a set of default options:
- [Coinbase Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/coinbase.js)
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Coinbase({
clientId: process.env.COINBASE_CLIENT_ID,
clientSecret: process.env.COINBASE_CLIENT_SECRET
})
]
...
```
:::tip
This Provider template has a 2 hour access token to it. A refresh token is also returned.
:::

View File

@@ -29,15 +29,15 @@ The Credentials provider is specified like other providers, except that you need
1. A `user` object, which indicates the credentials are valid.
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
2. Either `false` or `null`, which indicates failure.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
3. You can throw an Error or a URL (a string).
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
The Credentials provider's `authorize()` method also provides the request object as the second parameter (see example below).
@@ -88,43 +88,34 @@ You can also use them in conjunction with other provider options.
As with all providers, the order you specify them is the order they are displayed on the sign in page.
```js
providers: [
Providers.Credentials({
id: "domain-login",
name: "Domain Account",
async authorize(credentials, req) {
const user = {
/* add function to get user */
}
return user
},
credentials: {
domain: {
label: "Domain",
type: "text ",
placeholder: "CORPNET",
value: "CORPNET",
providers: [
Providers.Credentials({
id: 'domain-login',
name: "Domain Account",
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},
username: { label: "Username", type: "text ", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
}),
Providers.Credentials({
id: "intranet-credentials",
name: "Two Factor Auth",
async authorize(credentials, req) {
const user = {
/* add function to get user */
}
return user
},
credentials: {
email: { label: "Username", type: "text ", placeholder: "jsmith" },
"2fa-key": { label: "2FA Key" },
},
}),
/* ... additional providers ... /*/
]
credentials: {
domain: { label: "Domain", type: "text ", placeholder: "CORPNET", value: "CORPNET" },
username: { label: "Username", type: "text ", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
}
}),
Providers.Credentials({
id: 'intranet-credentials',
name: "Two Factor Auth",
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},
credentials: {
email: { label: "Username", type: "text ", placeholder: "jsmith" },
"2fa-key": { label: "2FA Key" }
},
}),
/* ... additional providers ... /*/
]
```
### Example UI
@@ -135,8 +126,7 @@ You can also [use a custom sign in page](/configuration/pages#credentials-sign-i
<Image src="/img/signin-complex.png"/>
export const Image = ({ children, src, alt = '' }) => (
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',

View File

@@ -13,7 +13,7 @@ The Email provider can be used in conjunction with (or instead of) one or more O
### How it works
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used within that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
If someone provides the email address of an _existing account_ when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
@@ -187,7 +187,7 @@ const html = ({ url, site, email }) => {
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>

View File

@@ -0,0 +1,33 @@
---
id: freshbooks
title: Freshbooks
---
## Documentation
https://www.freshbooks.com/api/authenticating-with-oauth-2-0-on-the-new-freshbooks-api
## Configuration
https://my.freshbooks.com/#/developer
## Options
The Freshbooks Provider comes with a set of default options:
https://www.freshbooks.com/api/start
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Freshbooks({
clientId: process.env.FRESHBOOKS_CLIENT_ID,
clientSecret: process.env.FRESHBOOKS_CLIENT_SECRET,
})
]
...
```

View File

@@ -34,7 +34,7 @@ providers: [
```
:::warning
Google only provide the Refresh Token to an application the first time a user signs in.
Google only provides Refresh Token to an application the first time a user signs in.
To force Google to re-issue a Refresh Token, the user needs to remove the application from their account and sign in again:
https://myaccount.google.com/permissions

View File

@@ -19,7 +19,7 @@ From the Auth tab get the client ID and client secret. On the same tab, add redi
The **LinkedIn Provider** comes with a set of default options:
- [LinkedIn Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/linked-in.js)
- [LinkedIn Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/linkedin.js)
You can override any of the options to suit your own use case.

View File

@@ -0,0 +1,34 @@
---
id: naver
title: Naver
---
## Documentation
https://developers.naver.com/docs/login/overview/overview.md
## Configuration
https://developers.naver.com/docs/login/api/api.md
## Options
The **Naver Provider** comes with a set of default options:
- [Naver Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/naver.js)
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Naver({
clientId: process.env.NAVER_CLIENT_ID,
clientSecret: process.env.NAVER_CLIENT_SECRET
})
]
...
```

View File

@@ -0,0 +1,31 @@
---
id: onelogin
title: OneLogin
---
## Documentation
https://developers.onelogin.com/openid-connect
## Options
The **OneLogin Provider** comes with a set of default options:
- [OneLogin Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/onelogin.js)
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.OneLogin({
clientId: process.env.ONELOGIN_CLIENT_ID,
clientSecret: process.env.ONELOGIN_CLIENT_SECRET,
domain: process.env.ONELOGIN_DOMAIN
})
]
...
```

View File

@@ -51,15 +51,15 @@ To add a custom login page, you can use the `pages` option:
We can then add a custom login page that displays an input where the user can enter their email address. We then extract the domain from the user's email address and pass it to the `authorizationParams` parameter on the `signIn` function:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from "next-auth/client"
import { getProviders, signIn } from 'next-auth/client'
export default function SignIn({ providers }) {
const [email, setEmail] = useState("")
const [email, setEmail] = useState('')
return (
<>
{Object.values(providers).map((provider) => {
if (provider.id === "workos") {
if (provider.id === 'workos') {
return (
<div key={provider.id}>
<input
@@ -71,14 +71,14 @@ export default function SignIn({ providers }) {
<button
onClick={() =>
signIn(provider.id, undefined, {
domain: email.split("@")[1],
domain: email.split('@')[1],
})
}
>
Sign in with SSO
</button>
</div>
)
);
}
return (
@@ -87,17 +87,17 @@ export default function SignIn({ providers }) {
Sign in with {provider.name}
</button>
</div>
)
);
})}
</>
)
);
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context) {
export async function getServerSideProps(context){
const providers = await getProviders()
return {
props: { providers },
props: { providers }
}
}

View File

@@ -11,7 +11,7 @@ _New submissions and edits are welcome!_
### [NextJS Authentication Crash Course with NextAuth.js](https://youtu.be/o_wZIVmWteQ)
This tutorial dives in to the ins and outs of NextAuth including email, Github, Twitter and integrating with Auth0 in under hour.
This tutorial dives in to the ins and outs of NextAuth including email, GitHub, Twitter and integrating with Auth0 in under hour.
### [Create your own NextAuth.js Login Pages](https://youtu.be/kB6YNYZ63fw)
@@ -83,6 +83,10 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and API routes behind that authentication, etc.
### [Introduction to NextAuth.js](https://www.youtube.com/watch?v=npZsJxWntJM)
This is an introductory video to NextAuth.js for beginners. In this video, it is explained how to set up authentication in a few easy steps and add different configurations to make it more robust and secure.
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.
@@ -90,3 +94,7 @@ This tutorial walks step by step on how to get Sign In with Apple working (both
### [How to Authenticate Next.js Apps with Twitter & NextAuth.js](https://spacejelly.dev/posts/how-to-authenticate-next-js-apps-with-twitter-nextauth-js/)
Learn how to add Twitter authentication and login to a Next.js app both clientside and serverside with NextAuth.js.
### [Using NextAuth.js with Magic links](https://dev.to/narciero/using-nextauth-js-with-magic-links-df4)
Learn how to use [Magic](https://magic.link) link authentication with [NextAuth.js](https://next-auth.js.org) to enable passwordless authentication without a database.

View File

@@ -3,16 +3,16 @@ id: ldap-auth-example
title: LDAP Authentication
---
NextAuth.js provides the ability to setup a [custom Credential provider](/configuration/providers#sign-in-with-credentials) which we can take advantage of to authenticate users against an existing LDAP server.
NextAuth.js provides the ability to setup a [custom Credential provider](/configuration/providers#sign-in-with-credentials) which we can take advantage of to authenticate users against an existing LDAP server.
You will need an additional dependency, `ldapjs`, which you can install by running `npm install ldapjs`.
Then you must setup the `Providers.Credentials()` provider key like so:
```js title="[...nextauth].js"
const ldap = require("ldapjs")
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
const ldap = require("ldapjs");
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
export default NextAuth({
providers: [
@@ -26,37 +26,37 @@ export default NextAuth({
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: process.env.LDAP_URI,
})
});
// Essentially promisify the LDAPJS client.bind function
return new Promise((resolve, reject) => {
client.bind(credentials.username, credentials.password, (error) => {
if (error) {
console.error("Failed")
reject()
console.error("Failed");
reject();
} else {
console.log("Logged in")
console.log("Logged in");
resolve({
username: credentials.username,
password: credentials.password,
})
});
}
})
})
});
});
},
}),
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
const isSignIn = user ? true : false
const isSignIn = user ? true : false;
if (isSignIn) {
token.username = user.username
token.password = user.password
token.username = user.username;
token.password = user.password;
}
return token
return token;
},
async session(session, user) {
return { ...session, user: { username: user.username } }
return { ...session, user: { username: user.username } };
},
},
secret: process.env.NEXTAUTH_SECRET,
@@ -64,19 +64,19 @@ export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
encryption: true, // Very important to encrypt the JWT, otherwise you're leaking username+password into the browser
},
})
});
```
The idea is that once one is authenticated with the LDAP server, one can pass through both the username/DN and password to the JWT stored in the browser.
The idea is that once one is authenticated with the LDAP server, one can pass through both the username/DN and password to the JWT stored in the browser.
This is then passed back to any API routes and retrieved as such:
```js title="/pages/api/doLDAPWork.js"
token = await jwt.getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
})
const { username, password } = token
req,
secret: process.env.NEXTAUTH_SECRET,
});
const {username, password} = token;
```
> Thanks to [Winwardo](https://github.com/Winwardo) for the code example

View File

@@ -48,7 +48,7 @@ export default {
```
:::note
[View source for built-in TypeORM models and schemas](https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy/src/models)
[View source for built-in TypeORM models and schemas](https://github.com/nextauthjs/adapters/tree/main/packages/typeorm-legacy/src/models)
:::
## Using custom models

View File

@@ -46,4 +46,32 @@ You can use [node-jose-tools](https://www.npmjs.com/package/node-jose-tools) to
**Option 2**: Specify custom encode/decode functions on the jwt object. This gives you complete control over signing / verification / etc.
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY
#### SIGNIN_CALLBACK_REJECT_REDIRECT
You returned something in the `signIn` callback, that is being deprecated.
You probably had something similar in the callback:
```js
return Promise.reject("/some/url")
```
or
```js
throw "/some/url"
```
To remedy this, simply return the url instead:
```js
return "/some/url"
```
#### STATE_OPTION_DEPRECATION
You provided `state: true` or `state: false` as a provider option. This is being deprecated in a later release in favour of `protection: "state"` and `protection: "none"` respectively. To remedy this warning:
- If you use `state: true`, just simply remove it. The default is `protection: "state"` already..
- If you use `state: false`, set `protection: "none"`.

View File

@@ -42,12 +42,12 @@ module.exports = {
position: "left",
},
{
href: "https://www.npmjs.com/package/next-auth",
to: "https://www.npmjs.com/package/next-auth",
label: "npm",
position: "right",
},
{
href: "https://github.com/nextauthjs/next-auth",
to: "https://github.com/nextauthjs/next-auth",
label: "GitHub",
position: "right",
},
@@ -69,12 +69,8 @@ module.exports = {
to: "/getting-started/introduction",
},
{
label: "Contributors",
to: "/contributors",
},
{
label: "Canary documentation",
to: "https://next-auth-git-canary.nextauthjs.vercel.app/",
label: "Next documentation",
to: "https://next-auth-git-next.nextauthjs.vercel.app",
},
],
},
@@ -95,8 +91,12 @@ module.exports = {
title: "Acknowledgements",
items: [
{
label: "Docusaurus",
to: "https://v2.docusaurus.io/",
label: "Contributors",
to: "/contributors",
},
{
label: "Sponsors",
to: "https://opencollective.com/nextauth",
},
{
label: "Images by unDraw",
@@ -118,6 +118,13 @@ module.exports = {
],
copyright: "NextAuth.js &copy; Iain Collins 2021",
},
colorMode: {
respectPrefersColorScheme: true,
switchConfig: {
darkIcon: "🌑",
lightIcon: "💡",
},
},
},
presets: [
[

View File

@@ -1,34 +1,10 @@
{
"redirects": [
{
"source": "/schemas/models",
"destination": "/adapters/models",
"permanent": true
},
{
"source": "/schemas/mysql",
"destination": "/adapters/typeorm/mysql",
"permanent": true
},
{
"source": "/schemas/postgres",
"destination": "/adapters/typeorm/postgres",
"permanent": true
},
{
"source": "/schemas/mssql",
"destination": "/adapters/typeorm/mssql",
"permanent": true
},
{
"source": "/schemas/mongodb",
"destination": "/adapters/typeorm/mongodb",
"permanent": true
},
{
"source": "/schemas/adapters",
"destination": "/adapters/overview",
"permanent": true
}
{ "source": "/schemas/models", "destination": "/adapters/models", "permanent": true },
{ "source": "/schemas/mysql", "destination": "/adapters/typeorm/mysql", "permanent": true },
{ "source": "/schemas/postgres", "destination": "/adapters/typeorm/postgres", "permanent": true },
{ "source": "/schemas/mssql", "destination": "/adapters/typeorm/mssql", "permanent": true },
{ "source": "/schemas/mongodb", "destination": "/adapters/typeorm/mongodb", "permanent": true },
{ "source": "/schemas/adapters", "destination": "/adapters/overview", "permanent": true }
]
}