Compare commits

...

239 Commits

Author SHA1 Message Date
Cody Ogden
cdc1ac52b2 docs: Update Providers.Credential Example Block [skip release] (#1225)
Closing curly bracket where it should have been a square bracket.
2021-02-01 10:01:10 +01:00
Balázs Orbán
f2a7ee0b34 fix: make OAuth 1 work after refactoring (#1218)
* chore: add twitter provider to dev app

* feat: bind client instance to overriden methods

* fix: don't add extra params to getOAuthRequestToken

* chore: add twitter to env example, add secret gen instructions
2021-02-01 10:01:10 +01:00
Balázs Orbán
d67f1b7718 docs: remove announcement bar [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
396f5d8bbc docs: more emphasis on req methods [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
d7e78d5996 fix: leave accessTokenExpires as null
Forwarding expires_in as is to accessTokenExpires has shown to cause issues with Prisma, and maybe with other flows as well. Setting it back to `null` for now. We still forward `expires_in`, so users can use it if they want to.

Fixes #1216
2021-02-01 10:01:10 +01:00
Vova
ad3b0b6a7d feat(provider): Add Medium (#1213) 2021-02-01 10:01:10 +01:00
Balázs Orbán
f4a954ccbb feat: send all params to logger function (#1214) 2021-02-01 10:01:10 +01:00
Balázs Orbán
93f051ce08 refactor: provide raw idToken through account object (#1211)
* refactor: provide raw idToken through account object

* docs: clear up accessToken naming

* refactor: provide raw token response to account

* chore: fix grammar in comments
2021-02-01 10:01:10 +01:00
Samson Zhang
645b53ee49 docs: Fix grammar in "Feature Requests" section of FAQs [skip release] (#1212) 2021-02-01 10:01:10 +01:00
Balázs Orbán
9c9744f30a fix: forward second argument to fetch body in signIn
fixes #1206
2021-02-01 10:01:10 +01:00
Aishah
9860ad8c8c docs(adapter): add adapter repo to documentation [skip release] (#1173)
* docs(adapter): add adapter repo to documentation

* docs(adapter): elaborate on custom repo
2021-02-01 10:01:10 +01:00
Carmelo Scandaliato
23ada52f97 docs(provider): fix typos in providers code snippets [skip release] (#1204) 2021-02-01 10:01:10 +01:00
Dillon Mulroy
65e4910618 fix: Add a null check to the window 'storage' event listener (#1198)
* Add a null check to the window 'storage' event listener

While testing in Cypress it's possible to receive a null value on Storage Events when 'clear' is called and will cause errors as seen in #1125.

* Update index.js

typo

* Update src/client/index.js

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

* formatting

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Balázs Orbán
30c6d6360f fix: send /authorize params through url 2021-02-01 10:01:10 +01:00
Balázs Orbán
214b22ecbb feat(provider): re-add state, expand protection provider options (#1184)
* refactor: move OAuthCallbackError to errors file

* refactor: improve pkce handling

* feat(provider): re-introduce state to provider options

* docs(provider): mention protection options "state" and "none"

* docs(provider): document state property deprecation

* fix: only add code_verifier param if protection is pkce

* docs: explain state deprecation better

* chore: unify string
2021-02-01 10:01:10 +01:00
Balázs Orbán
f50ac194aa chore(provider): remove Mixer (#1178)
"Thank you to our amazing community and Partners.
As of July 22, the Mixer service has closed."
2021-02-01 10:01:10 +01:00
Balázs Orbán
b40e1441ec docs: fix typo in callbacks.md [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
90a8f7c890 docs: clear things up around using access_token [skip release]
#1078
2021-02-01 10:01:10 +01:00
Balázs Orbán
a7bae0395b fix: fix lint issues 2021-02-01 10:01:10 +01:00
Balázs Orbán
71b3122fd1 fix: use startsWith for protocol matching in parseUrl
closes #842
2021-02-01 10:01:10 +01:00
Luke Lau
1caa9bb813 fix(provider): use authed_user on slack instead of spotify (#1174) 2021-02-01 10:01:10 +01:00
Balázs Orbán
daed68aee2 docs(provider): add Salesforce provider 2021-02-01 10:01:10 +01:00
Mohamed El Mahallawy
2f3ed7507b feat(provider): add Salesforce provider (#1027) 2021-02-01 10:01:10 +01:00
Balázs Orbán
a15bdc191b fix: correct logger import 2021-02-01 10:01:10 +01:00
Balázs Orbán
ea71a1fb2f feat: add PKCE support (#941)
* chore(deps): upgrade dependencies

* chore(deps): add pkce-challenge

* feat(pkce): initial implementation of PCKE support

* chore: remove URLSearchParams

* chore(deps): upgrade lockfile

* refactor: store code_verifier in a cookie

* refactor: add pkce handlers

* docs: add PKCE documentation

* chore: remove unused param

* chore: revert unnecessary code change

* fix: correct variable names
2021-02-01 10:01:10 +01:00
Balázs Orbán
0069095bce docs: update info about TypeScript [skip release] 2021-02-01 10:01:10 +01:00
Zhao Lei
42a822c407 feat(provider): add option to generate email verification token (#541)
* Add option to generate email verification token

* chore: remove unused import

* refactor: define default generateVerificationToken in-place

* refactor: define default generateVerificationToken in-place

Co-authored-by: Nico Domino <yo@ndo.dev>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Henrik Wenz
2cfe5ad879 fix(adapter): fix ISO Datetime type error in Prisma updateSession (#640)
Co-authored-by: Nico Domino <yo@ndo.dev>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Balázs Orbán
cba149f4b6 feat: forward signIn auth params to /authorize (#1149)
* refactor: authorisation -> authorization

* feat: forward authorizationParams from signIn function

* refactor: take auth params as third argument

* docs: document signIn authorizationParams
2021-02-01 10:01:10 +01:00
Balázs Orbán
31bb2c342c chore(adapters): remove fauna (#1148) 2021-02-01 10:01:10 +01:00
Radhika
af30be1fa4 docs: remove v1 documentation (#1142) 2021-02-01 10:01:10 +01:00
Yuri Gor
ebaa28f04e chore(deps): upgrade typeorm to v0.2.30 (#1145) 2021-02-01 10:01:10 +01:00
t.kuriyama
e26297b901 feat: add native hkdf (#1124)
* feat: add native hkdf

* feat: import only needed to do hkdf

* feat: tweak digest and arguments
2021-02-01 10:01:10 +01:00
Balázs Orbán
2562b3c5d8 chore: fix lint issues [skip release] 2021-02-01 10:01:10 +01:00
suraj10k
f8e5a79ce1 chore: Comply to Vercel Open Source sponsorship [skip release] (#1087)
* added banner

* Changed banner image allignment

* changed location of banner again

* added to acknowledgement

* added to acknowledgement 1

* changed image size

* k

* l

* s

* s

* .

* added link to the banner in readme.md

* fixed image redirect

* fixed image allignment

* made changes in readme and index.js

* Changed the source of the banner image

* added banner to the footer of the site
2021-02-01 10:01:10 +01:00
Balázs Orbán
be53ef0f71 chore: define providers in single file for docs [skip release] 2021-02-01 10:01:10 +01:00
Aymeric
fbb5a12cbd feat(provider): finish Reddit provider and add documentation (#1094)
* Create reddit.md

* uncommented profile callback

* Update reddit.md

* fix lint issues

* added reddit provider

* added reddit provider

* Add Reddit Provider

For some reason a bunch of providers got deleted in the last commit

* Add Reddit Provider

* Add Reddit Provider
2021-02-01 10:01:10 +01:00
Balázs Orbán
70a186c183 style: make p system theme aware [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
cff9d3e294 fix: export getSession [skip release]
somehow the default export does not work in the dev app
2021-02-01 10:01:10 +01:00
Balázs Orbán
2865b8cc2d refactor: show signin page in dev app [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
b84f1b681c refactor: be explicit about path in jsonconfig [skip release] 2021-02-01 10:01:10 +01:00
Ben
ea1d09bf83 feat(provider): add LINE provider (#1091) 2021-02-01 10:01:10 +01:00
Balázs Orbán
a18ec09307 feat(pages): add dark theme support (#1088)
* feat(pages): add dark theme support

* docs: document theme option

* chore: remove ts-check from dev app

* style(pages): fix some text colors in dark mode
2021-02-01 10:01:10 +01:00
Balázs Orbán
37cb81094f docs: update some urls in the docs [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
040d7c5017 chore: update caiuse-lite db 2021-02-01 10:01:10 +01:00
Balázs Orbán
f53ea6c9a9 docs: improve FAQ docs [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
7f670c5222 docs: clarify .env usage in CONTRIBUTING.md [skip release] (#1085) 2021-02-01 10:01:10 +01:00
Alex B
b1a99ec32f feat: replace blur/focus event to visibility API for getSession (#1081) 2021-02-01 10:01:10 +01:00
Balázs Orbán
ca065604a3 fix: pass csrfToken to signin renderer 2021-02-01 10:01:10 +01:00
Balázs Orbán
77de2abd14 feat: improve package development experience (#1064)
* chore(deps): add next and react to dev dependencies

* chore: move build configs to avoid crash with next dev

* chore: add next js dev app

* chore: remove .txt extension from LICENSE file

* chore: update CONTRIBUTING.md

* chore: watch css under development

* style(lint): run linter on index.css

* chore: fix some imports for dev server

* refactor: simplify client code

* chore: mention VSCode extension for linting

* docs: reword CONTRIBUTING.md

* chore: ignore linting pages and components
2021-02-01 10:01:10 +01:00
Balázs Orbán
f6d6c4344c refactor: code base improvements 3 (#1072)
* refactor: extend res.{end,send,json}, redirect

* refactor: chain res methods, remove unnecessary ones

* refactor: simplify oauth callback signature

* refactor: code simplifications

* refactor: re-export everything from routes in one

* refactor: split up main index.js to multiple files

* refactor: simplify passing of provider(s) around

* refactor: extend req with callbackUrl inline

* refactor: simplify page rendering

* refactor: move error page redirects to main file, simplify renderer

* refactor: inline req.options definition

* refactor: simplify error fallbacks

* refactor: remove else branches and unnecessary try..catch

* refactor: add docs, and simplify jwt functions

* refactor: prefer errors object over switch..case in signin page

* feat: log all params sent to logger instead of only first

* refactor: fewer lines input validation

* refactor: remove even more unnecessary else branches
2021-02-01 10:01:10 +01:00
Evgeniy Boreyko
e8b1513899 feat(provider): add vk.com provider (#1060)
* feat(provider): add vk.com provider

* refactor(provider): reduce vk.com provider api
2021-02-01 10:01:10 +01:00
Balázs Orbán
505efc8a5d fix: remove async from NextAuth default handler
This function should not return a Promise
2021-02-01 10:01:10 +01:00
Balázs Orbán
76b983229a feat(provider): reduce user facing API (#1023)
Co-authored-by: Balazs Orban <balazs@nhi.no>
2021-02-01 10:01:10 +01:00
Balázs Orbán
ecddaf696b fix: use authorizationUrl correctly 2021-02-01 10:01:10 +01:00
Balázs Orbán
b43e7dca43 fix: trigger release 2021-02-01 10:01:10 +01:00
Balázs Orbán
e7c34fd74b refactor: code base improvements 2 (#1045) 2021-02-01 10:01:10 +01:00
Balázs Orbán
e0dd8e400b docs: misc improvements [skip release] (#1043) 2021-02-01 10:01:10 +01:00
Balázs Orbán
21d22a7e08 chore: run tests on canary [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
cb2fe0cae6 docs: add powered by vercel logo [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
9ed75c7788 fix: don't chain on res.end on non-chainable res methods (#1031) 2021-02-01 10:01:10 +01:00
Balázs Orbán
8e8713755a fix: miscellaneous bugfixes (#1030)
* fix: use named params to fix order

* fix: avoid recursive redirects

* fix: revert to use parsed baseUrl

* fix: avoid recursive res.end calls

* fix: use named params in renderPage

* fix: promisify lib/oauth/callback result
2021-02-01 10:01:10 +01:00
Balázs Orbán
7fdde6268e chore: rename labeler.yaml to labeler.yml [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
5e949a3b97 chore: add auto labeling to PRs [skip release] (#1025)
* chore: add auto labeling to PRs [skip release]

* chore: allow any file type for test label to be added
2021-02-01 10:01:10 +01:00
Balázs Orbán
a979e040cd feat: forward id_token to jwt and signIn callbacks (#1024) 2021-02-01 10:01:10 +01:00
Didi Keke
2205cfa754 feat(provider): Add Mail.ru OAuth Service Provider and Callback snippet (#522)
* Update callback.js

- Fix Mail.ru bug (missing request parameter: access_token)

Note: setGetAccessTokenProfileUrl should be added to Mail.ru provider to enable support.

* Add Mail.ru OAuth Service Provider

* Update callbacks.md

- Fix broken callbacks snippet.

* Update callback.js

- Bug fix https://github.com/nextauthjs/next-auth/pull/522#issuecomment-669851914
- Minor refactoring.

* Fix: Code linting.

* Update callback.js

Improve approach for building of URL based review recommendation.

* Feat: Reduce API surface expansion

Make use of provider.id === "mailru" as suggested in review discussion in place of setGetAccessTokenProfileUrl.

* Fix: Code linting
2021-02-01 10:01:10 +01:00
Balázs Orbán
0989ef6171 refactor: code base improvements (#959)
* chore: fix casing of OAuth

* refacotr: simplify default callbacks lib file

* refactor: use native URL instead of string concats

* refactor: move redirect to res.redirect, done to res.end

* refactor: move options to req

* refactor: improve IntelliSense, name all functions

* fix(lint): fix lint errors

* refactor: remove jwt-decode dependency

* refactor: refactor some callbacks to Promises

* revert: "refactor: use native URL instead of string concats"

Refs: 690c55b04089e4f3157424c816d43ee4cecb77a0

* chore: misc changes

Co-authored-by: Balazs Orban <balazs@nhi.no>
2021-02-01 10:01:10 +01:00
Balázs Orbán
7979b1069e docs: fix typos in CONTRIBUTING.md [skip release] 2021-02-01 10:01:10 +01:00
Balázs Orbán
71b50082f8 docs: update contributing information [skip release] (#1011)
* docs: update CONTRIBUTING.md

* docs:  use db instead of database for more space

* docs: update CONTRIBUTING.md

* docs: update PR template

* docs: add note about skipping a release
2021-02-01 10:01:10 +01:00
Melanie Seltzer
f3cc4d1018 docs: small update to sign in/out examples (#1016)
* Update examples in client.md

* Update more examples

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Balázs Orbán
15570b7479 feat: allow to return string in signIn callback (#1019) 2021-02-01 10:01:10 +01:00
Balázs Orbán
a5187b69e8 docs: Remove unnecessary promises (#915) 2021-02-01 10:01:10 +01:00
Florian Michaut
751fd7bb0e feat(db): make Fauna DB collections & indexes configurable (#968)
* Add collections & indexes overrides for Fauna DB

* Fix the name of the verification token index

Co-authored-by: Florian Michaut <florian@coding-days.com>
2021-02-01 10:01:10 +01:00
Ben West
94054db3f3 Change image to text from varchar (#777)
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Yuma Matsune
f93dbbbfee fix(adapter): use findOne for typeorm (#1014) 2021-02-01 10:01:10 +01:00
Balázs Orbán
e3fd0ad450 fix: treat user.id as optional param (#1010) 2021-02-01 10:01:10 +01:00
Balázs Orbán
e06816a374 chore(release): trigger release on docs type 2021-02-01 10:01:10 +01:00
Balázs Orbán
284118e708 chore(release): delete old workflow 2021-02-01 10:01:10 +01:00
Junior Vidotti
84bcecbec1 docs(database): add mssql indexes in docs, fix typos (#925)
* added mssql indexes in docs, fixed typo

* docs: fix typo in www/docs/schemas/mssql.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Balazs Orban
5060bd7f9b chore(release): change semantic-release/git to semantic-release/github 2021-02-01 10:01:10 +01:00
Balázs Orbán
16a8720b1d feat: add semantic-release (#920) 2021-02-01 10:01:10 +01:00
Nico Domino
3c056d7ff5 Update README.md 2021-02-01 10:01:10 +01:00
Nico Domino
49dd7a807d Update README.md 2021-02-01 10:01:10 +01:00
Paul Kenneth Kent
42596fbca5 feat: add strava provider (#986)
* Add Strava as a provider

* Add documentation for Strava provider

* Fix lint errors

Co-authored-by: Paul Kenneth Kent <paul@ventureharbour.com>
2021-02-01 10:01:10 +01:00
Nico Domino
68d0f9465a Update README.md
Updated the readme to include the projects logo, fixed some typos, and added license info and contributor image.
2021-02-01 10:01:10 +01:00
Balázs Orbán
71b4af0894 chore: hide comments from pull request template 2021-02-01 10:01:10 +01:00
pkabore
8bbb0ec344 docs: Correcting a typo. "available" Line 70 (#965)
* chore: use stale label, instead of wontfix

* chore: add link to issue explaining stalebot

* chore: fix typo in stalebot comment

* chore: run build GitHub Action on canary also

* chore: run build GitHub Actions on canary as well

* chore: add reproduction section to questions

* feat(provider): Add Azure Active Directory B2C (#809)

* add provider: Microsoft

* documentation

* support no tenant setup

* fix code style

* chore: rename Microsoft provider to AzureADB2C

* chore: alphabetical order in providers/index

* Revert "feat(provider): Add Azure Active Directory B2C (#809)" (#919)

This reverts commit 6e6a24a7af.

* chore: add myself to the contributors list 🙈

* Correcting a typo. "available" Line 70

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Vladimir Evdokimov <evdokimov.vladimir@gmail.com>
2021-02-01 10:01:10 +01:00
pkabore
b2770d5a1f docs: We have twice the word "side" (#964)
* chore: use stale label, instead of wontfix

* chore: add link to issue explaining stalebot

* chore: fix typo in stalebot comment

* chore: run build GitHub Action on canary also

* chore: run build GitHub Actions on canary as well

* chore: add reproduction section to questions

* feat(provider): Add Azure Active Directory B2C (#809)

* add provider: Microsoft

* documentation

* support no tenant setup

* fix code style

* chore: rename Microsoft provider to AzureADB2C

* chore: alphabetical order in providers/index

* Revert "feat(provider): Add Azure Active Directory B2C (#809)" (#919)

This reverts commit 6e6a24a7af.

* chore: add myself to the contributors list 🙈

* We have twice the word "side"

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Vladimir Evdokimov <evdokimov.vladimir@gmail.com>
2021-02-01 10:01:10 +01:00
imgregduh
192e5bf07e docs: fix typo Adapater -> Adapter (#960)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Vladimir Evdokimov <evdokimov.vladimir@gmail.com>
2021-02-01 10:01:10 +01:00
dependabot[bot]
9abdbb57eb chore(deps): Bump ini from 1.3.5 to 1.3.8 in /www (#953)
Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-01 10:01:10 +01:00
Jakub Naskręski
989d23e827 feat: Display error if no [...nextauth].js found (#678)
* Display error if no [...nextauth].js found

fixes #647

* Log the error and describe it inside errors.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Haldun Anil
025f33a91f docs: fix incorrect references in cypress docs (#932)
* chore: use stale label, instead of wontfix

* chore: add link to issue explaining stalebot

* chore: fix typo in stalebot comment

* chore: run build GitHub Action on canary also

* chore: run build GitHub Actions on canary as well

* chore: add reproduction section to questions

* feat(provider): Add Azure Active Directory B2C (#809)

* add provider: Microsoft

* documentation

* support no tenant setup

* fix code style

* chore: rename Microsoft provider to AzureADB2C

* chore: alphabetical order in providers/index

* Revert "feat(provider): Add Azure Active Directory B2C (#809)" (#919)

This reverts commit 6e6a24a7af.

* chore: add myself to the contributors list 🙈

* docs: fix incorrect references in cypress docs

* chore: add additional docs clarification

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Vladimir Evdokimov <evdokimov.vladimir@gmail.com>
2021-02-01 10:01:10 +01:00
Luke Lau
6e2fc11d64 feat: Store user ID in sub claim of default JWT (#784)
This allows us to check if the user is signed in when using JWTs

Part of #625
2021-02-01 10:01:10 +01:00
Balázs Orbán
6b1b8613d0 chore: reword PR template 2021-02-01 10:01:10 +01:00
Balázs Orbán
6d023aa533 chore: create PULL_REQUEST_TEMPLATE.md 2021-02-01 10:01:10 +01:00
Balázs Orbán
080dd5f569 chore: add note about conveting questions to discussions 2021-02-01 10:01:10 +01:00
Balázs Orbán
a6867b3564 chore: disallow issues without template 2021-02-01 10:01:10 +01:00
dependabot[bot]
6750accc0a chore(dep): Bump highlight.js from 9.18.1 to 9.18.5 (#880)
Bumps [highlight.js](https://github.com/highlightjs/highlight.js) from 9.18.1 to 9.18.5.
- [Release notes](https://github.com/highlightjs/highlight.js/releases)
- [Changelog](https://github.com/highlightjs/highlight.js/blob/9.18.5/CHANGES.md)
- [Commits](https://github.com/highlightjs/highlight.js/compare/9.18.1...9.18.5)

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>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Kristóf Poduszló
9aae7bbc54 refactor(db): update Prisma calls to support 2.12+ (#881)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Cathy Chen
a84fe596af update(provider): Update Slack provider to use V2 OAuth endpoints (#895)
* Update Slack to v2 authorize urls, option for additional authorize params
* acessTokenGetter + documentation
2021-02-01 10:01:10 +01:00
Vladimir Evdokimov
18840ead40 feat(provider): Add Azure Active Directory B2C (#921)
* add provider: Microsoft

* documentation

* support no tenant setup

* fix code style

* chore: rename Microsoft provider to AzureADB2C

* chore: alphabetical order in providers/index

* doc: add provider to FAQ
2021-02-01 10:01:10 +01:00
Joe Bell
f72ee5ec06 feat: add foursquare (#584) 2021-02-01 10:01:10 +01:00
RobertCraigie
958c31a4ee feat(provider): Add Bungie (#589)
* Add Bungie provider

* Use absolute URL for images

* Correct image URL and use consistent formatting

Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
dependabot[bot]
f47f5c6c62 Bump next from 9.5.3 to 9.5.4 in /test/docker/app (#759)
Bumps [next](https://github.com/vercel/next.js) from 9.5.3 to 9.5.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v9.5.3...v9.5.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Joost Jansky
d5f5157366 feat(provider): add netlify (#555)
Co-authored-by: styxlab <cws@DE01WP777.scdom.net>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Joseph Vaughan
9fbfd90bb2 add(db): Add support for Fauna DB (#708)
* Add support for Fauna DB

* Add integration tests

Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Fabrizio Ruggeri
97d6f19fab Include callbackUrl in newUser page (#790)
* Include callbackUrl in newUser page

* Update src/server/routes/callback.js

Co-authored-by: Iain Collins <me@iaincollins.com>

* Update src/server/routes/callback.js

Co-authored-by: Iain Collins <me@iaincollins.com>

Co-authored-by: Iain Collins <me@iaincollins.com>
Co-authored-by: Nico Domino <yo@ndo.dev>
2021-02-01 10:01:10 +01:00
Nico Domino
fdcc62bd26 WIP: Update Docusaurus + Site dependencies (#802)
* update: deps

* fix: broken link

* fix: search upgrade change
2021-02-01 10:01:10 +01:00
Aymeric
4764d60268 Fix for Reddit Authentication (#866)
* Fixed Reddit Authentication

* updated fix for build test

* updated buffer to avoid deprecation message

* Updated for passing tests
2021-02-01 10:01:10 +01:00
Manish Chiniwalar
5f51c975c9 docs: Update default ports for support Databases (#839)
https://next-auth.js.org/configuration/databases
2021-02-01 10:01:10 +01:00
Balázs Orbán
0c9104c13a fix(provider): handle no profile image for Spotify (#914)
* chore(deps): upgrade "standard"

* style(lint): run lint fix

* fix(provider): optional chain Spotify provider profile img
2021-02-01 10:01:10 +01:00
Alan Ray
8a6f0944ef fix: update Okta routes (#763)
the current routing for the Okta provider does not follow the standard
set by Okta, and as such doesn't allow for custom subdomains. this
update amends the routes to allow for customer subdomains, and also
aligns next-auth with Okta's documentation.
2021-02-01 10:01:10 +01:00
Daggy1234
672cedc712 fix: ensure Images are produced for discord (#734) 2021-02-01 10:01:10 +01:00
Josh Padnick
05b275956b fix: update nodemailer version in response to CVE. (#860)
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7769 reports a high-severity issue with the current version of nodemailer. This should be merged and released right away if possible.
2021-02-01 10:01:10 +01:00
Pauldic
219b01724b docs: fix typo in callbacks.md (#815)
This is a simple typographical error changed accesed to accessed
2021-02-01 10:01:10 +01:00
Joshua K. Martinez
94b0c68c8f docs: fix discord example code (#850) 2021-02-01 10:01:10 +01:00
James Perkins
81ebff897f docs: update for Now to Vercel (#847)
Vercel archived their now packages a while back, so you can use vercel env pull to pull in the .env
2021-02-01 10:01:10 +01:00
Luke Lau
8ac14ed298 feat: allow react 17 as a peer dependency (#819)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-02-01 10:01:10 +01:00
Balázs Orbán
51cfec92ba feat: simplify NextAuth instantiation (#911) 2021-02-01 10:01:10 +01:00
Balázs Orbán
78fd783bac docs: announce canary docs [skip release] (#1044) 2021-01-04 23:15:23 +01:00
Balázs Orbán
cec46b0d67 chore: add myself to the contributors list 🙈 2020-12-07 22:33:18 +01:00
Balázs Orbán
7bedd4afa9 Revert "feat(provider): Add Azure Active Directory B2C (#809)" (#919)
This reverts commit 6e6a24a7af.
2020-12-06 21:43:13 +01:00
Vladimir Evdokimov
6e6a24a7af feat(provider): Add Azure Active Directory B2C (#809)
* add provider: Microsoft

* documentation

* support no tenant setup

* fix code style

* chore: rename Microsoft provider to AzureADB2C

* chore: alphabetical order in providers/index
2020-12-06 21:34:40 +01:00
Balázs Orbán
6f067be7b0 chore: add reproduction section to questions 2020-12-05 21:40:12 +01:00
Balázs Orbán
e4cc3a92e5 chore: run build GitHub Actions on canary as well 2020-12-05 19:47:09 +01:00
Balázs Orbán
610ab39b40 chore: run build GitHub Action on canary also 2020-12-05 18:53:31 +01:00
Balázs Orbán
7d1168637d chore: fix typo in stalebot comment 2020-12-05 16:30:09 +01:00
Balázs Orbán
b2c1f32057 chore: add link to issue explaining stalebot 2020-12-05 14:19:45 +01:00
Balázs Orbán
9247495fee chore: use stale label, instead of wontfix 2020-12-05 14:15:55 +01:00
Balázs Orbán
341fae28d4 Revert "feat: simplify NextAuth instantiation" (#910)
This reverts commit b86ffa5dd5.
2020-12-05 10:39:03 +01:00
Balázs Orbán
b86ffa5dd5 feat: simplify NextAuth instantiation (#867) 2020-12-05 10:34:32 +01:00
Damien Guard
5415a9c3ab docs: remove redundant 3rd arg to sessions example (#874)
There is no third argument as per 8115a7c66c/src/server/routes/session.js (L82)
2020-12-05 10:15:28 +01:00
sankara
dc516e8be8 docs: fix typo in options.md (#873) 2020-12-05 10:13:43 +01:00
sAy
29a0d9d295 docs: update API docs about server-side getProviders (#879) 2020-12-05 10:12:17 +01:00
Balázs Orbán
5f5174f6e2 refactor: define _getSession in useEffect scope (#724) 2020-12-05 10:00:30 +01:00
Dennis Morello
424b4ee257 Update apple.md
Fixed a typo
2020-12-05 09:57:29 +01:00
Balázs Orbán
545a7e752e feat: forward auth params from signin to provider (#823) 2020-12-05 09:54:27 +01:00
Balázs Orbán
c564b84182 chore: add stalebot configuration 2020-12-05 09:37:27 +01:00
Siddharth Sharma
0db233d208 Add Sign in With Apple Tutorial (#896)
Co-authored-by: Iain Collins <me@iaincollins.com>
2020-12-02 11:13:53 +00:00
Yana Agun Siswanto
5126f4e342 CONTRIBUTING.md: Fix spelling and other issue (#810)
Co-authored-by: Iain Collins <me@iaincollins.com>
2020-10-30 17:09:42 +00:00
Michael McQuade
e09dfc6a7f Change definitively typed to definitely typed (#813) 2020-10-30 17:08:12 +00:00
Iain Collins
ccfa1d55bb Update errors.md 2020-10-30 14:18:25 +00:00
ndom91
d7dc7b0753 add: ndom91 dev.to tutorial 2020-10-27 15:39:30 +01:00
Alex Vilchis
0407e130c4 Fix grammar (#801) 2020-10-27 10:12:07 +00:00
Kristóf Poduszló
64084d634b fix: capitalization of errors in url params (#795) 2020-10-24 01:55:01 +01:00
Iain Collins
438a737837 Update providers.md 2020-10-20 11:54:19 +01:00
Iain Collins
a482a64f10 Update providers.md 2020-10-19 16:29:09 +01:00
Iain Collins
2227d34725 Update documentation (#786)
* Improved homepage and getting started guide
* Improved experience viewing docs on mobile devices
* Fixed typos
2020-10-19 14:19:09 +01:00
Jesus Castro (Tony)
9c6ef951a1 Typo: retuning should be returning
It ain't much but its honest work
2020-10-08 15:45:41 +01:00
Kristóf Poduszló
01c897f23e Correct typo of 'column' in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
ea65d87d07 Correct tip about adapters in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
8d1e479d12 Correct mistakenly duplicated word in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
435b630849 Correct missing word in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
773c74a756 Correct a typo in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
6867bc92c8 Correct two typos of the same word in the docs 2020-10-07 07:48:48 +01:00
Kristóf Poduszló
eb6a4c46d9 Correct a typo in the docs 2020-10-06 20:50:28 +01:00
Iain Collins
cd3d2a138b Update tutorials and explainers 2020-10-06 13:50:42 +01:00
Iain Collins
0c356456bb Minor style changes 2020-10-06 13:50:42 +01:00
Iain Collins
6d44a34f7d Reduce image size 2020-10-06 13:50:42 +01:00
Iain Collins
7bda639361 Update branding (icons, homepage) 2020-10-06 13:50:42 +01:00
Francis Udeji
40e453076e Remove arrow syntax
Remove arrow syntax from regular function declaration
2020-10-02 17:34:47 +01:00
Daniel Jahodka
e065552784 Fix missing response_type=code in battlenet
Battle net's /oauth/authorize requires response_type query parameter. For authorization, this must be set to code.
2020-09-30 00:34:00 +01:00
Lluis Agusti
a3104a009c docs(readme): mention Typescript support 2020-09-25 17:38:23 +01:00
Iain Collins
e9eb6bc57e Add conditional to integration test workflow 2020-09-25 17:36:33 +01:00
Blocksmith
95e31b46af Update testing-with-cypress.md (#680)
misspelling depency cypress-social-logins
2020-09-24 13:52:50 +01:00
Jimmy Merritello
d5e70323f0 Update documentation for getProviders 2020-09-24 13:51:29 +01:00
S. Suzuki
4e4d1eac28 Update links to Slack documentation 2020-09-24 13:47:19 +01:00
Iain Collins
15316f069e Update FAQ to address automatic account linking (#702) 2020-09-24 13:45:45 +01:00
Wédney Yuri
e6995d21cd Update mongodb.md (#673)
Fix typo: MonogDB => MongoDB

Co-authored-by: Lori Karikari <lori.karikari@gmail.com>
2020-09-14 11:08:27 +02:00
Lance Hasson
433f096a63 jst -> jwt (#668)
Co-authored-by: Lori Karikari <lori.karikari@gmail.com>
2020-09-14 11:06:13 +02:00
Lance Hasson
9f487593fa add missing comma in type object (#669) 2020-09-14 11:04:16 +02:00
Matthieu
65caaa6c4c [providers]: Add Atlassian (#664)
* Atlassian provider

* rollback indentation

* fix alphabetical order

* add missing entry in menu sidebar
2020-09-12 19:49:21 +02:00
Iain Collins
0adfba8c5c Improve Puppeteer configuration (#658)
* Centralises configuration for Puppeteer used in tests to make it easier to maintain.
* Adds support for running tests on ARM, so we can use Raspberry Pi test runners off the cloud to get around block lists.
* Includes improved stealth mode to avoid detection which breaks integration tests.
2020-09-11 01:41:02 +01:00
Ugo Onali
2f0f738e2e Fix typo in Prisma adapter doc (#629)
Co-authored-by: Iain Collins <me@iaincollins.com>
2020-09-08 17:26:40 +01:00
Iain Collins
1777a87be3 Increase slow times for integration tests to 5 seconds
This seems like a reasonable theshold for these  integration tests.
2020-09-08 13:12:19 +01:00
Iain Collins
e94fd3b484 Fix typo in package.json 2020-09-08 12:55:26 +01:00
Iain Collins
3b40335202 Add full end-to-end integration tests
Full end-to-end integration tests for Twitter (OAuth 1) and GitHub (OAuth 2) using Puppeteer and Mocha.

This replaces Cypress tests due to issues with Cypress not being able to run tests against external URLs, which we need for our integration tests.

The integration test runner is hosted outside of GitHub Actions (it cannot be hosted by GitHub or on AWS due to IP access controls placed on sign in by providers like Twitter and GitHub) and so the integration tests may not pass if the test runner is offline. If this happens, tests can be re-run later when the test runner is available.

See Pull Request #641 for details.
2020-09-08 12:41:30 +01:00
John
6d63b74db9 Update faq.md 2020-09-07 11:20:55 +01:00
dependabot[bot]
eb26722833 Bump bl from 2.2.0 to 2.2.1
Bumps [bl](https://github.com/rvagg/bl) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/rvagg/bl/releases)
- [Commits](https://github.com/rvagg/bl/compare/v2.2.0...v2.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 11:29:48 +01:00
Iain Collins
4937047d19 Update GitHub workflows 2020-09-03 23:47:40 +01:00
Iain Collins
4305964864 Fix linting errors & ignore Cypress tests for now 2020-09-03 23:47:40 +01:00
Iain Collins
91d93fb8fd Integrate Cypress with Docker and GitHub Actions 2020-09-03 23:47:40 +01:00
Iain Collins
e2e28fcfd0 Update package-lock.json 2020-09-03 23:47:40 +01:00
Iain Collins
66afc69a57 Update workflow to run test app 2020-09-03 23:47:40 +01:00
Iain Collins
3046691119 Pass env vars through to test app
Using env vars for options we want to test means we can stop and restart the app to test different options without needing to rebuild the image.
2020-09-03 23:47:40 +01:00
Iain Collins
88b87a53ff Fix linter errors 2020-09-03 23:47:40 +01:00
Iain Collins
f1ae26efb6 Add Dockerfile to run build inside a container
Adds commands to start/rebuild/stop a Docker image of a sample Next.js app that loads the latest build of NextAuth.js from the current directory.

* `npm run test:app:start`
* `npm run test:app:rebuild`
* `npm run test:app:stop`

It is intended for further development for automated testing.

### About the build process

* The Dockerfile uses a multi-stage build process to optimise build performance, but the nature of the process is slow.
* Build times vary depending on computer speed and internet connection.
* Inital build times are slow (it may take 10 minutes or more).
* Subsequent builds on the same computer should be faster (1 minute or less).
* To ensure the package.json is valid, modules required in the next-auth package.json file are re-downloaded* on every build.
* A Docker compose file is used to allow us to extend the test app to run it again multiple databases.

Subsequent updates may look to improve performance, but it's important checks like checking package.json is valid and running the build in isolation are performed.
2020-09-03 23:47:40 +01:00
Matheus Calegaro
ba83685a86 docs(email): fix typo (#628) 2020-09-03 13:23:49 +01:00
Francis Udeji
d514733f13 Remove arrow syntax from examples on pages docs 2020-09-03 11:45:40 +01:00
Jefferson Bledsoe
15cd608b19 Add initial end-to-end tests (#298)
* Add cypress, testing-library/cypress and server dev helper to package dev dependencies

* Add initial signin test and placeholder cypress files

* Add initial signout tests

* Add initial verify-request test

* Move page-only tests into a 'pages' directory

* Add an invalid email signup workflow test

* Use home-page sign in button for email workflow

* Some tests to check that clicking the button takes the user to the correct OAuth page (warning: fragile!)

* Add a couple of npm scripts to make it easier to run/ developer e2e tests

Co-authored-by: Iain Collins <me@iaincollins.com>
2020-09-03 11:41:11 +01:00
Anish
08d7f5d778 Set Discord to Prompt = None (#605)
* Update discord.js

* Migrating from discordapp.com to discord.com
2020-09-01 10:42:10 +01:00
Francis Udeji
a2ba7e9229 Fix typo in options.md file 2020-09-01 10:40:42 +01:00
Iain Collins
7c71a15699 Fix getUserByProviderAccountId in Prisma adapter
Resolves #559
2020-09-01 10:39:38 +01:00
Mr D
351b804606 Adding 'nextauth' user as sysadmin. 2020-08-31 17:55:03 +01:00
Mr D
8f0501b7fe standardize the behaviour of the Docker images 2020-08-31 17:55:03 +01:00
Josh Pollock
73d21e66dd adjust URL for warnings in logger to match docs. (#593)
Fixes #592
2020-08-29 12:49:39 +02:00
youpy
6310311d52 Fix error when profile image is not set (#612) 2020-08-29 12:45:29 +02:00
Esteban Dalel R
d0caba1933 Update pages.md (#585) 2020-08-29 12:36:13 +02:00
Madusha Prasanjith
2f3291e48f [providers]: Add FusionAuth provider (#599)
* Add FusionAuth provider

* Fix issue with FusionAuth docs.
2020-08-29 12:35:13 +02:00
Ray Ma
43d8e3b894 [providers]: Updating Discord provider domain (#590)
Discord is migrating to discord.com, including their OAuth2 API routes. Support for the old domain, discordapp.com, will be dropped on 7 Nov 2020.

Note that the cdn.discordapp.com domain is unchanged. This is intentional, as the cdn domain will not be migrated due to technical restraints on Discord's side.
2020-08-29 12:32:59 +02:00
Nick Noble
5d4eb5d4e0 Fix some typos (#606) 2020-08-29 12:27:43 +02:00
S. Suzuki
7ccdec22cb Update slack.md (#617)
Fix code
2020-08-29 12:23:42 +02:00
Liam norris
2ea64045cb Documentation typos (#575)
Am I misreading this?  These changes should make the parameter to attribute mapping less ambiguous...
2020-08-13 14:34:13 +01:00
Iain Collins
daf97d298d Create SECURITY.md 2020-08-13 12:19:17 +01:00
Iain Collins
ababc7ecdb Merge branch 'main' of https://github.com/nextauthjs/next-auth into main 2020-08-13 12:00:05 +01:00
Iain Collins
33e72b2ae1 Update issue templates 2020-08-13 12:00:02 +01:00
Nico Domino
bf5716c674 Add: ldap auth tutorial example (#566)
* add: ldap auth tutorial example

* update: tutorials page list

* update: NEXTAUTH_SECRET

* Update tutorials.md

Co-authored-by: Iain Collins <me@iaincollins.com>
2020-08-11 13:33:44 +01:00
Iain Collins
c17a3b94f5 Update typeorm-custom-models.md 2020-08-11 12:38:36 +01:00
Iain Collins
19a9c313e0 Delete node.js.yml 2020-08-11 02:29:15 +01:00
Iain Collins
68043e65e4 Create test-build.yml 2020-08-11 02:28:50 +01:00
dependabot[bot]
a6ec60284d Bump prismjs from 1.20.0 to 1.21.0 in /www (#560)
Bumps [prismjs](https://github.com/PrismJS/prism) from 1.20.0 to 1.21.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.20.0...v1.21.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-08-11 01:11:54 +01:00
Nico Domino
ff79c4b95b add: warnings page (#567) 2020-08-11 01:11:18 +01:00
Iain Collins
9c4e41a4c6 Update callbacks.md 2020-08-10 16:16:56 +01:00
Bowen
07ef3d59c5 Update pages.md (#534)
Correct typos with getInitialProps

Co-authored-by: Iain Collins <me@iaincollins.com>
2020-08-06 01:07:10 +01:00
Eli José Carrasquero
4fe7162652 Update pages.md 2020-08-06 01:02:49 +01:00
Alex Cory
950a937633 Update callbacks.md
I was getting this error due to it being a string value.
```sh
[next-auth][error][jwt_session_error] JWTClaimInvalid: "auth_time" claim must be a JSON numeric value
    at isTimestamp (/Users/alex/code/trufans/node_modules/jose/lib/jwt/verify.js:24:11)
    at validateTypes (/Users/alex/code/trufans/node_modules/jose/lib/jwt/verify.js:159:3)
    at Object.module.exports [as verify] (/Users/alex/code/trufans/node_modules/jose/lib/jwt/verify.js:236:3)
    at Object.<anonymous> (/Users/alex/code/trufans/node_modules/next-auth/dist/lib/jwt.js:100:30)
    at Generator.next (<anonymous>)
    at asyncGeneratorStep (/Users/alex/code/trufans/node_modules/next-auth/dist/lib/jwt.js:22:103)
    at _next (/Users/alex/code/trufans/node_modules/next-auth/dist/lib/jwt.js:24:194)
    at /Users/alex/code/trufans/node_modules/next-auth/dist/lib/jwt.js:24:364
    at new Promise (<anonymous>)
    at Object.<anonymous> (/Users/alex/code/trufans/node_modules/next-auth/dist/lib/jwt.js:24:97) {
  code: 'ERR_JWT_CLAIM_INVALID',
  claim: 'auth_time',
  reason: 'invalid'
}
```
2020-08-06 01:01:57 +01:00
Nick Parks
1cc31def3e Update Google Provider example to be functional
Currently the Google Provider example will always fail due to checking for `email_verified` when the correct response from the server is `verified_email`

next-auth debug output for validation:

```
[next-auth][debug][profile_data] {
  id: 'XXXXXXX',
  email: 'nick@example',
  verified_email: true,
  name: 'Nick Parks',
  given_name: 'Nick',
  family_name: 'XXXX',
  picture: 'XXXX,
  locale: 'en',
  hd: 'example.com'
}
```
2020-08-06 01:00:45 +01:00
dependabot[bot]
92f53c532b Bump elliptic from 6.5.2 to 6.5.3 in /www
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-05 20:49:07 +01:00
Iain Collins
c6d6d9c002 Update documentation for Google 2020-07-31 13:06:22 +01:00
Iain Collins
85b859231c Update documentation 2020-07-31 10:17:07 +01:00
Iain Collins
ea093dc0fc Merge branch 'main' of github.com:iaincollins/next-auth into main 2020-07-31 10:01:04 +01:00
Iain Collins
cd61178f44 Bump version number to 3.1.0 2020-07-31 10:00:56 +01:00
Iain Collins
eb53219cbd Update website and documentation for mssql 2020-07-31 09:39:24 +01:00
Iain Collins
18d70ffbe9 Fix linting 2020-07-31 09:39:24 +01:00
Iain Collins
bdcf823d26 Prevent warning when using mssql conection string 2020-07-31 09:39:24 +01:00
Iain Collins
3aeba2aa09 Allow duplicate NULL email address in MSSQL 2020-07-31 09:39:24 +01:00
Mr D
0793e2c8d8 mssql: back to SnakeCaseNamingStrategy 2020-07-31 09:39:24 +01:00
Mr D
0f01279c91 mssqlTransform 2020-07-31 09:39:24 +01:00
Mr D
8fa9d00958 mssql support 2020-07-31 09:39:24 +01:00
Deeptesh Chagan
ab6ef8a19c re-sync entities on connection if changed in development 2020-07-31 00:26:21 +01:00
Iain Collins
8d68807bfe Remove node-jose dependency
This is an unused dependancy.

`jose` is used instead.
2020-07-30 23:33:29 +01:00
Ben Fox
35fc38c328 More explicit wording 2020-07-30 16:49:18 +01:00
Ben Fox
85eeda5755 Remove comments from config 2020-07-30 16:49:18 +01:00
Ben Fox
2e52c500a1 Add note to local environment setup on hot-reloading server files 2020-07-30 16:49:18 +01:00
Marc
5886f9bea8 Add Provider: Basecamp (#511)
Changes Include:
• Added Basecamp as a provider
• Added Basecamp provider to documentation
• Bumped Version to 3.0.1 in package-lock
2020-07-30 15:12:26 +02:00
Iain Collins
c497dcba26 Update options.md 2020-07-30 09:27:34 +01:00
Ben Silverman
493c45a864 Fix typo in typeorm-custom-models.md
Adapter expects `customModels` property, but doc uses `models`.
2020-07-29 23:10:16 +01:00
Iain Collins
b243b26a3d Update faq.md 2020-07-29 11:11:51 +01:00
Zeb Pykosz
1d0749970a fix(docs): remove arrow from function example 2020-07-29 09:23:02 +01:00
227 changed files with 29372 additions and 6583 deletions

4
.dockerignore Normal file
View File

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

15
.env.example Normal file
View File

@@ -0,0 +1,15 @@
# 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=

15
.env.local.example Normal file
View File

@@ -0,0 +1,15 @@
# Rename file to .env.local and populate values
# to be able to run the dev app
NEXTAUTH_URL=http://localhost:3000
SECRET= Linux: `openssl rand -hex 32` or https://generate-secret.now.sh/32
AUTH0_ID=
AUTH0_DOMAIN=
AUTH0_SECRET=
GITHUB_ID=
GITHUB_SECRET=
TWITTER_ID=
TWITTER_SECRET=

View File

@@ -6,23 +6,25 @@ assignees: ''
---
**Describe the bug**
A clear and concise description of what the defect is.
A clear and concise description of the bug in NextAuth.js.
**To Reproduce**
Do not report bugs with your own project here, ask from help by raising a question instead - this helps us a lot with administration overhead.
**Steps to reproduce**
Steps to reproduce the behavior.
Include example code (or link to public repository) which can be used to reproduce the behaviour.
Include a link to public repository which can be used to reproduce the behaviour.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or error logs**
If applicable, add screenshots or error logs to help explain the problem.
If applicable add screenshots or error logs to help explain the problem.
**Additional context**
Add any other context about the problem here.
**Documentation feedback**
**Feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -5,8 +5,6 @@ labels: enhancement
assignees: ''
---
*Please stick to one distinct feature request per issue where possible and raise additional feature quests as separate issues. Try to avoid adding feature requests to existing issues in the comments of issues raised by other users.*
**Summary of proposed feature**
A clear and concise description of the feature being proposed.

View File

@@ -4,16 +4,18 @@ about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ''
---
*Please refer to the [documentation](https://next-auth.js.org/getting-started/introduction), the [example project](https://github.com/iaincollins/next-auth-example) and existing issues before creating a new issue.*
<!-- 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.
<!-- A clear and concise question. -->
**What are you trying to do**
A description of what you are trying to do.
<!-- A description of what you are trying to do, for context. -->
**Documentation feedback**
**Reproduction**
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
**Feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful

41
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,41 @@
<!--
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
Please make sure that you are familiar with and follow the Code of Conduct for
this project (found in the CODE_OF_CONDUCT.md file).
Also, please make sure you're familiar with and follow the instructions in the
contributing guidelines (found in the CONTRIBUTING.md file).
If you're new to contributing to open source projects, you might find this free
video course helpful: https://kcd.im/pull-request
Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
**What**:
<!-- Why are these changes necessary? -->
**Why**:
<!-- How were these changes implemented? -->
**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" -->
- [ ] 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 -->

21
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
test:
- test/**/*
documentation:
- www/**/*
- ./**/*.md
providers:
- src/providers/**/*
- www/docs/configuration/providers.md
- test/integration/**/*
adapters:
- src/adapters/**/*
- www/docs/schemas/adapters.md
databases:
- www/docs/schemas/*.md
- test/docker/databases/**/*
- www/docs/configuration/databases.md
- test/fixtures/**/*

24
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- priority
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
Hi there! It looks like this issue hasn't had any activity for a while.
It will be closed if no further activity occurs. If you think your issue
is still relevant, feel free to comment on it to keep it open. (Read more at #912)
Thanks!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: >
Hi there! It looks like this issue hasn't had any activity for a while.
To keep things tidy, I am going to close this issue for now.
If you think your issue is still relevant, just leave a comment
and I will reopen it. (Read more at #912)
Thanks!

31
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# 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: Build Test
on:
push:
branches:
- main
- canary
pull_request:
branches:
- main
- canary
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- run: npm run lint

55
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: Integration Test
on:
push:
branches: [ main, canary ]
pull_request:
branches: [ main, canary ]
jobs:
test:
# Only run tests integration against Pull Requests from branches in
# this repository. We do this as integration tests require access to
# secrets in GitHub and they are not exposed to tests run against
# forks (for security reasons), so integration test against
# Pull Requests from external repos just fail and generate noise.
if: github.event.pull_request.head.repo.full_name == github.repository
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
# fail due to IP Address checks done by providers, which enforce
# CAPTCHA checks on login request from cloud compute IP addresses to
# prevent abuse.
runs-on: self-hosted
# Target time is under 5 minutes to run all tests. If it takes longer than
# 10 minutes should look at running tests in parallel. No individual flow
# should take longer than 5 minutes to build and run.
timeout-minutes: 10
strategy:
matrix:
node-version: [12.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
# Install dependencies
- run: npm ci
# 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}}

12
.github/workflows/labeler.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
sync-labels: true

View File

@@ -1,29 +0,0 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x, 12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build --if-present
- run: npm test

View File

@@ -1,36 +0,0 @@
# Publishes module to registry when a new release is created.
#
# The following secrets need to be configured for this workflow:
#
# * NPM_TOKEN - Auth token from npmjs.com
name: Publish to NPM
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
- run: npm ci
- run: npm test
publish-npm:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: 12
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}

30
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Release
on:
push:
branches:
- main
- canary
jobs:
release:
name: Release
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 12
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npx semantic-release

39
.gitignore vendored
View File

@@ -1,20 +1,7 @@
.next
.env
.vscode
node_modules
dist
.DS_Store# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
@@ -24,10 +11,26 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docusaurus
www/build
# Dependencies
node_modules
#VS
# Build dirs
.next
/build
/dist
/www/build
# Generated files
.docusaurus
.cache-loader
.next
# VS
/.vs/slnx.sqlite-journal
/.vs/slnx.sqlite
/.vs
.vscode
# GitHub Actions runner
/actions-runner
/_work

39
.releaserc.json Normal file
View File

@@ -0,0 +1,39 @@
{
"branches": [
"main",
{ "name": "canary", "prerelease": true }
],
"plugins": [
["@semantic-release/commit-analyzer", {
"preset": "conventionalcommits",
"releaseRules": [
{ "breaking": true, "release": "major" },
{ "revert": true, "release": "patch" },
{ "type": "feat", "release": "minor" },
{ "type": "fix", "release": "patch" },
{ "type": "perf", "release": "patch" },
{ "type": "docs", "release": "patch" }
]
}],
["@semantic-release/release-notes-generator", {
"preset": "conventionalcommits",
"presetConfig": {
"types": [
{ "type": "feat", "section": "Features", "hidden": false },
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
{ "type": "revert", "section": "Reverts", "hidden": false },
{ "type": "docs", "section": "Documentation", "hidden": false },
{ "type": "style", "section": "Styles", "hidden": false },
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
{ "type": "test", "section": "Tests", "hidden": false },
{ "type": "build", "section": "Build System", "hidden": false },
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
]
}
}],
"@semantic-release/github",
"@semantic-release/npm"
]
}

5
CHANGELOG.md Normal file
View File

@@ -0,0 +1,5 @@
# CHANGELOG
The changelog is automatically updated using
[semantic-release](https://github.com/semantic-release/semantic-release). You
can see it on the [releases page](../../releases).

View File

@@ -2,108 +2,118 @@
Contributions and feedback on your experience of using this software are welcome.
This includes bug reports, feature requests, ideas, pull requests and examples of how you have used this software.
This includes bug reports, feature requests, ideas, pull requests, and examples of how you have used this software.
Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates configured in GitHub when reporting bugs, requesting enhancements or contributing code.
Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates configured in GitHub when reporting bugs, requesting enhancements, or contributing code.
Please raise any significant new functionality or breaking change an issue for discussion before raising a Pull Request for it.
## Pull Requests
## For contributors
* The latest changes are always in `main`
* Pull Requests should be raised for larger changes
* Pull Requests do not need approval before merging for those with contributor access (it's just helpful to have them to track changes)
* Rebasing in Pull Requests is prefered to keep a clean commit history (see below)
* Running `npm run lint:fix` before committing can make resolving conflicts easier, but is not required
* Merge commits (and pushing merge commits to `main`) are disabled in this repo; but commits in PR can be squashed so this is not a blocker
* Pushing directly to main should ideally be reserved for minor updates (e.g. correcting typos) or small single-commit fixes
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
### Pull Requests
## Rebasing
* The latest changes are always in `canary`, 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
* Rebasing in Pull Requests is preferred to keep a clean commit history (see below)
* 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 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
*If you don't rebase and end up with merge commits in a PR then it's not a blocker, we can alway squash the commits when merging!*
### Setting up local environment
If you create a branch and there are conflicting updates in the `main` branch, you can resolve them by rebasing from a check out of your branch:
git fetch
git rebase origin/main
If there are any conflicts, you can resolve them and stage the files, then run:
git rebase --continue
*If there are a lot of changes you may be prompted to step more than once.*
When the rebase is complete (i.e. there are no more conflicts) you should push your changes to your branch before doing anyhing else:
git push --force-with-lease
You should see that any conflicts in your PR are now resolved. You can review changes to make sure it contains changes you intended to make.
*If you accidentally sync before pushing, it will trigger a merge. Uou can use `git merge --abort` to undo the merge.*
You can use `npm run lint:fix` to automatically apply Standard JS rules to resolve formatting differences (tabs vs spaces, line endings, etc).
## Setting up local environment
A quick and dirty guide on how to setup *next-auth* locally to work on it and test out any changes:
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
1. Clone the repo:
```sh
git clone git@github.com:nextauthjs/next-auth.git
cd next-auth
```
git clone git@github.com:iaincollins/next-auth.git
cd next-auth/
2. Install packages:
```sh
npm i
```
2. Install packages and run the build command:
3. Populate `.env.local`:
Copy `.env.local.example` to `.env.local`, and add your env variables for each provider you want to test.
npm i
npm run build
> 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`.
3. Link your project back to your local copy of next auth:
1. Start the dev application/server and CSS watching:
```sh
npm run dev
```
cd ../your-application
npm link ../next-auth
Your dev application will be available on ```http://localhost:3000```
4. Finally link React between the repo and the version installed in your project:
cd ../next-auth
npm link ../your-application/node_modules/react
*This is an annoying step and not obvious, but is needed because of how React has been written (otherwise React crashes when you try to use the `useSession()` hook in your project).*
That's it!
Notes: You may need to repeat both `npm link` steps if you install / update additional dependancies with `npm i`.
That's it! 🎉
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
### Hot reloading
#### Hot reloading
You might find it helpful to use the `npm run watch` command in the next-auth project, which will automatically (and silently) rebuild JS and CSS files as you edit them.
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.
cd next-auth/
npm run watch
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
If you are working on `next-auth/src/client/index.js` hot reloading will work as normal in your Next.js app.
#### Databases
However if you are working on anything else (e.g. `next-auth/src/server/*` etc) then you will need to *stop and start* your app for changes to apply as **Next.js will not hot reload those changes**.
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.
### Databases
Included is a Docker Compose file that starts up MySQL, Postgres and MongoDB databases on localhost.
It will use port 3306, 5432 and 27017 on localhost respectively; it will not work if are running existing databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
You will need Docker installed to be able to start / stop the databases.
You will need Docker and Docker Compose installed to be able to start / stop the databases.
When stop the databases, it will reset their contents.
When stopping the databases, it will reset their contents.
### Testing
#### Testing
Tests can be run with `npm run test`.
Automated tests are currently crude and limited in functionality, but improvements are in development.
Currently to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
## For maintainers
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
When accepting Pull Requests, make sure the following:
* Use "Squash and merge"
* Make sure you merge contributor PRs into `canary`
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
### Recommended Scopes
A typical conventional commit looks like this:
```
type(scope): title
body
```
Scope is the part that will help groupping the different commit types in the release notes.
Some recommened scopes are:
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
### Skipping a release
Every commit that contains [skip release] or [release skip] in their message will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.

30
Dockerfile Normal file
View File

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

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2018-2020, Iain Collins
Copyright (c) 2018-2021, Iain Collins
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above

View File

@@ -1,4 +1,20 @@
# NextAuth.js
<p align="center">
<br/>
<a href="https://next-auth.js.org" target="_blank"><img width="150px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>
<h3 align="center">NextAuth.js</h3>
<p align="center">Authentication for Next.js</p>
<p align="center">
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
</p>
</p>
## Overview
@@ -6,9 +22,15 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
It is designed from the ground up to support Next.js and Serverless.
[Follow the examples](https://next-auth.js.org/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
## Getting Started
Install: `npm i next-auth`
```
npm install --save next-auth
```
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
@@ -29,7 +51,7 @@ NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)
@@ -47,6 +69,16 @@ NextAuth.js can be used with or without a database.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
### TypeScript
You can install the appropriate types via the following command:
```
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.
## Example
### Add API Route
@@ -55,7 +87,7 @@ Advanced options allow you to define your own routines to handle controlling wha
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
export default NextAuth({
providers: [
// OAuth authentication providers
Providers.Apple({
@@ -74,45 +106,51 @@ const options = {
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL
}
export default (req, res) => NextAuth(req, res, options)
})
```
### Add React Component
```javascript
import React from 'react'
import {
useSession,
signin,
signout
import {
useSession, signIn, signOut
} from 'next-auth/client'
export default () => {
export default function Component() {
const [ session, loading ] = useSession()
return <p>
{!session && <>
Not signed in <br/>
<button onClick={signin}>Sign in</button>
</>}
{session && <>
if(session) {
return <>
Signed in as {session.user.email} <br/>
<button onClick={signout}>Sign out</button>
</>}
</p>
<button onClick={() => signOut()}>Sign out</button>
</>
}
return <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>
}
```
## Acknowledgement
## Acknowledgements
[NextAuth.js is possible thanks to its contributors.](https://next-auth.js.org/contributors)
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
## Getting started
[Follow the examples to get started.](https://next-auth.js.org/getting-started/example)
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
</a>
<div>
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
</a>
</div>
<div>
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
</div>
## Contributing
If you'd like to contribute to you can find useful information in our [Contributing Guide](https://github.com/iaincollins/next-auth/blob/main/CONTRIBUTING.md).
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
## License
ISC

24
SECURITY.md Normal file
View File

@@ -0,0 +1,24 @@
# Security Policy
NextAuth.js practices responsible disclosure.
## Supported Versions
Security updates are only released for the current version.
Old releases are not maintained and do not receive updates.
## Reporting a Vulnerability
We request that you contact us directly to report serious issues that might impact the security of sites using NextAuth.js.
If you contact us regarding a serious issue:
* We will endeavor to get back to you within 72 hours.
* We will aim to publish a fix within 30 days.
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publically.
Currently, the best way to report an issue is by emailing me@iaincollins.com
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.

View File

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

View File

@@ -0,0 +1,16 @@
import { signIn } from 'next-auth/client'
export default function AccessDenied () {
return (
<>
<h1>Access Denied</h1>
<p>
<a href="/api/auth/signin"
onClick={(e) => {
e.preventDefault()
signIn()
}}>You must be signed in to view this page</a>
</p>
</>
)
}

18
components/footer.js Normal file
View File

@@ -0,0 +1,18 @@
import Link from 'next/link'
import styles from './footer.module.css'
import { version } from 'package.json'
export default function Footer () {
return (
<footer className={styles.footer}>
<hr />
<ul className={styles.navItems}>
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
<li className={styles.navItem}><em>{version}</em></li>
</ul>
</footer>
)
}

View File

@@ -0,0 +1,14 @@
.footer {
margin-top: 2rem;
}
.navItems {
margin-bottom: 1rem;
padding: 0;
list-style: none;
}
.navItem {
display: inline-block;
margin-right: 1rem;
}

103
components/header.js Normal file
View File

@@ -0,0 +1,103 @@
import Link from 'next/link'
import { signIn, signOut, useSession } from 'next-auth/client'
import * as client from 'next-auth/client'
import styles from './header.module.css'
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header () {
const [session, loading] = useSession()
return (
<header>
<noscript>
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
}`}
>
{!session && (
<>
<span className={styles.notSignedInText}>
You are not signed in
</span>
<a
href="/api/auth/signin"
className={styles.buttonPrimary}
onClick={(e) => {
e.preventDefault()
signIn()
}}
>
Sign in
</a>
</>
)}
{session && (
<>
{session.user.image && (
<span
style={{ backgroundImage: `url(${session.user.image})` }}
className={styles.avatar}
/>
)}
<span className={styles.signedInText}>
<small>Signed in as</small>
<br />
<strong>{session.user.email || session.user.name}</strong>
</span>
<a
href="/api/auth/signout"
className={styles.button}
onClick={(e) => {
e.preventDefault()
signOut()
}}
>
Sign out
</a>
</>
)}
</p>
</div>
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href='/'>
<a>Home</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/client'>
<a>Client</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/server'>
<a>Server</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected'>
<a>Protected</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected-ssr'>
<a>Protected(SSR)</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/api-example'>
<a>API</a>
</Link>
</li>
</ul>
</nav>
</header>
)
}

View File

@@ -0,0 +1,92 @@
/* Set min-height to avoid page reflow while session loading */
.signedInStatus {
display: block;
min-height: 4rem;
width: 100%;
}
.loading,
.loaded {
position: relative;
top: 0;
opacity: 1;
overflow: hidden;
border-radius: 0 0 .6rem .6rem;
padding: .6rem 1rem;
margin: 0;
background-color: rgba(0,0,0,.05);
transition: all 0.2s ease-in;
}
.loading {
top: -2rem;
opacity: 0;
}
.signedInText,
.notSignedInText {
position: absolute;
padding-top: .8rem;
left: 1rem;
right: 6.5rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: inherit;
z-index: 1;
line-height: 1.3rem;
}
.signedInText {
padding-top: 0rem;
left: 4.6rem;
}
.avatar {
border-radius: 2rem;
float: left;
height: 2.8rem;
width: 2.8rem;
background-color: white;
background-size: cover;
background-repeat: no-repeat;
}
.button,
.buttonPrimary {
float: right;
margin-right: -.4rem;
font-weight: 500;
border-radius: .3rem;
cursor: pointer;
font-size: 1rem;
line-height: 1.4rem;
padding: .7rem .8rem;
position: relative;
z-index: 10;
background-color: transparent;
color: #555;
}
.buttonPrimary {
background-color: #346df1;
border-color: #346df1;
color: #fff;
text-decoration: none;
padding: .7rem 1.4rem;
}
.buttonPrimary:hover {
box-shadow: inset 0 0 5rem rgba(0,0,0,0.2)
}
.navItems {
margin-bottom: 2rem;
padding: 0;
list-style: none;
}
.navItem {
display: inline-block;
margin-right: 1rem;
}

14
components/layout.js Normal file
View File

@@ -0,0 +1,14 @@
import Header from 'components/header'
import Footer from 'components/footer'
export default function Layout ({ children }) {
return (
<>
<Header />
<main>
{children}
</main>
<Footer />
</>
)
}

12
config/babel.config.json Normal file
View File

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

View File

@@ -1,6 +1,6 @@
// Serverless target in Next.js does not work if you try to read in files at runtime
// that are not JavaScript or JSON (e.g. CSS files).
// https://github.com/iaincollins/next-auth/issues/281
// https://github.com/nextauthjs/next-auth/issues/281
//
// To work around this issue, this script is a manual step that wraps CSS in a
// JavaScript file that has the compiled CSS embedded in it, and exports only

12
jsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/client": ["./src/client"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"]
}
}
}

14377
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "3.0.1",
"version": "0.0.0-semantically-released",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -8,24 +8,24 @@
"main": "index.js",
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "babel src --out-dir dist",
"build:css": "postcss src/**/*.css --base src --dir dist && node scripts/wrap-css.js",
"build:js": "babel --config-file ./config/babel.config.json 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",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --watch src --out-dir dist",
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
"test": "npm run lint",
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
"watch:js": "babel --config-file ./config/babel.config.json --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",
"db:start": "docker-compose -f test/docker/docker-compose.yml up -d",
"db:start:mongo": "docker-compose -f test/docker/mongo.yml up -d",
"db:start:mysql": "docker-compose -f test/docker/mysql.yml up -d",
"db:start:postgres": "docker-compose -f test/docker/postgres.yml up -d",
"db:stop": "docker-compose -f test/docker/docker-compose.yml down",
"db:stop:mongo": "docker-compose -f test/docker/mongo.yml down",
"db:stop:mysql": "docker-compose -f test/docker/mysql.yml down",
"db:stop:postgres": "docker-compose -f test/docker/postgres.yml down",
"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",
"prepublishOnly": "npm run build",
"publish:beta": "npm publish --tag beta",
"publish:canary": "npm publish --tag canary",
@@ -46,38 +46,59 @@
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^2.2.0",
"node-jose": "^1.1.4",
"nodemailer": "^6.4.6",
"nodemailer": "^6.4.16",
"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.24"
"typeorm": "^0.2.30"
},
"peerDependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.3.0"
"@prisma/client": "^2.12.0"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"@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",
"autoprefixer": "^9.7.6",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"next": "^10.0.5",
"pg": "^8.2.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"standard": "^14.3.3"
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"standard": "^16.0.3"
},
"standard": {
"ignore": [
"test/",
"pages/",
"components/"
]
}
}

31
pages/_app.js Normal file
View File

@@ -0,0 +1,31 @@
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 }) {
return (
<Provider
// Provider options are not required but can be useful in situations where
// you have a short session maxAge time. Shown here with default values.
options={{
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
clientMaxAge: 0,
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0
}}
session={pageProps.session}
>
<Component {...pageProps} />
</Provider>
)
}

17
pages/api-example.js Normal file
View File

@@ -0,0 +1,17 @@
import Layout from '../components/layout'
export default function Page () {
return (
<Layout>
<h1>API Example</h1>
<p>The examples below show responses from the example API endpoints.</p>
<p><em>You must be signed in to see responses.</em></p>
<h2>Session</h2>
<p>/api/examples/session</p>
<iframe src="/api/examples/session"/>
<h2>JSON Web Token</h2>
<p>/api/examples/jwt</p>
<iframe src="/api/examples/jwt"/>
</Layout>
)
}

View File

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

View File

@@ -0,0 +1,9 @@
// This is an example of how to read a JSON Web Token from an API route
import jwt from "next-auth/jwt"
const secret = process.env.SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret, encryption: true })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -0,0 +1,12 @@
// This is an example of to protect an API route
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
} else {
res.send({ error: 'You must be sign in to view the protected content on this page.' })
}
}

View File

@@ -0,0 +1,7 @@
// This is an example of how to access a session from an API route
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
res.send(JSON.stringify(session, null, 2))
}

22
pages/client.js Normal file
View File

@@ -0,0 +1,22 @@
import Layout from '../components/layout'
export default function Page () {
return (
<Layout>
<h1>Client Side Rendering</h1>
<p>
This page uses the <strong>useSession()</strong> React Hook in the <strong>&lt;/Header&gt;</strong> component.
</p>
<p>
The <strong>useSession()</strong> React Hook easy to use and allows pages to render very quickly.
</p>
<p>
The advantage of this approach is that session state is shared between pages by using the <strong>Provider</strong> in <strong>_app.js</strong> so
that navigation between pages using <strong>useSession()</strong> is very fast.
</p>
<p>
The disadvantage of <strong>useSession()</strong> is that it requires client side JavaScript.
</p>
</Layout>
)
}

12
pages/index.js Normal file
View File

@@ -0,0 +1,12 @@
import Layout from 'components/layout'
export default function Page () {
return (
<Layout>
<h1>NextAuth.js Example</h1>
<p>
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
</p>
</Layout>
)
}

30
pages/policy.js Normal file
View File

@@ -0,0 +1,30 @@
import Layout from '../components/layout'
export default function Page () {
return (
<Layout>
<p>
This is an example site to demonstrate how to use <a href={`https://next-auth.js.org`}>NextAuth.js</a> for authentication.
</p>
<h2>Terms of Service</h2>
<p>
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</p>
<h2>Privacy Policy</h2>
<p>
This site uses JSON Web Tokens and an in-memory database which resets every ~2 hours.
</p>
<p>
Data provided to this site is exclusively used to support signing in
and is not passed to any third party services, other than via SMTP or OAuth for the
purposes of authentication.
</p>
</Layout>
)
}

37
pages/protected-ssr.js Normal file
View File

@@ -0,0 +1,37 @@
// This is an example of how to protect content using server rendering
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
export default function Page ({ content, session }) {
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied/></Layout> }
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
</Layout>
)
}
export async function getServerSideProps(context) {
const session = await getSession(context)
let content = null
if (session) {
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) { content = json.content }
}
return {
props: {
session,
content
}
}
}

33
pages/protected.js Normal file
View File

@@ -0,0 +1,33 @@
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
export default function Page () {
const [ session, loading ] = useSession()
const [ content , setContent ] = useState()
// Fetch content from protected route
useEffect(()=>{
const fetchData = async () => {
const res = await fetch('/api/examples/protected')
const json = await res.json()
if (json.content) { setContent(json.content) }
}
fetchData()
},[session])
// When rendering client side don't display anything until loading is complete
if (typeof window !== 'undefined' && loading) return null
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied/></Layout> }
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
</Layout>
)
}

38
pages/server.js Normal file
View File

@@ -0,0 +1,38 @@
import { useSession, getSession } from 'next-auth/client'
import Layout from '../components/layout'
export default function Page () {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
// is used by `useSession()`.
const [ session, loading ] = useSession()
return (
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to render.
</p>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps(context) {
return {
props: {
session: await getSession(context)
}
}
}

30
pages/styles.css Normal file
View File

@@ -0,0 +1,30 @@
body {
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
padding: 0 1rem 1rem 1rem;
max-width: 680px;
margin: 0 auto;
background: #fff;
color: #333;
}
li,
p {
line-height: 1.5rem;
}
a {
font-weight: 500;
}
hr {
border: 1px solid #ddd;
}
iframe {
background: #ccc;
border: 1px solid #ccc;
height: 10rem;
width: 100%;
border-radius: .5rem;
filter: invert(1);
}

View File

@@ -57,7 +57,7 @@ const Adapter = (config) => {
async function getUser (id) {
debug('GET_USER', id)
try {
return prisma[User].findOne({ where: { id } })
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))
@@ -68,7 +68,7 @@ const Adapter = (config) => {
debug('GET_USER_BY_EMAIL', email)
try {
if (!email) { return Promise.resolve(null) }
return prisma[User].findOne({ where: { email } })
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))
@@ -78,7 +78,9 @@ const Adapter = (config) => {
async function getUserByProviderAccountId (providerId, providerAccountId) {
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
try {
return prisma[Account].findOne({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
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))
@@ -172,7 +174,7 @@ const Adapter = (config) => {
async function getSession (sessionToken) {
debug('GET_SESSION', sessionToken)
try {
const session = await prisma[Session].findOne({ where: { sessionToken } })
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) {
@@ -217,7 +219,7 @@ const Adapter = (config) => {
}
const { id, expires } = session
return prisma[Session].update({ where: { id }, data: { expires } })
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))
@@ -278,7 +280,7 @@ const Adapter = (config) => {
// 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].findOne({ where: { token: hashedToken } })
const verificationRequest = await prisma[VerificationRequest].findUnique({ where: { token: hashedToken } })
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
// Delete verification entry so it cannot be used again

View File

@@ -7,6 +7,7 @@ import adapterConfig from './lib/config'
import adapterTransform from './lib/transform'
import Models from './models'
import logger from '../../lib/logger'
import { updateConnectionEntities } from './lib/utils'
const Adapter = (typeOrmConfig, options = {}) => {
// Ensure typeOrmConfigObject is normalized to an object
@@ -68,6 +69,10 @@ const Adapter = (typeOrmConfig, options = {}) => {
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

View File

@@ -25,6 +25,7 @@ const parseConnectionString = (configString) => {
config.username = parsedUrl.username
config.password = parsedUrl.password
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
config.options = {}
}
// This option is recommended by mongodb
@@ -32,6 +33,11 @@ const parseConnectionString = (configString) => {
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('=')

View File

@@ -74,14 +74,15 @@ const mongodbTransform = (models, options) => {
// 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
models.User.schema.indices = [
{
name: 'email',
unique: true,
sparse: true,
columns: ['email']
}
]
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) => {
@@ -107,6 +108,37 @@ const sqliteTransform = (models, options) => {
}
}
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')) ||
@@ -121,6 +153,9 @@ export default (config, 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.

View File

@@ -0,0 +1,18 @@
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

@@ -48,7 +48,7 @@ if (typeof window !== 'undefined') {
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message.event && message.event === 'session' && message.data) {
if (message?.event === 'session' && message.data) {
// Ignore storage events fired from the same window that created them
if (__NEXTAUTH._clientId === message.clientId) {
return
@@ -67,9 +67,20 @@ if (typeof window !== 'undefined') {
}
})
// Listen for window focus/blur events
window.addEventListener('focus', async (event) => __NEXTAUTH._getSession({ event: 'focus' }))
window.addEventListener('blur', async (event) => __NEXTAUTH._getSession({ event: 'blur' }))
// Listen for document visibilitychange events
let hidden, visibilityChange
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden'
visibilityChange = 'visibilitychange'
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden'
visibilityChange = 'msvisibilitychange'
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden'
visibilityChange = 'webkitvisibilitychange'
}
const handleVisibilityChange = () => !document[hidden] && __NEXTAUTH._getSession({ event: visibilityChange })
document.addEventListener('visibilitychange', handleVisibilityChange, false)
}
}
@@ -104,7 +115,7 @@ const setOptions = ({
}
// Universal method (client + server)
const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
export const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getSession() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
@@ -142,7 +153,7 @@ const getProviders = async () => {
const SessionContext = createContext()
// Client side method
const useSession = (session) => {
export const useSession = (session) => {
// Try to use context if we can
const value = useContext(SessionContext)
@@ -158,83 +169,85 @@ const useSession = (session) => {
const _useSessionHook = (session) => {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const _getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = (event !== null)
const triggeredByStorageEvent = !!((event && event === 'storage'))
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = Math.floor(new Date().getTime() / 1000)
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (triggeredByStorageEvent === false && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const triggerEvent = (triggeredByStorageEvent === false)
const newClientSessionData = await getSession({ triggerEvent })
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
}
}
__NEXTAUTH._getSession = _getSession
useEffect(() => {
const _getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = (event !== null)
const triggeredByStorageEvent = !!((event && event === 'storage'))
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = Math.floor(new Date().getTime() / 1000)
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (triggeredByStorageEvent === false && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const triggerEvent = (triggeredByStorageEvent === false)
const newClientSessionData = await getSession({ triggerEvent })
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
}
}
__NEXTAUTH._getSession = _getSession
_getSession()
})
return [data, loading]
}
// Client side method
const signIn = async (provider, args = {}) => {
export const signIn = async (provider, args = {}, authorizationParams = {}) => {
const baseUrl = _apiBaseUrl()
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
const callbackUrl = args?.callbackUrl ?? window.location
const providers = await getProviders()
// Redirect to sign in page if no valid provider specified
if (!provider || !providers[provider]) {
if (!(provider in providers)) {
// If Provider not recognized, redirect to sign in page
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
} else {
const signInUrl = (providers[provider].type === 'credentials')
? `${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 = {
@@ -249,15 +262,16 @@ const signIn = async (provider, args = {}) => {
json: true
})
}
const res = await fetch(signInUrl, fetchOptions)
const _signInUrl = `${signInUrl}?${_encodedForm(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
window.location = data.url ? data.url : callbackUrl
window.location = data.url ?? callbackUrl
}
}
// Client side method
const signOut = async (args = {}) => {
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
export const signOut = async (args = {}) => {
const callbackUrl = args.callbackUrl ?? window.location
const baseUrl = _apiBaseUrl()
const fetchOptions = {
@@ -274,11 +288,11 @@ const signOut = async (args = {}) => {
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
window.location = data.url ? data.url : callbackUrl
window.location = data.url ?? callbackUrl
}
// Provider to wrap the app in to make session data available globally
const Provider = ({ children, session, options }) => {
export const Provider = ({ children, session, options }) => {
setOptions(options)
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
}

View File

@@ -1,16 +1,44 @@
:root {
--color-background: #fff;
--color-primary: #444;
--color-control-border: #bbb;
--color-button-active-background: #f9f9f9;
--color-button-active-border: #aaa;
--border-width: 1px;
--border-radius: .3rem;
--color-error: #c94b4b;
--color-info: #157efb;
}
.__next-auth-theme-auto,
.__next-auth-theme-light {
--color-background: #fff;
--color-text: #000;
--color-primary: #444;
--color-control-border: #bbb;
--color-button-active-background: #f9f9f9;
--color-button-active-border: #aaa;
--color-seperator: #ccc;
}
.__next-auth-theme-dark {
--color-background: #000;
--color-text: #fff;
--color-primary: #ccc;
--color-control-border: #555;
--color-button-active-background: #060606;
--color-button-active-border: #666;
--color-seperator: #444;
}
@media (prefers-color-scheme: dark) {
.__next-auth-theme-auto {
--color-background: #000;
--color-text: #fff;
--color-primary: #ccc;
--color-control-border: #555;
--color-button-active-background: #060606;
--color-button-active-border: #666;
--color-seperator: #444;
}
}
body {
background-color: var(--color-background);
margin: 0;
@@ -22,6 +50,11 @@ h1 {
font-weight: 400;
margin-bottom: 1.5rem;
padding: 0 1rem;
color: var(--color-text);
}
p {
color: var(--color-text)
}
form {
@@ -46,7 +79,7 @@ input[type] {
background: var(--color-background);
font-size: 1rem;
border-radius: var(--border-radius);
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
box-shadow: inset 0 .1rem .2rem rgba(0, 0, 0, .2);
&:focus {
box-shadow: none;
@@ -63,6 +96,7 @@ p {
a.button {
text-decoration: none;
line-height: 1rem;
&:link,
&:visited {
background-color: var(--color-background);
@@ -79,17 +113,17 @@ a.button {
background-color: var(--color-background);
font-size: 1rem;
border-radius: var(--border-radius);
transition: all .1s ease-in-out;
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.05);
transition: all .1s ease-in-out;
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .05);
font-weight: 500;
position: relative;
&:hover {
cursor: pointer;
}
&:active {
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.1);
box-shadow: 0 0.15rem 0.3rem rgba(0, 0, 0, .15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .1);
background-color: var(--color-button-active-background);
border-color: var(--color-button-active-border);
cursor: pointer;
@@ -101,20 +135,21 @@ a.site {
text-decoration: none;
font-size: 1rem;
line-height: 2rem;
&:hover {
text-decoration: underline;
}
}
.page {
position: absolute;
width: 100%;
position: absolute;
width: 100%;
height: 100%;
display: table;
margin: 0;
padding: 0;
> div {
>div {
display: table-cell;
vertical-align: middle;
text-align: center;
@@ -129,12 +164,14 @@ a.site {
padding-right: 2rem;
margin-top: .5rem;
}
.message {
margin-bottom: 1.5rem;
}
}
.signin {
button,
a.button,
input[type="text"] {
@@ -165,7 +202,8 @@ a.site {
font-weight: 500;
border-radius: 0.3rem;
background: var(--color-info);
color: #fff;
color: var(--color-text);
p {
text-align: left;
padding: 0.5rem 1rem;
@@ -174,16 +212,19 @@ a.site {
}
}
> div,
>div,
form {
display: block;
margin: 0 auto 0.5rem auto;
input[type] {
margin-bottom: 0.5rem;
}
button {
width: 100%;
}
max-width: 300px;
}
}

View File

@@ -4,7 +4,8 @@
import fs from 'fs'
import path from 'path'
const pathToCss = path.join(__dirname, '/index.css')
const css = fs.readFileSync(pathToCss, 'utf8')
const pathToCss = path.join(process.cwd(), '/dist/css/index.css')
export default () => css
export default function css () {
return fs.readFileSync(pathToCss, 'utf8')
}

View File

@@ -1,8 +1,7 @@
class UnknownError extends Error {
export class UnknownError extends Error {
constructor (message) {
super(message)
this.name = 'UnknownError'
this.message = message
}
toJSON () {
@@ -16,26 +15,25 @@ class UnknownError extends Error {
}
}
class CreateUserError extends UnknownError {
export class CreateUserError extends UnknownError {
constructor (message) {
super(message)
this.name = 'CreateUserError'
this.message = message
}
}
// 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.
class AccountNotLinkedError extends UnknownError {
// 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'
this.message = message
}
}
module.exports = {
UnknownError,
CreateUserError,
AccountNotLinkedError
export class OAuthCallbackError extends UnknownError {
constructor (message) {
super(message)
this.name = 'OAuthCallbackError'
}
}

View File

@@ -1,5 +1,5 @@
import crypto from 'crypto'
import jose from 'jose'
import hkdf from 'futoin-hkdf'
import logger from './logger'
// Set default algorithm to use for auto-generated signing key
@@ -13,7 +13,7 @@ const DEFAULT_ENCRYPTION_ENABLED = false
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
const encode = async ({
async function encode ({
token = {},
maxAge = DEFAULT_MAX_AGE,
secret,
@@ -28,9 +28,9 @@ const encode = async ({
zip: 'DEF'
},
encryption = DEFAULT_ENCRYPTION_ENABLED
} = {}) => {
} = {}) {
// Signing Key
const _signingKey = (signingKey)
const _signingKey = signingKey
? jose.JWK.asKey(JSON.parse(signingKey))
: getDerivedSigningKey(secret)
@@ -39,18 +39,17 @@ const encode = async ({
if (encryption) {
// Encryption Key
const _encryptionKey = (encryptionKey)
const _encryptionKey = encryptionKey
? jose.JWK.asKey(JSON.parse(encryptionKey))
: getDerivedEncryptionKey(secret)
// Encrypt token
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
} else {
return signedToken
}
return signedToken
}
const decode = async ({
async function decode ({
secret,
token,
maxAge = DEFAULT_MAX_AGE,
@@ -66,14 +65,14 @@ const decode = async ({
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
},
encryption = DEFAULT_ENCRYPTION_ENABLED
} = {}) => {
} = {}) {
if (!token) return null
let tokenToVerify = token
if (encryption) {
// Encryption Key
const _encryptionKey = (decryptionKey)
const _encryptionKey = decryptionKey
? jose.JWK.asKey(JSON.parse(decryptionKey))
: getDerivedEncryptionKey(secret)
@@ -83,7 +82,7 @@ const decode = async ({
}
// Signing Key
const _signingKey = (verificationKey)
const _signingKey = verificationKey
? jose.JWK.asKey(JSON.parse(verificationKey))
: getDerivedSigningKey(secret)
@@ -91,7 +90,16 @@ const decode = async ({
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
}
const getToken = async (args) => {
/**
* Server-side method to retrieve the JWT from `req`.
* @param {{
* req: NextApiRequest
* secureCookie?: boolean
* cookieName?: string
* raw?: boolean
* }} params
*/
async function getToken (params) {
const {
req,
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
@@ -99,7 +107,7 @@ const getToken = async (args) => {
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
raw = false
} = args
} = params
if (!req) throw new Error('Must pass `req` to JWT getToken()')
// Try to get token from cookie
@@ -108,7 +116,7 @@ const getToken = async (args) => {
// 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 && req.headers.authorization.split(' ')[0] === 'Bearer') {
if (!token && req.headers.authorization?.split(' ')[0] === 'Bearer') {
const urlEncodedToken = req.headers.authorization.split(' ')[1]
token = decodeURIComponent(urlEncodedToken)
}
@@ -118,8 +126,8 @@ const getToken = async (args) => {
}
try {
return await decode({ token, ...args })
} catch (error) {
return decode({ token, ...params })
} catch {
return null
}
}
@@ -128,24 +136,46 @@ const getToken = async (args) => {
let DERIVED_SIGNING_KEY_WARNING = false
let DERIVED_ENCRYPTION_KEY_WARNING = false
const getDerivedSigningKey = (secret) => {
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
if (crypto.hkdfSync) {
return Buffer.from(
crypto.hkdfSync(
digest,
secret,
Buffer.alloc(0),
encryptionInfo,
byteLength
)
)
}
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
}
function getDerivedSigningKey (secret) {
if (!DERIVED_SIGNING_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
DERIVED_SIGNING_KEY_WARNING = true
}
const buffer = hkdf(secret, 64, { info: 'NextAuth.js Generated Signing Key', hash: 'SHA-256' })
const buffer = hkdf(secret, {
byteLength: 64,
encryptionInfo: 'NextAuth.js Generated Signing Key'
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
return key
}
const getDerivedEncryptionKey = (secret) => {
function getDerivedEncryptionKey (secret) {
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
DERIVED_ENCRYPTION_KEY_WARNING = true
}
const buffer = hkdf(secret, 32, { info: 'NextAuth.js Generated Encryption Key', hash: 'SHA-256' })
const buffer = hkdf(secret, {
byteLength: 32,
encryptionInfo: 'NextAuth.js Generated Encryption Key'
})
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
return key
}

View File

@@ -1,31 +1,24 @@
const logger = {
error: (errorCode, ...text) => {
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
error (code, ...message) {
console.error(
`[next-auth][error][${errorCode.toLowerCase()}]`,
text,
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
`[next-auth][error][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
...message
)
},
warn: (warnCode, ...text) => {
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
warn (code, ...message) {
console.warn(
`[next-auth][warn][${warnCode.toLowerCase()}]`,
text,
`\nhttps://next-auth.js.org/warning#${warnCode.toLowerCase()}`
`[next-auth][warn][${code.toLowerCase()}]`,
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
...message
)
},
debug: (debugCode, ...text) => {
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
console.log(
`[next-auth][debug][${debugCode.toLowerCase()}]`,
text
)
}
debug (code, ...message) {
if (!process?.env?._NEXTAUTH_DEBUG) return
console.log(
`[next-auth][debug][${code.toLowerCase()}]`,
...message
)
}
}

View File

@@ -1,8 +1,11 @@
// Simple universal (client/server) function to split host and path
// We use this rather than a library because we need to use the same logic both
// client and server side and we only need to parse out the host and path, while
// supporting a default value, so a simple split is sufficent.
export default (url) => {
/**
* Simple universal (client/server) function to split host and path
* We use this rather than a library because we need to use the same logic both
* client and server side and we only need to parse out the host and path, while
* supporting a default value, so a simple split is sufficent.
* @param {string} url
*/
export default function parseUrl (url) {
// Default values
const defaultHost = 'http://localhost:3000'
const defaultPath = '/api/auth'
@@ -10,7 +13,7 @@ export default (url) => {
if (!url) { url = `${defaultHost}${defaultPath}` }
// Default to HTTPS if no protocol explictly specified
const protocol = url.match(/^http?:\/\//) ? 'http' : 'https'
const protocol = url.startsWith('http:') ? 'http' : 'https'
// Normalize URLs by stripping protocol and no trailing slash
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
@@ -20,8 +23,5 @@ export default (url) => {
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
return {
baseUrl,
basePath
}
return { baseUrl, basePath }
}

View File

@@ -1,5 +1,3 @@
import jwt from 'jsonwebtoken'
export default (options) => {
return {
id: 'apple',
@@ -12,7 +10,6 @@ export default (options) => {
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
profileUrl: null,
idToken: true,
state: false, // Apple doesn't support state verfication
profile: (profile) => {
// The name of the user will only return on first login
return {
@@ -23,30 +20,11 @@ export default (options) => {
},
clientId: null,
clientSecret: {
appleId: null,
teamId: null,
privateKey: null,
keyId: null
},
clientSecretCallback: async ({ appleId, keyId, teamId, privateKey }) => {
const response = jwt.sign(
{
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
aud: 'https://appleid.apple.com',
sub: appleId
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, '\n'),
{
algorithm: 'ES256',
keyid: keyId
}
)
return Promise.resolve(response)
},
protection: 'none', // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
...options
}
}

View File

@@ -0,0 +1,24 @@
export default (options) => {
return {
id: 'atlassian',
name: 'Atlassian',
type: 'oauth',
version: '2.0',
params: {
grant_type: 'authorization_code'
},
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) => {
return {
id: profile.account_id,
name: profile.name,
email: profile.email,
image: profile.picture
}
},
...options
}
}

View File

@@ -0,0 +1,24 @@
export default (options) => {
const tenant = options.tenantId ? options.tenantId : 'common'
return {
id: 'azure-ad-b2c',
name: 'Azure Active Directory B2C',
type: 'oauth',
version: '2.0',
params: {
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) => {
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName
}
},
...options
}
}

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

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

View File

@@ -13,8 +13,8 @@ export default (options) => {
: `https://${region}.battle.net/oauth/token`,
authorizationUrl:
region === 'CN'
? 'https://www.battlenet.com.cn/oauth/authorize'
: `https://${region}.battle.net/oauth/authorize`,
? '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) => {
return {

30
src/providers/bungie.js Normal file
View File

@@ -0,0 +1,30 @@
export default (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) => {
const { bungieNetUser: user } = profile.Response
return {
id: user.membershipId,
name: user.displayName,
image: `https://www.bungie.net${user.profilePicturePath.startsWith('/') ? '' : '/'}${user.profilePicturePath}`,
email: null
}
},
headers: {
'X-API-Key': null
},
clientId: null,
clientSecret: null,
...options
}
}

View File

@@ -6,15 +6,21 @@ export default (options) => {
version: '2.0',
scope: 'identify email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://discordapp.com/api/oauth2/token',
authorizationUrl:
'https://discordapp.com/api/oauth2/authorize?response_type=code&prompt=consent',
profileUrl: 'https://discordapp.com/api/users/@me',
accessTokenUrl: 'https://discord.com/api/oauth2/token',
authorizationUrl: 'https://discord.com/api/oauth2/authorize?response_type=code&prompt=none',
profileUrl: 'https://discord.com/api/users/@me',
profile: (profile) => {
if (profile.avatar === null) {
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
} else {
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
}
return {
id: profile.id,
name: profile.username,
image: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
image: profile.image_url,
email: profile.email
}
},

View File

@@ -0,0 +1,22 @@
export default ({ apiVersion, ...options }) => {
return {
id: 'foursquare',
name: 'Foursquare',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://foursquare.com/oauth2/access_token',
authorizationUrl:
'https://foursquare.com/oauth2/authenticate?response_type=code',
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
profile: (profile) => {
return {
id: profile.id,
name: `${profile.firstName} ${profile.lastName}`,
image: `${profile.prefix}original${profile.suffix}`,
email: profile.contact.email
}
},
...options
}
}

View File

@@ -0,0 +1,27 @@
export default (options) => {
let authorizationUrl = `https://${options.domain}/oauth2/authorize?response_type=code`
if (options.tenantId) {
authorizationUrl += `&tenantId=${options.tenantId}`
}
return {
id: 'fusionauth',
name: 'FusionAuth',
type: 'oauth',
version: '2.0',
scope: 'openid',
params: { grant_type: 'authorization_code' },
accessTokenUrl: `https://${options.domain}/oauth2/token`,
authorizationUrl,
profileUrl: `https://${options.domain}/oauth2/userinfo`,
profile: (profile) => {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture
}
},
...options
}
}

View File

@@ -12,7 +12,6 @@ export default (options) => {
profile: (profile) => {
return { ...profile, id: profile.sub }
},
setGetAccessTokenAuthHeader: false,
...options
}
}

View File

@@ -1,45 +1,71 @@
import Auth0 from './auth0'
import Apple from './apple'
import Box from './box'
import Credentials from './credentials'
import Atlassian from './atlassian'
import Auth0 from './auth0'
import AzureADB2C from './azure-ad-b2c'
import Basecamp from './basecamp'
import BattleNet from './battlenet'
import Box from './box'
import Bungie from './bungie'
import Cognito from './cognito'
import Credentials from './credentials'
import Discord from './discord'
import Email from './email'
import Facebook from './facebook'
import Foursquare from './foursquare'
import FusionAuth from './fusionauth'
import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
import IdentityServer4 from './identity-server4'
import LINE from './line'
import LinkedIn from './linkedin'
import Mixer from './mixer'
import MailRu from './mailru'
import Medium from './medium'
import Netlify from './netlify'
import Okta from './okta'
import Reddit from './reddit'
import Salesforce from './salesforce'
import Slack from './slack'
import Spotify from './spotify'
import Strava from './strava'
import Twitch from './twitch'
import Twitter from './twitter'
import VK from './vk'
import Yandex from './yandex'
export default {
Auth0,
Apple,
Box,
Credentials,
Atlassian,
Auth0,
AzureADB2C,
Basecamp,
BattleNet,
Box,
Bungie,
Cognito,
Credentials,
Discord,
Email,
Facebook,
Foursquare,
FusionAuth,
GitHub,
GitLab,
Google,
IdentityServer4,
LINE,
LinkedIn,
Mixer,
MailRu,
Medium,
Netlify,
Okta,
Reddit,
Salesforce,
Slack,
Spotify,
Twitter,
Strava,
Twitch,
Twitter,
VK,
Yandex
}

22
src/providers/line.js Normal file
View File

@@ -0,0 +1,22 @@
export default (options) => {
return {
id: 'line',
name: 'LINE',
type: 'oauth',
version: '2.0',
scope: 'profile openid',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.line.me/oauth2/v2.1/token',
authorizationUrl: 'https://access.line.me/oauth2/v2.1/authorize?response_type=code',
profileUrl: 'https://api.line.me/v2/profile',
profile: (profile) => {
return {
id: profile.userId,
name: profile.displayName,
email: null,
image: profile.pictureUrl
}
},
...options
}
}

25
src/providers/mailru.js Normal file
View File

@@ -0,0 +1,25 @@
export default (options) => {
return {
id: 'mailru',
name: 'Mail.ru',
type: 'oauth',
version: '2.0',
scope: 'userinfo',
params: {
grant_type: 'authorization_code'
},
accessTokenUrl: 'https://oauth.mail.ru/token',
requestTokenUrl: 'https://oauth.mail.ru/token',
authorizationUrl: 'https://oauth.mail.ru/login?response_type=code',
profileUrl: 'https://oauth.mail.ru/userinfo',
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.image
}
},
...options
}
}

22
src/providers/medium.js Normal file
View File

@@ -0,0 +1,22 @@
export default (options) => {
return {
id: 'medium',
name: 'Medium',
type: 'oauth',
version: '2.0',
scope: 'basicProfile',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.medium.com/v1/tokens',
authorizationUrl: 'https://medium.com/m/oauth/authorize?response_type=code',
profileUrl: 'https://api.medium.com/v1/me',
profile: (profile) => {
return {
id: profile.data.id,
name: profile.data.name,
email: null,
image: profile.data.imageUrl
}
},
...options
}
}

View File

@@ -1,22 +0,0 @@
export default (options) => {
return {
id: 'mixer',
name: 'Mixer',
type: 'oauth',
version: '2.0',
scope: 'user:details:self',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://mixer.com/api/v1/oauth/token',
authorizationUrl: 'https://mixer.com/oauth/authorize?response_type=code',
profileUrl: 'https://mixer.com/api/v1/users/current',
profile: (profile) => {
return {
id: profile.id,
name: profile.username,
image: profile.avatarUrl,
email: profile.email
}
},
...options
}
}

21
src/providers/netlify.js Normal file
View File

@@ -0,0 +1,21 @@
export default (options) => {
return {
id: 'netlify',
name: 'Netlify',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.netlify.com/oauth/token',
authorizationUrl: 'https://app.netlify.com/authorize?response_type=code',
profileUrl: 'https://api.netlify.com/api/v1/user',
profile: (profile) => {
return {
id: profile.id,
name: profile.full_name,
email: profile.email,
image: profile.avatar_url
}
},
...options
}
}

View File

@@ -11,13 +11,12 @@ export default (options) => {
client_secret: options.clientSecret
},
// These will be different depending on the Org.
accessTokenUrl: `https://${options.domain}/oauth2/v1/token`,
authorizationUrl: `https://${options.domain}/oauth2/v1/authorize/?response_type=code`,
profileUrl: `https://${options.domain}/oauth2/v1/userinfo/`,
accessTokenUrl: `https://${options.domain}/v1/token`,
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
profileUrl: `https://${options.domain}/v1/userinfo/`,
profile: (profile) => {
return { ...profile, id: profile.sub }
},
setGetAccessTokenAuthHeader: false,
...options
}
}

View File

@@ -1,4 +1,3 @@
// Logging in works but trying to retrieve the profile results in 401 unauthorized
export default (options) => {
return {
id: 'reddit',
@@ -12,12 +11,12 @@ export default (options) => {
'https://www.reddit.com/api/v1/authorize?response_type=code',
profileUrl: 'https://oauth.reddit.com/api/v1/me',
profile: (profile) => {
// return {
// id: profile.id,
// name: profile.name,
// image: null,
// email: null,
// };
return {
id: profile.id,
name: profile.name,
image: null,
email: null
}
},
...options
}

View File

@@ -0,0 +1,21 @@
export default (options) => {
return {
id: 'salesforce',
name: 'Salesforce',
type: 'oauth',
version: '2.0',
params: { display: 'page', grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.salesforce.com/services/oauth2/token',
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize?response_type=code',
profileUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
protection: 'none', // REVIEW: Can we use "pkce" ?
profile: (profile) => {
return {
...profile,
id: profile.user_id,
image: profile.picture
}
},
...options
}
}

View File

@@ -4,10 +4,11 @@ export default (options) => {
name: 'Slack',
type: 'oauth',
version: '2.0',
scope: 'identity.basic identity.email identity.avatar',
scope: [],
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://slack.com/api/oauth.access',
authorizationUrl: 'https://slack.com/oauth/authorize?response_type=code',
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
authorizationParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
profileUrl: 'https://slack.com/api/users.identity',
profile: (profile) => {
const { user } = profile

View File

@@ -15,7 +15,7 @@ export default (options) => {
id: profile.id,
name: profile.display_name,
email: profile.email,
image: profile.images[0].url
image: profile.images?.[0]?.url
}
},
...options

22
src/providers/strava.js Normal file
View File

@@ -0,0 +1,22 @@
export default (options) => {
return {
id: 'strava',
name: 'Strava',
type: 'oauth',
version: '2.0',
scope: 'read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://www.strava.com/api/v3/oauth/token',
authorizationUrl:
'https://www.strava.com/api/v3/oauth/authorize?response_type=code',
profileUrl: 'https://www.strava.com/api/v3/athlete',
profile: (profile) => {
return {
id: profile.id,
name: profile.firstname,
image: profile.profile
}
},
...options
}
}

30
src/providers/vk.js Normal file
View File

@@ -0,0 +1,30 @@
export default (options) => {
const apiVersion = '5.126' // https://vk.com/dev/versions
return {
id: 'vk',
name: 'vk.com',
type: 'oauth',
version: '2.0',
scope: 'email',
params: {
grant_type: 'authorization_code'
},
accessTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
requestTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
authorizationUrl:
`https://oauth.vk.com/authorize?response_type=code&v=${apiVersion}`,
profileUrl: `https://api.vk.com/method/users.get?fields=photo_100&v=${apiVersion}`,
profile: (result) => {
const profile = result.response?.[0] ?? {}
return {
id: profile.id,
name: [profile.first_name, profile.last_name].filter(Boolean).join(' '),
email: profile.email,
image: profile.photo_100
}
},
...options
}
}

View File

@@ -1,19 +1,19 @@
import { createHash, randomBytes } from 'crypto'
import adapters from '../adapters'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import cookie from './lib/cookie'
import callbackUrlHandler from './lib/callback-url-handler'
import parseProviders from './lib/providers'
import events from './lib/events'
import callbacks from './lib/callbacks'
import providers from './routes/providers'
import signin from './routes/signin'
import signout from './routes/signout'
import callback from './routes/callback'
import session from './routes/session'
import pages from './pages'
import adapters from '../adapters'
import logger from '../lib/logger'
import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-req'
import * as routes from './routes'
import renderPage from './pages'
import csrfTokenHandler from './lib/csrf-token-handler'
import createSecret from './lib/create-secret'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
@@ -21,184 +21,72 @@ if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}
export default async (req, res, userSuppliedOptions) => {
async function NextAuthHandler (req, res, userOptions) {
// If debug enabled, set ENV VAR so that logger logs debug messages
if (userOptions.debug) {
process.env._NEXTAUTH_DEBUG = true
}
// To the best of my knowledge, we need to return a promise here
// to avoid early termination of calls to the serverless function
// (and then return that promise when we are done) - eslint
// complains but I'm not sure there is another way to do this.
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
// This is passed to all methods that handle responses, and must be called
// when they are complete so that the serverless function knows when it is
// safe to return and that no more data will be sent.
const done = resolve
extendRes(req, res, resolve)
if (!req.query.nextauth) {
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
return res.status(500).end(`Error: ${error}`)
}
const { url, query, body } = req
const {
nextauth,
action = nextauth[0],
provider = nextauth[1],
providerId = nextauth[1],
error = nextauth[1]
} = query
} = req.query
const {
csrfToken: csrfTokenFromPost
} = body
// @todo refactor all existing references to baseUrl and basePath
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
// @todo refactor all existing references to site, baseUrl and basePath
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
const baseUrl = parsedUrl.baseUrl
const basePath = parsedUrl.basePath
const cookies = {
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
// Allow user cookie options to override any cookie settings above
...userOptions.cookies
}
const secret = createSecret({ userOptions, basePath, baseUrl })
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const provider = providers.find(({ id }) => id === providerId)
if (provider &&
provider.type === 'oauth' && provider.version?.startsWith('2') &&
(!provider.protection && provider.state !== false)
) {
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
// Parse database / adapter
let adapter
if (userSuppliedOptions.adapter) {
// If adapter is provided, use it (advanced usage, overrides database)
adapter = userSuppliedOptions.adapter
} else if (userSuppliedOptions.database) {
// If database URI or config object is provided, use it (simple usage)
adapter = adapters.Default(userSuppliedOptions.database)
}
// Secret used salt cookies and tokens (e.g. for CSRF protection).
// If no secret option is specified then it creates one on the fly
// based on options passed here. A options contains unique data, such as
// oAuth provider secrets and database credentials it should be sufficent.
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
// Use secure cookies if the site uses HTTPS
// This being conditional allows cookies to work non-HTTPS development URLs
// Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
// prefix, but enable them by default if the site URL is HTTPS; but not for
// non-HTTPS URLs like http://localhost which are used in development).
// For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
const useSecureCookies = userSuppliedOptions.useSecureCookies || baseUrl.startsWith('https://')
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
// @TODO Review cookie settings (names, options)
const cookies = {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
// Allow user cookie options to override any cookie settings above
...userSuppliedOptions.cookies
}
// Session options
const sessionOptions = {
jwt: false,
maxAge: 30 * 24 * 60 * 60, // Sessions expire after 30 days of being idle
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userSuppliedOptions.session
}
// JWT options
const jwtOptions = {
secret, // Use application secret if no keys specified
maxAge: sessionOptions.maxAge, // maxAge is dereived from session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userSuppliedOptions.jwt
}
// If no adapter specified, force use of JSON Web Tokens (stateless)
if (!adapter) {
sessionOptions.jwt = true
}
// Event messages
const eventsOptions = {
...events,
...userSuppliedOptions.events
}
// Callback functions
const callbacksOptions = {
...callbacks,
...userSuppliedOptions.callbacks
}
// Ensure CSRF Token cookie is set for any subsequent requests.
// Used as part of the strateigy for mitigation for CSRF tokens.
//
// Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
// where 'token' is the CSRF token and 'hash' is a hash made of the token and
// the secret, and the two values are joined by a pipe '|'. By storing the
// value and the hash of the value (with the secret used as a salt) we can
// verify the cookie was set by the server and not by a malicous attacker.
//
// For more details, see the following OWASP links:
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
// https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
let csrfToken
let csrfTokenVerified = false
if (req.cookies[cookies.csrfToken.name]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
// If hash matches then we trust the CSRF token value
csrfToken = csrfTokenValue
// If this is a POST request and the CSRF Token in the Post request matches
// the cookie we have already verified is one we have set, then token is verified!
if (req.method === 'POST' && csrfToken === csrfTokenFromPost) { csrfTokenVerified = true }
}
}
if (!csrfToken) {
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
// (e.g. because it's been modifed or because the secret has changed) create a new token.
csrfToken = randomBytes(32).toString('hex')
const newCsrfTokenCookie = `${csrfToken}|${createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')}`
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
// Helper method for handling redirects, this is passed to all routes
// @TODO Refactor into a lib instead of passing as an option
// e.g. and call as redirect(req, res, url)
const redirect = (redirectUrl) => {
const reponseAsJson = !!((req.body && req.body.json === 'true'))
if (reponseAsJson) {
res.json({ url: redirectUrl })
} else {
res.status(302).setHeader('Location', redirectUrl)
res.end()
}
return done()
}
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
// User provided options are overriden by other options,
// except for the options with special handling above
const options = {
// Defaults options can be overidden
debug: false, // Enable debug messages to be displayed
pages: {}, // Custom pages (e.g. sign in, sign out, errors)
req.options = {
debug: false,
pages: {},
theme: 'auto',
// Custom options override defaults
...userSuppliedOptions,
// These computed settings can values in userSuppliedOptions but override them
...userOptions,
// These computed settings can have values in userOptions but we override them
// and are request-specific.
adapter,
baseUrl,
@@ -208,108 +96,137 @@ export default async (req, res, userSuppliedOptions) => {
cookies,
secret,
csrfToken,
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
session: sessionOptions,
jwt: jwtOptions,
events: eventsOptions,
callbacks: callbacksOptions,
callbackUrl: baseUrl,
redirect
providers,
// Session options
session: {
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
maxAge,
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userOptions.session
},
// JWT options
jwt: {
secret, // Use application secret if no keys specified
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userOptions.jwt
},
// Event messages
events: {
...defaultEvents,
...userOptions.events
},
// Callback functions
callbacks: {
...defaultCallbacks,
...userOptions.callbacks
},
pkce: {}
}
// If debug enabled, set ENV VAR so that logger logs debug messages
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }
await callbackUrlHandler(req, res)
// Get / Set callback URL based on query param / cookie + validation
options.callbackUrl = await callbackUrlHandler(req, res, options)
const render = renderPage(req, res)
const { pages } = req.options
if (req.method === 'GET') {
switch (action) {
case 'providers':
providers(req, res, options, done)
break
return routes.providers(req, res)
case 'session':
session(req, res, options, done)
break
return routes.session(req, res)
case 'csrf':
res.json({ csrfToken })
return done()
return res.json({ csrfToken })
case 'signin':
if (options.pages.signIn) {
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
return redirect(redirectUrl)
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
if (error) { signinUrl = `${signinUrl}&error=${error}` }
return res.redirect(signinUrl)
}
pages.render(req, res, 'signin', { baseUrl, basePath, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
break
return render.signin()
case 'signout':
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }
pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
break
if (pages.signOut) {
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
}
return render.signout()
case 'callback':
if (provider && options.providers[provider]) {
callback(req, res, options, done)
} else {
res.status(400).end(`Error: HTTP GET is not supported for ${url}`)
return done()
if (provider) {
if (await pkce.handleCallback(req, res)) return
if (await state.handleCallback(req, res)) return
return routes.callback(req, res)
}
break
case 'verify-request':
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
pages.render(req, res, 'verify-request', { baseUrl }, done)
break
if (pages.verifyRequest) {
return res.redirect(pages.verifyRequest)
}
return render.verifyRequest()
case 'error':
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
if (pages.error) {
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
}
pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
break
// These error messages are displayed in line on the sign in page
if ([
'Signin',
'OAuthSignin',
'OAuthCallback',
'OAuthCreateAccount',
'EmailCreateAccount',
'Callback',
'OAuthAccountNotLinked',
'EmailSignin',
'CredentialsSignin'
].includes(error)) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
}
return render.error({ error })
default:
res.status(404).end()
return done()
}
} else if (req.method === 'POST') {
switch (action) {
case 'signin':
// Verified CSRF Token required for all sign in routes
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
if (csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
if (await state.handleSignin(req, res)) return
return routes.signin(req, res)
}
if (provider && options.providers[provider]) {
signin(req, res, options, done)
}
break
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
// Verified CSRF Token required for signout
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
if (csrfTokenVerified) {
return routes.signout(req, res)
}
signout(req, res, options, done)
break
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
if (provider && options.providers[provider]) {
if (provider) {
// Verified CSRF Token required for credentials providers only
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
if (provider.type === 'credentials' && !csrfTokenVerified) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
callback(req, res, options, done)
} else {
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
return done()
if (await pkce.handleCallback(req, res)) return
if (await state.handleCallback(req, res)) return
return routes.callback(req, res)
}
break
default:
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
return done()
}
} else {
res.status(400).end(`Error: HTTP ${req.method} is not supported for ${url}`)
return done()
}
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
})
}
/** Tha main entry point to next-auth */
export default function NextAuth (...args) {
if (args.length === 1) {
return (req, res) => NextAuthHandler(req, res, args[0])
}
return NextAuthHandler(...args)
}

View File

@@ -1,223 +1,221 @@
// This function handles the complex flow of signing users in, and either creating,
// linking (or not linking) accounts depending on if the user is currently logged
// in, if they have account already and the authentication mechanism they are using.
//
// It prevents insecure behaviour, such as linking oAuth accounts unless a user is
// signed in and authenticated with an existing valid account.
//
// All verification (e.g. oAuth flows or email address verificaiton flows) are
// done prior to this handler being called to avoid additonal complexity in this
// handler.
import { AccountNotLinkedError } from '../../lib/errors'
import dispatchEvent from '../lib/dispatch-event'
export default async (sessionToken, profile, providerAccount, options) => {
try {
// Input validation
if (!profile) { throw new Error('Missing profile') }
if (!providerAccount || !providerAccount.id || !providerAccount.type) { throw new Error('Missing or invalid provider account') }
/**
* This function handles the complex flow of signing users in, and either creating,
* linking (or not linking) accounts depending on if the user is currently logged
* in, if they have account already and the authentication mechanism they are using.
*
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
* signed in and authenticated with an existing valid account.
*
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* handler.
*/
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
// Input validation
if (!profile) throw new Error('Missing profile')
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
const { adapter, jwt, events } = options
const useJwtSession = options.session.jwt
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return {
user: profile,
account: providerAccount,
session: {}
}
const {
adapter,
jwt,
events,
session: {
jwt: useJwtSession
}
} = options
const {
createUser,
updateUser,
getUser,
getUserByProviderAccountId,
getUserByEmail,
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return {
user: profile,
account: providerAccount,
session: {}
}
}
let session = null
let user = null
let isSignedIn = null
let isNewUser = false
const {
createUser,
updateUser,
getUser,
getUserByProviderAccountId,
getUserByEmail,
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session && session.user) {
user = await getUser(session.user.id)
isSignedIn = !!user
}
} catch (e) {
// If session can't be verified, treat as no session
}
} else {
session = await getSession(sessionToken)
if (session && session.userId) {
user = await getUser(session.userId)
let session = null
let user = null
let isSignedIn = null
let isNewUser = false
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session?.sub) {
user = await getUser(session.sub)
isSignedIn = !!user
}
} catch {
// If session can't be verified, treat as no session
}
}
session = await getSession(sessionToken)
if (session?.userId) {
user = await getUser(session.userId)
isSignedIn = !!user
}
}
if (providerAccount.type === 'email') {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
if (isSignedIn) {
if (user.id !== userByEmail.id && !useJwtSession) {
// Delete existing session if they are currently signed in as another user.
// This will switch user accounts for the session in cases where the user was
// already logged in with a different account.
await deleteSession(sessionToken)
}
if (providerAccount.type === 'email') {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
if (isSignedIn) {
if (user.id !== userByEmail.id && !useJwtSession) {
// Delete existing session if they are currently signed in as another user.
// This will switch user accounts for the session in cases where the user was
// already logged in with a different account.
await deleteSession(sessionToken)
}
// Update emailVerified property on the user object
const currentDate = new Date()
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
await dispatchEvent(events.updateUser, user)
} else {
// Create user account if there isn't one for the email address already
const currentDate = new Date()
user = await createUser({ ...profile, emailVerified: currentDate })
await dispatchEvent(events.createUser, user)
isNewUser = true
}
// Create new session
session = useJwtSession ? {} : await createSession(user)
// Update emailVerified property on the user object
const currentDate = new Date()
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
await dispatchEvent(events.updateUser, user)
} else {
// Create user account if there isn't one for the email address already
const currentDate = new Date()
user = await createUser({ ...profile, emailVerified: currentDate })
await dispatchEvent(events.createUser, user)
isNewUser = true
}
// Create new session
session = useJwtSession ? {} : await createSession(user)
return {
session,
user,
isNewUser
}
} else if (providerAccount.type === 'oauth') {
// If signing in with oauth account, check to see if the account exists already
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
if (userByProviderAccountId) {
if (isSignedIn) {
// If the user is already signed in with this account, we don't need to do anything
// Note: These are cast as strings here to ensure they match as in
// some flows (e.g. JWT with a database) one of the values might be a
// string and the other might be an ObjectID and would otherwise fail.
if (`${userByProviderAccountId.id}` === `${user.id}`) {
return {
session,
user,
isNewUser
}
}
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another account, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError()
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
return {
session,
user: userByProviderAccountId,
isNewUser
}
} else {
if (isSignedIn) {
// If the user is already signed in and the OAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
// As they are already signed in, we don't need to do anything after linking them
return {
session,
user,
isNewUser
}
}
// If the user is not signed in and it looks like a new OAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the OAuth profile.
//
// This step is often overlooked in OAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party OAuth service.
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
if (userByEmail) {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// OAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinkedError()
}
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
user = await createUser(profile)
await dispatchEvent(events.createUser, user)
await linkAccount(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
session = useJwtSession ? {} : await createSession(user)
isNewUser = true
return {
session,
user,
isNewUser
}
} else if (providerAccount.type === 'oauth') {
// If signing in with oauth account, check to see if the account exists already
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
if (userByProviderAccountId) {
if (isSignedIn) {
// If the user is already signed in with this account, we don't need to do anything
// Note: These are cast as strings here to ensure they match as in
// some flows (e.g. JWT with a database) one of the values might be a
// string and the other might be an ObjectID and would otherwise fail.
if (`${userByProviderAccountId.id}` === `${user.id}`) {
return {
session,
user,
isNewUser
}
} else {
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another account, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError()
}
} else {
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
return {
session,
user: userByProviderAccountId,
isNewUser
}
}
} else {
if (isSignedIn) {
// If the user is already signed in and the oAuth account isn't already associated
// with another user account then we can go ahead and link the accounts safely.
await linkAccount(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount })
// As they are already signed in, we don't need to do anything after linking them
return {
session,
user,
isNewUser
}
}
// If the user is not signed in and it looks like a new oAuth account then we
// check there also isn't an user account already associated with the same
// email address as the one in the oAuth profile.
//
// This step is often overlooked in oAuth implementations, but covers the following cases:
//
// 1. It makes it harder for someone to accidentally create two accounts.
// e.g. by signin in with email, then again with an oauth account connected to the same email.
// 2. It makes it harder to hijack a user account using a 3rd party oAuth account.
// e.g. by creating an oauth account then changing the email address associated with it.
//
// It's quite common for services to automatically link accounts in this case, but it's
// better practice to require the user to sign in *then* link accounts to be sure
// someone is not exploiting a problem with a third party oAuth service.
//
// oAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
if (userByEmail) {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
// oAuth profile the user has just tried to sign in with.
//
// We don't want to have two accounts with the same email address, and we don't
// want to link them in case it's not safe to do so, so instead we prompt the user
// to sign in via email to verify their identity and then link the accounts.
throw new AccountNotLinkedError()
} else {
// If the current user is not logged in and the profile isn't linked to any user
// accounts (by email or provider account id)...
//
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the oAuth acccount and
// create a new session for them so they are signed in with it.
user = await createUser(profile)
await dispatchEvent(events.createUser, user)
await linkAccount(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount })
session = useJwtSession ? {} : await createSession(user)
isNewUser = true
return {
session,
user,
isNewUser
}
}
}
} else {
return Promise.reject(new Error('Provider not supported'))
}
} catch (error) {
return Promise.reject(error)
}
}

View File

@@ -1,14 +1,18 @@
import cookie from '../lib/cookie'
import * as cookie from '../lib/cookie'
export default async (req, res, options) => {
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
* @note: `req.options` must already be defined when called.
*/
export default async function callbackUrlHandler (req, res) {
const { query } = req
const { body } = req
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = options
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || baseUrl
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
@@ -21,7 +25,9 @@ export default async (req, res, options) => {
}
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) { cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options) }
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
}
return Promise.resolve(callbackUrl)
req.options.callbackUrl = callbackUrl
}

View File

@@ -1,12 +1,14 @@
// Function to set cookies server side
//
// Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
// * https://github.com/jshttp/cookie/blob/master/index.js
// * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
//
// As only partial functionlity is required, only the code we need has been incorporated here
// (with fixes for specific issues) to keep dependancy size down.
const set = (res, name, value, options = {}) => {
/**
* Function to set cookies server side
*
* Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
* * https://github.com/jshttp/cookie/blob/master/index.js
* * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
*
* As only partial functionlity is required, only the code we need has been incorporated here
* (with fixes for specific issues) to keep dependancy size down.
*/
export function set (res, name, value, options = {}) {
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
if ('maxAge' in options) {
@@ -99,6 +101,56 @@ function _serialize (name, val, options) {
return str
}
export default {
set
/**
* Use secure cookies if the site uses HTTPS
* This being conditional allows cookies to work non-HTTPS development URLs
* Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
* prefix, but enable them by default if the site URL is HTTPS; but not for
* non-HTTPS URLs like http://localhost which are used in development).
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
*
* @TODO Review cookie settings (names, options)
*/
export function defaultCookies (useSecureCookies) {
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
return {
// default cookie options
sessionToken: {
name: `${cookiePrefix}next-auth.session-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
callbackUrl: {
name: `${cookiePrefix}next-auth.callback-url`,
options: {
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
pkceCodeVerifier: {
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
}
}
}

View File

@@ -0,0 +1,13 @@
import { createHash } from 'crypto'
/**
* Secret used salt cookies and tokens (e.g. for CSRF protection).
* If no secret option is specified then it creates one on the fly
* based on options passed here. A options contains unique data, such as
* OAuth provider secrets and database credentials it should be sufficent.
*/
export default function createSecret ({ userOptions, basePath, baseUrl }) {
return userOptions.secret || createHash('sha256').update(JSON.stringify({
baseUrl, basePath, ...userOptions
})).digest('hex')
}

View File

@@ -0,0 +1,42 @@
import { createHash, randomBytes } from 'crypto'
import * as cookie from './cookie'
/**
* Ensure CSRF Token cookie is set for any subsequent requests.
* Used as part of the strateigy for mitigation for CSRF tokens.
*
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
* the secret, and the two values are joined by a pipe '|'. By storing the
* value and the hash of the value (with the secret used as a salt) we can
* verify the cookie was set by the server and not by a malicous attacker.
*
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
*/
export default function csrfTokenHandler (req, res, cookies, secret) {
const { csrfToken: csrfTokenFromRequest } = req.body
let csrfTokenFromCookie
let csrfTokenVerified = false
if (req.cookies[cookies.csrfToken.name]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
// If hash matches then we trust the CSRF token value
csrfTokenFromCookie = csrfTokenValue
// If this is a POST request and the CSRF Token in the Post request matches
// the cookie we have already verified is one we have set, then token is verified!
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
}
}
if (!csrfTokenFromCookie) {
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
// (e.g. because it's been modifed or because the secret has changed) create a new token.
csrfTokenFromCookie = randomBytes(32).toString('hex')
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
}

View File

@@ -9,19 +9,14 @@
* requests to sign in and again when they activate the link in the sign in
* email.
*
* @param {object} profile User profile (e.g. user id, name, email)
* @param {object} account Account used to sign in (e.g. OAuth account)
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
* @return {boolean|object} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
* @param {object} profile User profile (e.g. user id, name, email)
* @param {object} account Account used to sign in (e.g. OAuth account)
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
const signIn = async (profile, account, metadata) => {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
export async function signIn () {
return true
}
/**
@@ -31,12 +26,13 @@ const signIn = async (profile, account, metadata) => {
*
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {string} URL the client will be redirect to
* @return {Promise<string>} URL the client will be redirect to
*/
const redirect = async (url, baseUrl) => {
return url.startsWith(baseUrl)
? Promise.resolve(url)
: Promise.resolve(baseUrl)
export async function redirect (url, baseUrl) {
if (url.startsWith(baseUrl)) {
return url
}
return baseUrl
}
/**
@@ -45,31 +41,24 @@ const redirect = async (url, baseUrl) => {
*
* @param {object} session Session object
* @param {object} token JSON Web Token (if enabled)
* @return {object} Session that will be returned to the client
* @return {Promise<object>} Session that will be returned to the client
*/
const session = async (session, token) => {
return Promise.resolve(session)
export async function session (session) {
return session
}
/**
* This callback is called whenever a JSON Web Token is created / updated.
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* On initial sign in, the raw oAuthProfile is passed if the user is signing in
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
* with an OAuth provider. It is not avalible on subsequent calls. You can
* take advantage of this to persist additional data you need to in the JWT.
*
* @param {object} token Decrypted JSON Web Token
* @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {object} JSON Web Token that will be saved
* @return {Promise<object>} JSON Web Token that will be saved
*/
const jwt = async (token, oAuthProfile) => {
return Promise.resolve(token)
}
export default {
signIn,
redirect,
session,
jwt
export async function jwt (token) {
return token
}

View File

@@ -0,0 +1,23 @@
/** Event triggered on successful sign in */
export async function signIn (message) {}
/** Event triggered on sign out */
export async function signOut (message) {}
/** Event triggered on user creation */
export async function createUser (message) {}
/** Event triggered when a user object is updated */
export async function updateUser (message) {}
/** Event triggered when an account is linked to a user */
export async function linkAccount (message) {}
/** Event triggered when a session is active */
export async function session (message) {}
/**
* @TODO Event triggered when something goes wrong in an authentication flow
* This event may be fired multiple times when an error occurs
*/
export async function error (message) {}

View File

@@ -1,6 +1,6 @@
import logger from '../../lib/logger'
export default async (event, message) => {
export default async function dispatchEvent (event, message) {
try {
await event(message)
} catch (e) {

View File

@@ -1,38 +0,0 @@
const signIn = async (message) => {
// Event triggered on successful sign in
}
const signOut = async (message) => {
// Event triggered on sign out
}
const createUser = async (message) => {
// Event triggered on user creation
}
const updateUser = async (message) => {
// Event triggered when a user object is updated
}
const linkAccount = async (message) => {
// Event triggered when an account is linked to a user
}
const session = async (message) => {
// Event triggered when a session is active
}
const error = async (message) => {
// @TODO Event triggered when something goes wrong in an authentication flow
// This event may be fired multiple times when an error occurs
}
export default {
signIn,
signOut,
createUser,
updateUser,
linkAccount,
session,
error
}

View File

@@ -0,0 +1,35 @@
/**
* Extends res.{end,json,send} with `done()`,
* and redirect to support sending url as json.
*
* When a response is complete, it will call the `done` method,
* so that the serverless function knows when it is
* safe to return and that no more data will be sent.
*/
export default function extendRes (req, res, done) {
const originalResEnd = res.end.bind(res)
res.end = (...args) => {
done()
return originalResEnd(...args)
}
const originalResJson = res.json.bind(res)
res.json = (...args) => {
done()
return originalResJson(...args)
}
const originalResSend = res.send.bind(res)
res.send = (...args) => {
done()
return originalResSend(...args)
}
res.redirect = (url) => {
if (req.body?.json === 'true') {
return res.json({ url })
}
res.status(302).setHeader('Location', url)
return res.end()
}
}

View File

@@ -1,50 +1,33 @@
import { createHash } from 'crypto'
import querystring from 'querystring'
import jwtDecode from 'jwt-decode'
import { decode as jwtDecode } from 'jsonwebtoken'
import oAuthClient from './client'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
// These methods have been forked from `node-oauth` to fix bugs; it may make
// sense to migrate all the methods we need from node-oauth to nexth-auth (with
// appropriate credit) to make it easier to maintain and address issues as they
// come up, as the node-oauth package does not seem to be actively maintained.
// @TODO Refactor to use promises and not callbacks
// @TODO Refactor to use jsonwebtoken instead of jwt-decode & remove dependancy
export default async (req, provider, csrfToken, callback) => {
// The "user" object is specific to apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { oauth_token, oauth_verifier, code, user, state } = req.query // eslint-disable-line camelcase
export default async function oAuthCallback (req) {
const { provider, pkce } = req.options
const client = oAuthClient(provider)
if (provider.version && provider.version.startsWith('2.')) {
// For OAuth 2.0 flows, check state returned and matches expected value
// (a hash of the NextAuth.js CSRF token).
//
// This check can be disabled for providers that do not support it by
// setting `state: false` as a option on the provider (defaults to true).
if (!Object.prototype.hasOwnProperty.call(provider, 'state') || provider.state === true) {
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
if (state !== expectedState) {
return callback(new Error('Invalid state returned from oAuth provider'))
}
}
if (provider.version?.startsWith('2.')) {
// The "user" object is specific to the Apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { code, user } = req.query // eslint-disable-line camelcase
if (req.method === 'POST') {
try {
const body = JSON.parse(JSON.stringify(req.body))
if (body.error) { throw new Error(body.error) }
if (body.error) {
throw new Error(body.error)
}
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (e) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', e, req.body, provider.id, code)
return callback()
} catch (error) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
throw error
}
}
// REVIEW: Is this used by any of the providers?
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
@@ -52,104 +35,93 @@ export default async (req, provider, csrfToken, callback) => {
client.useAuthorizationHeaderforGET(true)
}
// Use custom getOAuthAccessToken() method for oAuth2 flows
client.getOAuthAccessToken = _getOAuthAccessToken
await client.getOAuthAccessToken(
code,
provider,
(error, accessToken, refreshToken, results) => {
if (error || results.error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, results, provider.id, code)
return callback(error || results.error)
try {
const tokens = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
let profileData
if (provider.idToken) {
if (!tokens?.id_token) {
throw new OAuthCallbackError('Missing JWT ID Token')
}
if (provider.idToken) {
// If we don't have an ID Token most likely the user hit a cancel
// button when signing in (or the provider is misconfigured).
//
// Unfortunately, we can't tell which, so we can't treat it as an
// error, so instead we just returning nothing, which will cause the
// user to be redirected back to the sign in page.
if (!results || !results.id_token) {
return callback()
}
// Support services that use OpenID ID Tokens to encode profile data
_decodeToken(
provider,
accessToken,
refreshToken,
results.id_token,
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider, user)
callback(error, profile, account, OAuthProfile)
}
)
} else {
// Use custom get() method for oAuth2 flows
client.get = _get
client.get(
provider,
accessToken,
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
// Support services that use OpenID ID Tokens to encode profile data
profileData = jwtDecode(tokens.id_token, { json: true })
} else {
profileData = await client.get(provider, tokens.accessToken, tokens)
}
)
} else {
// Handle oAuth v1.x
await client.getOAuthAccessToken(
oauth_token,
null,
oauth_verifier,
(error, accessToken, refreshToken, results) => {
// @TODO Handle error
if (error || results.error) {
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error, results)
}
client.get(
provider.profileUrl,
accessToken,
refreshToken,
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
return getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
throw error
}
}
try {
// Handle OAuth v1.x
const {
oauth_token: oauthToken, oauth_verifier: oauthVerifier
} = req.query
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
const profileData = await client.get(
provider.profileUrl,
tokens.accessToken,
tokens.refreshToken
)
return getProfile({ profileData, tokens, provider })
} catch (error) {
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
throw error
}
}
/**
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
* Returns profile, raw profile and auth provider details
* @param {{
* profileData: object | string
* tokens: {
* accessToken: string
* idToken?: string
* refreshToken?: string
* access_token: string
* expires_in?: string | Date | null
* refresh_token?: string
* id_token?: string
* }
* provider: object
* user?: object
* }} profileParams
*/
async function _getProfile (error, profileData, accessToken, refreshToken, provider, userData) {
// @TODO Handle error
if (error) {
logger.error('OAUTH_GET_PROFILE_ERROR', error)
}
let profile = {}
async function getProfile ({ profileData, tokens, provider, user }) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) { profileData = JSON.parse(profileData) }
if (typeof profileData === 'string' || profileData instanceof String) {
profileData = JSON.parse(profileData)
}
// If a user object is supplied (e.g. Apple provider) add it to the profile object
if (userData != null) {
profileData.user = userData
if (user != null) {
profileData.user = user
}
logger.debug('PROFILE_DATA', profileData)
profile = await provider.profile(profileData)
const profile = await provider.profile(profileData)
// Return profile, raw profile and auth provider details
return {
profile: {
...profile,
email: profile.email?.toLowerCase() ?? null
},
account: {
provider: provider.id,
type: provider.type,
id: profile.id,
...tokens
},
OAuthProfile: profileData
}
} catch (exception) {
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
@@ -165,111 +137,4 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
OAuthProfile: profileData
}
}
// Return profile, raw profile and auth provider details
return {
profile: {
name: profile.name,
email: profile.email ? profile.email.toLowerCase() : null,
image: profile.image
},
account: {
provider: provider.id,
type: provider.type,
id: profile.id,
refreshToken,
accessToken,
accessTokenExpires: null
},
OAuthProfile: profileData
}
}
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
async function _getOAuthAccessToken (code, provider, callback) {
const url = provider.accessTokenUrl
const setGetAccessTokenAuthHeader = (provider.setGetAccessTokenAuthHeader !== null) ? provider.setGetAccessTokenAuthHeader : true
const params = { ...provider.params } || {}
const headers = { ...provider.headers } || {}
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
if (!params[codeParam]) { params[codeParam] = code }
if (!params.client_id) { params.client_id = provider.clientId }
if (!params.client_secret) {
// For some providers it useful to be able to generate the secret on the fly
// e.g. For Sign in With Apple a JWT token using the properties in clientSecret
if (provider.clientSecretCallback) {
params.client_secret = await provider.clientSecretCallback(provider.clientSecret)
} else {
params.client_secret = provider.clientSecret
}
}
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
// Added as a fix to accomodate change in Twitch oAuth API
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
// Okta errors when this is set. Maybe there are other Providers that also wont like this.
if (setGetAccessTokenAuthHeader) {
if (!headers.Authorization) { headers.Authorization = `Bearer ${code}` }
}
const postData = querystring.stringify(params)
this._request(
'POST',
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
return callback(error)
}
let results
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
results = JSON.parse(data)
} catch (e) {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
results = querystring.parse(data)
}
const accessToken = results.access_token
const refreshToken = results.refresh_token
callback(null, accessToken, refreshToken, results)
}
)
}
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
function _get (provider, accessToken, callback) {
const url = provider.profileUrl
const headers = provider.headers || {}
if (this._useAuthorizationHeaderForGET) {
headers.Authorization = this.buildAuthHeader(accessToken)
// This line is required for Twitch
headers['Client-ID'] = provider.clientId
accessToken = null
}
this._request('GET', url, headers, null, accessToken, callback)
}
function _decodeToken (provider, accessToken, refreshToken, idToken, callback) {
if (!idToken) { throw new Error('Missing JWT ID Token', provider, idToken) }
const decodedToken = jwtDecode(idToken)
const profileData = JSON.stringify(decodedToken)
callback(null, profileData, accessToken, refreshToken, provider)
}

View File

@@ -1,31 +1,236 @@
// @TODO Refactor to remove dependancy on 'oauth' package
// It is already quite monkey patched, we don't use all the features and and it
// would be easier to maintain if all the code was native to next-auth.
import { OAuth, OAuth2 } from 'oauth'
import querystring from 'querystring'
import logger from '../../../lib/logger'
import { sign as jwtSign } from 'jsonwebtoken'
export default (provider) => {
if (provider.version && provider.version.startsWith('2.')) {
// Handle oAuth v2.x
const basePath = new URL(provider.authorizationUrl).origin
const authorizePath = new URL(provider.authorizationUrl).pathname
/**
* @TODO Refactor to remove dependancy on 'oauth' package
* It is already quite monkey patched, we don't use all the features and and it
* would be easier to maintain if all the code was native to next-auth.
*/
export default function oAuthClient (provider) {
if (provider.version?.startsWith('2.')) {
// Handle OAuth v2.x
const authorizationUrl = new URL(provider.authorizationUrl)
const basePath = authorizationUrl.origin
const authorizePath = authorizationUrl.pathname
const accessTokenPath = new URL(provider.accessTokenUrl).pathname
return new OAuth2(
const oauth2Client = new OAuth2(
provider.clientId,
provider.clientSecret,
basePath,
authorizePath,
accessTokenPath,
provider.headers)
} else {
// Handle oAuth v1.x
return new OAuth(
provider.requestTokenUrl,
provider.accessTokenUrl,
provider.clientId,
provider.clientSecret,
(provider.version || '1.0'),
provider.callbackUrl,
(provider.encoding || 'HMAC-SHA1')
provider.headers
)
oauth2Client.getOAuthAccessToken = getOAuth2AccessToken
oauth2Client.get = getOAuth2
return oauth2Client
}
// Handle OAuth v1.x
const oauth1Client = new OAuth(
provider.requestTokenUrl,
provider.accessTokenUrl,
provider.clientId,
provider.clientSecret,
provider.version || '1.0',
provider.callbackUrl,
provider.encoding || 'HMAC-SHA1'
)
// Promisify get() and getOAuth2AccessToken() for OAuth1
const originalGet = oauth1Client.get.bind(oauth1Client)
oauth1Client.get = (...args) => {
return new Promise((resolve, reject) => {
originalGet(...args, (error, result) => {
if (error) {
return reject(error)
}
resolve(result)
})
})
}
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
if (error) {
return reject(error)
}
resolve({ accessToken, refreshToken, results })
})
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (...args) => {
return new Promise((resolve, reject) => {
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
if (error) {
return reject(error)
}
resolve(oauthToken)
})
})
}
return oauth1Client
}
/**
* @TODO Refactor monkey patching in OAuth2.getOAuthAccessToken() and OAuth2.get()
* These methods have been forked from `node-oauth` to fix bugs; it may make
* sense to migrate all the methods we need from node-oauth to nexth-auth (with
* appropriate credit) to make it easier to maintain and address issues as they
* come up, as the node-oauth package does not seem to be actively maintained.
*/
/**
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
*/
async function getOAuth2AccessToken (code, provider, codeVerifier) {
const url = provider.accessTokenUrl
const params = { ...provider.params }
const headers = { ...provider.headers }
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
if (!params[codeParam]) { params[codeParam] = code }
if (!params.client_id) { params.client_id = provider.clientId }
// For Apple the client secret must be generated on-the-fly.
// Using the properties in clientSecret to create a JWT.
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') {
const { keyId, teamId, privateKey } = provider.clientSecret
const clientSecret = jwtSign({
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
aud: 'https://appleid.apple.com',
sub: provider.clientId
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, '\n'),
{ algorithm: 'ES256', keyid: keyId }
)
params.client_secret = clientSecret
} else {
params.client_secret = provider.clientSecret
}
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
// Added as a fix for Reddit Authentication
if (provider.id === 'reddit') {
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
}
if ((provider.id === 'okta' || provider.id === 'identity-server4') && !headers.Authorization) {
headers.Authorization = `Bearer ${code}`
}
if (provider.protection === 'pkce') {
params.code_verifier = codeVerifier
}
const postData = querystring.stringify(params)
return new Promise((resolve, reject) => {
this._request(
'POST',
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
return reject(error)
}
let raw
try {
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
// responses should be in JSON
raw = JSON.parse(data)
} catch {
// However both Facebook + Github currently use rev05 of the spec and neither
// seem to specify a content-type correctly in their response headers. :(
// Clients of these services suffer a minor performance cost.
raw = querystring.parse(data)
}
const accessToken = provider.id === 'slack'
? raw.authed_user.access_token
: raw.access_token
resolve({
accessToken,
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw
})
}
)
})
}
/**
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
*
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
* e.g. see providers/bungie
*/
async function getOAuth2 (provider, accessToken, results) {
let url = provider.profileUrl
const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) {
headers.Authorization = this.buildAuthHeader(accessToken)
// Mail.ru & vk.com require 'access_token' as URL request parameter
if (['mailru', 'vk'].includes(provider.id)) {
const safeAccessTokenURL = new URL(url)
safeAccessTokenURL.searchParams.append('access_token', accessToken)
url = safeAccessTokenURL.href
}
// This line is required for Twitch
if (provider.id === 'twitch') {
headers['Client-ID'] = provider.clientId
}
accessToken = null
}
if (provider.id === 'bungie') {
url = prepareProfileUrl({ provider, url, results })
}
return new Promise((resolve, reject) => {
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
if (error) {
return reject(error)
}
resolve(profileData)
})
})
}
/** Bungie needs special handling */
function prepareProfileUrl ({ provider, url, results }) {
if (!results.membership_id) {
// internal error
// @TODO: handle better
throw new Error('Expected membership_id to be passed.')
}
if (!provider.headers?.['X-API-Key']) {
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
}
return url.replace('{membershipId}', results.membership_id)
}

View File

@@ -0,0 +1,82 @@
import pkceChallenge from 'pkce-challenge'
import * as cookie from '../cookie'
import jwt from '../../../lib/jwt'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
const PKCE_LENGTH = 64
const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
/** Adds `code_verifier` to `req.options.pkce`, and removes the corresponding cookie */
export async function handleCallback (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
return
}
if (!(cookies.pkceCodeVerifier.name in req.cookies)) {
throw new OAuthCallbackError('The code_verifier cookie was not found.')
}
const pkce = await jwt.decode({
...req.options.jwt,
token: req.cookies[cookies.pkceCodeVerifier.name],
maxAge: PKCE_MAX_AGE,
encryption: true
})
req.options.pkce = pkce
logger.debug('OAUTH_CALLBACK_PROTECTION', 'Read PKCE verifier from cookie', {
code_verifier: pkce.code_verifier,
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD
})
cookie.set(res, cookies.pkceCodeVerifier.name, null, { maxAge: 0 }) // remove PKCE after it has been used
} catch (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
}
/** Adds `code_challenge` and `code_challenge_method` to `req.options.pkce`. */
export async function handleSignin (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
return
}
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie
const pkce = pkceChallenge(PKCE_LENGTH)
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE challenge/verifier', {
...pkce,
pkceLength: PKCE_LENGTH,
method: PKCE_CODE_CHALLENGE_METHOD
})
provider.authorizationParams = {
...provider.authorizationParams,
code_challenge: pkce.code_challenge,
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD
}
const encryptedCodeVerifier = await jwt.encode({
...req.options.jwt,
maxAge: PKCE_MAX_AGE,
token: { code_verifier: pkce.code_verifier },
encryption: true
})
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (PKCE_MAX_AGE * 1000))
cookie.set(res, cookies.pkceCodeVerifier.name, encryptedCodeVerifier, {
expires: cookieExpires.toISOString(),
...cookies.pkceCodeVerifier.options
})
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE code_verifier saved in cookie')
} catch (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
}
export default { handleSignin, handleCallback }

View File

@@ -0,0 +1,64 @@
import { createHash } from 'crypto'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
/**
* For OAuth 2.0 flows, if the provider supports state,
* check if state matches the one sent on signin
* (a hash of the NextAuth.js CSRF token).
*/
export async function handleCallback (req, res) {
const { csrfToken, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
return
}
const { state } = req.query
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
logger.debug(
'OAUTH_CALLBACK_PROTECTION',
'Comparing received and expected state',
{ state, expectedState }
)
if (state !== expectedState) {
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
}
} catch (error) {
logger.error('STATE_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
}
/** Adds CSRF token to the authorizationParams. */
export async function handleSignin (req, res) {
const { provider, baseUrl, basePath, csrfToken } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
return
}
if ('state' in provider) {
logger.warn(
'STATE_OPTION_DEPRECATION',
'The `state` provider option is being replaced with `protection`. See the docs.'
)
}
// A hash of the NextAuth.js CSRF token is used as the state
const state = createHash('sha256').update(csrfToken).digest('hex')
provider.authorizationParams = { ...provider.authorizationParams, state }
logger.debug(
'OAUTH_CALLBACK_PROTECTION',
'Added state to authorization params',
{ state }
)
} catch (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
}
export default { handleSignin, handleCallback }

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