Compare commits

...

79 Commits

Author SHA1 Message Date
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
197 changed files with 54548 additions and 23909 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

@@ -2,33 +2,42 @@
name: Bug report
about: Report a defect with NextAuth.js
labels: bug
assignees: ''
assignees: ""
---
**Describe the bug**
A clear and concise description of the bug in NextAuth.js.
## Description 🐜
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.
Please provide a clear and concise description of the bug in NextAuth.js.
**Steps to reproduce**
Steps to reproduce the behavior.
🚧 _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
Include a link to public repository which can be used to reproduce the behaviour.
## How to reproduce ☕️
**Expected behavior**
A clear and concise description of what you expected to happen.
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
**Screenshots or error logs**
If applicable add screenshots or error logs to help explain the problem.
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
**Additional context**
Add any other context about the problem here.
🚧 _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
**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).*
## Screenshots / Logs 📽
* [ ] 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
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
## Environment 🖥
Please run this command:
```
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
```
and paste the output here.
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -2,25 +2,38 @@
name: Feature request
about: Suggest an idea for NextAuth.js
labels: enhancement
assignees: ''
assignees: ""
---
**Summary of proposed feature**
A clear and concise description of the feature being proposed.
## Summary 💭
**Purpose of proposed feature**
A clear and concise description of why this feature is necessary and what problems it solves.
A clear and concise summary of the feature being proposed.
**Detail about proposed feature**
A detailed description of how the proposal might work (if you have one).
## Description 📓
**Potential problems**
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
Please provide a more in-depth description of the feature proposed.
**Describe any alternatives you've considered**
A clear and concise description of any alternative options you've considered.
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
**Additional context**
Any other context, screenshots, etc.
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
*Please indicate if you are willing and able to help implement the proposed feature.*
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
- **purpose of the feature**
- **potential problems**
- **potential alternatives**
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

View File

@@ -2,24 +2,31 @@
name: Question
about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ''
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. -->
## Question 💬
**What are you trying to do**
<!-- A description of what you are trying to do, for context. -->
Please provide an in-depth description of the question you have.
**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. -->
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
**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).*
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
* [ ] 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
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

36
.github/ISSUE_TEMPLATE/typescript.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: TypeScript
about: Ask a question about NextAuth.js TypeScript integration
labels:
- question
- TypeScript
assignees:
- lluia
- balazsorban44
---
## Question 💬
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js
function myAwesomeNextAuthFeature() {
return 💚
}
```
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
## How to reproduce ☕️
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
## Contributing 🙌🏽
It takes a lot of work 🏋🏻‍♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.

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: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run lint
- run: npm run build

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: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
# Run tests (build library, build + start test app in Docker, run tests)
- run: npm test
# TODO Tests should exit out if env vars not set (currently hangs)
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}

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
name: Release Flow
on:
push:
branches:
@@ -7,20 +8,35 @@ on:
- "next"
- "3.x"
pull_request:
jobs:
release:
name: "Release"
test:
name: Tests
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
- name: Dependencies
uses: bahmutov/npm-install@v1
- run: npx semantic-release@17
- name: Run tests
run: npm test
- name: Build
run: npm run build
release:
name: Release
needs: test
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Release
run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}

View File

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

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

@@ -32,7 +32,7 @@ cd next-auth
2. Install packages:
```sh
npm i && npm dev:setup
npm i && npm run dev:setup
```
3. Populate `.env.local`:

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"/>

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=

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,15 +0,0 @@
{
"presets": [
["@babel/preset-env", { "targets": { "esmodules": true } }]
],
"plugins": [
"@babel/plugin-proposal-class-properties"
],
"comments": false,
"overrides": [
{
"test": ["../src/server/pages/**"],
"presets": ["preact"]
}
]
}

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

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

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

@@ -0,0 +1,8 @@
module.exports = {
transform: {
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
},
roots: ["../src"],
setupFilesAfterEnv: ["./jest-setup.js"],
testMatch: ["**/*.test.js"],
}

41111
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,29 +30,17 @@
},
"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": "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"
},
@@ -74,7 +62,9 @@
],
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
@@ -83,13 +73,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",
@@ -101,17 +89,22 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@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",
"@babel/preset-react": "^7.13.13",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.9",
"@types/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",
@@ -121,25 +114,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",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^10.0.5",
"pg": "^8.2.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",
"pretty-quick": "^3.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"prettier": {
"semi": false
@@ -166,7 +156,23 @@
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"overrides": [
{
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"release": {
"branches": [

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,87 @@
import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
user: {
image: null,
name: "John",
email: "john@email.com",
},
expires: 123213139,
}
export const mockProviders = {
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 = {
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,97 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getBroadcastEvents } from "./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.restoreAllMocks()
})
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 "./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.resetAllMocks()
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 "./mocks"
import { signOut } from ".."
import { rest } from "msw"
import { getBroadcastEvents } from "./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.resetAllMocks()
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 setSignOutRes() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
return (
<>
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={() => setSignOutRes()}>Sign out</button>
</>
)
}

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

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

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

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

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

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

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,24 @@
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")
}
/**
@@ -26,7 +26,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 +39,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 +55,48 @@ 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 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 +105,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 +132,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 +140,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 +162,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 +238,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 +259,33 @@ 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: HTTP ${req.method} is not supported for ${req.url}`)
})
}
/** 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

@@ -215,6 +215,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
*/
async function getOAuth2 (provider, accessToken, results) {
let url = provider.profileUrl
let httpMethod = 'GET'
const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) {
@@ -238,8 +239,15 @@ async function getOAuth2 (provider, accessToken, results) {
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) => {
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
if (error) {
return reject(error)
}

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,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,35 +1,43 @@
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) {
/**
* 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
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'
@@ -39,36 +47,43 @@ export default async function signin (req, res) {
const email = req.body.email?.toLowerCase() ?? null
// If is an existing user return a user object (otherwise use placeholder)
const profile = await getUserByEmail(email) || { email }
const 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,16 +0,0 @@
# To be able to run tests:
# 1. copy to the root folder and rename to .env
# 2. Populate with values
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=

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

View File

@@ -1,52 +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: ./test/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))
}

View File

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

View File

@@ -1,11 +0,0 @@
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'Protected content.' })
} else {
res.send({ content: 'Unprotected content.' })
}
}

View File

@@ -1,6 +0,0 @@
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
res.send(JSON.stringify(session, null, 2))
}

View File

@@ -1,5 +0,0 @@
import Package from 'next-auth/package.json'
export default (req, res) => {
res.send(Package.version)
}

View File

@@ -1,7 +0,0 @@
export default function IndexPage () {
return (
<div id='nextauth-test-app'>
<h1>NextAuth.js Test App</h1>
</div>
)
}

View File

@@ -1,13 +0,0 @@
import { useSession } from 'next-auth/client'
export default function TestPage () {
const [ session, loading ] = useSession()
return (
<div id='nextauth-test-page'>
<h1>NextAuth.js Test Page</h1>
{session && <p id="nextauth-signed-in">Signed in</p>}
{!session && !loading && <p id="nextauth-signed-out">Signed out</p>}
</div>
)
}

View File

@@ -1,36 +0,0 @@
# Start Mongo, MSSQL, MySQL and Postgres databases on the current host running
# on their respective default ports. This is intended for developer convenience
# to make it easier to develop and test features manually.
#
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
version: '2'
services:
mongo:
extends:
file: databases/mongo.yml
service: mongo
ports:
- "27017:27017"
mssql:
extends:
file: databases/mssql.yml
service: mssql
ports:
- "1433:1433"
mysql:
extends:
file: databases/mysql.yml
service: mysql
ports:
- "3306:3306"
postgres:
extends:
file: databases/postgres.yml
service: postgres
ports:
- "5432:5432"

View File

@@ -1,11 +0,0 @@
version: '2'
services:
mongo:
image: bitnami/mongodb
restart: always
environment:
MONGODB_USERNAME: nextauth
MONGODB_PASSWORD: password
MONGODB_DATABASE: nextauth

View File

@@ -1,13 +0,0 @@
version: "2"
services:
mssql:
image: mcr.microsoft.com/mssql/server:2017-latest
restart: always
environment:
SA_PASSWORD: Pa55w0rd # minimum password complexity
ACCEPT_EULA: Y
# WARN: command overrides, default image start sequence, start.sh starts 'sql-server'
command: '/var/setup/start.sh'
volumes:
- ./mssql:/var/setup # mount setup files

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env sh
# see https://github.com/Microsoft/mssql-docker
# no way to know when sql server is ready
until /opt/mssql-tools/bin/sqlcmd -S 127.0.01 -U sa -P Pa55w0rd -d master -i /var/setup/setup.sql
do sleep 1;
done
echo "NEXT_AUTH: setup completed"

View File

@@ -1,29 +0,0 @@
USE master;
/* did you tear down the container ? */
if not exists (select name
from sys.syslogins
where name = 'nextauth')
CREATE LOGIN nextauth
WITH PASSWORD = 'password',
CHECK_POLICY = OFF;
GO
/* did you tear down the container ? */
if not exists (select name
from sys.databases
where name = 'nextauth' )
CREATE database nextauth
GO
/* did you tear down the container ? */
if not exists(select [name]
from sys.sysusers
where name= 'nextauth')
CREATE USER nextauth
WITH DEFAULT_SCHEMA =[dbo];
GO
/*
* Adding user as sysadmin,
* So you can easily drop/create/re-create/alter the database
* You will need to login to 'master' to do that
*/
exec sp_addsrvrolemember @loginame = N'nextauth', @rolename = N'sysadmin'

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
# launch setup on the background & start server
# otherise sqlservr won't start
/var/setup/setup.sh & /opt/mssql/bin/sqlservr

View File

@@ -1,13 +0,0 @@
version: '2'
services:
mysql:
image: mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
MYSQL_USER: nextauth
MYSQL_PASSWORD: password
MYSQL_DATABASE: nextauth
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'

View File

@@ -1,11 +0,0 @@
version: '2'
services:
postgres:
image: postgres
restart: always
environment:
POSTGRES_USER: nextauth
POSTGRES_PASSWORD: password
POSTGRES_DB: nextauth

View File

@@ -1,141 +0,0 @@
{
"users": {
"id": {
"type": "int",
"nullable": false
},
"name": {
"type": "varchar",
"nullable": true,
"default": null
},
"email": {
"type": "varchar",
"nullable": true,
"default": null
},
"email_verified": {
"type": "datetime",
"nullable": true,
"default": null
},
"image": {
"type": "varchar",
"nullable": true,
"default": null
},
"created_at": {
"type": "datetime",
"nullable": false
},
"updated_at": {
"type": "datetime",
"nullable": false
}
},
"accounts": {
"id": {
"type": "int",
"nullable": false
},
"compound_id": {
"type": "varchar",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"provider_type": {
"type": "varchar",
"nullable": false
},
"provider_id": {
"type": "varchar",
"nullable": false
},
"provider_account_id": {
"type": "varchar",
"nullable": false
},
"refresh_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token_expires": {
"type": "datetime",
"nullable": true,
"default": null
},
"created_at": {
"type": "datetime",
"nullable": false
},
"updated_at": {
"type": "datetime",
"nullable": false
}
},
"sessions": {
"id": {
"type": "int",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"expires": {
"type": "datetime",
"nullable": false
},
"session_token": {
"type": "varchar",
"nullable": false
},
"access_token": {
"type": "varchar",
"nullable": false
},
"created_at": {
"type": "datetime",
"nullable": false
},
"updated_at": {
"type": "datetime",
"nullable": false
}
},
"verification_requests": {
"id": {
"type": "int",
"nullable": false
},
"identifier": {
"type": "varchar",
"nullable": false
},
"token": {
"type": "varchar",
"nullable": false
},
"expires": {
"type": "datetime",
"nullable": false
},
"created_at": {
"type": "datetime",
"nullable": false
},
"updated_at": {
"type": "datetime",
"nullable": false
}
}
}

View File

@@ -1,141 +0,0 @@
{
"users": {
"id": {
"type": "int",
"nullable": false
},
"name": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"email": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"email_verified": {
"type": "timestamp(6)",
"nullable": true,
"default": null
},
"image": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
},
"accounts": {
"id": {
"type": "int",
"nullable": false
},
"compound_id": {
"type": "varchar(255)",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"provider_type": {
"type": "varchar(255)",
"nullable": false
},
"provider_id": {
"type": "varchar(255)",
"nullable": false
},
"provider_account_id": {
"type": "varchar(255)",
"nullable": false
},
"refresh_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token_expires": {
"type": "timestamp(6)",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
},
"sessions": {
"id": {
"type": "int",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"expires": {
"type": "timestamp(6)",
"nullable": false
},
"session_token": {
"type": "varchar(255)",
"nullable": false
},
"access_token": {
"type": "varchar(255)",
"nullable": false
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
},
"verification_requests": {
"id": {
"type": "int",
"nullable": false
},
"identifier": {
"type": "varchar(255)",
"nullable": false
},
"token": {
"type": "varchar(255)",
"nullable": false
},
"expires": {
"type": "timestamp(6)",
"nullable": false
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
}
}

View File

@@ -1,141 +0,0 @@
{
"users": {
"id": {
"type": "integer",
"nullable": false
},
"name": {
"type": "character varying",
"nullable": true,
"default": null
},
"email": {
"type": "character varying",
"nullable": true,
"default": null
},
"email_verified": {
"type": "timestamp with time zone",
"nullable": true,
"default": null
},
"image": {
"type": "character varying",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
},
"accounts": {
"id": {
"type": "integer",
"nullable": false
},
"compound_id": {
"type": "character varying",
"nullable": false
},
"user_id": {
"type": "integer",
"nullable": false
},
"provider_type": {
"type": "character varying",
"nullable": false
},
"provider_id": {
"type": "character varying",
"nullable": false
},
"provider_account_id": {
"type": "character varying",
"nullable": false
},
"refresh_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token_expires": {
"type": "timestamp with time zone",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
},
"sessions": {
"id": {
"type": "integer",
"nullable": false
},
"user_id": {
"type": "integer",
"nullable": false
},
"expires": {
"type": "timestamp with time zone",
"nullable": false
},
"session_token": {
"type": "character varying",
"nullable": false
},
"access_token": {
"type": "character varying",
"nullable": false
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
},
"verification_requests": {
"id": {
"type": "integer",
"nullable": false
},
"identifier": {
"type": "character varying",
"nullable": false
},
"token": {
"type": "character varying",
"nullable": false
},
"expires": {
"type": "timestamp with time zone",
"nullable": false
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
}
}

View File

@@ -1,47 +0,0 @@
-- FIXME Missing indexes!
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);

View File

@@ -1,74 +0,0 @@
CREATE TABLE accounts
(
id INT NOT NULL AUTO_INCREMENT,
compound_id VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
provider_type VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT,
access_token_expires TIMESTAMP(6),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
CREATE TABLE sessions
(
id INT NOT NULL AUTO_INCREMENT,
user_id INTEGER NOT NULL,
expires TIMESTAMP(6) NOT NULL,
session_token VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
CREATE TABLE users
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
email_verified TIMESTAMP(6),
image VARCHAR(255),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
CREATE TABLE verification_requests
(
id INT NOT NULL AUTO_INCREMENT,
identifier VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
expires TIMESTAMP(6) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);

View File

@@ -1,74 +0,0 @@
CREATE TABLE accounts
(
id SERIAL,
compound_id VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
provider_type VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT,
access_token_expires TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TABLE sessions
(
id SERIAL,
user_id INTEGER NOT NULL,
expires TIMESTAMPTZ NOT NULL,
session_token VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TABLE users
(
id SERIAL,
name VARCHAR(255),
email VARCHAR(255),
email_verified TIMESTAMPTZ,
image VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE TABLE verification_requests
(
id SERIAL,
identifier VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
expires TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);

View File

@@ -1,71 +0,0 @@
require('dotenv').config()
const assert = require('assert')
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
const BASE_URL = 'http://localhost:3000'
const CALLBACK_URL = `${BASE_URL}/test`
const {
NEXTAUTH_GITHUB_USERNAME: USERNAME,
NEXTAUTH_GITHUB_PASSWORD: PASSWORD
} = process.env
describe('GitHub (OAuth 2.0 flow)', function () {
this.slow(5000)
this.timeout(1000 * 60)
let browser,page
before(async () => {
browser = await puppeteer.launch(puppeteerOptions)
page = await browser.newPage()
return Promise.resolve()
})
it('should show button on sign in page', async function () {
page.setDefaultTimeout(1000 * 60)
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
await page.click(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
return Promise.resolve()
})
it('should be redirected to provider', async function () {
// Enter username
await page.waitForSelector('input[name="login"]')
await page.click('input[name="login"]')
await page.keyboard.type(USERNAME)
// Enter password
await page.waitForSelector('input[name="password"]')
await page.click('input[name="password"]')
await page.keyboard.type(PASSWORD)
return Promise.resolve()
})
it('should be able to sign in with provider', async function () {
// Click submit
await page.click('form[action="/session"] [type="submit"]')
return Promise.resolve()
})
it('should be returned to callback URL', async function () {
// Wait for page to return to callback URL
await page.waitForSelector('#nextauth-test-page')
// Check we are at the correct callback URL
assert.equal(page.url(), CALLBACK_URL)
return Promise.resolve()
})
it('should be signed in', async function () {
// Check we are signed in
await page.waitForSelector('#nextauth-signed-in')
return Promise.resolve()
})
after(async () => {
await browser.close()
return Promise.resolve()
})
})

View File

@@ -1,81 +0,0 @@
require('dotenv').config()
const assert = require('assert')
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
const BASE_URL = 'http://localhost:3000'
const CALLBACK_URL = `${BASE_URL}/test`
const {
NEXTAUTH_GOOGLE_USERNAME: USERNAME,
NEXTAUTH_GOOGLE_PASSWORD: PASSWORD
} = process.env
// This seems to stall because of a popup that is displayed only when using
// puppeteer. See FIXME below. Would appreciate any help resolving it.
describe.skip('Google (OAuth 2.0 flow)', function () {
this.slow(5000)
this.timeout(1000 * 60)
let browser,page
before(async () => {
browser = await puppeteer.launch(puppeteerOptions)
page = await browser.newPage()
return Promise.resolve()
})
it('should show button on sign in page', async function () {
page.setDefaultTimeout(1000 * 60)
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
await page.click(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
return Promise.resolve()
})
it('should be redirected to provider', async function () {
// Enter username
await page.waitForSelector('input[type="email"]')
await page.click('input[type="email"]')
await page.keyboard.type(USERNAME)
// FIXME Work out how to dismiss popup
// A popup *only* appears when using puppeteer (not manually) but I can't
// get the xPath selectors to work to dismiss it. This is as close as I got.
// await page.waitForXPath("(//span[contains(text(), 'Got it')])[2]")
// const element = await page.$x("(//span[contains(text(), 'Got it')])[2]")
// await element.click()
// Enter password
await page.waitForSelector('input[type="password"]')
await page.click('input[type="password"]')
await page.keyboard.type(PASSWORD)
return Promise.resolve()
})
it('should be able to sign in with provider', async function () {
// Click submit
await page.click('button[type="button"]')
return Promise.resolve()
})
it('should be returned to callback URL', async function () {
// Wait for page to return to callback URL
await page.waitForSelector('#nextauth-test-page')
// Check we are at the correct callback URL
// Note: Google OAuth appends a # to the end of the URL in Chrome
assert.equal(page.url(), `${CALLBACK_URL}#`)
return Promise.resolve()
})
it('should be signed in', async function () {
// Check we are signed in
await page.waitForSelector('#nextauth-signed-in')
return Promise.resolve()
})
after(async () => {
await browser.close()
return Promise.resolve()
})
})

View File

@@ -1,71 +0,0 @@
require('dotenv').config()
const assert = require('assert')
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
const BASE_URL = 'http://localhost:3000'
const CALLBACK_URL = `${BASE_URL}/test`
const {
NEXTAUTH_TWITTER_USERNAME: USERNAME,
NEXTAUTH_TWITTER_PASSWORD: PASSWORD,
} = process.env
describe('Twitter (OAuth 1.1 flow)', async function () {
this.slow(5000)
this.timeout(1000 * 60)
let browser,page
before(async () => {
browser = await puppeteer.launch(puppeteerOptions)
page = await browser.newPage()
return Promise.resolve()
})
it('should show button on sign in page', async function () {
page.setDefaultTimeout(1000 * 60)
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
await page.click(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
return Promise.resolve()
})
it('should be redirected to provider', async function () {
// Enter username
await page.waitForSelector('input[name="session[username_or_email]"]')
await page.click('input[name="session[username_or_email]"]')
await page.keyboard.type(USERNAME)
// Enter password
await page.waitForSelector('input[name="session[password]"]')
await page.click('input[name="session[password]"]')
await page.keyboard.type(PASSWORD)
return Promise.resolve()
})
it('should be able to sign in with provider', async function () {
// Click submit
await page.click('form[action="https://api.twitter.com/oauth/authenticate"] [type="submit"]')
return Promise.resolve()
})
it('should be returned to callback URL', async function () {
// Wait for page to return to callback URL
await page.waitForSelector('#nextauth-test-page')
// Check we are at the correct callback URL
assert.equal(page.url(), CALLBACK_URL)
return Promise.resolve()
})
it('should be signed in', async function () {
// Check we are signed in
await page.waitForSelector('#nextauth-signed-in')
return Promise.resolve()
})
after(async () => {
await browser.close()
return Promise.resolve()
})
})

View File

@@ -1,36 +0,0 @@
exports.compareSchemas = (expected, actual) => {
const errors = []
// Check all models and properties that are expected exist
for (const objectName in expected) {
if (actual[objectName]) {
for (const propertyName in expected[objectName]) {
if (actual[objectName][propertyName]) {
if (JSON.stringify(expected[objectName][propertyName]) !== JSON.stringify(actual[objectName][propertyName])) {
errors.push(`${objectName}.${propertyName} does not match expected result`)
}
} else {
errors.push(`${objectName}.${propertyName} not found (should exist)`)
}
}
} else {
errors.push(`${objectName} not found (should exist)`)
}
}
// Check for models and properties that exist but are not expected
for (const objectName in actual) {
if (expected[objectName]) {
for (const propertyName in actual[objectName]) {
if (!expected[objectName][propertyName]) {
errors.push(`${objectName}.${propertyName} found (not expected)`)
}
}
} else {
errors.push(`${objectName} found (not expected)`)
}
}
// Return true if no errors, else return array of errors
return errors.length > 0 ? errors : true
}

View File

@@ -1,35 +0,0 @@
const puppeteerExtra = require('puppeteer-extra')
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
const stealthPlugin = StealthPlugin()
// Override Puppeteer user agent to set 'navigator.platform' explicitly to
// prevent detection on some providers (e.g. GitHub OAuth) as they force 2FA
// on sign in if they detect sign in from a platform they haven't seen before.
const puppeteerExtraPluginUserAgentOverride = require("puppeteer-extra-plugin-stealth/evasions/user-agent-override")
stealthPlugin.enabledEvasions.delete("user-agent-override")
puppeteerExtra.use(stealthPlugin)
const pluginUserAgentOverride = puppeteerExtraPluginUserAgentOverride({
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36",
platform: "MacIntel"
})
puppeteerExtra.use(pluginUserAgentOverride)
// CI is set to true by GitHub Actions to indicate is running in CD/CI
const { CI } = process.env
const puppeteerOptions = {
headless: true // Set to 'false' to debug more easily
}
// When running on remote test runner (which is ARM) the executable path
// needs to be set to 'chromium-browser' so it uses the ARM build of Chromium
// not the x86 build that Puppeteer uses by default. Supporting this allows us
// to test easily from remote locations that are outside cloud networks like
// AWS, GPC, Azure, etc. and avoids tests being thwarted by IP blocklists.
if (CI)
puppeteerOptions.executablePath = 'chromium-browser'
module.exports = {
puppeteer: puppeteerExtra,
puppeteerOptions
}

View File

@@ -1,25 +0,0 @@
/* eslint-disable */
// Placeholder for schema test (will use test framework, this is temporary)
const Adapters = require('../adapters')
;(async () => {
try {
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
// Connect to local MongoDB instance
// Note: MongoDB doesn't thrown a connection error right away if is a
// problem with the credentials or host configuration, but after a few
// seconds it throws a Timeout error (which is caught by the adapter).
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
await adapter.getAdapter()
// @TODO create objects in database, check format of objects returned
console.log('MongoDB loaded ok')
process.exit()
} catch (error) {
console.error('MongoDB error', error)
process.exit(1)
}
})()

View File

@@ -1,106 +0,0 @@
/* eslint-disable */
// Placeholder for schema test (will use test framework, this is temporary)
const fs = require('fs');
const path = require('path');
const mssql = require('mssql');
const Adapters = require('../adapters');
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mssql.json');
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE));
const TABLES = Object.keys(expectedSchema);
const databaseUrl = `mssql://nextauth:password@127.0.0.1:1433/nextauth?synchronize=true`;
function printSchema() {
return new Promise(async (resolve) => {
/**
* @type {import('mssql').ConnectionPool}
*/
let connection;
try {
connection = await mssql.connect(databaseUrl);
// Invoke adapter to sync schema
await (Adapters.Default(databaseUrl)).getAdapter();
// query schema
const { recordset } = await connection.query(
`use [nextauth]; ` +
TABLES.map(
(table) =>
`select * from INFORMATION_SCHEMA.COLUMNS` +
` where TABLE_NAME = '${table}'`
).join(' UNION ALL ')
);
// build result
return resolve(
TABLES.reduce(
(out, next) => ({
...out,
[next]: collect(recordset, next),
}),
{}
)
);
} catch (error) {
return Promise.reject(error);
} finally {
if (connection) {
connection.close();
}
}
});
}
const assert = require('assert');
/** RUN */
(async () => {
try {
const testResultSchema = await printSchema();
const actualTables = Object.keys(testResultSchema);
assert.equal(
TABLES,
actualTables.join(),
`MSSQL Schema: Expected tables [${TABLES.join()}]\n to be [${actualTables.join()}]`
);
//cheap deepEquals, with hints
for (const tableName of TABLES) {
const newLocal = expectedSchema[tableName];
for (const columnName of Object.keys(newLocal)) {
const expected = expectedSchema[tableName][columnName];
const actual = testResultSchema[tableName][columnName];
for (const propKey of Object.keys(expected)) {
assert.equal(
expected[propKey],
actual[propKey],
`Expected ${tableName}.${columnName}.${propKey}=${actual[propKey]}` +
` to be ${expected[propKey]}`
);
}
}
}
console.log('mssql: schema ok');
} catch (error) {
return Promise.reject(error);
}
})()
.then(() => process.exit())
.catch((error) => {
console.error(error);
process.exit(-1);
});
/** collect results */
const collect = (records, tableName) => {
const keys = Object.keys(expectedSchema[tableName]);
const ret = records
.filter((x) => x.TABLE_NAME === tableName)
.reduce((out, x) => {
if (keys.indexOf(x.COLUMN_NAME) === -1) return out; //map only required columns
const nullable = x.IS_NULLABLE === 'YES';
return {
...out,
[x.COLUMN_NAME]: {
nullable,
type: x.DATA_TYPE,
default: (nullable && x.COLUMN_DEFAULT) || undefined,
},
};
}, {});
return ret;
};

View File

@@ -1,74 +0,0 @@
/* eslint-disable */
// Placeholder for schema test (will use test framework, this is temporary)
const fs = require('fs')
const path = require('path')
const mysql = require('mysql')
const { compareSchemas } = require('./lib/db')
const Adapters = require('../adapters')
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mysql.json')
function printSchema () {
return new Promise(async (resolve) => {
// Invoke adapter to sync schema
const adapter = Adapters.Default('mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true')
await adapter.getAdapter()
const connection = mysql.createConnection({
host: '127.0.0.1',
user: 'nextauth',
password: 'password',
database: 'nextauth',
port: 3306,
multipleStatements: true
})
connection.connect()
connection.query(
TABLES.map(table => `DESCRIBE ${table}`).join(';'),
(error, result) => {
if (error) { throw error }
const getColumnSchema = (column) => {
const nullable = column.Null === 'YES' ? true : false
return {
type: column.Type,
nullable,
default: nullable ? column.Default : undefined
}
}
const users = {}
const accounts = {}
const sessions = {}
const verification_requests = {}
result[0].forEach(column => { users[column.Field] = getColumnSchema(column) })
result[1].forEach(column => { accounts[column.Field] = getColumnSchema(column) })
result[2].forEach(column => { sessions[column.Field] = getColumnSchema(column) })
result[3].forEach(column => { verification_requests[column.Field] = getColumnSchema(column) })
connection.end()
resolve({ users, accounts, sessions, verification_requests })
}
)
})
}
(async () => {
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
const testResultSchema = await printSchema()
const compareResult = compareSchemas(expectedSchema, testResultSchema)
if (compareResult === true) {
console.log('MySQL schema ok')
process.exit()
} else {
console.error('MySQL schema errors')
compareResult.forEach(error => console.log(` * ${error}`))
console.log('MySQL schema found:', JSON.stringify(testResultSchema, null, 2))
process.exit(1)
}
})()

View File

@@ -1,73 +0,0 @@
/* eslint-disable */
// Placeholder for schema test (will use test framework, this is temporary)
const fs = require('fs')
const path = require('path')
const { Client } = require('pg')
const { compareSchemas } = require('./lib/db')
const Adapters = require('../adapters')
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/postgres.json')
function printSchema () {
return new Promise(async (resolve) => {
// Invoke adapter to sync schema
const adapter = Adapters.Default('postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true')
await adapter.getAdapter()
const connection = new Client({
host: '127.0.0.1',
user: 'nextauth',
password: 'password',
database: 'nextauth',
port: 5432
})
connection.connect()
connection.query(
TABLES.map(table => `SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '${table}' ORDER BY ordinal_position`).join(';'),
(error, result) => {
if (error) { throw error }
const getColumnSchema = (column) => {
const nullable = column.is_nullable === 'YES' ? true : false
return {
type: column.data_type,
nullable,
default: nullable ? column.column_default : undefined
}
}
const users = {}
const accounts = {}
const sessions = {}
const verification_requests = {}
result[0].rows.forEach(column => { users[column.column_name] = getColumnSchema(column) })
result[1].rows.forEach(column => { accounts[column.column_name] = getColumnSchema(column) })
result[2].rows.forEach(column => { sessions[column.column_name] = getColumnSchema(column) })
result[3].rows.forEach(column => { verification_requests[column.column_name] = getColumnSchema(column) })
connection.end()
resolve({ users, accounts, sessions, verification_requests })
}
)
})
}
(async () => {
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
const testResultSchema = await printSchema()
const compareResult = compareSchemas(expectedSchema, testResultSchema)
if (compareResult === true) {
console.log('Postgres schema ok')
process.exit()
} else {
console.error('Postgres schema errors')
compareResult.forEach(error => console.log(` * ${error}`))
console.log('Postgres schema found:', JSON.stringify(testResultSchema, null, 2))
process.exit(1)
}
})()

View File

@@ -3,31 +3,15 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"types": [
"./types"
],
"next-auth": [
"./src/server"
],
"next-auth/adapters": [
"./src/adapters"
],
"next-auth/client": [
"./src/client"
],
"next-auth/jwt": [
"./src/lib/jwt"
],
"next-auth/providers": [
"./src/providers"
]
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/client": ["./src/client"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"]
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@@ -44,9 +28,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.js"
"**/*.js",
".eslintrc.js"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

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