Compare commits

...

177 Commits
v3.15.10 ... v3

Author SHA1 Message Date
Balázs Orbán
8d73daf343 update lock file 2022-08-01 13:53:04 +02:00
Balázs Orbán
3470b582a1 fix: normalize email 2022-08-01 13:50:21 +02:00
Balázs Orbán
6d127e33fc chore: update lock file 2022-07-25 13:06:18 +02:00
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
Camille Gabrieli
9f16e3f0fb docs(client): fix typo (#2139) 2021-06-08 09:02:41 +02:00
Adrian Artiles
1042e9a93d docs: fix typos (#2136) 2021-06-08 08:57:13 +02:00
Nico Domino
aa57f2dd7e docs(prisma-legacy): update tip location
Move client tip up to client section of docs
2021-06-07 22:44:04 +02:00
Nico Domino
1817286ce3 Update pouchdb.md 2021-06-07 22:21:39 +02:00
Nico Domino
b942dd34f3 docs(pouchdb): add pouchdb page (#2140) 2021-06-07 17:10:42 +02:00
Lluis Agusti
4d9622e1cc chore(git): fix git hooks (#2130)
Contains the following squashed commits:

* chore(git): fix husky pre-commit
* chore(husky): install git hooks on `postinstall`
2021-06-04 12:55:41 +02:00
sanctuxm
a7eadf80e5 docs(provider): fix ngrok typo on instagram provider docs (#2121) 2021-06-03 10:35:07 +02:00
Manish Kumar
75c7dbc3e7 docs(adapter): fix file location in DynamoDB docs (#2120) 2021-06-03 10:11:45 +02:00
Yi-Ru Lin
d36b89cb12 feat(provider): add Zoom provider (#2110)
* feat(provider): add Zoom provider

* Update src/providers/zoom.js

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

* Update src/providers/zoom.js

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

* Update www/docs/providers/zoom.md

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

* fix: syntax error

* Update www/docs/providers/zoom.md

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

* Update www/docs/providers/zoom.md

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

* Update www/docs/providers/zoom.md

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

* remove the default protection setting of Zoom for now

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-03 00:44:22 +02:00
Nico Domino
349cd03fbd docs(adapters): update adapter install instructions to canary branch (#2119) 2021-06-02 23:50:01 +02:00
Lluis Agusti
5cd130669b chore(lint): format files on pre-commit (#2117)
Contains the following squashed commits:

* chore(lint): run prettier on pre-commit
* chore(lint): format files on pre-commit
* chore(npm): update lock file
2021-06-02 13:59:53 +02:00
Lluis Agusti
638233f4a0 docs(readme): update release flow badge 2021-06-01 18:01:22 +02:00
Lluis Agusti
37e175195f chore(github): re-organize workflows (#2109)
Contains:

* chore(github): re-organize workflows
* chore(github): rename workflows structure
2021-06-01 17:52:17 +02:00
Lluis Agusti
e8a9e8aeb6 fix(client): unit tests setup and providers error handling (#1992)
* test(client): initial Jest + RTL setup

* test(client): add tests for `getSession`

* test(client): document expect cases and fix regex

* test(client): small refactors

* chore(npm): re-generate package-lock.json

* test(client): initial test for `signIn`

* test(client): refactor session tests for consistency

* test(client): credentials/email signin scenarios

* test(client): finish sign-in tests

* chore(github): add test to ci

* test(client): refactor and extend use cases

* test(client): sign-out tests

* refactor(client): code review suggestions (1)

* test(client): add few more sign-in/sign-out cases

* test(client): broadcasting session events

* fix(client): handle fetch providers error
2021-06-01 17:12:13 +02:00
Balázs Orbán
1fb308a6f4 docs(adapter): correct npm install script 2021-06-01 00:44:07 +02:00
Paul van Dyk
613c303315 docs: fix spelling in docs (#2105)
`restriected` => `restricted`
2021-05-31 19:22:39 +02:00
Nico Domino
d24fe1cebb docs: add error + warning pages to sidebar (#2100) 2021-05-31 02:14:27 +02:00
Manten
885b02ca95 chore(dev): add property to decrypt JWT (#2095) 2021-05-31 01:07:46 +02:00
Balázs Orbán
f218697fd6 docs(adapter): remove unnecessary section from prisma 2021-05-30 23:22:00 +02:00
Balázs Orbán
dbead0ad85 docs(adapter): fix API mixup in legacy adapter 2021-05-30 23:17:57 +02:00
Nico Domino
704ded5310 docs(prisma): add prisma-legacy separate docs page (#2097) 2021-05-30 21:44:58 +02:00
Manten
25fbcb4648 docs(FAQ): fix typo (#2088) 2021-05-29 16:47:06 +02:00
Nico Domino
53a439b44b docs(firebase): update firebase usage and options (#2076)
* docs(firebase): update firebase usage and options

* docs(firebase): add firebase tips/warnings

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-28 16:05:15 +02:00
Ben Orozco
16a2e37fd6 feat: Allow client to override scope (#2079)
* Ref[Signin]: Allow client to override scope

Allow client to override `scope` via query params

* Doc[Client]: Signin no longer overrides scope server-side
2021-05-28 10:07:42 +02:00
Colby Fayock
0392a8df9a docs(website): add Twitter Provider tutorial 2021-05-26 09:29:08 +02:00
Olav Fosse
a459b95c5b docs(website): fix typo (#2061) 2021-05-25 20:53:37 +02:00
Balázs Orbán
13df7eb81d docs: update urls to .vercel.app (#2039) 2021-05-25 00:35:57 +02:00
Kiran Paul
62f261209c docs(provider): improve authorize code example (#2046)
* Updated user fetch code as per review comments
2021-05-24 16:54:50 +02:00
Nico Domino
da43d0d896 docs(adapters): reorganise adapter docs for new pkg (#2051)
* docs(adapters): reorganise adapter docs for new pkg

* docs(adapters): fix link typos

* docs(adapters): add vercel.json redirects for new adapters URLs
2021-05-23 22:16:14 +02:00
Ben West
4b1271ba75 docs: Remove claim that new users do not have an ID (#1737)
I'm not sure when this changed, but it's no longer true. If the person logging in doesn't have a stored user account, the ID will be the provider_account_id
2021-05-22 13:47:48 +02:00
Marshall Bowers
d30da0170f fix(provider): make WorkOS domain configurable from signIn (#2038)
* Don't pass `domain` to the WorkOS provider

* Update docs

* Change `apiUrl` to `domain`
2021-05-22 13:40:48 +02:00
Nico Domino
887b2985fc docs(adapters): update copy regarding adapters (#2026)
* docs(adapters): update copy regarding adapters

* docs(adapters): add prisma schema page

* docs(adapters): add fauna schema/setup page

* docs(adapters): address PR comments

* Update www/docs/schemas/adapters.md

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

* docs(adapters): update adapters.md

* docs(adapters): update adapters.md

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-22 12:10:26 +02:00
Nico Domino
d2bbac1164 docs: explain where pageProps come from in Provider docs (#2016)
* docs: explain where pageProps come from in Provider docs

* chore: formatting

* docs(getting-started): add alternative client session handling methods

* docs(getting-started): update alternative client api docs
2021-05-22 11:30:38 +02:00
Balázs Orbán
35583a513d fix: ts type, and transpilation (#2037)
* fix(ts): mark getUserByEmail param as nullable

* fix(build): transpile with optional-catch-binding
2021-05-20 20:40:45 +02:00
Nico Domino
665d91019f style: small tweaks to navbar (#2024) 2021-05-20 16:22:31 +02:00
Daniel Sabbagh
f2b816b7b9 docs: fix minor typo (#2022)
* fix minor typo

* fix typo again

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-19 20:59:00 +02:00
Nico Domino
2e770fb0bf docs: update github PR template comments (#2025) 2021-05-19 01:11:42 +02:00
Nico Domino
e83e7231fb docs(search): add new algolia docsearch (#2023)
* docs(search): add new algolia docsearch

* style(search): fix algolia docsearch mobile style
2021-05-18 21:49:43 +02:00
Marco Valsecchi
4593ec8b01 docs(provider): Fix Using a custom OAuth Provider index link (#2019) 2021-05-18 14:30:17 +02:00
Nico Domino
12517f629b docs(style): add github star counter to navbar (#2015)
* docs(style): add github star counter to navbar

* chore: cleanup kFormatter logic
2021-05-18 00:23:44 +02:00
Balázs Orbán
77012bc00c fix(deps): pin down legacy adapter versions (#2009)
* fix(deps): pin down legacy adapter versions

* chore: trigger github actions
2021-05-16 20:52:04 +02:00
Chalk
60fdf26a56 fix(provider): support multiple image formats for Twitter profile (#1995)
see supported formats: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-15 23:28:34 +02:00
Igor Danchenko
0fae0c7a8e feat(provider): forward request to authorize (#1979)
* feat/add-request-to-credentials-authorize

* Update src/server/routes/callback.js

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

* Update types/providers.d.ts

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

* Update www/docs/providers/credentials.md

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

* Update www/docs

* Update test app

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-15 03:00:39 +02:00
Nico Domino
eba79f4445 feat: upgrade docusaurus + style a bit (#1993) 2021-05-13 23:24:02 +02:00
Balázs Orbán
e3bb9881ea chore(dev): fix dev app imports (#1991) 2021-05-13 12:36:28 +02:00
Balázs Orbán
827049cb35 docs(www): Docusaurus webpack 5 (#1989)
This reverts commit bc9805d1ba.
2021-05-13 01:28:36 +02:00
Nico Domino
ad8100d402 docs: max cookie size information (#1949)
* fix: max cookie information

* fix: typo

* fix: wording regarding cookie size

* Update www/docs/faq.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:22:08 +02:00
dependabot[bot]
7b5defff16 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#1976)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:20:24 +02:00
Balázs Orbán
bc9805d1ba docs: revert "Docusaurus webpack 5" (#1982)
This reverts commit c823016b36.
2021-05-12 10:19:30 +02:00
Sébastien Lorber
c823016b36 docs(www): update Docusaurus to webpack 5 (#1826)
* upgrade

* upgrade

* fix lunr plugin bug

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-12 10:01:33 +02:00
dependabot[bot]
ca0f4c6fba chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /www (#1977)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 21:04:27 +02:00
Balázs Orbán
c0d2f2d852 fix(adapter): upgrade legacy adapters (#1952)
* refactor(adapter): upgrade typeorm-legacy-adapter

* fix(ts): correct exported typeorm types

* fix(adapter): correct adapter exports

* chore(deps): upgrade typeorm-legacy-adapter

* chore(deps): upgrade dependencies

* chore: match comment for legacy adapters

* fix(ts): correctly export Prisma legacy types

* chore(deps): upgrade prisma legacy adapter

* chore(deps): remove unused dependencies

* test(ts): only run TS tests on latest TS version

* chore(deps): remove unused dev dependencies

* chore(deps): upgrade prisma adapter
2021-05-11 00:15:01 +02:00
Balázs Orbán
71f63117a9 fix(oauth): correctly set internal protection value (#1962) 2021-05-09 23:00:06 +02:00
i-palindrome-i
d04ce29314 feat(provider): add WorkOS provider (#1939)
* feat(provider): add WorkOS provider

* Update www/docs/providers/workos.md

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

* Update workos.md

Co-authored-by: Adam Kaczmarek <adamkaz+workos@gmail.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-09 21:45:37 +02:00
Lluis Agusti
d2882f1958 fix(deps): unpin react-dom version (#1956) 2021-05-09 21:43:51 +02:00
Lluis Agusti
66db563ca5 docs(provider): link to providers' source code (#1955) 2021-05-09 21:41:28 +02:00
Marcus Reinhardt
9619077363 docs(typeorm): update link to source (#1957) 2021-05-08 23:37:55 +02:00
dependabot[bot]
013ccb4cb0 chore(deps): bump lodash from 4.17.19 to 4.17.21 (#1954)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-08 00:40:35 +02:00
dependabot[bot]
6eb41259d1 chore(deps): bump underscore from 1.10.2 to 1.13.1 (#1951)
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.10.2 to 1.13.1.
- [Release notes](https://github.com/jashkenas/underscore/releases)
- [Commits](https://github.com/jashkenas/underscore/compare/1.10.2...1.13.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 17:58:22 +02:00
Nico Domino
141f8d07e2 docs(provider): clarify where user is created with email provider (#1950) 2021-05-07 00:54:52 +02:00
Zack Sheppard
ffd0601ab0 fix(ts): improve events handlers' types (#1853)
* Constrain the adapters type generics more accurately

* Add types for the incoming messages to events callbacks

* Code review comments from @lluia

* Rebase from trunk and fix merge conflicts

* Update documentation

* Rip out generics

* fix(build): export aliases from client (#1909)

* docs(provider): update providers documentation (#1900)

* docs(providers): update providers documentation

- delineate clearly the 3 provider types (oauth, email, credentials)
- make each section structure consistent
- update the option list for every provider type
- use emojis

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

To stay consistent with the rest of our documentation, for now we should not emojis on the sections of our documentation pages.

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

* docs(providers): clarify `params` option usage

* docs(providers): make names list inline

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

* fix(ts): unset generics defaults for overriding (#1891)

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

* fix(ts): tweak Adapter related types (#1914)

Contains the following squashed commits:

* fix(ts): make first adapter parameter non-optional
* fix(ts): make defaulted values non-optional internally
* test(ts): fix linting

* fix(page): don't pass params to custom signout page (#1912)

* For the custom signout page addressed two issues with the query params being added to the signout url. A conditional check on the error value is now made before adding it as a query param. Also added a conditional check on the callbackUrl and if present that then gets appended as a query param to the signout api call.

* Changed fix for bug #192 to have no querystring params in the custom signout page url.

Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(www): fix typo (#1922)

* docs(provider): Update IdentityServer 4 demo configuration (#1932)

* Responding to code review comments

* Fix tests

* Fix lint error

Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Kristóf Poduszló <kripod@protonmail.com>
Co-authored-by: Anubisoft <1471887+anubisoft@users.noreply.github.com>
Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Ernie Miranda <emiranda04@users.noreply.github.com>
Co-authored-by: Mathis Møller <thisen-dk@hotmail.com>
2021-05-06 11:44:30 +02:00
Lluis Agusti
7864d4705d docs(adapter): mention new types (#1916)
Containts the following squashed commits:

* docs(adapters): mention new types
* docs(adapters): rename interface on example
* docs(adapters): move section above
* docs(adapters): fix casing
* docs(adapters): fix example import
* fix(www): Typescript -> TypeScript
2021-05-06 11:10:37 +02:00
i-palindrome-i
98dc82e5d6 docs: fix command in CONTRIBUTING.md (#1940)
Co-authored-by: Adam Kaczmarek <adamkaz+workos@gmail.com>
2021-05-06 10:19:50 +02:00
Balázs Orbán
86baefdd9d feat(adapter): take away error handling from adapters (#1871) 2021-05-05 19:45:11 +02:00
Manish Chiniwalar
332e237c3e feat(provider): add Dropbox (#1756)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Adam Bergman <adam@fransvilhelm.com>
2021-05-05 19:42:55 +02:00
Mathis Møller
2fce08c0b5 docs(provider): Update IdentityServer 4 demo configuration (#1932) 2021-05-05 15:17:22 +02:00
Ernie Miranda
adf3fb669f docs(www): fix typo (#1922) 2021-05-04 19:34:06 +02:00
Anubisoft
5323be3594 fix(page): don't pass params to custom signout page (#1912)
* For the custom signout page addressed two issues with the query params being added to the signout url. A conditional check on the error value is now made before adding it as a query param. Also added a conditional check on the callbackUrl and if present that then gets appended as a query param to the signout api call.

* Changed fix for bug #192 to have no querystring params in the custom signout page url.

Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-03 22:43:38 +02:00
Balázs Orbán
6df0d04a1e fix(ts): tweak Adapter related types (#1914)
Contains the following squashed commits:

* fix(ts): make first adapter parameter non-optional
* fix(ts): make defaulted values non-optional internally
* test(ts): fix linting
2021-05-03 21:24:19 +02:00
Kristóf Poduszló
aa9c1e7c96 fix(ts): unset generics defaults for overriding (#1891)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-03 14:31:56 +02:00
Lluis Agusti
66473054f5 docs(provider): update providers documentation (#1900)
* docs(providers): update providers documentation

- delineate clearly the 3 provider types (oauth, email, credentials)
- make each section structure consistent
- update the option list for every provider type
- use emojis

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

To stay consistent with the rest of our documentation, for now we should not emojis on the sections of our documentation pages.

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

* docs(providers): clarify `params` option usage

* docs(providers): make names list inline

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-02 22:07:08 +02:00
Balázs Orbán
e8ddbc5c11 fix(build): export aliases from client (#1909) 2021-05-02 12:11:11 +02:00
Ernie Miranda
dfe4620056 docs(www): fix minor typo. (#1902)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-01 11:09:01 +02:00
leeoocca
848224e2c5 fix(ts): optional variables for custom provider options (#1876)
Contains the following squashed commits:

* fix optional variables for custom provider options
* revert some types for custom provider
* docs: client secret required in provider options
* Revert "docs: client secret required in provider options"
2021-05-01 10:46:04 +02:00
dependabot[bot]
aee376cc57 chore(deps): bump ssri from 6.0.1 to 6.0.2 in /www (#1901)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-30 21:17:23 +02:00
Amir Ali
0d2a81cd39 docs(www): syntax error on JWT_SESSION_ERROR code example (#1899) 2021-04-30 16:51:02 +02:00
Balázs Orbán
61e99c9489 fix(ts): wrap adapter option in ReturnType (#1887)
* fix(ts): wrap adapter option in ReturnType

* test(ts): fix adapter tests
2021-04-29 19:43:34 +02:00
Balázs Orbán
0eb4159737 fix(ts): fix updateSession return type 2021-04-28 22:23:13 +02:00
Balázs Orbán
9f0008375f fix(ts): fix createVerificationRequest type (#1877) 2021-04-28 22:16:09 +02:00
leeoocca
0cf1823e70 docs: fix typos in custom provider page (#1875)
* fix typo on custom provider options table

* fix typo in custom provider code example
2021-04-28 20:49:27 +02:00
Mohamed Ouyizme
7f39669053 feat(provider): add 42 School provider (#1872)
* feat(provider): add 42 School provider

* fix(docs): fix provider import

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

* fix(provider): change provider id

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

* fix(provider): change provider id

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

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-28 19:10:05 +02:00
Balázs Orbán
7b82d6e985 fix(ts): typo in Adapter interface 2021-04-28 11:59:34 +02:00
Balázs Orbán
53b0a7aa74 fix(ts): improve adapter TypeScript support (#1870)
* fix(ts): clean up adapter interfaces

* fix(ts): add accessTokenExpires to TokenSet

* docs(adapter): do not recommend getUserByCredentials

* fix(ts): make whole EmailConfig required in AdapterInstance

* fix(ts): fix tests

* refactor(ts): remove legacy adapter types

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-04-28 11:20:03 +02:00
Lluis Agusti
fbb09303af docs(website): fix layout on small screens (#1869) 2021-04-27 19:30:01 +02:00
Balázs Orbán
ff05ac1e41 feat(adapter): split out adapters (#1862)
* refactor(adapter): remove example adapter

* chore(deps): add legacy adapter dependencies

* refactor(adapter): reference legacy adapters

* chore(deps): upgrade legacy adapters

* test(adapter): remove duplicate tests

* test: remove disfunctional tests

* chore: remove accidentally pushed file

* chore: revert unnecessary file changes
2021-04-27 10:01:11 +02:00
Lluis Agusti
a6f6c1590d chore(github): fix typos on issue templates (#1858)
* chore(github): fix typos on issue templates...

* chore(github): use statements rather than comments

on the PR template

* chore(github): Typescript -> TypeScript

* chore(github): add links to Codesanbox on issue templates

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-26 16:02:37 +02:00
Lluis Agusti
52c2466b9e chore(github): update PR/issue templates (#1829)
Contains the following squashed commits: 

* chore(github): update PR template
* chore(github): refine PR template again
* chore(github): improve issue templates
* chore(github): fix typos on issue templates
* chore(github): improve "affected issues" section on PR template
* chore(github): link question bug report template
* chore(github): fix typo on issue template
* chore(github): add TypeScript issue template
2021-04-26 11:58:32 +02:00
Ashutosh Kumar
fb04ab4e76 fix(ts): make GetSessionOptions optional (#1851) 2021-04-25 15:39:19 +02:00
Ashutosh Kumar
07e2a83ccb fix(ts): make ctxOrReq optional in getCsrfToken() (#1850) 2021-04-25 14:10:21 +02:00
Balázs Orbán
065d9eb310 chore(release): do not mark released PRs/issues (#1845) 2021-04-24 23:17:24 +02:00
Thanayut T
5da19f3c9a feat(provider): add WordPress.com provider (#1837) 2021-04-24 10:48:44 +02:00
Balázs Orbán
88ec3bad71 chore: move files from root 2021-04-24 00:44:08 +02:00
Lluis Agusti
5ab7868533 chore(ci): remove Node 10 and add Node 16 (#1830)
* chore(github): add CODEOWNERS

* chore(ci): remove Node 10 and add Node 16
2021-04-24 00:20:50 +02:00
Lluis Agusti
835dda0899 chore(github): add CODEOWNERS (#1827) 2021-04-24 00:17:19 +02:00
Wilkins Fernandez
ad4709764a docs: update import for providers (#1823)
Updates the names export from `providers` to `getProviders`.
2021-04-23 14:58:53 +02:00
Michał Bundyra
55a2932973 fix(ts): add Mailchimp provider (#1821) 2021-04-23 13:11:13 +02:00
Michał Bundyra
49cb7e5bd7 feat(provider): add Mailchimp provider (#1781) 2021-04-23 12:15:25 +02:00
Balázs Orbán
b95182ded7 fix(ts): expose errors type delcarations (#1817) 2021-04-22 23:45:23 +02:00
Balázs Orbán
be28672fd4 fix(errors): expose custom errors (#1816)
* chore(deps): add class-properties babel plugin

* feat(errors): expand list of custom error classes

* build(errors): expose errors as a submodule
2021-04-22 23:28:38 +02:00
Balázs Orbán
e26c5fc905 fix(ts): adjust AppOptions (#1815) 2021-04-22 23:04:27 +02:00
235 changed files with 48686 additions and 28694 deletions

View File

@@ -1,4 +0,0 @@
# Exclude directories we don't need from Docker context to improve build time
node_modules
www
src

View File

@@ -1,15 +0,0 @@
# Rename file to .env and populate values
# to be able to run tests
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_TWITTER_ID=
NEXTAUTH_TWITTER_SECRET=
NEXTAUTH_TWITTER_USERNAME=
NEXTAUTH_TWITTER_PASSWORD=
NEXTAUTH_GITHUB_ID=
NEXTAUTH_GITHUB_SECRET=
NEXTAUTH_GITHUB_USERNAME=
NEXTAUTH_GITHUB_PASSWORD=
NEXTAUTH_GOOGLE_ID=
NEXTAUTH_GOOGLE_SECRET=
NEXTAUTH_GOOGLE_USERNAME=
NEXTAUTH_GOOGLE_PASSWORD=

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
/types/ @balazsorban44 @lluia

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,34 +0,0 @@
---
name: Bug report
about: Report a defect with NextAuth.js
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of the bug in NextAuth.js.
Do not report bugs with your own project here, ask from help by raising a question instead - this helps us a lot with administration overhead.
**Steps to reproduce**
Steps to reproduce the behavior.
Include a link to public repository which can be used to reproduce the behaviour.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or error logs**
If applicable add screenshots or error logs to help explain the problem.
**Additional context**
Add any other context about the problem here.
**Feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful
* [ ] Found documentation but was incomplete
* [ ] Could not find relevant documentation
* [ ] Found the example project helpful
* [ ] Did not find the example project helpful

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,26 +0,0 @@
---
name: Feature request
about: Suggest an idea for NextAuth.js
labels: enhancement
assignees: ''
---
**Summary of proposed feature**
A clear and concise description of the feature being proposed.
**Purpose of proposed feature**
A clear and concise description of why this feature is necessary and what problems it solves.
**Detail about proposed feature**
A detailed description of how the proposal might work (if you have one).
**Potential problems**
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
**Describe any alternatives you've considered**
A clear and concise description of any alternative options you've considered.
**Additional context**
Any other context, screenshots, etc.
*Please indicate if you are willing and able to help implement the proposed feature.*

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,25 +0,0 @@
---
name: Question
about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ''
---
<!-- NOTE: Questions will be converted to Discussions. You can find them at https://github.com/nextauthjs/next-auth/discussions! -->
**Your question**
<!-- A clear and concise question. -->
**What are you trying to do**
<!-- A description of what you are trying to do, for context. -->
**Reproduction**
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
**Feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful
* [ ] Found documentation but was incomplete
* [ ] Could not find relevant documentation
* [ ] Found the example project helpful
* [ ] Did not find the example project helpful

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 💚

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

@@ -16,26 +16,33 @@ merge of your pull request!
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:
## Reasoning 💡
<!-- Why are these changes necessary? -->
<!-- What changes are being made? What feature/bug is being fixed here? -->
**Why**:
## Checklist 🧢
<!-- How were these changes implemented? -->
<!-- Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
**How**:
<!-- Have you done all of these things? -->
**Checklist**:
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
To check an item, place an `x` in the box like so: `- [x] Documentation`. -->
- [ ] Documentation
- [ ] Tests
- [ ] Ready to be merged
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
<!-- feel free to add additional comments -->
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
## Affected issues 🎟
<!--
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
If you write `"Fixes"` or `"Closes"` before the issue link like so:
```
Fixes #359
```
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
-->

View File

@@ -1,32 +0,0 @@
# Simple check that the build is valid and no linting errors.
# Currently is run as a seperate workflow as it's fast to fail.
name: Lint/Build
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run lint
- run: npm run build

View File

@@ -1,67 +1,27 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: Code Analysis
on:
push:
branches: [ main, beta, next ]
branches: [main, beta, next]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '43 17 * * 2'
- cron: "43 17 * * 2"
jobs:
analyze:
name: Analyze
name: Verify
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
language: ["javascript"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,57 +0,0 @@
name: Integration Test
on:
push:
branches:
- main
- beta
- next
pull_request:
jobs:
test:
# Only run tests integration against Pull Requests from branches in
# this repository. We do this as integration tests require access to
# secrets in GitHub and they are not exposed to tests run against
# forks (for security reasons), so integration test against
# Pull Requests from external repos just fail and generate noise.
if: github.event.pull_request.head.repo.full_name == github.repository
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
# fail due to IP Address checks done by providers, which enforce
# CAPTCHA checks on login request from cloud compute IP addresses to
# prevent abuse.
runs-on: self-hosted
# Target time is under 5 minutes to run all tests. If it takes longer than
# 10 minutes should look at running tests in parallel. No individual flow
# should take longer than 5 minutes to build and run.
timeout-minutes: 10
strategy:
matrix:
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
# Run tests (build library, build + start test app in Docker, run tests)
- run: npm test
# TODO Tests should exit out if env vars not set (currently hangs)
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}

View File

@@ -1,11 +1,13 @@
name: "Pull Request Labeler"
name: PR Labeler
on:
- pull_request_target
- pull_request_target
jobs:
triage:
name: Triage
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,4 +1,5 @@
name: Release
on:
push:
branches:
@@ -7,20 +8,69 @@ on:
- "next"
- "3.x"
pull_request:
jobs:
release:
name: "Release"
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Init
uses: actions/checkout@v2
- name: Setup Node.js
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- run: npx semantic-release@17
- name: Build
run: npm run build
- name: Run tests
run: npm test -- --coverage --verbose && npm run test:types
- name: Coverage
uses: codecov/codecov-action@v1
with:
directory: ./coverage
fail_ci_if_error: false
release-branch:
name: Publish branch
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'push' }}
environment: Production
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Publish to npm and GitHub
run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release-pr:
name: Publish PR
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'pull_request' }}
environment: Preview
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Publish to npm
run: |
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
npm run version:pr
npm publish --access public --tag experimental
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PR_NUMBER: ${{ github.event.number }}

View File

@@ -1,25 +0,0 @@
name: Types
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Check types
run: npm run test:types

35
.gitignore vendored
View File

@@ -25,20 +25,21 @@ node_modules
# Generated files
.docusaurus
.cache-loader
.next
www/providers.json
src/providers/index.js
internals
adapters.d.ts
adapters.js
client.d.ts
client.js
index.d.ts
index.js
jwt.d.ts
jwt.js
providers.d.ts
providers.js
/internals
/adapters.d.ts
/adapters.js
/client.d.ts
/client.js
/index.d.ts
/index.js
/jwt.d.ts
/jwt.js
/providers.d.ts
/providers.js
/errors.js
/errors.d.ts
# Development app
app/next-auth
@@ -57,4 +58,12 @@ app/yarn.lock
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations
# Tests
/coverage
# v4
packages
apps
docs/providers.json

1
.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

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

View File

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

@@ -11,43 +11,49 @@ Please raise any significant new functionality or breaking change an issue for d
## For contributors
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
### Pull Requests
* The latest changes are always in `main`, so please make your Pull Request against that branch.
* Pull Requests should be raised for any change
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
- The latest changes are always in `main`, so please make your Pull Request against that branch.
- Pull Requests should be raised for any change
- Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
- We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
- We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
- If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
### Setting up local environment
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
The dev application requires you to use `npm@7`.
1. Clone the repo:
```sh
git clone git@github.com:nextauthjs/next-auth.git
cd next-auth
```
2. Install packages:
2. Install packages, set up the dev application:
```sh
npm i && npm dev:setup
npm run dev:setup
```
3. Populate `.env.local`:
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
1. Start the dev application/server:
```sh
npm run dev
```
Your dev application will be available on ```http://localhost:3000```
Your dev application will be available on `http://localhost:3000`
That's it! 🎉
@@ -64,6 +70,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
#### Providers
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
@@ -73,57 +80,43 @@ You can look at the existing built-in providers for inspiration.
#### Databases
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
You will need Docker and Docker Compose installed to be able to start / stop the databases.
When stopping the databases, it will reset their contents.
If you would like to contribute to an existing database adapter or help create a new one, head over to the [nextauthjs/adapters](https://www.github.com/nextauthjs/adapters) repository and follow the instructions provided there.
#### Testing
Tests can be run with `npm run test`.
Automated tests are currently crude and limited in functionality, but improvements are in development.
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
## For maintainers
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
When accepting Pull Requests, make sure the following:
* Use "Squash and merge"
* Make sure you merge contributor PRs into `main`
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
- Use "Squash and merge"
- Make sure you merge contributor PRs into `main`
- Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
- Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
### Recommended Scopes
A typical conventional commit looks like this:
```
type(scope): title
body
```
Scope is the part that will help groupping the different commit types in the release notes.
Scope is the part that will help grouping the different commit types in the release notes.
Some recommened scopes are:
Some recommended scopes are:
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
### Skipping a release

View File

@@ -1,30 +0,0 @@
# Multi stage build to allow us to improve performance
FROM node:10-alpine as base
WORKDIR /usr/src/app
# Install basic dependancies (Next.js, React)
COPY test/docker/app/package*.json ./
RUN npm ci --only=production
FROM node:10-alpine as app
COPY --from=base /usr/src/app ./
# Copy last build of library into the image and install dependences for it.
# This ensures the build is valid and package.json contains everything needed
# to actually run the library.
# Note: You must run `npm run build` first to build a release of the library
RUN mkdir -p node_modules/next-auth
# Copy all entrypoints for the library (if creating a new one, add it here)
COPY index.js providers.js adapters.js client.js jwt.js node_modules/next-auth/
# Copy the dist dir
COPY dist node_modules/next-auth/dist
# Copy the package.json for the library and install it's dependences
COPY package*.json node_modules/next-auth/
RUN cd node_modules/next-auth/ && npm ci --only=production
# Copy test pages across
COPY test/docker/app/pages ./pages
RUN npm run build
CMD [ "npm", "start" ]

135
README.md
View File

@@ -7,11 +7,8 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
</a>
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
</a>
<a href="https://bundlephobia.com/result?p=next-auth">
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
@@ -41,7 +38,7 @@ It is designed from the ground up to support Next.js and Serverless.
npm install --save next-auth
```
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
@@ -51,40 +48,40 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
### Flexible and easy to use
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
* Supports email / passwordless authentication
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
* Supports both JSON Web Tokens and database sessions
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
- Supports email / passwordless authentication
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)
- An open source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
- Works great with databases from popular hosting providers
- Can also be used _without a database_ (e.g. OAuth + JWT)
### Secure by default
* Promotes the use of passwordless sign in mechanisms
* Designed to be secure by default and encourage best practice for safeguarding user data
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
* Auto-generates symmetric signing and encryption keys for developer convenience
* Features tab/window syncing and keepalive messages to support short lived sessions
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
- Promotes the use of passwordless sign in mechanisms
- Designed to be secure by default and encourage best practice for safeguarding user data
- Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and keepalive messages to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
### TypeScript
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
The package at `@types/next-auth` is now deprecated.
@@ -93,50 +90,52 @@ The package at `@types/next-auth` is now deprecated.
### Add API Route
```javascript
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
export default NextAuth({
providers: [
// OAuth authentication providers
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET
clientSecret: process.env.APPLE_SECRET,
}),
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
clientSecret: process.env.GOOGLE_SECRET,
}),
// Sign in with passwordless email link
Providers.Email({
server: process.env.MAIL_SERVER,
from: '<no-reply@example.com>'
from: "<no-reply@example.com>",
}),
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL
database: process.env.DATABASE_URL,
})
```
### Add React Component
```javascript
import {
useSession, signIn, signOut
} from 'next-auth/client'
import { useSession, signIn, signOut } from "next-auth/client"
export default function Component() {
const [ session, loading ] = useSession()
if(session) {
return <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>
const [session, loading] = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
```
@@ -148,14 +147,44 @@ export default function Component() {
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
</div>
<div>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss"></a>
</div>
### Support
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
<!--sponsors start-->
<table>
<tbody>
<tr>
<td align="center" valign="top">
<a href="https://vercel.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/14985020?v=4" alt="Vercel Logo" />
</a><br />
<div>Vercel</div><br />
<sub>🥉 Bronze Financial Sponsor <br /> ☁️ Infrastructure Support</sub>
</td>
<td align="center" valign="top">
<a href="https://prisma.io" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/17219288?v=4" alt="Prisma Logo" />
</a><br />
<div>Prisma</div><br />
<sub>🥉 Bronze Financial Sponsor</sub>
</td>
<td align="center" valign="top">
<a href="https://checklyhq.com" target="_blank">
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
</a><br />
<div>Checkly</div><br />
<sub>☁️ Infrastructure Support</sub>
</td>
</tr><tr></tr>
</tbody>
</table>
<br />
<!--sponsors end-->
## Contributing
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).

View File

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

View File

@@ -4,7 +4,7 @@
NEXTAUTH_URL=http://localhost:3000
# You can use `openssl rand -hex 32` or
# https://generate-secret.now.sh/32 to generate a secret.
# https://generate-secret.vercel.app/32 to generate a secret.
# Note: Changing a secret may invalidate existing sessions
# and/or verificaion tokens.
SECRET=
@@ -23,8 +23,6 @@ TWITTER_SECRET=
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
EMAIL_FROM=user@gmail.com
# You can use any of these as the "DATABASE_URL" for
# databases started with Docker using `npm run db:start`.
# Note: If using with Prisma adapter, you need to use a `.env`
# file rather than a `.env.local` file to configure env vars.
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true

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,9 @@
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
@@ -28,15 +32,15 @@ export default NextAuth({
// }
// },
providers: [
Providers.Email({
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
from: process.env.EMAIL_FROM,
}),
Providers.GitHub({
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Auth0({
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
@@ -45,36 +49,36 @@ export default NextAuth({
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
protection: "pkce",
}),
Providers.Twitter({
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
clientSecret: process.env.TWITTER_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
CredentialsProvider({
name: "Credentials",
credentials: {
password: { label: 'Password', type: 'password' }
password: { label: "Password", type: "password" },
},
async authorize (credentials) {
if (credentials.password === 'password') {
async authorize(credentials, req) {
if (credentials.password === "password") {
return {
id: 1,
name: 'Fill Murray',
email: 'bill@fillmurray.com',
image: 'https://www.fillmurray.com/64/64'
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
}
}
return null
}
})
},
}),
],
jwt: {
encryption: true,
secret: process.env.SECRET
secret: process.env.SECRET,
},
debug: false,
theme: 'auto'
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL

View File

@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
const secret = process.env.SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
const token = await jwt.getToken({ req, secret, encryption: true })
res.send(JSON.stringify(token, null, 2))
}

33
config/babel.config.js Normal file
View File

@@ -0,0 +1,33 @@
// We aim to have the same support as Next.js
// https://nextjs.org/docs/getting-started#system-requirements
// https://nextjs.org/docs/basic-features/supported-browsers-features
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
plugins: [
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
],
comments: false,
overrides: [
{
test: ["../src/client/**"],
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
},
{
test: ["../src/server/pages/**"],
presets: ["preact"],
},
{
test: ["../src/**/*.test.js"],
presets: [
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
},
],
}

View File

@@ -1,12 +0,0 @@
{
"presets": [
["@babel/preset-env", { "targets": { "esmodules": true } }]
],
"comments": false,
"overrides": [
{
"test": ["../src/server/pages/**"],
"presets": ["preact"]
}
]
}

View File

@@ -7,6 +7,7 @@ const MODULE_ENTRIES = {
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
ERRORS: "errors",
}
// Building submodule entries
@@ -17,6 +18,7 @@ const BUILD_TARGETS = {
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
@@ -34,6 +36,7 @@ const TYPES_TARGETS = [
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
`${MODULE_ENTRIES.ERRORS}.d.ts`,
"internals",
]

2
config/jest-setup.js Normal file
View File

@@ -0,0 +1,2 @@
import "@testing-library/jest-dom"
import "whatwg-fetch"

11
config/jest.config.js Normal file
View File

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

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

37673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,49 @@
{
"name": "next-auth",
"version": "0.0.0-semantically-released",
"version": "3.29.10",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"types": "./index.d.ts",
"keywords": ["react", "nodejs", "oauth", "jwt", "oauth2", "authentication", "nextjs", "csrf", "oidc", "nextauth"],
"keywords": [
"react",
"nodejs",
"oauth",
"jwt",
"oauth2",
"authentication",
"nextjs",
"csrf",
"oidc",
"nextauth"
],
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js"
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
"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.json --watch src --out-dir dist",
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
"test:app:stop": "docker-compose -f test/docker/app.yml down",
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
"test:db:mysql": "node test/mysql.js",
"test:db:postgres": "node test/postgres.js",
"test:db:mongodb": "node test/mongodb.js",
"test:db:mssql": "node test/mssql.js",
"test:integration": "mocha test/integration",
"test:types": "dtslint types",
"db:start": "docker-compose -f test/docker/databases.yml up -d",
"db:stop": "docker-compose -f test/docker/databases.yml down",
"test": "jest --config ./config/jest.config.js",
"test:ci": "npm run lint && npm run test:types && npm run test -- --ci",
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"publish:beta": "npm publish --tag beta",
"publish:canary": "npm publish --tag canary",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr"
},
"files": [
"dist",
@@ -54,13 +55,17 @@
"adapters.d.ts",
"client.js",
"client.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
"jwt.d.ts",
"internals"
],
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"@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",
@@ -69,13 +74,11 @@
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
"querystring": "^0.2.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "16.13.1 || ^17"
"react-dom": "^16.13.1 || ^17"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
@@ -87,16 +90,19 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@babel/preset-react": "^7.13.13",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.9",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
@@ -106,25 +112,22 @@
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"next": "^10.0.5",
"pg": "^8.2.1",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^11.0.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"prisma": "^2.16.1",
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3"
"pretty-quick": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"prettier": {
"semi": false
@@ -151,14 +154,48 @@
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"overrides": [
{
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
{ "name": "beta", "prerelease": true },
{ "name": "next", "prerelease": true }
{
"name": "beta",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"releasedLabels": false,
"successComment": false
}
]
]
},
"funding": [

View File

@@ -0,0 +1,36 @@
import { UnknownError } from "../lib/errors"
/**
* Handles adapter induced errors.
* @param {import("types/adapters").AdapterInstance} adapter
* @param {import("types").LoggerInstance} logger
* @return {import("types/adapters").AdapterInstance}
*/
export default function adapterErrorHandler(adapter, logger) {
return Object.keys(adapter).reduce((acc, method) => {
const name = capitalize(method)
const code = upperSnake(name, adapter.displayName)
const adapterMethod = adapter[method]
acc[method] = async (...args) => {
try {
logger.debug(code, ...args)
return await adapterMethod(...args)
} catch (error) {
logger.error(`${code}_ERROR`, error)
const e = new UnknownError(error)
e.name = `${name}Error`
throw e
}
}
return acc
}, {})
}
function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
function upperSnake(s, prefix = "ADAPTER") {
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
}

View File

@@ -1,110 +0,0 @@
const Adapter = (config, options = {}) => {
async function getAdapter (appOptions) {
const { logger } = appOptions
// Display debug output if debug option enabled
function debug (debugCode, ...args) {
logger.debug(`ADAPTER_${debugCode}`, ...args)
}
async function createUser (profile) {
debug('createUser', profile)
return null
}
async function getUser (id) {
debug('getUser', id)
return null
}
async function getUserByEmail (email) {
debug('getUserByEmail', email)
return null
}
async function getUserByProviderAccountId (providerId, providerAccountId) {
debug('getUserByProviderAccountId', providerId, providerAccountId)
return null
}
async function updateUser (user) {
debug('updateUser', user)
return null
}
async function deleteUser (userId) {
debug('deleteUser', userId)
return null
}
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
return null
}
async function unlinkAccount (userId, providerId, providerAccountId) {
debug('unlinkAccount', userId, providerId, providerAccountId)
return null
}
async function createSession (user) {
debug('createSession', user)
return null
}
async function getSession (sessionToken) {
debug('getSession', sessionToken)
return null
}
async function updateSession (session, force) {
debug('updateSession', session)
return null
}
async function deleteSession (sessionToken) {
debug('deleteSession', sessionToken)
return null
}
async function createVerificationRequest (identifier, url, token, secret, provider) {
debug('createVerificationRequest', identifier)
return null
}
async function getVerificationRequest (identifier, token, secret, provider) {
debug('getVerificationRequest', identifier, token)
return null
}
async function deleteVerificationRequest (identifier, token, secret, provider) {
debug('deleteVerification', identifier, token)
return null
}
return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
})
}
return {
getAdapter
}
}
export default {
Adapter
}

View File

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

6
src/adapters/prisma.js Normal file
View File

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

View File

@@ -1,340 +0,0 @@
import { createHash, randomBytes } from 'crypto'
import { CreateUserError } from '../../lib/errors'
const Adapter = (config) => {
const {
prisma,
modelMapping = {
User: 'user',
Account: 'account',
Session: 'session',
VerificationRequest: 'verificationRequest'
}
} = config
const { User, Account, Session, VerificationRequest } = modelMapping
function getCompoundId (providerId, providerAccountId) {
return createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
}
async function getAdapter (appOptions) {
const { logger } = appOptions
function debug (debugCode, ...args) {
logger.debug(`PRISMA_${debugCode}`, ...args)
}
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
}
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
? appOptions.session.maxAge * 1000
: defaultSessionMaxAge
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
? appOptions.session.updateAge * 1000
: 0
async function createUser (profile) {
debug('CREATE_USER', profile)
try {
return prisma[User].create({
data: {
name: profile.name,
email: profile.email,
image: profile.image,
emailVerified: profile.emailVerified ? profile.emailVerified.toISOString() : null
}
})
} catch (error) {
logger.error('CREATE_USER_ERROR', error)
return Promise.reject(new CreateUserError(error))
}
}
async function getUser (id) {
debug('GET_USER', id)
try {
return prisma[User].findUnique({ where: { id } })
} catch (error) {
logger.error('GET_USER_BY_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
}
}
async function getUserByEmail (email) {
debug('GET_USER_BY_EMAIL', email)
try {
if (!email) { return Promise.resolve(null) }
return prisma[User].findUnique({ where: { email } })
} catch (error) {
logger.error('GET_USER_BY_EMAIL_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
}
}
async function getUserByProviderAccountId (providerId, providerAccountId) {
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
try {
const account = await prisma[Account].findUnique({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
if (!account) { return null }
return prisma[User].findUnique({ where: { id: account.userId } })
} catch (error) {
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
}
}
async function updateUser (user) {
debug('UPDATE_USER', user)
try {
const { id, name, email, image, emailVerified } = user
return prisma[User].update({
where: { id },
data: {
name,
email,
image,
emailVerified: emailVerified ? emailVerified.toISOString() : null
}
})
} catch (error) {
logger.error('UPDATE_USER_ERROR', error)
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
}
}
async function deleteUser (userId) {
debug('DELETE_USER', userId)
try {
return prisma[User].delete({ where: { id: userId } })
} catch (error) {
logger.error('DELETE_USER_ERROR', error)
return Promise.reject(new Error('DELETE_USER_ERROR', error))
}
}
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
try {
return prisma[Account].create({
data: {
accessToken,
refreshToken,
compoundId: getCompoundId(providerId, providerAccountId),
providerAccountId: `${providerAccountId}`,
providerId,
providerType,
accessTokenExpires,
userId
}
})
} catch (error) {
logger.error('LINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
}
}
async function unlinkAccount (userId, providerId, providerAccountId) {
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
try {
return prisma[Account].delete({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
} catch (error) {
logger.error('UNLINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
}
}
async function createSession (user) {
debug('CREATE_SESSION', user)
try {
let expires = null
if (sessionMaxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
expires = dateExpires.toISOString()
}
return prisma[Session].create({
data: {
expires,
userId: user.id,
sessionToken: randomBytes(32).toString('hex'),
accessToken: randomBytes(32).toString('hex')
}
})
} catch (error) {
logger.error('CREATE_SESSION_ERROR', error)
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
}
}
async function getSession (sessionToken) {
debug('GET_SESSION', sessionToken)
try {
const session = await prisma[Session].findUnique({ where: { sessionToken } })
// Check session has not expired (do not return it if it has)
if (session && session.expires && new Date() > session.expires) {
await prisma[Session].delete({ where: { sessionToken } })
return null
}
return session
} catch (error) {
logger.error('GET_SESSION_ERROR', error)
return Promise.reject(new Error('GET_SESSION_ERROR', error))
}
}
async function updateSession (session, force) {
debug('UPDATE_SESSION', session)
try {
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
// Calculate last updated date, to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
//
// Default for sessionMaxAge is 30 days.
// Default for sessionUpdateAge is 1 hour.
const dateSessionIsDueToBeUpdated = new Date(session.expires)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (new Date() > dateSessionIsDueToBeUpdated) {
const newExpiryDate = new Date()
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
session.expires = newExpiryDate
} else if (!force) {
return null
}
} else {
// If session MaxAge, session UpdateAge or session.expires are
// missing then don't even try to save changes, unless force is set.
if (!force) { return null }
}
const { id, expires } = session
return prisma[Session].update({ where: { id }, data: { expires: expires.toISOString() } })
} catch (error) {
logger.error('UPDATE_SESSION_ERROR', error)
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
}
}
async function deleteSession (sessionToken) {
debug('DELETE_SESSION', sessionToken)
try {
return prisma[Session].delete({ where: { sessionToken } })
} catch (error) {
logger.error('DELETE_SESSION_ERROR', error)
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
}
}
async function createVerificationRequest (identifier, url, token, secret, provider) {
debug('CREATE_VERIFICATION_REQUEST', identifier)
try {
const { baseUrl } = appOptions
const { sendVerificationRequest, maxAge } = provider
// Store hashed token (using secret as salt) so that tokens cannot be exploited
// even if the contents of the database is compromised.
// @TODO Use bcrypt function here instead of simple salted hash
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
let expires = null
if (maxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
expires = dateExpires.toISOString()
}
// Save to database
const verificationRequest = await prisma[VerificationRequest].create({
data: {
identifier,
token: hashedToken,
expires
}
})
// With the verificationCallback on a provider, you can send an email, or queue
// an email to be sent, or perform some other action (e.g. send a text message)
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
return verificationRequest
} catch (error) {
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
}
}
async function getVerificationRequest (identifier, token, secret, provider) {
debug('GET_VERIFICATION_REQUEST', identifier, token)
try {
// Hash token provided with secret before trying to match it with database
// @TODO Use bcrypt instead of salted SHA-256 hash for token
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
const verificationRequest = await prisma[VerificationRequest].findFirst({
where: {
identifier,
token: hashedToken
}
})
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
// Delete verification entry so it cannot be used again
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
return null
}
return verificationRequest
} catch (error) {
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
}
}
async function deleteVerificationRequest (identifier, token, secret, provider) {
debug('DELETE_VERIFICATION', identifier, token)
try {
// Delete verification entry so it cannot be used again
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
} catch (error) {
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
}
}
return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
})
}
return {
getAdapter
}
}
export default {
Adapter
}

9
src/adapters/typeorm.js Normal file
View File

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

View File

@@ -1,384 +0,0 @@
import { createConnection, getConnection } from 'typeorm'
import { createHash } from 'crypto'
import require_optional from 'require_optional' // eslint-disable-line camelcase
import { CreateUserError } from '../../lib/errors'
import adapterConfig from './lib/config'
import adapterTransform from './lib/transform'
import Models from './models'
import { updateConnectionEntities } from './lib/utils'
const Adapter = (typeOrmConfig, options = {}) => {
// Ensure typeOrmConfigObject is normalized to an object
const typeOrmConfigObject = (typeof typeOrmConfig === 'string')
? adapterConfig.parseConnectionString(typeOrmConfig)
: typeOrmConfig
// Load any custom models passed as an option, default to built in models
const { models: customModels = {} } = options
const models = {
User: customModels.User ? customModels.User : Models.User,
Account: customModels.Account ? customModels.Account : Models.Account,
Session: customModels.Session ? customModels.Session : Models.Session,
VerificationRequest: customModels.VerificationRequest ? customModels.VerificationRequest : Models.VerificationRequest
}
// The models are designed for ANSI SQL databases first (as a baseline).
// For databases that use a different pragma, we transform the models at run
// time *unless* the models are user supplied (in which case we don't do
// anything to do them). This function updates arguments by reference.
adapterTransform(typeOrmConfigObject, models, options)
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
// Create objects from models that can be consumed by functions in the adapter
const User = models.User.model
const Account = models.Account.model
const Session = models.Session.model
const VerificationRequest = models.VerificationRequest.model
let connection = null
async function getAdapter (appOptions) {
const { logger } = appOptions
// Display debug output if debug option enabled
function debug (debugCode, ...args) {
logger.debug(`TYPEORM_${debugCode}`, ...args)
}
// Helper function to reuse / restablish connections
// (useful if they drop when after being idle)
async function _connect () {
// Get current connection by name
connection = getConnection(config.name)
// If connection is no longer established, reconnect
if (!connection.isConnected) { connection = await connection.connect() }
}
if (!connection) {
// If no connection, create new connection
try {
connection = await createConnection(config)
} catch (error) {
if (error.name === 'AlreadyHasActiveConnectionError') {
// If creating connection fails because it's already
// been re-established, check it's really up
await _connect()
} else {
logger.error('ADAPTER_CONNECTION_ERROR', error)
}
}
} else {
// If the connection object already exists, ensure it's valid
await _connect()
}
if (process.env.NODE_ENV !== 'production') {
await updateConnectionEntities(connection, config.entities)
}
// Get manager from connection object
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
const { manager } = connection
// The models are primarily designed for ANSI SQL database, but some
// flexiblity is required in the adapter to support non-SQL databases such
// as MongoDB which have different pragmas.
//
// TypeORM does some abstraction, but doesn't handle everything (e.g. it
// handles translating `id` and `_id` in models, but not queries) so we
// need to handle somethings in the adapter to make it compatible.
let idKey = 'id'
let ObjectId
if (config.type === 'mongodb') {
idKey = '_id'
// Using a dynamic import causes problems for some compilers/bundlers
// that don't handle dynamic imports. To try and work around this we are
// using the same method mongodb uses to load Object ID type, which is to
// use the require_optional loader.
const mongodb = require_optional('mongodb')
ObjectId = mongodb.ObjectId
}
// These values are stored as seconds, but to use them with dates in
// JavaScript we convert them to milliseconds.
//
// Use a conditional to default to 30 day session age if not set - it should
// always be set but a meaningful fallback is helpful to facilitate testing.
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
}
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
? appOptions.session.maxAge * 1000
: defaultSessionMaxAge
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
? appOptions.session.updateAge * 1000
: 0
async function createUser (profile) {
debug('CREATE_USER', profile)
try {
// Create user account
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
return await manager.save(user)
} catch (error) {
logger.error('CREATE_USER_ERROR', error)
return Promise.reject(new CreateUserError(error))
}
}
async function getUser (id) {
debug('GET_USER', id)
// In the very specific case of both using JWT for storing session data
// and using MongoDB to store user data, the ID is a string rather than
// an ObjectId and we need to turn it into an ObjectId.
//
// In all other scenarios it is already an ObjectId, because it will have
// come from another MongoDB query.
if (ObjectId && !(id instanceof ObjectId)) {
id = ObjectId(id)
}
try {
return manager.findOne(User, { [idKey]: id })
} catch (error) {
logger.error('GET_USER_BY_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
}
}
async function getUserByEmail (email) {
debug('GET_USER_BY_EMAIL', email)
try {
if (!email) { return Promise.resolve(null) }
return manager.findOne(User, { email })
} catch (error) {
logger.error('GET_USER_BY_EMAIL_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
}
}
async function getUserByProviderAccountId (providerId, providerAccountId) {
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
try {
const account = await manager.findOne(Account, { providerId, providerAccountId })
if (!account) { return null }
return manager.findOne(User, { [idKey]: account.userId })
} catch (error) {
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
}
}
async function updateUser (user) {
debug('UPDATE_USER', user)
return manager.save(User, user)
}
async function deleteUser (userId) {
debug('DELETE_USER', userId)
// @TODO Delete user from DB
return false
}
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
try {
// Create provider account linked to user
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
return manager.save(account)
} catch (error) {
logger.error('LINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
}
}
async function unlinkAccount (userId, providerId, providerAccountId) {
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
// @TODO Get current user from DB
// @TODO Delete [provider] object from user object
// @TODO Save changes to user object in DB
return false
}
async function createSession (user) {
debug('CREATE_SESSION', user)
try {
let expires = null
if (sessionMaxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
expires = dateExpires
}
const session = new Session(user.id, expires)
return manager.save(session)
} catch (error) {
logger.error('CREATE_SESSION_ERROR', error)
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
}
}
async function getSession (sessionToken) {
debug('GET_SESSION', sessionToken)
try {
const session = await manager.findOne(Session, { sessionToken })
// Check session has not expired (do not return it if it has)
if (session && session.expires && new Date() > new Date(session.expires)) {
// @TODO Delete old sessions from database
return null
}
return session
} catch (error) {
logger.error('GET_SESSION_ERROR', error)
return Promise.reject(new Error('GET_SESSION_ERROR', error))
}
}
async function updateSession (session, force) {
debug('UPDATE_SESSION', session)
try {
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
// Calculate last updated date, to throttle write updates to database
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
// e.g. ({expiry date} - 30 days) + 1 hour
//
// Default for sessionMaxAge is 30 days.
// Default for sessionUpdateAge is 1 hour.
const dateSessionIsDueToBeUpdated = new Date(session.expires)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
// Trigger update of session expiry date and write to database, only
// if the session was last updated more than {sessionUpdateAge} ago
if (new Date() > dateSessionIsDueToBeUpdated) {
const newExpiryDate = new Date()
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
session.expires = newExpiryDate
} else if (!force) {
return null
}
} else {
// If session MaxAge, session UpdateAge or session.expires are
// missing then don't even try to save changes, unless force is set.
if (!force) { return null }
}
return manager.save(Session, session)
} catch (error) {
logger.error('UPDATE_SESSION_ERROR', error)
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
}
}
async function deleteSession (sessionToken) {
debug('DELETE_SESSION', sessionToken)
try {
return await manager.delete(Session, { sessionToken })
} catch (error) {
logger.error('DELETE_SESSION_ERROR', error)
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
}
}
async function createVerificationRequest (identifier, url, token, secret, provider) {
debug('CREATE_VERIFICATION_REQUEST', identifier)
try {
const { baseUrl } = appOptions
const { sendVerificationRequest, maxAge } = provider
// Store hashed token (using secret as salt) so that tokens cannot be exploited
// even if the contents of the database is compromised.
// @TODO Use bcrypt function here instead of simple salted hash
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
let expires = null
if (maxAge) {
const dateExpires = new Date()
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
expires = dateExpires
}
// Save to database
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
const verificationRequest = await manager.save(newVerificationRequest)
// With the verificationCallback on a provider, you can send an email, or queue
// an email to be sent, or perform some other action (e.g. send a text message)
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
return verificationRequest
} catch (error) {
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
}
}
async function getVerificationRequest (identifier, token, secret, provider) {
debug('GET_VERIFICATION_REQUEST', identifier, token)
try {
// Hash token provided with secret before trying to match it with database
// @TODO Use bcrypt instead of salted SHA-256 hash for token
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
// Delete verification entry so it cannot be used again
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
return null
}
return verificationRequest
} catch (error) {
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
}
}
async function deleteVerificationRequest (identifier, token, secret, provider) {
debug('DELETE_VERIFICATION', identifier, token)
try {
// Delete verification entry so it cannot be used again
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
} catch (error) {
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
}
}
return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
})
}
return {
getAdapter
}
}
export default {
Adapter,
Models
}

View File

@@ -1,84 +0,0 @@
import { EntitySchema } from 'typeorm'
const parseConnectionString = (configString) => {
if (typeof configString !== 'string') { return configString }
// If the input is URL string, automatically convert the string to an object
// to make configuration easier (in most use cases).
//
// TypeORM accepts connection string as a 'url' option, but unfortunately
// not for all databases (e.g. SQLite) or for all options, so we handle
// parsing it in this function.
try {
const parsedUrl = new URL(configString)
const config = {}
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
// Special case handling is required for mongodb+srv with TypeORM
config.type = 'mongodb'
config.url = configString.replace(/\?(.*)$/, '')
config.useNewUrlParser = true
} else {
config.type = parsedUrl.protocol.replace(/:$/, '')
config.host = parsedUrl.hostname
config.port = Number(parsedUrl.port)
config.username = parsedUrl.username
config.password = parsedUrl.password
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
config.options = {}
}
// This option is recommended by mongodb
if (config.type === 'mongodb') {
config.useUnifiedTopology = true
}
// Prevents warning about deprecated option (sets default value)
if (config.type === 'mssql') {
config.options.enableArithAbort = true
}
if (parsedUrl.search) {
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
let [key, value] = keyValuePair.split('=')
// Converts true/false strings to actual boolean values
if (value === 'true') { value = true }
if (value === 'false') { value = false }
config[key] = value
})
}
return config
} catch (error) {
// If URL parsing fails for any reason, try letting TypeORM handle it
return {
url: configString
}
}
}
const loadConfig = (config, { models, namingStrategy }) => {
const defaultConfig = {
name: 'nextauth',
autoLoadEntities: true,
entities: [
new EntitySchema(models.User.schema),
new EntitySchema(models.Account.schema),
new EntitySchema(models.Session.schema),
new EntitySchema(models.VerificationRequest.schema)
],
timezone: 'Z', // Required for timestamps to be treated as UTC in MySQL
logging: false,
namingStrategy
}
return {
...defaultConfig,
...config
}
}
export default {
parseConnectionString,
loadConfig
}

View File

@@ -1,45 +0,0 @@
// Inspired by https://github.com/tonivj5/typeorm-naming-strategies
import { DefaultNamingStrategy } from 'typeorm'
import { snakeCase, camelCase } from 'typeorm/util/StringUtils'
export class SnakeCaseNamingStrategy extends DefaultNamingStrategy {
// Pluralise table names (set customName to override)
tableName (className, customName) {
return customName || snakeCase(`${className}s`)
}
columnName (propertyName, customName, embeddedPrefixes) {
return `${snakeCase(embeddedPrefixes.join('_'))}${customName || snakeCase(propertyName)}`
}
relationName (propertyName) {
return snakeCase(propertyName)
}
joinColumnName (relationName, referencedColumnName) {
return snakeCase(`${relationName}_${referencedColumnName}`)
}
joinTableName (firstTableName, secondTableName, firstPropertyName, secondPropertyName) {
return snakeCase(`${firstTableName}_${firstPropertyName.replace(/\./gi, '_')}_${secondTableName}`)
}
joinTableColumnName (tableName, propertyName, columnName) {
return snakeCase(`${tableName}_${(columnName || propertyName)}`)
}
classTableInheritanceParentColumnName (parentTableName, parentTableIdPropertyName) {
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`)
}
eagerJoinRelationAlias (alias, propertyPath) {
return `${alias}__${propertyPath.replace('.', '_')}`
}
}
export class CamelCaseNamingStrategy extends DefaultNamingStrategy {
// Pluralise collection names, uses (set customName to override)
tableName (className, customName) {
return customName || camelCase(`${className}s`)
}
}

View File

@@ -1,166 +0,0 @@
// Perform transforms on SQL models so they can be used with other databases
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
const postgresTransform = (models, options) => {
// Apply snake case naming strategy for Postgres databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// For Postgres we need to use the `timestamp with time zone` type
// aka `timestamptz` to store timestamps correctly in UTC.
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
models[model].schema.columns[column].type = 'timestamptz'
}
}
}
}
const mysqlTransform = (models, options) => {
// Apply snake case naming strategy for MySQL databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// For MySQL we default milisecond precision of all timestamps to 6 digits.
// This ensures all timestamp fields use the same precision (unless explictly
// configured otherwise) and that values in MySQL match those Postgress.
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
// If precision explictly set (including to null) don't change it
if (typeof models[model].schema.columns[column].precision === 'undefined') {
models[model].schema.columns[column].precision = 6
}
}
}
}
}
const mongodbTransform = (models, options) => {
// A CamelCase naming strategy is used for all document databases
if (!options.namingStrategy) {
options.namingStrategy = new CamelCaseNamingStrategy()
}
// Important!
//
// 1. You must set 'objectId: true' on one property on a model in MongoDB.
//
// 'objectId' MUST be set on the primary ID field. This overrides other
// values on that object in TypeORM (e.g. type: 'int' or 'primary').
//
// 2. Other properties that are Object IDs in the same model MUST be set to
// type: 'objectId' (and should not be set to `objectId: true`).
//
// If you set 'objectId: true' on multiple properties on a model you will
// see the result of queries like find() is wrong. You will see the same
// Object ID in every property of type Object ID in the result (but the
// database will look fine); so use `type: 'objectId'` for them instead.
for (const model in models) {
delete models[model].schema.columns.id.type
models[model].schema.columns.id.objectId = true
}
// Ensure reference to User ID in other models are Object IDs
// This needs to done for any properties that reference another entity by ID
models.Account.schema.columns.userId.type = 'objectId'
models.Session.schema.columns.userId.type = 'objectId'
// The options `unique: true` and `nullable: true` don't work the same
// with MongoDB as they do with SQL databases like MySQL and Postgres,
// we need to create a sparse index to only allow unique values, while
// still allowing multiple entires to omit the email address.
delete models.User.schema.columns.email.unique
if (!models.User.schema.indices) { models.User.schema.indices = [] }
models.User.schema.indices.push({
name: 'email',
unique: true,
sparse: true,
columns: ['email']
})
}
const sqliteTransform = (models, options) => {
// Apply snake case naming strategy for SQLite databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// SQLite does not support `timestamp` fields so we remap them to `datetime`
// in all models.
//
// `timestamp` is an ANSI SQL specification and widely supported by other
// databases so this transform is a specific workaround required for SQLite.
//
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
// specific to SQLite and so we ignore that behaviour.
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
models[model].schema.columns[column].type = 'datetime'
}
}
}
}
const mssqlTransform = (models, options) => {
// Apply snake case naming strategy for SQL Server databases
if (!options.namingStrategy) {
// @TODO Add TitleCase instead as more common MSSQL convention?
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// SQL Server deprecated TIMESTAMP in favor of ROWVERSION.
// But ROWVERSION is not what it was intended in the other adapters.
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
models[model].schema.columns[column].type = 'datetime'
}
}
}
// Support UNIQUE on on User.email that allows duplicate NULL values
// Note: This is ANSI SQL behaviour for UNIQUE not default in SQL Server
delete models.User.schema.columns.email.unique
if (!models.User.schema.indices) { models.User.schema.indices = [] }
models.User.schema.indices.push({
name: 'email',
columns: ['email'],
unique: true,
where: 'email IS NOT NULL'
})
}
export default (config, models, options) => {
// @TODO Refactor into switch statement
if ((config.type && config.type.startsWith('mongodb')) ||
(config.url && config.url.startsWith('mongodb'))) {
mongodbTransform(models, options)
} else if ((config.type && config.type.startsWith('postgres')) ||
(config.url && config.url.startsWith('postgres'))) {
postgresTransform(models, options)
} else if ((config.type && config.type.startsWith('mysql')) ||
(config.url && config.url.startsWith('mysql'))) {
mysqlTransform(models, options)
} else if ((config.type && config.type.startsWith('sqlite')) ||
(config.url && config.url.startsWith('sqlite'))) {
sqliteTransform(models, options)
} else if ((config.type && config.type.startsWith('mssql')) ||
(config.url && config.url.startsWith('mssql'))) {
mssqlTransform(models, options)
} else {
// For all other SQL databases (e.g. MySQL) apply snake case naming
// strategy, but otherwise use the models and schemas as they are.
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
}
}

View File

@@ -1,18 +0,0 @@
const entitiesChanged = (prevEntities, newEntities) => {
if (prevEntities.length !== newEntities.length) return true
for (let i = 0; i < prevEntities.length; i++) {
if (prevEntities[i] !== newEntities[i]) return true
}
return false
}
export const updateConnectionEntities = async (connection, entities) => {
// Check if the entities passed have changed and if so replace them
// and re-sync the typeorm connection.
if (!connection || !entitiesChanged(connection.options.entities, entities)) return
connection.options.entities = entities
connection.buildMetadatas()
if (connection.options.synchronize) {
await connection.synchronize()
}
}

View File

@@ -1,94 +0,0 @@
import { createHash } from 'crypto'
export class Account {
constructor (
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires
) {
// The compound ID ensures there is only one entry for a given provider and account
this.compoundId = createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
this.userId = userId
this.providerType = providerType
this.providerId = providerId
this.providerAccountId = providerAccountId
this.refreshToken = refreshToken
this.accessToken = accessToken
this.accessTokenExpires = accessTokenExpires
}
}
export const AccountSchema = {
name: 'Account',
target: Account,
columns: {
id: {
// This property has `objectId: true` instead of `type: int` in MongoDB
primary: true,
type: 'int',
generated: true
},
compoundId: {
// The compound ID ensures that there there is only one instance of an
// OAuth account in a way that works across different databases.
// It is not used for anything else.
type: 'varchar',
unique: true
},
userId: {
// This property is set to `type: objectId` on MongoDB databases
type: 'int'
},
providerType: {
type: 'varchar'
},
providerId: {
type: 'varchar'
},
providerAccountId: {
type: 'varchar'
},
refreshToken: {
type: 'text',
nullable: true
},
accessToken: {
// AccessTokens are not (yet) automatically rotated by NextAuth.js
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
type: 'text',
nullable: true
},
accessTokenExpires: {
// AccessTokens expiry times are not (yet) updated by NextAuth.js
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
type: 'timestamp',
nullable: true
},
createdAt: {
type: 'timestamp',
createDate: true
},
updatedAt: {
type: 'timestamp',
updateDate: true
}
},
indices: [
{
name: 'userId',
columns: ['userId']
},
{
name: 'providerId',
columns: ['providerId']
},
{
name: 'providerAccountId',
columns: ['providerAccountId']
}
]
}

View File

@@ -1,23 +0,0 @@
import { Account, AccountSchema } from './account'
import { User, UserSchema } from './user'
import { Session, SessionSchema } from './session'
import { VerificationRequest, VerificationRequestSchema } from './verification-request'
export default {
Account: {
model: Account,
schema: AccountSchema
},
User: {
model: User,
schema: UserSchema
},
Session: {
model: Session,
schema: SessionSchema
},
VerificationRequest: {
model: VerificationRequest,
schema: VerificationRequestSchema
}
}

View File

@@ -1,50 +0,0 @@
import { randomBytes } from 'crypto'
export class Session {
constructor (userId, expires, sessionToken, accessToken) {
this.userId = userId
this.expires = expires
this.sessionToken = sessionToken || randomBytes(32).toString('hex')
this.accessToken = accessToken || randomBytes(32).toString('hex')
}
}
export const SessionSchema = {
name: 'Session',
target: Session,
columns: {
id: {
// This property has `objectId: true` instead of `type: int` in MongoDB
primary: true,
type: 'int',
generated: true
},
userId: {
// This property is set to `type: objectId` on MongoDB databases
type: 'int'
},
expires: {
// The date the session expires (is updated when a session is active)
type: 'timestamp'
},
sessionToken: {
// The sessionToken should never be exposed to client side JavaScript
type: 'varchar',
unique: true
},
accessToken: {
// The accessToken can be safely exposed to client side JavaScript to
// to identify the owner of a session without exposing the sessionToken
type: 'varchar',
unique: true
},
createdAt: {
type: 'timestamp',
createDate: true
},
updatedAt: {
type: 'timestamp',
updateDate: true
}
}
}

View File

@@ -1,58 +0,0 @@
export class User {
constructor (name, email, image, emailVerified) {
if (name) { this.name = name }
if (email) { this.email = email }
if (image) { this.image = image }
if (emailVerified) {
const currentDate = new Date()
this.emailVerified = currentDate
}
}
}
export const UserSchema = {
name: 'User',
target: User,
columns: {
id: {
// This property has `objectId: true` instead of `type: int` in MongoDB
primary: true,
type: 'int',
generated: true
},
name: {
type: 'varchar',
nullable: true
},
email: {
// This is inherited from the one in the OAuth provider profile on
// initial sign in, if one is specified in that profile.
type: 'varchar',
unique: true,
nullable: true
},
emailVerified: {
// Contains a timestamp of the last time an action was performed that
// confirmed this email address was active and used by the user (e.g.
// when an email sign in link is clicked on and verified). Is null
// if the email address specified has never been verified.
type: 'timestamp',
nullable: true
},
image: {
// A URL that points to an avatar to use for the user.
// This is inherited from the one in the OAuth provider profile on
// initial sign in, if one is specified in that profile.
type: 'varchar',
nullable: true
},
createdAt: {
type: 'timestamp',
createDate: true
},
updatedAt: {
type: 'timestamp',
updateDate: true
}
}
}

View File

@@ -1,44 +0,0 @@
// This model is used for sign in emails, but is designed to support other
// mechanisms in future (e.g. 2FA via text message or short codes)
export class VerificationRequest {
constructor (identifier, token, expires) {
if (identifier) { this.identifier = identifier }
if (token) { this.token = token }
if (expires) { this.expires = expires }
}
}
export const VerificationRequestSchema = {
name: 'VerificationRequest',
target: VerificationRequest,
columns: {
id: {
// This property has `objectId: true` instead of `type: int` in MongoDB
primary: true,
type: 'int',
generated: true
},
identifier: {
// An email address, phone number, username or other unique identifier
// associated with the request (used to track who it was on behalf of)
type: 'varchar'
},
token: {
// The token used verify the request (maybe hashed or encrypted)
type: 'varchar',
unique: true
},
expires: {
// After this time, the request will no longer ve valid
type: 'timestamp'
},
createdAt: {
type: 'timestamp',
createDate: true
},
updatedAt: {
type: 'timestamp',
updateDate: true
}
}
}

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

@@ -0,0 +1,90 @@
import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
ok: true,
user: {
image: null,
name: "John",
email: "john@email.com",
},
expires: 123213139,
}
export const mockProviders = {
ok: true,
github: {
id: "github",
name: "Github",
type: "oauth",
signinUrl: "path/to/signin",
callbackUrl: "path/to/callback",
},
credentials: {
id: "credentials",
name: "Credentials",
type: "credentials",
authorize: null,
credentials: null,
},
email: {
id: "email",
type: "email",
name: "Email",
},
}
export const mockCSRFToken = {
ok: true,
csrfToken: randomBytes(32).toString("hex"),
}
export const mockGithubResponse = {
ok: true,
status: 200,
url: "https://path/to/github/url",
}
export const mockCredentialsResponse = {
ok: true,
status: 200,
url: "https://path/to/credentials/url",
}
export const mockEmailResponse = {
ok: true,
status: 200,
url: "https://path/to/email/url",
}
export const mockSignOutResponse = {
ok: true,
status: 200,
url: "https://path/to/signout/url",
}
export const server = setupServer(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSignOutResponse))
),
rest.get("/api/auth/session", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSession))
),
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCSRFToken))
),
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockProviders))
),
rest.post("/api/auth/signin/github", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockGithubResponse))
),
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCredentialsResponse))
),
rest.post("/api/auth/signin/email", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockEmailResponse))
),
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
)

View File

@@ -0,0 +1,8 @@
export function getBroadcastEvents() {
return window.localStorage.setItem.mock.calls
.filter((call) => call[0] === "nextauth.message")
.map(([eventName, value]) => {
const { timestamp, ...rest } = JSON.parse(value)
return { eventName, value: rest }
})
}

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

@@ -0,0 +1,98 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => server.listen())
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("if it can fetch the session, it should store it in `localStorage`", async () => {
render(<SessionFlow />)
// In the start, there is no session
const noSession = await screen.findByText("No session")
expect(noSession).toBeInTheDocument()
// After we fetched the session, it should have been rendered by `<SessionFlow />`
const session = await screen.findByText(new RegExp(mockSession.user.name))
expect(session).toBeInTheDocument()
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "getSession",
},
event: "session",
})
})
test("if there's an error fetching the session, it should log it", async () => {
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
return res(ctx.status(500), ctx.body("Server error"))
})
)
render(<SessionFlow />)
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"session",
new SyntaxError("Unexpected token S in JSON at position 0")
)
})
})
function SessionFlow() {
const [session, setSession] = useState(null)
useEffect(() => {
async function fetchUserSession() {
try {
const result = await getSession()
setSession(result)
} catch (e) {
console.error(e)
}
}
fetchUserSession()
}, [])
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
return <p>No session</p>
}

View File

@@ -0,0 +1,290 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import logger from "../../lib/logger"
import {
server,
mockCredentialsResponse,
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from ".."
import { rest } from "msw"
const { location } = window
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider, it redirects to the default sign-in page",
async ({ provider }) => {
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
})
}
)
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider supplied and no callback URL, redirects using the current location",
async ({ provider }) => {
render(<SignInFlow providerId={provider} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(
window.location.href
)}`
)
})
}
)
test.each`
provider | mockUrl
${`email`} | ${mockEmailResponse.url}
${`credentials`} | ${mockCredentialsResponse.url}
`(
"$provider provider redirects if `redirect` is `true`",
async ({ provider, mockUrl }) => {
render(<SignInFlow providerId={provider} redirect={true} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
})
}
)
test("redirection can't be stopped using an oauth provider", async () => {
render(
<SignInFlow
providerId="github"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
})
})
test("redirection can be stopped using the 'credentials' provider", async () => {
render(
<SignInFlow
providerId="credentials"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockCredentialsResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn`
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/credentials/url",
}
`)
})
test("redirection can be stopped using the 'email' provider", async () => {
render(
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockEmailResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn` oauth
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/email/url",
}
`)
})
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signin/email", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockEmailResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
expect(window.location.reload).toHaveBeenCalledTimes(1)
})
})
test("params are propagated to the signin URL when supplied", async () => {
let matchedParams = ""
const authParams = "foo=bar&bar=foo"
server.use(
rest.post("/api/auth/signin/github", (req, res, ctx) => {
matchedParams = req.url.search
return res(ctx.status(200), ctx.json(mockGithubResponse))
})
)
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(matchedParams).toEqual(`?${authParams}`)
})
})
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
const errorMsg = "Error when retrieving providers"
server.use(
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.json(errorMsg))
)
)
render(<SignInFlow providerId="github" />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
errorMsg
)
})
})
function SignInFlow({
providerId,
callbackUrl,
redirect = true,
authorizationParams = {},
}) {
const [response, setResponse] = useState(null)
async function handleSignIn() {
const result = await signIn(
providerId,
{
callbackUrl,
redirect,
},
authorizationParams
)
setResponse(result)
}
return (
<>
<p data-testid="signin-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignIn}>Sign in</button>
</>
)
}

View File

@@ -0,0 +1,129 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from ".."
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"
const { location } = window
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test("by default it redirects to the current URL if the server did not provide one", async () => {
server.use(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
)
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
})
})
test("it redirects to the URL allowed by the server", async () => {
render(<SignOutFlow callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
mockSignOutResponse.url
)
})
})
test("if url contains a hash during redirection a page reload happens", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signout", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockSignOutResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.reload).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
})
})
test("will broadcast the signout event to other tabs", async () => {
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "signout",
},
event: "session",
})
})
})
function SignOutFlow({ callbackUrl, redirect = true }) {
const [response, setResponse] = useState(null)
async function handleSignOut() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
return (
<>
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignOut}>Sign out</button>
</>
)
}

View File

@@ -8,9 +8,15 @@
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import _logger, { proxyLogger } from '../lib/logger'
import parseUrl from '../lib/parse-url'
import {
useState,
useEffect,
useContext,
createContext,
createElement,
} from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {}
_getSession: () => {},
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener('visibilitychange', () => {
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
}, false)
document.addEventListener(
"visibilitychange",
() => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
},
false
)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
export function useSession (session) {
export function useSession(session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
function _useSessionHook (session) {
function _useSessionHook(session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(!data)
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === 'storage'
const triggeredByStorageEvent = event === "storage"
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
} else if (
clientMaxAge > 0 &&
currentTime < clientLastSync + clientMaxAge
) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
if (clientSession === undefined) {
__NEXTAUTH._clientSession = null
}
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent
triggerEvent: !triggeredByStorageEvent,
})
// Save session state internally, just so we can track that we've checked
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
logger.error("CLIENT_USE_SESSION_ERROR", error)
setLoading(false)
}
}
@@ -137,114 +158,112 @@ function _useSessionHook (session) {
return [data, loading]
}
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
export async function getCsrfToken(ctx) {
return (await _fetchData("csrf", ctx))?.csrfToken
}
export async function getProviders () {
return _fetchData('providers')
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
// Redirect to sign in page if no valid provider specified
if (!(provider in providers)) {
// If Provider not recognized, redirect to sign in page
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
return
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
const isCredentials = providers[provider].type === 'credentials'
const isEmail = providers[provider].type === 'email'
const canRedirectBeDisabled = isCredentials || isEmail
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
// If is any other provider type, POST to provider URL with CSRF Token,
// callback URL and any other parameters supplied.
const fetchOptions = {
method: 'post',
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
}
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !canRedirectBeDisabled) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get('error')
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: 'storage' })
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url
url: error ? null : data.url,
}
}
export async function signOut (options = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: 'post',
method: "post",
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location = url
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: 'storage' })
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
@@ -252,13 +271,18 @@ export async function signOut (options = {}) {
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
export function setOptions({
baseUrl,
basePath,
clientMaxAge,
keepAlive,
} = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === 'undefined') return
if (typeof window === "undefined") return
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
@@ -269,12 +293,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: 'timer' })
await __NEXTAUTH._getSession({ event: "timer" })
}, keepAlive * 1000)
}
}
export function Provider ({ children, session, options }) {
export function Provider({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
@@ -290,24 +314,25 @@ export function Provider ({ children, session, options }) {
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', path, error)
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl () {
if (typeof window === 'undefined') {
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
@@ -318,7 +343,7 @@ function _apiBaseUrl () {
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now () {
function _now() {
return Math.floor(Date.now() / 1000)
}
@@ -328,33 +353,48 @@ function _now () {
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel (name = 'nextauth.message') {
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive (onReceive) {
if (typeof window === 'undefined') return
window.addEventListener('storage', async (event) => {
receive(onReceive) {
if (typeof window === "undefined") return
window.addEventListener("storage", async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return
if (message?.event !== "session" || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post (message) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(name,
post(message) {
if (typeof localStorage === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
}
},
}
}
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases. These should be removed in a newer release, as it only
// creates problems for bundlers and adds confusion to users. TypeScript declarations
// will provide sufficient help when importing
export {
setOptions as options,
getSession as session,
getProviders as providers,
getCsrfToken as csrfToken,
signIn as signin,
signOut as signout,
}
export default {
getSession,
getCsrfToken,
@@ -374,5 +414,5 @@ export default {
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut
signout: signOut,
}

View File

@@ -1,39 +1,98 @@
/**
* Same as the default `Error`, but it is JSON serializable.
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
*/
export class UnknownError extends Error {
constructor (message) {
super(message)
this.name = 'UnknownError'
constructor(error) {
// Support passing error or string
super(error?.message ?? error)
this.name = "UnknownError"
if (error instanceof Error) {
this.stack = error.stack
}
}
toJSON () {
toJSON() {
return {
error: {
name: this.name,
message: this.message
// stack: this.stack
}
name: this.name,
message: this.message,
stack: this.stack,
}
}
}
export class CreateUserError extends UnknownError {
constructor (message) {
super(message)
this.name = 'CreateUserError'
}
}
// Thrown when an Email address is already associated with an account
// but the user is trying an OAuth account that is not linked to it.
export class AccountNotLinkedError extends UnknownError {
constructor (message) {
super(message)
this.name = 'AccountNotLinkedError'
}
}
export class OAuthCallbackError extends UnknownError {
constructor (message) {
super(message)
this.name = 'OAuthCallbackError'
}
name = "OAuthCallbackError"
}
/**
* Thrown when an Email address is already associated with an account
* but the user is trying an OAuth account that is not linked to it.
*/
export class AccountNotLinkedError extends UnknownError {
name = "AccountNotLinkedError"
}
export class CreateUserError extends UnknownError {
name = "CreateUserError"
}
export class GetUserError extends UnknownError {
name = "GetUserError"
}
export class GetUserByEmailError extends UnknownError {
name = "GetUserByEmailError"
}
export class GetUserByIdError extends UnknownError {
name = "GetUserByIdError"
}
export class GetUserByProviderAccountIdError extends UnknownError {
name = "GetUserByProviderAccountIdError"
}
export class UpdateUserError extends UnknownError {
name = "UpdateUserError"
}
export class DeleteUserError extends UnknownError {
name = "DeleteUserError"
}
export class LinkAccountError extends UnknownError {
name = "LinkAccountError"
}
export class UnlinkAccountError extends UnknownError {
name = "UnlinkAccountError"
}
export class CreateSessionError extends UnknownError {
name = "CreateSessionError"
}
export class GetSessionError extends UnknownError {
name = "GetSessionError"
}
export class UpdateSessionError extends UnknownError {
name = "UpdateSessionError"
}
export class DeleteSessionError extends UnknownError {
name = "DeleteSessionError"
}
export class CreateVerificationRequestError extends UnknownError {
name = "CreateVerificationRequestError"
}
export class GetVerificationRequestError extends UnknownError {
name = "GetVerificationRequestError"
}
export class DeleteVerificationRequestError extends UnknownError {
name = "DeleteVerificationRequestError"
}

20
src/providers/42.js Normal file
View File

@@ -0,0 +1,20 @@
export default function FortyTwo(options) {
return {
id: '42-school',
name: '42 School',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
authorizationUrl:
'https://api.intra.42.fr/oauth/authorize?response_type=code',
profileUrl: 'https://api.intra.42.fr/v2/me',
profile: (profile) => ({
id: profile.id,
email: profile.email,
image: profile.image_url,
name: profile.usual_full_name,
}),
...options,
}
}

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,
}
}

54
src/providers/dropbox.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* @param {import("../server").Provider} options
* @example
*
* ```js
* // pages/api/auth/[...nextauth].js
* import Providers from `next-auth/providers`
* ...
* providers: [
* Providers.Dropbox({
* clientId: process.env.DROPBOX_CLIENT_ID,
* clientSecret: process.env.DROPBOX_CLIENT_SECRET
* })
* ]
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("dropbox")}>
* Sign in
* </button>
* ...
* ```
* *Resources:*
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/dropbox)
* - [Dropbox Documentation](https://developers.dropbox.com/oauth-guide)
* - [Configuration](https://www.dropbox.com/developers/apps)
*/
export default function Dropbox(options) {
return {
id: 'dropbox',
name: 'Dropbox',
type: 'oauth',
version: '2.0',
scope: 'account_info.read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
authorizationUrl:
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
profile: (profile) => {
return {
id: profile.account_id,
name: profile.name.display_name,
email: profile.email,
image: profile.profile_photo_url,
email_verified: profile.email_verified
}
},
protection: ["state", "pkce"],
...options
}
}

View File

@@ -43,7 +43,7 @@ const sendVerificationRequest = ({
},
(error) => {
if (error) {
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
logger.error("SEND_VERIFICATION_EMAIL_ERROR", error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
@@ -53,12 +53,11 @@ const sendVerificationRequest = ({
}
// Email HTML body
const html = ({ url, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
const html = ({ url, site }) => {
// Insert invisible space into domains to prevent the
// the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, "&#8203;.")}`
// like they are supposed to click it to sign in.
const escapedSite = `${site.replace(/\./g, "&#8203;.")}`
// Some simple styling options
@@ -73,17 +72,12 @@ const html = ({ url, site, email }) => {
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong>
<td align="center" style="padding: 10px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in to <strong>${escapedSite}</strong>
</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">

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
};
}

View File

@@ -0,0 +1,22 @@
export default function Mailchimp(options) {
return {
id: 'mailchimp',
name: 'Mailchimp',
type: 'oauth',
version: '2.0',
scope: '',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.mailchimp.com/oauth2/token',
authorizationUrl: 'https://login.mailchimp.com/oauth2/authorize?response_type=code',
profileUrl: 'https://login.mailchimp.com/oauth2/metadata',
profile: (profile) => {
return {
id: profile.login.login_id,
name: profile.accountname,
email: profile.login.email,
image: null
}
},
...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,7 +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$/, ".jpg"),
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
}
},
...options,

View File

@@ -0,0 +1,23 @@
export default function WordPress(options) {
return {
id: "wordpress",
name: "WordPress.com",
type: "oauth",
version: "2.0",
scope: "auth",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://public-api.wordpress.com/oauth2/token",
authorizationUrl:
"https://public-api.wordpress.com/oauth2/authorize?response_type=code",
profileUrl: "https://public-api.wordpress.com/rest/v1/me",
profile(profile) {
return {
id: profile.ID,
name: profile.display_name,
email: profile.email,
image: profile.avatar_URL,
}
},
...options,
}
}

26
src/providers/workos.js Normal file
View File

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

20
src/providers/zoom.js Normal file
View File

@@ -0,0 +1,20 @@
export default function Zoom(options) {
return {
id: "zoom",
name: "Zoom",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://zoom.us/oauth/token",
authorizationUrl: "https://zoom.us/oauth/authorize?response_type=code",
profileUrl: "https://api.zoom.us/v2/users/me",
profile(profile) {
return {
id: profile.id,
name: `${profile.first_name} ${profile.last_name}`,
email: profile.email,
}
},
...options,
}
}

View File

@@ -1,24 +1,34 @@
import adapters from '../adapters'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import logger, { setLogger } from '../lib/logger'
import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import * as routes from './routes'
import renderPage from './pages'
import createSecret from './lib/create-secret'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-res'
import csrfTokenHandler from './lib/csrf-token-handler'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
import * as cookie from "./lib/cookie"
import * as defaultEvents from "./lib/default-events"
import * as defaultCallbacks from "./lib/default-callbacks"
import parseProviders from "./lib/providers"
import * as routes from "./routes"
import renderPage from "./pages"
import createSecret from "./lib/create-secret"
import callbackUrlHandler from "./lib/callback-url-handler"
import extendRes from "./lib/extend-res"
import csrfTokenHandler from "./lib/csrf-token-handler"
import * as pkce from "./lib/oauth/pkce-handler"
import * as state from "./lib/oauth/state-handler"
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
logger.warn("NEXTAUTH_URL", "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
}
}
/**
@@ -26,7 +36,7 @@ if (!process.env.NEXTAUTH_URL) {
* @param {import("next").NextApiResponse} res
* @param {import("types").NextAuthOptions} userOptions
*/
async function NextAuthHandler (req, res, userOptions) {
async function NextAuthHandler(req, res, userOptions) {
if (userOptions.logger) {
setLogger(userOptions.logger)
}
@@ -39,13 +49,15 @@ async function NextAuthHandler (req, res, userOptions) {
// to avoid early termination of calls to the serverless function
// (and then return that promise when we are done) - eslint
// complains but I'm not sure there is another way to do this.
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
extendRes(req, res, resolve)
if (!req.query.nextauth) {
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
const error =
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
return res.status(500).end(`Error: ${error}`)
}
@@ -53,31 +65,65 @@ async function NextAuthHandler (req, res, userOptions) {
nextauth,
action = nextauth[0],
providerId = nextauth[1],
error = nextauth[1]
error = nextauth[1],
} = req.query
// @todo refactor all existing references to baseUrl and basePath
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
const { basePath, baseUrl } = parseUrl(
process.env.NEXTAUTH_URL || process.env.VERCEL_URL
)
const cookies = {
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
...cookie.defaultCookies(
userOptions.useSecureCookies || baseUrl.startsWith("https://")
),
// Allow user cookie options to override any cookie settings above
...userOptions.cookies
...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({ providers: userOptions.providers, baseUrl, basePath })
const providers = parseProviders({
providers: userOptions.providers,
baseUrl,
basePath,
})
const provider = providers.find(({ id }) => id === providerId)
// Protection only works on OAuth 2.x providers
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
// When provider.state is undefined, we still want this to pass
if (!provider.protection && provider.state !== false) {
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = ['state']
} else if (typeof provider.protection === 'string') {
provider.protection = [provider.protection]
// TODO:
// - rename to `checks` in 4.x, so it is similar to `openid-client`
// - stop supporting `protection` as string
// - remove `state` property
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
// Priority: (protection array > protection string) > state > default
if (provider.protection) {
provider.protection = Array.isArray(provider.protection)
? provider.protection
: [provider.protection]
} else if (provider.state !== undefined) {
provider.protection = [provider.state ? "state" : "none"]
} else {
// Default to state, as we did in 3.1
// REVIEW: should we use "pkce" or "none" as default?
provider.protection = ["state"]
}
}
@@ -86,14 +132,16 @@ async function NextAuthHandler (req, res, userOptions) {
// 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))
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 = {
debug: false,
pages: {},
theme: 'auto',
theme: "auto",
// Custom options override defaults
...userOptions,
// These computed settings can have values in userOptions but we override them
@@ -111,7 +159,7 @@ async function NextAuthHandler (req, res, userOptions) {
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
...userOptions.session,
},
// JWT options
jwt: {
@@ -119,20 +167,20 @@ async function NextAuthHandler (req, res, userOptions) {
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userOptions.jwt
...userOptions.jwt,
},
// Event messages
events: {
...defaultEvents,
...userOptions.events
...userOptions.events,
},
// Callback functions
callbacks: {
...defaultCallbacks,
...userOptions.callbacks
...userOptions.callbacks,
},
pkce: {},
logger
logger,
}
csrfTokenHandler(req, res)
@@ -141,65 +189,74 @@ async function NextAuthHandler (req, res, userOptions) {
const render = renderPage(req, res)
const { pages } = req.options
if (req.method === 'GET') {
if (req.method === "GET") {
switch (action) {
case 'providers':
case "providers":
return routes.providers(req, res)
case 'session':
case "session":
return routes.session(req, res)
case 'csrf':
case "csrf":
return res.json({ csrfToken: req.options.csrfToken })
case 'signin':
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
if (error) { signinUrl = `${signinUrl}&error=${error}` }
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${req.options.callbackUrl}`
if (error) {
signinUrl = `${signinUrl}&error=${error}`
}
return res.redirect(signinUrl)
}
return render.signin()
case 'signout':
if (pages.signOut) {
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
}
case "signout":
if (pages.signOut) return res.redirect(pages.signOut)
return render.signout()
case 'callback':
case "callback":
if (provider) {
if (await pkce.handleCallback(req, res)) return
if (await state.handleCallback(req, res)) return
return routes.callback(req, res)
}
break
case 'verify-request':
case "verify-request":
if (pages.verifyRequest) {
return res.redirect(pages.verifyRequest)
}
return render.verifyRequest()
case 'error':
case "error":
if (pages.error) {
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
return res.redirect(
`${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`
)
}
// These error messages are displayed in line on the sign in page
if ([
'Signin',
'OAuthSignin',
'OAuthCallback',
'OAuthCreateAccount',
'EmailCreateAccount',
'Callback',
'OAuthAccountNotLinked',
'EmailSignin',
'CredentialsSignin'
].includes(error)) {
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
}
return render.error({ error })
default:
}
} else if (req.method === 'POST') {
} else if (req.method === "POST") {
switch (action) {
case 'signin':
case "signin":
// Verified CSRF Token required for all sign in routes
if (req.options.csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
@@ -208,16 +265,19 @@ async function NextAuthHandler (req, res, userOptions) {
}
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
case "signout":
// Verified CSRF Token required for signout
if (req.options.csrfTokenVerified) {
return routes.signout(req, res)
}
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
case "callback":
if (provider) {
// Verified CSRF Token required for credentials providers only
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
if (
provider.type === "credentials" &&
!req.options.csrfTokenVerified
) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
@@ -226,31 +286,35 @@ async function NextAuthHandler (req, res, userOptions) {
return routes.callback(req, res)
}
break
case '_log':
case "_log":
if (userOptions.logger) {
try {
const {
code = 'CLIENT_ERROR',
level = 'error',
message = '[]'
code = "CLIENT_ERROR",
level = "error",
message = "[]",
} = req.body
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error('LOGGER_ERROR', error)
logger.error("LOGGER_ERROR", error)
}
}
return res.end()
default:
}
}
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
return res
.status(400)
.end(
`Error: This action with HTTP ${req.method} is not supported by NextAuth.js`
)
})
}
/** Tha main entry point to next-auth */
export default function NextAuth (...args) {
export default function NextAuth(...args) {
if (args.length === 1) {
return (req, res) => NextAuthHandler(req, res, args[0])
}

View File

@@ -1,5 +1,6 @@
import { AccountNotLinkedError } from '../../lib/errors'
import dispatchEvent from '../lib/dispatch-event'
import { AccountNotLinkedError } from "../../lib/errors"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* This function handles the complex flow of signing users in, and either creating,
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* handler.
* @param {import("types").Session} sessionToken
* @param {import("types").Profile} profile
* @param {import("types").Account} account
* @param {import("types/internals").AppOptions} options
*/
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
export default async function callbackHandler(
sessionToken,
profile,
providerAccount,
options
) {
// Input validation
if (!profile) throw new Error('Missing profile')
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
if (!profile) throw new Error("Missing profile")
if (!providerAccount?.id || !providerAccount.type)
throw new Error("Missing or invalid provider account")
if (!["email", "oauth"].includes(providerAccount.type))
throw new Error("Provider not supported")
const {
adapter,
jwt,
events,
session: {
jwt: useJwtSession
}
session: { jwt: useJwtSession },
} = options
// If no adapter is configured then we don't have a database and cannot
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
user: profile,
account: providerAccount,
session: {}
session: {},
}
}
@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
deleteSession,
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
let session = null
let user = null
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
}
}
if (providerAccount.type === 'email') {
if (providerAccount.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
session,
user,
isNewUser
isNewUser,
}
} else if (providerAccount.type === 'oauth') {
} else if (providerAccount.type === "oauth") {
// If signing in with oauth account, check to see if the account exists already
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
const userByProviderAccountId = await getUserByProviderAccountId(
providerAccount.provider,
providerAccount.id
)
if (userByProviderAccountId) {
if (isSignedIn) {
// If the user is already signed in with this account, we don't need to do anything
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
session,
user,
isNewUser
isNewUser,
}
}
// If the user is currently signed in, but the new account they are signing in
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
session = useJwtSession
? {}
: await createSession(userByProviderAccountId)
return {
session,
user: userByProviderAccountId,
isNewUser
isNewUser,
}
} else {
if (isSignedIn) {
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
// As they are already signed in, we don't need to do anything after linking them
return {
session,
user,
isNewUser
isNewUser,
}
}
@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
session = useJwtSession ? {} : await createSession(user)
isNewUser = true
return {
session,
user,
isNewUser
isNewUser,
}
}
}

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

@@ -1,7 +1,7 @@
import { OAuth, OAuth2 } from 'oauth'
import querystring from 'querystring'
import logger from '../../../lib/logger'
import { sign as jwtSign } from 'jsonwebtoken'
import { OAuth, OAuth2 } from "oauth"
import querystring from "querystring"
import logger from "../../../lib/logger"
import { sign as jwtSign } from "jsonwebtoken"
/**
* @TODO Refactor to remove dependancy on 'oauth' package
@@ -9,8 +9,8 @@ import { sign as jwtSign } from 'jsonwebtoken'
* would be easier to maintain if all the code was native to next-auth.
* @param {import("types/providers").OAuthConfig} provider
*/
export default function oAuthClient (provider) {
if (provider.version?.startsWith('2.')) {
export default function oAuthClient(provider) {
if (provider.version?.startsWith("2.")) {
// Handle OAuth v2.x
const authorizationUrl = new URL(provider.authorizationUrl)
const basePath = authorizationUrl.origin
@@ -34,9 +34,9 @@ export default function oAuthClient (provider) {
provider.accessTokenUrl,
provider.clientId,
provider.clientSecret,
provider.version || '1.0',
provider.version || "1.0",
provider.callbackUrl,
provider.encoding || 'HMAC-SHA1'
provider.encoding || "HMAC-SHA1"
)
// Promisify get() and getOAuth2AccessToken() for OAuth1
@@ -51,40 +51,48 @@ export default function oAuthClient (provider) {
})
})
}
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
const originalGetOAuth1AccessToken =
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
originalGetOAuth1AccessToken(
...args,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params,
})
}
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params
})
})
)
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
const originalGetOAuthRequestToken =
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
originalGetOAuthRequestToken(
params,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret, params })
}
resolve({ oauth_token, oauth_token_secret, params })
})
)
})
}
return oauth1Client
@@ -104,103 +112,112 @@ export default function oAuthClient (provider) {
* @param {import("types/providers").OAuthConfig} provider
* @param {string | undefined} codeVerifier
*/
async function getOAuth2AccessToken (code, provider, codeVerifier) {
async function getOAuth2AccessToken(code, provider, codeVerifier) {
const url = provider.accessTokenUrl
const params = { ...provider.params }
const headers = { ...provider.headers }
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
const codeParam =
params.grant_type === "refresh_token" ? "refresh_token" : "code"
if (!params[codeParam]) { params[codeParam] = code }
if (!params[codeParam]) {
params[codeParam] = code
}
if (!params.client_id) { params.client_id = provider.clientId }
if (!params.client_id) {
params.client_id = provider.clientId
}
// For Apple the client secret must be generated on-the-fly.
// Using the properties in clientSecret to create a JWT.
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') {
if (provider.id === "apple" && typeof provider.clientSecret === "object") {
const { keyId, teamId, privateKey } = provider.clientSecret
const clientSecret = jwtSign({
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
aud: 'https://appleid.apple.com',
sub: provider.clientId
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, '\n'),
{ algorithm: 'ES256', keyid: keyId }
const clientSecret = jwtSign(
{
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
aud: "https://appleid.apple.com",
sub: provider.clientId,
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, "\n"),
{ algorithm: "ES256", keyid: keyId }
)
params.client_secret = clientSecret
} else {
params.client_secret = provider.clientSecret
}
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
// Added as a fix for Reddit Authentication
if (provider.id === 'reddit') {
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
if (!params.redirect_uri) {
params.redirect_uri = provider.callbackUrl
}
if (provider.id === 'identity-server4' && !headers.Authorization) {
if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers["Client-ID"]) {
headers["Client-ID"] = provider.clientId
}
// Added as a fix for Reddit Authentication
if (provider.id === "reddit") {
headers.Authorization =
"Basic " +
Buffer.from(provider.clientId + ":" + provider.clientSecret).toString(
"base64"
)
}
if (provider.id === "identity-server4" && !headers.Authorization) {
headers.Authorization = `Bearer ${code}`
}
if (provider.protection.includes('pkce')) {
if (provider.protection.includes("pkce")) {
params.code_verifier = codeVerifier
}
const postData = querystring.stringify(params)
return new Promise((resolve, reject) => {
this._request(
'POST',
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
this._request("POST", url, headers, postData, null, (error, data) => {
if (error) {
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error)
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
let accessToken
if (provider.id === 'slack') {
const { ok, error } = raw
if (!ok) {
return reject(error)
}
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw
})
accessToken = raw.authed_user.access_token
} else {
accessToken = raw.access_token
}
)
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw,
})
})
})
}
@@ -213,52 +230,69 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
* @param {string} accessToken
* @param {any} results
*/
async function getOAuth2 (provider, accessToken, results) {
async function getOAuth2(provider, accessToken, results) {
let url = provider.profileUrl
let httpMethod = "GET"
const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) {
headers.Authorization = this.buildAuthHeader(accessToken)
// Mail.ru & vk.com require 'access_token' as URL request parameter
if (['mailru', 'vk'].includes(provider.id)) {
if (["mailru", "vk"].includes(provider.id)) {
const safeAccessTokenURL = new URL(url)
safeAccessTokenURL.searchParams.append('access_token', accessToken)
safeAccessTokenURL.searchParams.append("access_token", accessToken)
url = safeAccessTokenURL.href
}
// This line is required for Twitch
if (provider.id === 'twitch') {
headers['Client-ID'] = provider.clientId
if (provider.id === "twitch") {
headers["Client-ID"] = provider.clientId
}
accessToken = null
}
if (provider.id === 'bungie') {
if (provider.id === "bungie") {
url = prepareProfileUrl({ provider, url, results })
}
/** Dropbox requires POST instead of GET
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
*/
if (provider.id === "dropbox") {
httpMethod = "POST"
}
return new Promise((resolve, reject) => {
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
if (error) {
return reject(error)
this._request(
httpMethod,
url,
headers,
null,
accessToken,
(error, profileData) => {
if (error) {
return reject(error)
}
resolve(profileData)
}
resolve(profileData)
})
)
})
}
/** Bungie needs special handling */
function prepareProfileUrl ({ provider, url, results }) {
function prepareProfileUrl({ provider, url, results }) {
if (!results.membership_id) {
// internal error
// @TODO: handle better
throw new Error('Expected membership_id to be passed.')
throw new Error("Expected membership_id to be passed.")
}
if (!provider.headers?.['X-API-Key']) {
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
if (!provider.headers?.["X-API-Key"]) {
throw new Error(
'The Bungie provider requires the X-API-Key option to be present in "headers".'
)
}
return url.replace('{membershipId}', results.membership_id)
return url.replace("{membershipId}", results.membership_id)
}

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,22 +1,44 @@
import { randomBytes } from 'crypto'
import { randomBytes } from "crypto"
import adapterErrorHandler from "../../../adapters/error-handler"
export default async function email (email, provider, options) {
/**
*
* @param {string} email
* @param {import("types/providers").EmailConfig} provider
* @param {import("types/internals").AppOptions} options
* @returns
*/
export default async function email(email, provider, options) {
try {
const { baseUrl, basePath, adapter } = options
const { baseUrl, basePath, adapter, logger } = options
const { createVerificationRequest } = await adapter.getAdapter(options)
const { createVerificationRequest } = adapterErrorHandler(
await adapter.getAdapter(options),
logger
)
// Prefer provider specific secret, but use default secret if none specified
const secret = provider.secret || options.secret
// Generate token
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
const token =
(await provider.generateVerificationToken?.()) ??
randomBytes(32).toString("hex")
// Send email with link containing token (the unhashed version)
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
provider.id
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
// @TODO Create invite (send secret so can be hashed)
await createVerificationRequest(email, url, token, secret, provider, options)
await createVerificationRequest(
email,
url,
token,
secret,
provider,
options
)
// Return promise
return Promise.resolve()

View File

@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
if (provider.version?.startsWith('2.')) {
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
scope: provider.scope,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
redirect_uri: provider.callbackUrl
})
// If the authorizationUrl specified in the config has query parameters on it

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

@@ -1,15 +1,15 @@
import oAuthCallback from '../lib/oauth/callback'
import callbackHandler from '../lib/callback-handler'
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* Handle callbacks from login services
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function callback (req, res) {
export default async function callback(req, res) {
const {
provider,
adapter,
@@ -22,21 +22,23 @@ export default async function callback (req, res) {
jwt,
events,
callbacks,
session: {
jwt: useJwtSession,
maxAge: sessionMaxAge
}
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
logger,
} = req.options
// Get session ID (if set)
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
if (provider.type === 'oauth') {
if (provider.type === "oauth") {
try {
const { profile, account, OAuthProfile } = await oAuthCallback(req)
try {
// Make it easier to debug when adding a new provider
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
logger.debug("OAUTH_CALLBACK_RESPONSE", {
profile,
account,
OAuthProfile,
})
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
@@ -56,52 +58,85 @@ export default async function callback (req, res) {
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
const { getUserByProviderAccountId } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
const userFromProviderAccountId = await getUserByProviderAccountId(
account.provider,
account.id
)
if (userFromProviderAccountId) {
userOrProfile = userFromProviderAccountId
}
}
try {
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
account,
OAuthProfile
)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
const { user, session, isNewUser } = await callbackHandler(
sessionToken,
profile,
account,
req.options
)
if (useJwtSession) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
OAuthProfile,
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
expires: session.expires || null,
...cookies.sessionToken.options,
})
}
await dispatchEvent(events.signIn, { user, account, isNewUser })
@@ -110,94 +145,145 @@ export default async function callback (req, res) {
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
return res.redirect(
`${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
// Callback URL is already verified at this point, so safe to use if specified
return res.redirect(callbackUrl || baseUrl)
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
if (error.name === "AccountNotLinkedError") {
// If the email on the account is already linked, but not with this OAuth account
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
return res.redirect(
`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`
)
} else if (error.name === "CreateUserError") {
return res.redirect(
`${baseUrl}${basePath}/error?error=OAuthCreateAccount`
)
}
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} catch (error) {
if (error.name === 'OAuthCallbackError') {
logger.error('CALLBACK_OAUTH_ERROR', error)
if (error.name === "OAuthCallbackError") {
logger.error("CALLBACK_OAUTH_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
logger.error('OAUTH_CALLBACK_ERROR', error)
logger.error("OAUTH_CALLBACK_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} else if (provider.type === 'email') {
} else if (provider.type === "email") {
try {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
const {
getVerificationRequest,
deleteVerificationRequest,
getUserByEmail,
} = adapterErrorHandler(await adapter.getAdapter(req.options), logger)
const verificationToken = req.query.token
const email = req.query.email
// Verify email and verification token exist in database
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
const invite = await getVerificationRequest(
email,
verificationToken,
secret,
provider
)
if (!invite) {
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
}
// If verification token is valid, delete verification request token from
// the database so it cannot be used again
await deleteVerificationRequest(email, verificationToken, secret, provider)
await deleteVerificationRequest(
email,
verificationToken,
secret,
provider
)
// 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 }
const profile = (await getUserByEmail(email)) || { email }
const account = {
id: provider.id,
type: "email",
providerAccountId: email,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
const signInCallbackResponse = await callbacks.signIn(
profile,
account,
{ email }
)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
const { user, session, isNewUser } = await callbackHandler(
sessionToken,
profile,
account,
req.options
)
if (useJwtSession) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
profile,
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
expires: session.expires || null,
...cookies.sessionToken.options,
})
}
await dispatchEvent(events.signIn, { user, account, isNewUser })
@@ -206,55 +292,93 @@ export default async function callback (req, res) {
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
return res.redirect(
`${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
// Callback URL is already verified at this point, so safe to use if specified
return res.redirect(callbackUrl || baseUrl)
} catch (error) {
if (error.name === 'CreateUserError') {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
if (error.name === "CreateUserError") {
return res.redirect(
`${baseUrl}${basePath}/error?error=EmailCreateAccount`
)
}
logger.error('CALLBACK_EMAIL_ERROR', error)
logger.error("CALLBACK_EMAIL_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} else if (provider.type === 'credentials' && req.method === 'POST') {
} else if (provider.type === "credentials" && req.method === "POST") {
if (!useJwtSession) {
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
logger.error(
"CALLBACK_CREDENTIALS_JWT_ERROR",
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
)
return res
.status(500)
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
if (!provider.authorize) {
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
logger.error(
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
"Must define an authorize() handler to use credentials authentication provider"
)
return res
.status(500)
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const credentials = req.body
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials, {...req, options: {}, cookies: {}}
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
return res
.status(401)
.redirect(
`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(
provider.id
)}`
)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const user = userObjectReturnedFromAuthorizeHandler
const account = { id: provider.id, type: 'credentials' }
const account = { id: provider.id, type: "credentials" }
try {
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
const signInCallbackResponse = await callbacks.signIn(
user,
account,
credentials
)
if (signInCallbackResponse === false) {
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
@@ -263,22 +387,33 @@ export default async function callback (req, res) {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
userObjectReturnedFromAuthorizeHandler,
false
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
await dispatchEvent(events.signIn, { user, account })
return res.redirect(callbackUrl || baseUrl)
}
return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`)
return res
.status(500)
.end(`Error: Callback for provider type ${provider.type} not supported`)
}

View File

@@ -5,13 +5,16 @@
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default function providers (req, res) {
export default function providers(req, res) {
const { providers } = req.options
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
}, {})
const result = providers.reduce(
(acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
},
{}
)
res.json(result)
}

View File

@@ -1,13 +1,15 @@
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* Return a session object (without any private fields)
* for Single Page App clients
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function session (req, res) {
const { cookies, adapter, jwt, events, callbacks } = req.options
export default async function session(req, res) {
const { cookies, adapter, jwt, events, callbacks, logger } = req.options
const useJwtSession = req.options.session.jwt
const sessionMaxAge = req.options.session.maxAge
const sessionToken = req.cookies[cookies.sessionToken.name]
@@ -24,7 +26,9 @@ export default async function session (req, res) {
// Generate new session expiry date
const sessionExpiresDate = new Date()
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
sessionExpiresDate.setTime(
sessionExpiresDate.getTime() + sessionMaxAge * 1000
)
const sessionExpires = sessionExpiresDate.toISOString()
// By default, only exposes a limited subset of information to the client
@@ -33,14 +37,17 @@ export default async function session (req, res) {
user: {
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null
image: decodedJwt.picture || null,
},
expires: sessionExpires
expires: sessionExpires,
}
// Pass Session and JSON Web Token through to the session callback
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
jwtPayload
)
// Return session payload as response
response = sessionPayload
@@ -49,17 +56,29 @@ export default async function session (req, res) {
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: sessionExpires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
await dispatchEvent(events.session, {
session: sessionPayload,
jwt: jwtPayload,
})
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error('JWT_SESSION_ERROR', error)
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
logger.error("JWT_SESSION_ERROR", error)
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0,
})
}
} else {
try {
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
const { getUser, getSession, updateSession } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
const session = await getSession(sessionToken)
if (session) {
// Trigger update to session object to update session expiry
@@ -73,29 +92,38 @@ export default async function session (req, res) {
user: {
name: user.name,
email: user.email,
image: user.image
image: user.image,
},
accessToken: session.accessToken,
expires: session.expires
expires: session.expires,
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
user
)
// Return session payload as response
response = sessionPayload
// Set cookie again to update expiry
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, sessionToken, {
expires: session.expires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, { session: sessionPayload })
} else if (sessionToken) {
// If sessionToken was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0,
})
}
} catch (error) {
logger.error('SESSION_ERROR', error)
logger.error("SESSION_ERROR", error)
}
}

View File

@@ -1,74 +1,95 @@
import getAuthorizationUrl from '../lib/signin/oauth'
import emailSignin from '../lib/signin/email'
import logger from '../../lib/logger'
import getAuthorizationUrl from "../lib/signin/oauth"
import emailSignin from "../lib/signin/email"
import adapterErrorHandler from "../../adapters/error-handler"
/** Handle requests to /api/auth/signin */
export default async function signin (req, res) {
const {
provider,
baseUrl,
basePath,
adapter,
callbacks
} = req.options
/**
* Handle requests to /api/auth/signin
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function signin(req, res) {
const { provider, baseUrl, basePath, adapter, callbacks, logger } =
req.options
if (!provider.type) {
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
}
if (provider.type === 'oauth' && req.method === 'POST') {
if (provider.type === "oauth" && req.method === "POST") {
try {
const authorizationUrl = await getAuthorizationUrl(req)
return res.redirect(authorizationUrl)
} catch (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
logger.error("SIGNIN_OAUTH_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
} else if (provider.type === 'email' && req.method === 'POST') {
} else if (provider.type === "email" && req.method === "POST") {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getUserByEmail } = await adapter.getAdapter(req.options)
const { getUserByEmail } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
// Note: Technically the part of the email address local mailbox element
// (everything before the @ symbol) should be treated as 'case sensitive'
// according to RFC 2821, but in practice this causes more problems than
// it solves. We treat email addresses as all lower case. If anyone
// complains about this we can make strict RFC 2821 compliance an option.
const email = req.body.email?.toLowerCase() ?? null
let email = req.body.email?.toLowerCase() ?? null
if (!email) {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
// Get the first two elements only,
// separated by `@` from user input.
let [local, domain] = email.split("@")
// The part before "@" can contain a ","
// but we remove it on the domain part
domain = domain.split(",")[0]
email = `${local}@${domain}`
// 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 }
const profile = (await getUserByEmail(email)) || { email }
const account = { id: provider.id, type: "email", providerAccountId: email }
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
const signInCallbackResponse = await callbacks.signIn(profile, account, {
email,
verificationRequest: true,
})
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
try {
await emailSignin(email, provider, req.options)
} catch (error) {
logger.error('SIGNIN_EMAIL_ERROR', error)
logger.error("SIGNIN_EMAIL_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`)
return res.redirect(
`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`
)
}
return res.redirect(`${baseUrl}${basePath}/signin`)
}

View File

@@ -1,10 +1,14 @@
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/** Handle requests to /api/auth/signout */
export default async function signout (req, res) {
const { adapter, cookies, events, jwt, callbackUrl } = req.options
/**
* Handle requests to /api/auth/signout
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function signout(req, res) {
const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options
const useJwtSession = req.options.session.jwt
const sessionToken = req.cookies[cookies.sessionToken.name]
@@ -18,7 +22,10 @@ export default async function signout (req, res) {
}
} else {
// Get session from database
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
const { getSession, deleteSession } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
try {
// Dispatch signout event
@@ -33,14 +40,14 @@ export default async function signout (req, res) {
await deleteSession(sessionToken)
} catch (error) {
// If error, log it but continue
logger.error('SIGNOUT_ERROR', error)
logger.error("SIGNOUT_ERROR", error)
}
}
// Remove Session Token
cookie.set(res, cookies.sessionToken.name, '', {
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0
maxAge: 0,
})
return res.redirect(callbackUrl)

View File

@@ -1,53 +0,0 @@
# Start test app with local databases inside the container.
#
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
# https://docs.docker.com/compose/compose-file/compose-file-v2/
version: '2.3'
services:
app:
build:
context: ../../
dockerfile: Dockerfile
environment:
# Set env vars in your current terminal or in .env in the root directory
- NEXTAUTH_URL=${NEXTAUTH_URL}
- NEXTAUTH_DATABASE_URL=${NEXTAUTH_DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_JWT_SESSIONS=${NEXTAUTH_JWT_SESSIONS}
- NEXTAUTH_AUTH0_ID=${NEXTAUTH_AUTH0_ID}
- NEXTAUTH_AUTH0_SECRET=${NEXTAUTH_AUTH0_SECRET}
- NEXTAUTH_AUTH0_DOMAIN=${NEXTAUTH_AUTH0_DOMAIN}
- NEXTAUTH_FACEBOOK_ID=${NEXTAUTH_FACEBOOK_ID}
- NEXTAUTH_FACEBOOK_SECRET=${NEXTAUTH_FACEBOOK_SECRET}
- NEXTAUTH_GITHUB_ID=${NEXTAUTH_GITHUB_ID}
- NEXTAUTH_GITHUB_SECRET=${NEXTAUTH_GITHUB_SECRET}
- NEXTAUTH_GOOGLE_ID=${NEXTAUTH_GOOGLE_ID}
- NEXTAUTH_GOOGLE_SECRET=${NEXTAUTH_GOOGLE_SECRET}
- NEXTAUTH_TWITTER_ID=${NEXTAUTH_TWITTER_ID}
- NEXTAUTH_TWITTER_SECRET=${NEXTAUTH_TWITTER_SECRET}
- NEXTAUTH_EMAIL_SERVER=${NEXTAUTH_EMAIL_SERVER}
- NEXTAUTH_EMAIL_FROM=${NEXTAUTH_EMAIL_FROM}
ports:
- "3000:3000"
# mongo:
# extends:
# file: databases/mongo.yml
# service: mongo
# mssql:
# extends:
# file: databases/mssql.yml
# service: mssql
# mysql:
# extends:
# file: databases/mysql.yml
# service: mysql
# postgres:
# extends:
# file: databases/postgres.yml
# service: postgres

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"name": "next-auth-test",
"version": "0.0.1",
"description": "Test application for NextAuth.js",
"main": "",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"author": "Iain Collins <me@iaincollins.com>",
"license": "ISC",
"dependencies": {
"next": "^10.0.6",
"react": "^17.0.1",
"react-dom": "^17.0.1"
}
}

View File

@@ -1,26 +0,0 @@
import { Provider } from 'next-auth/client'
export default function App ({ Component, pageProps }) {
return (
<Provider
options={{
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
clientMaxAge: 0,
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0
}}
session={pageProps.session}
>
<Component {...pageProps} />
</Provider>
)
}

View File

@@ -1,118 +0,0 @@
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
const options = {
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Email({
server: process.env.NEXTAUTH_EMAIL_SERVER,
from: process.env.NEXTAUTH_EMAIL_FROM
}),
Providers.Apple({
clientId: process.env.NEXTAUTH_APPLE_ID,
clientSecret: {
appleId: process.env.NEXTAUTH_APPLE_ID,
teamId: process.env.NEXTAUTH_APPLE_TEAM_ID,
privateKey: process.env.NEXTAUTH_APPLE_PRIVATE_KEY,
keyId: process.env.NEXTAUTH_APPLE_KEY_ID
}
}),
Providers.Auth0({
clientId: process.env.NEXTAUTH_AUTH0_ID,
clientSecret: process.env.NEXTAUTH_AUTH0_SECRET,
domain: process.env.NEXTAUTH_AUTH0_DOMAIN
}),
Providers.Facebook({
clientId: process.env.NEXTAUTH_FACEBOOK_ID,
clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET
}),
Providers.GitHub({
clientId: process.env.NEXTAUTH_GITHUB_ID,
clientSecret: process.env.NEXTAUTH_GITHUB_SECRET
}),
Providers.Google({
clientId: process.env.NEXTAUTH_GOOGLE_ID,
clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET
}),
Providers.Twitter({
clientId: process.env.NEXTAUTH_TWITTER_ID,
clientSecret: process.env.NEXTAUTH_TWITTER_SECRET
})
],
// Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
// https://next-auth.js.org/configuration/database
//
// Notes:
// * You must to install an appropriate node_module for your database
// * The Email provider requires a database (OAuth providers do not)
database: process.env.NEXTAUTH_DATABASE_URL,
// The secret should be set to a reasonably long random string.
// It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
// a seperate secret is defined explicitly for encrypting the JWT.
secret: process.env.NEXTAUTH_SECRET,
session: {
// Use JSON Web Tokens for session instead of database sessions.
// This option can be used with or without a database for users/accounts.
// Note: `jwt` is automatically set to `true` if no database is specified.
jwt: true
// Seconds - How long until an idle session expires and is no longer valid.
// maxAge: 30 * 24 * 60 * 60, // 30 days
// Seconds - Throttle how frequently to write to database to extend a session.
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
// updateAge: 24 * 60 * 60, // 24 hours
},
// JSON Web tokens are only used for sessions if the `jwt: true` session
// option is set - or by default if no database is specified.
// https://next-auth.js.org/configuration/options#jwt
jwt: {
// A secret to use for key generation (you should set this explicitly)
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// Set to true to use encryption (default: false)
// encryption: true,
// 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 }) {},
},
// You can define custom pages to override the built-in pages.
// The routes shown here are the default URLs that will be used when a custom
// pages is not specified for that route.
// https://next-auth.js.org/configuration/pages
pages: {
// signIn: '/api/auth/signin', // Displays signin buttons
// signOut: '/api/auth/signout', // Displays form with sign out button
// error: '/api/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/api/auth/verify-request', // Used for check email page
// newUser: null // If set, new users will be directed here on first sign in
},
// Callbacks are asynchronous functions you can use to control what happens
// when an action is performed.
// https://next-auth.js.org/configuration/callbacks
callbacks: {
// async signIn(user, account, profile) { return Promise.resolve(true) },
// async redirect(url, baseUrl) { return Promise.resolve(baseUrl) },
// async session(session, user) { return Promise.resolve(session) },
// async jwt(token, user, account, profile, isNewUser) { return Promise.resolve(token) }
},
// Events are useful for logging
// https://next-auth.js.org/configuration/events
events: { },
// Enable debug messages in the console if you are having problems
debug: false
}
export default (req, res) => NextAuth(req, res, options)

View File

@@ -1,3 +0,0 @@
export default (req, res) => {
res.send(JSON.stringify(process.env, null, 2))
}

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