Compare commits

...

144 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
Ashutosh Kumar
fb04ab4e76 fix(ts): make GetSessionOptions optional (#1851) 2021-04-25 15:39:19 +02:00
Ashutosh Kumar
07e2a83ccb fix(ts): make ctxOrReq optional in getCsrfToken() (#1850) 2021-04-25 14:10:21 +02:00
Balázs Orbán
065d9eb310 chore(release): do not mark released PRs/issues (#1845) 2021-04-24 23:17:24 +02:00
Thanayut T
5da19f3c9a feat(provider): add WordPress.com provider (#1837) 2021-04-24 10:48:44 +02:00
Balázs Orbán
88ec3bad71 chore: move files from root 2021-04-24 00:44:08 +02:00
Lluis Agusti
5ab7868533 chore(ci): remove Node 10 and add Node 16 (#1830)
* chore(github): add CODEOWNERS

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

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

* build(errors): expose errors as a submodule
2021-04-22 23:28:38 +02:00
Balázs Orbán
e26c5fc905 fix(ts): adjust AppOptions (#1815) 2021-04-22 23:04:27 +02:00
Balázs Orbán
543f812eb3 fix(build): export functions in jwt (#1814) 2021-04-22 19:28:17 +02:00
Joël Galeran
0c9f9777c5 docs(adapter): Remove --preview-feature flag (#1807)
* Remove --preview-feature flag

* Update [...nextauth].js
2021-04-22 18:11:30 +02:00
Balázs Orbán
34f334a71d fix(ts): make Profile/User interfaces overridable (#1801)
* fix(ts): create DefaultUser interface

* fix(ts): fix TypeORMUserModel

* fix(ts): create DefaultProfile
2021-04-22 01:04:23 +02:00
Balázs Orbán
172ad02f8c fix(ts): move AppProvider out of internals (#1800)
* fix(ts): move AppProvider out of internals

* fix(ts): fix import paths
2021-04-21 23:09:42 +02:00
Balázs Orbán
eed0001524 fix(ts): adjust properties on default interfaces (#1794)
* fix(ts): adjust properties on default interfaces

* fix(ts): make expires also optional

* fix(ts): don't require default session/jwt fields

* fix(ts): make all default fields optional
2021-04-21 17:17:38 +02:00
Gabrijel Gavranović
a2705fb5b9 fix(client): export getCsrfToken directly to support Webpack 5
Fixes `Attempted import error: 'getCsrfToken' is not exported from 'next-auth/client' (imported as 'getCsrfToken’).`-error.
2021-04-21 17:14:12 +02:00
Balázs Orbán
cb1e5a7174 docs(dev): add readme to dev app 2021-04-21 00:00:57 +02:00
Balázs Orbán
8cba5d06b5 build(provider): filter index.js to be more forgiving 2021-04-20 23:17:18 +02:00
Balázs Orbán
c52ce57296 fix: add skypack recommended fields (#1791) 2021-04-20 22:40:12 +02:00
Balázs Orbán
4dae822806 chore: move dev app into its own folder (#1753)
* chore: move dev app to its own folder

* docs: update CONTRIBUTING.md

* docs: fix typos in CONTRIBUTING

* chore: gitignore dev app lock files

* chore: move release config into package.json
2021-04-20 22:25:51 +02:00
Lluis Agusti
901f6fb189 docs: mention TS example repo on the website (#1786)
* docs(www): mention TS example repo

* Update www/docs/getting-started/typescript.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-20 21:40:06 +02:00
Balázs Orbán
bb2237d0f9 fix(build): remove unnecessary build before release 2021-04-20 21:35:10 +02:00
Balázs Orbán
fab7ce8f94 fix(build): trigger re-release 2021-04-20 21:33:01 +02:00
Balázs Orbán
2becdad990 fix(logger): attempt at fixing infinite loop (#1789) 2021-04-20 21:22:20 +02:00
Pop Stefan
e3c2c7756d docs: add Class components tutorial (#1784) 2021-04-20 17:34:05 +02:00
Balázs Orbán
718f2537cb build(provider): auto-generate Providers submodule (#1782) 2021-04-20 17:33:24 +02:00
dogomedia-github
ae26df091d fix(provider): add sub to defaultJwtPayload for credentials provider. (#1725)
Co-authored-by: Joseph Chen <jchen@dogomedia.com>
2021-04-20 12:59:48 +02:00
dependabot[bot]
1cbf73b2f6 chore(deps): bump jose from 1.27.2 to 1.28.1 (#1772)
Bumps [jose](https://github.com/panva/jose) from 1.27.2 to 1.28.1.
- [Release notes](https://github.com/panva/jose/releases)
- [Changelog](https://github.com/panva/jose/blob/v1.28.1/CHANGELOG.md)
- [Commits](https://github.com/panva/jose/compare/v1.27.2...v1.28.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 12:56:07 +02:00
Balázs Orbán
46b62d723c feat(ts): expose types from main package (#1773)
* chore: add beta to release flow/GH actions

* feat(ts): expose types from the package (#1665)

* chore(types): move existing types to the repo
* feat(ts): expose types from the main package
* chore(deps): bring back `react-dom` version range
* chore(ts): cleanup deps and comments
* chore(ci): run types tests on a separate workflow

* chore(ci): fix typo on types workflow

* fix(ts): correctly export sub-module types (#1677)

* chore(types): build types script

Adds a script that moves the declaration files we have in `./types` to `./dist` relative to the files they intend to type.

This is the first step, we still need to change what we declare in `package.json`, add the script to the CI pipeline if we're happy with it and figure out how to type `next-auth/jwt`.

* refactor(lint): fix build-types script

* fix(ts): add .d.ts sub-module files to package.json

#1677 seemed to miss this

* fix(built): typo in package.json

* fix(build): fix release

* feat(ts): support module augmentation (#1681)

* chore(ts): remove unused imports

* refactor(ts): clean up CallbackOptions

* docs(ts): explain Module Augmentation

* docs(ts): don't use @ in folder name "types"

* test(ts): make jwt params optional

* docs(ts): fix typo (TypeScript -> NextAuth.js)

* style: replace ts-standard with eslint/prettier (#1724)

* style: move from ts-standard to eslint/prettier

* fix: install remaining eslint-config-standard peer deps

* fix: add remaining missing dependencies/config

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

* docs(lint): update contributing.md (#1760)

Regarding ESLint / Prettier use and link to their VSCode extensions

* refactor(ts): de-duplicate types (#1690)

* refactor(ts): deduplicate internal types

* refactor(ts): ease up providers typings

* test(ts): fix failing TS tests

* test(ts): rename TS property to fix test

* docs(ts): mention TS docs in README.md

* feat(ts): move/update client types

* refactor(TS): rename some types

* test(ts): fix client tests

* docs(ts): move function descriptions to .d.ts

* chore: fix lint error

* refactor(ts): separate internal types

* chore: simplify build-types script

* chore: update type import paths in src

* chore(build): create root files at build

* chore: remove unnecessary .npmignore

* chore: run prettier on types

* fix(ts): clean up jwt types

* fix(ts): make getToken return type depend on raw param

* docs(page): explain page errors, add theming note

* docs(ts): add JSDoc to NextAuthOptions props

* chore(ts): remove unused import

* docs(ts): change JSDOC docs notation

* refactor(build): extract module entries into enum

* chore(ts): move ClientSafeProvider

* chore(ts): simplify GetTokenParams generic

* style(lint): fix linting errors

* chore: re-add generic extension to GetTokenParams

* fix(ts): extract EmailConfigServerOptions to interface

* fix(ts): use relative imports

* Merge branch 'main' into beta

* Merge main into beta

* fix(ts): fix typos, add more links to documentation

* test(ts): update JWT getToken test

* fix(build): fix tsconfig.json formatting

* test(ts): use absolute imports in test files

* fix(ts): add missing callbacks JSDoc

* docs: mention TS in FAQ, fix typos

* docs: fix some typos in the docs

Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-04-20 12:20:43 +02:00
Balázs Orbán
457952bb5a fix(jwt): make decode overrideable in getToken (#1751) 2021-04-17 12:40:50 +02:00
Balázs Orbán
17b789822d fix: make oauth_token_secret and oauth_token available (#1322)
* fix: add oauth_token_secret to requests

* chore: remove console.log

* refactor: follow casing from response
2021-04-14 21:26:15 +02:00
Ovidiu Dan
fd12194c0c docs(provider): Explain how to get access to LinkedIn authentication (#1706) 2021-04-12 18:46:20 +02:00
Balázs Orbán
1c662e9ddc fix(page): fall back to default error page (#1700) 2021-04-12 03:56:47 +02:00
Balázs Orbán
968903d227 fix(oauth): support response_mode=form_post (#1669)
* chore: alias dev script to next

* feat(core): fallback to body when reading state

* refactor: set csrfToken on req.options implicitly

Ensures we do this similarly than
in other handlers like pkce, state, extendRes, callbackUrlHandler etc.

* chore: add code comment for debugging
2021-04-12 00:24:05 +02:00
Balázs Orbán
3dedf6c26c fix(provider): proper check of protection property (#1694)
* fix(provider): proper check of protection property

* chore: add comment
2021-04-12 00:15:29 +02:00
Amauri Dias
d1dbfe1023 fix: truly replace .flat() to support Node <11 again (#1691) 2021-04-11 23:20:37 +02:00
David Colón
63171a0271 fix: validate provider existence before looking for protection property (#1687)
* Fix validation of provider existence before looking for protection property

* Use optional chaining
2021-04-11 15:20:01 +02:00
Amauri Dias
872e180339 fix: replace .flat() to support Node <11 again (#1684) 2021-04-11 10:57:25 +02:00
ifly7charlie
a7709df796 docs: Document the additional parameters in JWT (#1550)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-08 14:00:19 +02:00
Balázs Orbán
dbe283f0fa refactor: rename extend-res to extend-req 2021-04-07 22:26:54 +02:00
Balázs Orbán
727426bbec chore(ts): auto-label TypeScript related changes 2021-04-07 20:16:10 +02:00
Vinicius CR
5a3ee47337 feat(provider): accept array for protection to support multiple mechanisms (#1565)
* fix: add protection both option

* feat: update docs with new protection value

* fix: lint files

* refactor: change protection from string to array

* chore: reverting unespected change

* chore: lint files

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-06 19:20:56 +02:00
bhaveshmishra-code
8dd8f7c48a docs: fix typo in callbacks.md (#1657)
Fixed the spelling mistake.

existance -> existence
2021-04-05 19:35:26 +02:00
Jaime Martínez Rincón
072c59d85a docs: fix typo primsa (#1652) 2021-04-05 00:25:46 +02:00
dependabot[bot]
d0e8147a48 chore(deps): bump y18n from 4.0.0 to 4.0.1 (#1631)
Bumps [y18n](https://github.com/yargs/y18n) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 00:24:39 +02:00
Jasper Moelker
5bc8f8b986 docs(page): correct getCsrfToken and input types (#1651)
This fixes the a mismatch between the import (`csrfToken`) and the method (`getCsrfToken`) used in `getInitialProps`/`getServerSideProps`.
In addition the form input fields now have their correct type: `email` for email input (for better autocomplete, virtual keyboard support and native validation) and `password` for the password input (to hide password while typing).
2021-04-04 22:01:53 +02:00
hoangbits
136361e1f4 docs: rename command to vercel cli, now cli is deprecated (#1647) 2021-04-04 11:02:38 +02:00
hoangbits
cc9869592c docs: fix typo in providers.md (#1641) 2021-04-02 17:18:40 +02:00
Jay Liew
073da60c3d docs: Update pages.md (#1592)
* Update pages.md

Updated Credentials Sign-In code example to indicate how to use `getServerSideProps` but still also showing the older `getInitialProps` example

* Update www/docs/configuration/pages.md

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

* update documentation to show example using getServerSideProps()

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Jay Liew <jay@haute.tech>
2021-03-31 00:31:31 +02:00
ifly7charlie
aacc34bbfd docs(error): Add missing error message and technique to resolve (#1549)
* Add missing error message and technique to resolve

* Update errors.md

Correct with correct error message and more complete suggestions on resolving it
2021-03-26 23:09:21 +01:00
jgollhardt
074688d10e docs(provider): fix wrong param name in sendVerificationRequest example (#1595) 2021-03-26 23:01:25 +01:00
Macarse, Christian Ryan R
b3ffe50c03 docs(provider): removed misleading provider signin link (#1588) 2021-03-25 22:30:46 +01:00
Shubham Shukla
e6d063825d fix(provider): added options in instagram provider (#1570) 2021-03-23 22:28:54 +01:00
Balázs Orbán
985f7b3431 fix(logger): properly end request every time (#1557)
* fix(logger): properly end request every time

* chore: fix linting
2021-03-20 10:08:12 +01:00
Max
237b016378 fix(provider): reject access token if slack login flow was canceled (#1544)
* fix: reject access token if slack login flow was canceled

* style: fix lint errors in oauth client
2021-03-18 14:59:24 +01:00
Joshua Williams
776b9480da feat(provider): add Zoho provider (#1516)
* feat(provider): add zoho

* fix: use LF instead of CRLF

* fix: crlf to lf line endings

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-03-16 19:27:11 +01:00
Honman Yau
07a3f76cb3 docs: fix typos in REST API guide (#1528) 2021-03-16 19:25:24 +01:00
tclaude94
3726d68c49 feat(provider): add FACEIT provider (#1469) 2021-03-16 00:00:35 +01:00
dependabot[bot]
e31db1726a chore(deps): bump xmldom from 0.3.0 to 0.5.0 (#1510)
Bumps [xmldom](https://github.com/xmldom/xmldom) from 0.3.0 to 0.5.0.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/0.3.0...0.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-13 14:19:22 +01:00
James Perkins
a241199c11 docs(tutorials): Adding two more tutorials to the list.
[skip release]
2021-03-13 03:42:00 +00:00
dependabot[bot]
5385ec20a9 chore(deps): bump elliptic from 6.5.3 to 6.5.4 in /www (#1493)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-10 22:59:10 +01:00
Balázs Orbán
810d02e671 fix(deps): upgrade to latest preact-render-to-string (#1475) 2021-03-08 10:39:13 +01:00
310 changed files with 60405 additions and 25398 deletions

View File

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

View File

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

1
.github/CODEOWNERS vendored Normal file
View File

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

View File

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

6
.github/labeler.yml vendored
View File

@@ -1,5 +1,6 @@
test:
- test/**/*
- types/tests/**/*
documentation:
- www/**/*
@@ -32,4 +33,7 @@ client:
pages:
- src/server/pages/**/*
- www/docs/configuration/pages.md
- www/docs/configuration/pages.md
TypeScript:
- types/**/*

View File

@@ -1,30 +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
- next
pull_request:
branches:
- main
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run lint
- run: npm run build

View File

@@ -1,67 +1,27 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: Code Analysis
on:
push:
branches: [ main, 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,56 +0,0 @@
name: Integration Test
on:
push:
branches:
- main
- next
pull_request:
jobs:
test:
# Only run tests integration against Pull Requests from branches in
# this repository. We do this as integration tests require access to
# secrets in GitHub and they are not exposed to tests run against
# forks (for security reasons), so integration test against
# Pull Requests from external repos just fail and generate noise.
if: github.event.pull_request.head.repo.full_name == github.repository
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
# fail due to IP Address checks done by providers, which enforce
# CAPTCHA checks on login request from cloud compute IP addresses to
# prevent abuse.
runs-on: self-hosted
# Target time is under 5 minutes to run all tests. If it takes longer than
# 10 minutes should look at running tests in parallel. No individual flow
# should take longer than 5 minutes to build and run.
timeout-minutes: 10
strategy:
matrix:
node-version: [10, 12, 14]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
# Run tests (build library, build + start test app in Docker, run tests)
- run: npm test
# TODO Tests should exit out if env vars not set (currently hangs)
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}

View File

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

View File

@@ -1,26 +1,42 @@
name: Release
name: Release Flow
on:
push:
branches:
- 'main'
- 'next'
- '3.x'
- "main"
- "beta"
- "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: npm run build
- 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}}

23
.gitignore vendored
View File

@@ -25,8 +25,27 @@ node_modules
# Generated files
.docusaurus
.cache-loader
.next
www/providers.json
src/providers/index.js
/internals
/adapters.d.ts
/adapters.js
/client.d.ts
/client.js
/index.d.ts
/index.js
/jwt.d.ts
/jwt.js
/providers.d.ts
/providers.js
/errors.js
/errors.d.ts
# Development app
app/next-auth
app/dist/css
app/package-lock.json
app/yarn.lock
# VS
/.vs/slnx.sqlite-journal
@@ -39,4 +58,4 @@ www/providers.json
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations

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

@@ -16,7 +16,7 @@ Anyone can be a contributor. Either you found a typo, or you have an awesome fea
* The latest changes are always in `main`, so please make your Pull Request against that branch.
* Pull Requests should be raised for any change
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
* Run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this extension](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) to fix lint issues in development)
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
@@ -32,17 +32,17 @@ cd next-auth
2. Install packages:
```sh
npm i
npm i && npm run dev:setup
```
3. Populate `.env.local`:
Copy `.env.local.example` to `.env.local`, and add your env variables for each provider you want to test.
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
1. Start the dev application/server and CSS watching:
1. Start the dev application/server:
```sh
npm run dev
```
@@ -57,11 +57,23 @@ If you need an example project to link to, you can use [next-auth-example](https
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
> NOTE: When working on CSS, you will have to manually refresh the page after changes. The reason for this is our pages using CSS are server-side rendered. (Improving this through a PR is very welcome!)
> NOTE: The setup is as follows: The development application lives inside the `app` folder, and whenever you make a change to the `src` folder in the root (where next-auth is), it gets copied into `app` every time (gitignored), so Next.js can pick them up and apply hot reloading. This is to avoid some annoying issues with how symlinks are working with different React builds, and also to provide a super-fast feedback loop while developing core features.
#### Providers
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
That's it! 🎉 Others will be able to discover this provider much more easily now!
You can look at the existing built-in providers for inspiration.
#### Databases
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.

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

@@ -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"/>
@@ -84,13 +81,9 @@ Advanced options allow you to define your own routines to handle controlling wha
### TypeScript
You can install the appropriate types via the following command:
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
```
npm install --save-dev @types/next-auth
```
As of now, TypeScript is a community effort. If you encounter any problems with the types package, please create an issue at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth). Alternatively, you can open a pull request directly with your fixes there. We welcome anyone to start a discussion on migrating this package to TypeScript, or how to improve the TypeScript experience in general.
The package at `@types/next-auth` is now deprecated.
## Example

View File

@@ -1 +0,0 @@
module.exports = require('./dist/adapters').default

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=

6
app/README.md Normal file
View File

@@ -0,0 +1,6 @@
# NextAuth.js Development App
This folder contains a Next.js app using NextAuth.js for local development. See the following section on how to start:
[Setting up local environment
](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md#setting-up-local-environment)

5
app/jsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"compilerOptions": {
"baseUrl": "."
}
}

View File

19
app/next.config.js Normal file
View File

@@ -0,0 +1,19 @@
const path = require("path")
module.exports = {
webpack(config) {
config.resolve = {
...config.resolve,
alias: {
...config.resolve.alias,
"next-auth$": path.join(process.cwd(), "next-auth/server"),
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
},
}
return config
},
}

25
app/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "next-auth-app",
"version": "1.0.0",
"description": "NextAuth.js Developer app",
"private": true,
"scripts": {
"dev": "npm-run-all --parallel copy:app dev:css dev:next",
"dev:next": "next dev",
"copy:app": "cpx \"../src/**/*\" next-auth --watch",
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
"watch:css": "cd .. && npm run watch:css",
"dev:css": "npm-run-all --parallel watch:css copy:css",
"start": "next start"
},
"license": "ISC",
"dependencies": {
"next": "^10.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"cpx": "^1.5.0",
"npm-run-all": "^4.1.5"
}
}

View File

@@ -1,9 +1,9 @@
import { Provider } from 'next-auth/client'
import './styles.css'
import { Provider } from "next-auth/client"
import "./styles.css"
// Use the <Provider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App ({ Component, pageProps }) {
export default function App({ Component, pageProps }) {
return (
<Provider
// Provider options are not required but can be useful in situations where
@@ -21,7 +21,7 @@ export default function App ({ Component, pageProps }) {
//
// 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
keepAlive: 0,
}}
session={pageProps.session}
>

View File

@@ -0,0 +1,91 @@
import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
// csrfToken: {
// name: 'next-auth.csrf-token',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// },
// pkceCodeVerifier: {
// name: 'next-auth.pkce.code_verifier',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// }
// },
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// protection: ["pkce", "state"],
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: "pkce",
}),
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
CredentialsProvider({
name: "Credentials",
credentials: {
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
if (credentials.password === "password") {
return {
id: 1,
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
}
}
return null
},
}),
],
jwt: {
encryption: true,
secret: process.env.SECRET,
},
debug: false,
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
})

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

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'

View File

@@ -1 +0,0 @@
module.exports = require('./dist/client').default

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

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

View File

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

91
config/build.js Normal file
View File

@@ -0,0 +1,91 @@
const fs = require("fs-extra")
const path = require("path")
const MODULE_ENTRIES = {
SERVER: "index",
CLIENT: "client",
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
ERRORS: "errors",
}
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
fs.writeFile(path.join(process.cwd(), target), content, (err) => {
if (err) throw err
console.log(`[build] created "${target}" in root folder`)
})
})
// Building types
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
`${MODULE_ENTRIES.ERRORS}.d.ts`,
"internals",
]
TYPES_TARGETS.forEach((target) => {
fs.copy(
path.resolve("types", target),
path.join(process.cwd(), target),
(err) => {
if (err) throw err
console.log(`[build-types] copying "${target}" to root folder`)
}
)
})
// Building providers
const providersDir = path.join(process.cwd(), "/src/providers")
const files = fs
.readdirSync(providersDir, "utf8")
.filter((file) => file !== "index.js")
let importLines = ""
let exportLines = `export default {\n`
files.forEach((file) => {
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
try {
// NOTE: If this fails, the default export probably wasn't a named function.
// Always use a named function as default export.
// Eg.: export default function YourProvider ...
const { functionName } = provider.match(
/export default function (?<functionName>.+)\s?\(/
).groups
importLines += `import ${functionName} from "./${file}"\n`
exportLines += ` ${functionName},\n`
} catch (error) {
console.error(
[
`\nThe provider file '${file}' should have a single named default export`,
"Example: 'export default function YourProvider'\n\n",
].join("\n")
)
process.exit(1)
}
})
exportLines += `}\n`
fs.writeFile(
path.join(process.cwd(), "src/providers/index.js"),
[importLines, exportLines].join("\n")
)

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

View File

@@ -1 +0,0 @@
module.exports = require('./dist/server')

1
jwt.js
View File

@@ -1 +0,0 @@
module.exports = require('./dist/lib/jwt').default

42554
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,43 +6,65 @@
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"types": "./index.d.ts",
"keywords": [
"react",
"nodejs",
"oauth",
"jwt",
"oauth2",
"authentication",
"nextjs",
"csrf",
"oidc",
"nextauth"
],
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"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": "next | npm run watch:css",
"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",
"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",
"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": "ts-standard",
"lint:fix": "ts-standard --fix"
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"files": [
"dist",
"index.js",
"index.d.ts",
"providers.js",
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"client.js",
"jwt.js"
"client.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
"jwt.d.ts",
"internals"
],
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.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",
@@ -50,10 +72,8 @@
"oauth": "^0.9.15",
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.7",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
@@ -69,46 +89,115 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@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",
"dotenv": "^8.2.0",
"dtslint": "^4.0.8",
"eslint": "^7.19.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"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",
"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",
"prisma": "^2.16.1",
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"ts-standard": "^10.0.0",
"typescript": "^4.1.3"
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"ts-standard": {
"project": "./tsconfig.json",
"ignore": [
"test/",
"next-env.d.ts"
"prettier": {
"semi": false
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"extends": [
"standard-with-typescript",
"prettier"
],
"globals": [
"localStorage",
"location",
"fetch"
"ignorePatterns": [
"node_modules",
"test",
"next-env.d.ts",
"types",
"www",
".next",
"dist"
],
"globals": {
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
},
"overrides": [
{
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"release": {
"branches": [
"+([0-9])?(.{+([0-9]),x}).x",
"main",
{
"name": "beta",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
[
"@semantic-release/github",
{
"releasedLabels": false,
"successComment": false
}
]
]
},
"funding": [

View File

@@ -1,61 +0,0 @@
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
export default NextAuth({
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
}),
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
}),
Providers.Auth0({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
protection: 'pkce'
}),
Providers.Twitter({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
password: { label: 'Password', type: 'password' }
},
async authorize (credentials) {
if (credentials.password === 'password') {
return {
id: 1,
name: 'Fill Murray',
email: 'bill@fillmurray.com',
image: 'https://www.fillmurray.com/64/64'
}
}
return null
}
})
],
jwt: {
encryption: true,
secret: process.env.SECRET
},
debug: false,
theme: 'auto'
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev --preview-feature
// adapter: Adapters.Prisma.Adapter({ prisma })
})

View File

@@ -1 +0,0 @@
module.exports = require('./dist/providers').default

View File

@@ -1,7 +0,0 @@
module.exports = {
branches: [
'+([0-9])?(.{+([0-9]),x}).x',
'main',
{ name: 'next', prerelease: true }
]
}

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

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

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

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
@@ -18,12 +24,18 @@ import parseUrl from '../lib/parse-url'
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import(".").NextAuthConfig} */
/** @type {import("types/internals/client").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
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,32 +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()
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
* @type {import(".").UseSession}
*/
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)
@@ -83,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)
@@ -104,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
@@ -122,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
@@ -132,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)
}
}
@@ -143,154 +158,112 @@ function _useSessionHook (session) {
return [data, loading]
}
/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
* @type {import(".").GetSession}
*/
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
}
/**
* Returns the current Cross Site Request Forgery Token (CSRF Token)
* required to make POST requests (e.g. for signing in and signing out).
* You likely only need to use this if you are not using the built-in
* `signIn()` and `signOut()` methods.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
* @type {import(".").GetCsrfToken}
*/
async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
export async function getCsrfToken(ctx) {
return (await _fetchData("csrf", ctx))?.csrfToken
}
/**
* It calls `/api/auth/providers` and returns
* a list of the currently configured authentication providers.
* It can be useful if you are creating a dynamic custom sign in page.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
* @type {import(".").GetProviders}
*/
export async function getProviders () {
return _fetchData('providers')
export async function getProviders() {
return await _fetchData("providers")
}
/**
* Client-side method to initiate a signin flow
* or send the user to the signin page listing all possible providers.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
* @type {import(".").SignIn}
*/
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,
}
}
/**
* Signs the user out, by removing the session cookie.
* Automatically adds the CSRF token to the request.
*
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
* @type {import(".").SignOut}
*/
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
}
@@ -298,14 +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.
/** @type {import(".").SetOptions} */
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) {
@@ -316,20 +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)
}
}
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
* @type {import(".").Provider}
*/
export function Provider ({ children, session, options }) {
export function Provider({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
@@ -345,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
@@ -373,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)
}
@@ -383,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(".").BroadcastMessage) => void} onReceive
* @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(".").BroadcastMessage} */
/** @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,
@@ -429,5 +414,5 @@ export default {
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut
signout: signOut,
}

View File

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

View File

@@ -1,33 +1,33 @@
import crypto from 'crypto'
import jose from 'jose'
import logger from './logger'
import crypto from "crypto"
import jose from "jose"
import logger from "./logger"
// Set default algorithm to use for auto-generated signing key
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
// Set default algorithm for auto-generated symmetric encryption key
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
// Use encryption or not by default
const DEFAULT_ENCRYPTION_ENABLED = false
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
async function encode ({
export async function encode({
token = {},
maxAge = DEFAULT_MAX_AGE,
secret,
signingKey,
signingOptions = {
expiresIn: `${maxAge}s`
expiresIn: `${maxAge}s`,
},
encryptionKey,
encryptionOptions = {
alg: 'dir',
alg: "dir",
enc: DEFAULT_ENCRYPTION_ALGORITHM,
zip: 'DEF'
zip: "DEF",
},
encryption = DEFAULT_ENCRYPTION_ENABLED
encryption = DEFAULT_ENCRYPTION_ENABLED,
} = {}) {
// Signing Key
const _signingKey = signingKey
@@ -49,7 +49,7 @@ async function encode ({
return signedToken
}
async function decode ({
export async function decode({
secret,
token,
maxAge = DEFAULT_MAX_AGE,
@@ -57,14 +57,14 @@ async function decode ({
verificationKey = signingKey, // Optional (defaults to encryptionKey)
verificationOptions = {
maxTokenAge: `${maxAge}s`,
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
},
encryptionKey,
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
decryptionOptions = {
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
},
encryption = DEFAULT_ENCRYPTION_ENABLED
encryption = DEFAULT_ENCRYPTION_ENABLED,
} = {}) {
if (!token) return null
@@ -77,8 +77,12 @@ async function decode ({
: getDerivedEncryptionKey(secret)
// Decrypt token
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
tokenToVerify = decryptedToken.toString('utf8')
const decryptedToken = jose.JWE.decrypt(
token,
_encryptionKey,
decryptionOptions
)
tokenToVerify = decryptedToken.toString("utf8")
}
// Signing Key
@@ -99,16 +103,22 @@ async function decode ({
* raw?: boolean
* }} params
*/
async function getToken (params) {
export async function getToken(params) {
const {
req,
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
// or not set (e.g. development or test instance) case use unprefixed name
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
raw = false
secureCookie = !(
!process.env.NEXTAUTH_URL ||
process.env.NEXTAUTH_URL.startsWith("http://")
),
cookieName = secureCookie
? "__Secure-next-auth.session-token"
: "next-auth.session-token",
raw = false,
decode: _decode = decode,
} = params
if (!req) throw new Error('Must pass `req` to JWT getToken()')
if (!req) throw new Error("Must pass `req` to JWT getToken()")
// Try to get token from cookie
let token = req.cookies[cookieName]
@@ -116,8 +126,8 @@ async function getToken (params) {
// If cookie not found in cookie look for bearer token in authorization header.
// This allows clients that pass through tokens in headers rather than as
// cookies to use this helper function.
if (!token && req.headers.authorization?.split(' ')[0] === 'Bearer') {
const urlEncodedToken = req.headers.authorization.split(' ')[1]
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
const urlEncodedToken = req.headers.authorization.split(" ")[1]
token = decodeURIComponent(urlEncodedToken)
}
@@ -126,7 +136,7 @@ async function getToken (params) {
}
try {
return decode({ token, ...params })
return _decode({ token, ...params })
} catch {
return null
}
@@ -137,7 +147,7 @@ let DERIVED_SIGNING_KEY_WARNING = false
let DERIVED_ENCRYPTION_KEY_WARNING = false
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
if (crypto.hkdfSync) {
return Buffer.from(
crypto.hkdfSync(
@@ -149,39 +159,50 @@ function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
)
)
}
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
return require("futoin-hkdf")(secret, byteLength, {
info: encryptionInfo,
hash: digest,
})
}
function getDerivedSigningKey (secret) {
function getDerivedSigningKey(secret) {
if (!DERIVED_SIGNING_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
DERIVED_SIGNING_KEY_WARNING = true
}
const buffer = hkdf(secret, {
byteLength: 64,
encryptionInfo: 'NextAuth.js Generated Signing Key'
encryptionInfo: "NextAuth.js Generated Signing Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_SIGNATURE_ALGORITHM,
use: "sig",
kid: "nextauth-auto-generated-signing-key",
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
return key
}
function getDerivedEncryptionKey (secret) {
function getDerivedEncryptionKey(secret) {
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
DERIVED_ENCRYPTION_KEY_WARNING = true
}
const buffer = hkdf(secret, {
byteLength: 32,
encryptionInfo: 'NextAuth.js Generated Encryption Key'
encryptionInfo: "NextAuth.js Generated Encryption Key",
})
const key = jose.JWK.asKey(buffer, {
alg: DEFAULT_ENCRYPTION_ALGORITHM,
use: "enc",
kid: "nextauth-auto-generated-encryption-key",
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
return key
}
export default {
encode,
decode,
getToken
getToken,
}

10
src/lib/logger.d.ts vendored
View File

@@ -1,10 +0,0 @@
export interface LoggerInstance {
warn: (code?: string, ...message: unknown[]) => void
error: (code?: string, ...message: unknown[]) => void
debug: (code?: string, ...message: unknown[]) => void
}
export declare function proxyLogger (logger: LoggerInstance, basePath: string): LoggerInstance
const _logger: LoggerInstance
export default _logger

View File

@@ -1,34 +1,31 @@
/** @type {import("./logger").LoggerInstance} */
/** @type {import("types").LoggerInstance} */
const _logger = {
error (code, ...message) {
error(code, ...message) {
console.error(
`[next-auth][error][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
...message
)
},
warn (code, ...message) {
warn(code, ...message) {
console.warn(
`[next-auth][warn][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
...message
)
},
debug (code, ...message) {
debug(code, ...message) {
if (!process?.env?._NEXTAUTH_DEBUG) return
console.log(
`[next-auth][debug][${code.toLowerCase()}]`,
...message
)
}
console.log(`[next-auth][debug][${code.toLowerCase()}]`, ...message)
},
}
/**
* Override the built-in logger.
* Any `undefined` level will use the default logger.
* @param {Partial<import("./logger").LoggerInstance>} newLogger
* @param {Partial<import("types").LoggerInstance>} newLogger
*/
export function setLogger (newLogger = {}) {
export function setLogger(newLogger = {}) {
if (newLogger.error) _logger.error = newLogger.error
if (newLogger.warn) _logger.warn = newLogger.warn
if (newLogger.debug) _logger.debug = newLogger.debug
@@ -38,13 +35,13 @@ export default _logger
/**
* Serializes client-side log messages and sends them to the server
* @param {import("./logger").LoggerInstance} logger
* @param {import("types").LoggerInstance} logger
* @param {string} basePath
* @return {import("./logger").LoggerInstance}
* @return {import("types").LoggerInstance}
*/
export function proxyLogger (logger = _logger, basePath) {
export function proxyLogger(logger = _logger, basePath) {
try {
if (typeof window === 'undefined') {
if (typeof window === "undefined") {
return logger
}
@@ -57,21 +54,23 @@ export function proxyLogger (logger = _logger, basePath) {
const body = new URLSearchParams({
level,
code,
message: JSON.stringify(message.map(m => {
if (m instanceof Error) {
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
return { name: m.name, message: m.message, stack: m.stack }
}
return m
}))
message: JSON.stringify(
message.map((m) => {
if (m instanceof Error) {
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
return { name: m.name, message: m.message, stack: m.stack }
}
return m
})
),
})
if (navigator.sendBeacon) {
return navigator.sendBeacon(url, body)
}
return fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
method: "POST",
headers: { "Content-Type": "application/json" },
body,
})
}
}

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

View File

@@ -1,30 +1,34 @@
export default (options) => {
export default function Apple(options) {
return {
id: 'apple',
name: 'Apple',
type: 'oauth',
version: '2.0',
scope: 'name email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://appleid.apple.com/auth/token',
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
id: "apple",
name: "Apple",
type: "oauth",
version: "2.0",
scope: "name email",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://appleid.apple.com/auth/token",
authorizationUrl:
"https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post",
profileUrl: null,
idToken: true,
profile: (profile) => {
profile(profile) {
// The name of the user will only return on first login
return {
id: profile.sub,
name: profile.user != null ? profile.user.name.firstName + ' ' + profile.user.name.lastName : null,
email: profile.email
name:
profile.user != null
? profile.user.name.firstName + " " + profile.user.name.lastName
: null,
email: profile.email,
}
},
clientId: null,
clientSecret: {
teamId: null,
privateKey: null,
keyId: null
keyId: null,
},
protection: 'none', // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options
protection: "none", // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options,
}
}

View File

@@ -1,24 +1,24 @@
export default (options) => {
export default function Atlassian(options) {
return {
id: 'atlassian',
name: 'Atlassian',
type: 'oauth',
version: '2.0',
id: "atlassian",
name: "Atlassian",
type: "oauth",
version: "2.0",
params: {
grant_type: 'authorization_code'
grant_type: "authorization_code",
},
accessTokenUrl: 'https://auth.atlassian.com/oauth/token',
accessTokenUrl: "https://auth.atlassian.com/oauth/token",
authorizationUrl:
'https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent',
profileUrl: 'https://api.atlassian.com/me',
profile: (profile) => {
"https://auth.atlassian.com/authorize?audience=api.atlassian.com&response_type=code&prompt=consent",
profileUrl: "https://api.atlassian.com/me",
profile(profile) {
return {
id: profile.account_id,
name: profile.name,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Auth0(options) {
return {
id: 'auth0',
name: 'Auth0',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
scope: 'openid email profile',
id: "auth0",
name: "Auth0",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
scope: "openid email profile",
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
profileUrl: `https://${options.domain}/userinfo`,
profile: (profile) => {
profile(profile) {
return {
id: profile.sub,
name: profile.nickname,
email: profile.email,
image: profile.picture
image: profile.picture,
}
},
...options
...options,
}
}

View File

@@ -1,24 +1,24 @@
export default (options) => {
const tenant = options.tenantId ? options.tenantId : 'common'
export default function AzureADB2C(options) {
const tenant = options.tenantId ? options.tenantId : "common"
return {
id: 'azure-ad-b2c',
name: 'Azure Active Directory B2C',
type: 'oauth',
version: '2.0',
id: "azure-ad-b2c",
name: "Azure Active Directory B2C",
type: "oauth",
version: "2.0",
params: {
grant_type: 'authorization_code'
grant_type: "authorization_code",
},
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/v1.0/me/',
profile: (profile) => {
profileUrl: "https://graph.microsoft.com/v1.0/me/",
profile(profile) {
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName
email: profile.userPrincipalName,
}
},
...options
...options,
}
}

View File

@@ -1,20 +1,22 @@
export default (options) => {
export default function Basecamp(options) {
return {
id: 'basecamp',
name: 'Basecamp',
type: 'oauth',
version: '2.0',
accessTokenUrl: 'https://launchpad.37signals.com/authorization/token?type=web_server',
authorizationUrl: 'https://launchpad.37signals.com/authorization/new?type=web_server',
profileUrl: 'https://launchpad.37signals.com/authorization.json',
profile: (profile) => {
id: "basecamp",
name: "Basecamp",
type: "oauth",
version: "2.0",
accessTokenUrl:
"https://launchpad.37signals.com/authorization/token?type=web_server",
authorizationUrl:
"https://launchpad.37signals.com/authorization/new?type=web_server",
profileUrl: "https://launchpad.37signals.com/authorization.json",
profile(profile) {
return {
id: profile.identity.id,
name: `${profile.identity.first_name} ${profile.identity.last_name}`,
email: profile.identity.email_address,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,29 +1,29 @@
export default (options) => {
export default function BattleNet(options) {
const { region } = options
return {
id: 'battlenet',
name: 'Battle.net',
type: 'oauth',
version: '2.0',
scope: 'openid',
params: { grant_type: 'authorization_code' },
id: "battlenet",
name: "Battle.net",
type: "oauth",
version: "2.0",
scope: "openid",
params: { grant_type: "authorization_code" },
accessTokenUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/token'
region === "CN"
? "https://www.battlenet.com.cn/oauth/token"
: `https://${region}.battle.net/oauth/token`,
authorizationUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/authorize?response_type=code'
region === "CN"
? "https://www.battlenet.com.cn/oauth/authorize?response_type=code"
: `https://${region}.battle.net/oauth/authorize?response_type=code`,
profileUrl: 'https://us.battle.net/oauth/userinfo',
profile: (profile) => {
profileUrl: "https://us.battle.net/oauth/userinfo",
profile(profile) {
return {
id: profile.id,
name: profile.battletag,
email: null,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -1,22 +1,23 @@
export default (options) => {
export default function Box(options) {
return {
id: 'box',
name: 'Box',
type: 'oauth',
version: '2.0',
scope: '',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.box.com/oauth2/token',
authorizationUrl: 'https://account.box.com/api/oauth2/authorize?response_type=code',
profileUrl: 'https://api.box.com/2.0/users/me',
profile: (profile) => {
id: "box",
name: "Box",
type: "oauth",
version: "2.0",
scope: "",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.box.com/oauth2/token",
authorizationUrl:
"https://account.box.com/api/oauth2/authorize?response_type=code",
profileUrl: "https://api.box.com/2.0/users/me",
profile(profile) {
return {
id: profile.id,
name: profile.name,
email: profile.login,
image: profile.avatar_url
image: profile.avatar_url,
}
},
...options
...options,
}
}

View File

@@ -1,30 +1,34 @@
export default (options) => {
export default function Bungie(options) {
return {
id: 'bungie',
name: 'Bungie',
type: 'oauth',
version: '2.0',
scope: '',
params: { reauth: 'true', grant_type: 'authorization_code' },
accessTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
requestTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
authorizationUrl: 'https://www.bungie.net/en/OAuth/Authorize?response_type=code',
profileUrl: 'https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/',
profile: (profile) => {
id: "bungie",
name: "Bungie",
type: "oauth",
version: "2.0",
scope: "",
params: { reauth: "true", grant_type: "authorization_code" },
accessTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
requestTokenUrl: "https://www.bungie.net/platform/app/oauth/token/",
authorizationUrl:
"https://www.bungie.net/en/OAuth/Authorize?response_type=code",
profileUrl:
"https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/",
profile(profile) {
const { bungieNetUser: user } = profile.Response
return {
id: user.membershipId,
name: user.displayName,
image: `https://www.bungie.net${user.profilePicturePath.startsWith('/') ? '' : '/'}${user.profilePicturePath}`,
email: null
image: `https://www.bungie.net${
user.profilePicturePath.startsWith("/") ? "" : "/"
}${user.profilePicturePath}`,
email: null,
}
},
headers: {
'X-API-Key': null
"X-API-Key": null,
},
clientId: null,
clientSecret: null,
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Cognito(options) {
const { domain } = options
return {
id: 'cognito',
name: 'Cognito',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: { grant_type: 'authorization_code' },
id: "cognito",
name: "Cognito",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${domain}/oauth2/token`,
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
profileUrl: `https://${domain}/oauth2/userInfo`,
profile: (profile) => {
profile(profile) {
return {
id: profile.sub,
name: profile.username,
email: profile.email,
image: null
image: null,
}
},
...options
...options,
}
}

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