Compare commits

...

306 Commits
v1 ... v2.0

Author SHA1 Message Date
Iain Collins
388a1c4393 Remove references to beta URLS from docs 2020-06-21 17:00:33 +01:00
Iain Collins
5c9aaeae43 Set default site name and remove old slug param
* Removing the old 'slug' param as it was only used in early beta releases
* No default site name makes it harder to debug when you forget to set it
* Setting it to http://localhost:3000 makes it more obvious when you see it in errors
2020-06-21 17:00:33 +01:00
Iain Collins
0eaec78399 Bump version number for release 2020-06-21 17:00:33 +01:00
Iain Collins
9349ca3b34 Updates to documentation and homepage CSS 2020-06-21 16:20:38 +01:00
Iain Collins
52f2dd5c32 Add JS wrapper to CSS to support Serverless target
Resolves #281
2020-06-21 15:07:01 +01:00
Iain Collins
6eeed21872 Merge branch 'main' of github.com:iaincollins/next-auth into main 2020-06-21 11:23:32 +01:00
Iain Collins
929a7e5840 Fix broken links in documentation 2020-06-21 11:23:27 +01:00
Iain Collins
758b3a88d0 Update beta version to 85 2020-06-21 04:43:13 +01:00
Iain Collins
4477bd6c80 Remove session payload from triggers
Each message trigger needs a unique payload but we don't have to pass the session data.

It would not be terrible to pass the session data, but persisting it in localStorage is likely cause confusion and makes it easier for third party scripts to scrape.
2020-06-21 04:43:13 +01:00
Iain Collins
370d2cc121 Update documentation 2020-06-21 04:43:13 +01:00
Iain Collins
e6c4d6e737 Add session sync across windows/tabs
Uses the localStorage API to trigger events across tabs, as is more widely supported than the broadcast API.
2020-06-21 04:43:13 +01:00
SabariVig
5bd2936b90 Added Providers To Sidebar 2020-06-21 02:37:17 +01:00
Iain Collins
41b6bb7000 Fix linter errors 2020-06-21 02:32:03 +01:00
Iain Collins
fd8818c400 Add callbackUrl option to client methods
Resolves  #208

Also address fetchData not returning a promise when it should.
2020-06-21 02:26:34 +01:00
Iain Collins
5929de4249 Remove babel-preset-minify
It made debugging harder and I don't think it's currently worth the tradeoff.

In future we might bring it back conditionally (e.g. prod builds only).
2020-06-21 01:54:18 +01:00
Iain Collins
07c6cbccc0 Update beta version to 83 2020-06-21 01:44:41 +01:00
Iain Collins
f20843fcb1 Use timestamp with time zone Postgres databases 2020-06-21 01:44:41 +01:00
Iain Collins
eb6a7a45a5 Refactort timestamps, add email verified timestamp
* Saves email verified date on user when an email sign in link is used
* All timestamps now use UTC date objects in database
* Stored as UTC regardless of server timezone
* Tested with MySQL and MongoDB
* Investigating issues with Postgres
2020-06-21 01:44:41 +01:00
Iain Collins
75638db676 Bump beta version to 82 2020-06-20 11:58:28 +01:00
Iain Collins
76fcfa53f4 Add new documentation for credentials provider 2020-06-20 11:58:28 +01:00
Iain Collins
708ec9fbe7 Improve sign in page
* Add label to email sign in
* Add support for custom credentials to sign in form
* Add explict error for rejected credentials

Documentation to follow.
2020-06-20 11:58:28 +01:00
Iain Collins
ca98750604 Merge branch 'main' of github.com:iaincollins/next-auth into main 2020-06-20 09:27:36 +01:00
Iain Collins
eb49a47b0a Bump beta version to 81; hotfix for sqlite
Includes hot fix for SQLite transform.

It was not updated to support column name changes.

The fix applies the transform without relying on hard coded values so bug won't happen again.
2020-06-20 09:27:28 +01:00
Iain Collins
ac2fc85d18 Fix linting errors 2020-06-20 08:48:54 +01:00
Iain Collins
4ee2b4453d Refactor IdentityServer4 provider
NB: The ...options spread takes care of conditionally overriding default property values.
2020-06-20 08:48:54 +01:00
Iain Collins
0542b6a24a Add Yandex provider 2020-06-20 08:48:54 +01:00
Iain Collins
ca0053b6cd Add box.com provider
Resolves #242
2020-06-20 08:48:54 +01:00
Iain Collins
a902b98cfd Rename Gitlab to GitLab 2020-06-20 08:48:54 +01:00
SabariVig
99ff0ffd3d Doc Typo 2020-06-20 08:25:45 +01:00
SabariVig
5bb1830c9b Added Documentation for Gitlab 2020-06-20 08:25:45 +01:00
SabariVig
6055ecda24 Added to providers/index.js And link typos in Readme 2020-06-20 08:25:45 +01:00
SabariVig
8229a3b420 Gitlab Provider 2020-06-20 08:25:45 +01:00
Iain Collins
20723fd2b4 Update mongodb test 2020-06-20 08:23:02 +01:00
Iain Collins
31d9363954 Update schema fixtures and docs 2020-06-20 08:23:02 +01:00
Iain Collins
515facf39f Add support for mongodb+srv URLs 2020-06-20 08:23:02 +01:00
Lori Karikari
173ce2aae6 [docs] update custom page signin() value
Changed docs to use the provider id instead of the provider object as shown in #284
2020-06-19 17:20:12 +02:00
Iain Collins
935c4f2f82 Update CONTRIBUTING.md 2020-06-19 08:28:35 +01:00
Iain Collins
b893b6485b Refactor db tests an update docs 2020-06-19 08:28:35 +01:00
Iain Collins
6f5b3bd213 Refactor models to use created_at / updated_at 2020-06-19 08:28:35 +01:00
Iain Collins
14fd1c9b40 Update feature_request.md 2020-06-18 17:34:14 +01:00
Daniel Baker
c695ca98e5 Fix typo in auth0 provider config
auth0 was configured with `respponse_type` but should be `response_type`
2020-06-18 17:20:20 +01:00
Lars Eckervogt
7649eb7aed Fix documentation for getSession and update imports to be consistent 2020-06-16 22:03:56 +01:00
Iain Collins
0119622d18 Update issue templates to point to documentation
Have been the asked some questions a few times (including via email).

Most of the time these reports have already been addressed.

Trying to nudge behaviour to check the example project and docs first.

Also want to understand where people couldn't find what they needed.

Removed pre-filled issue title as people were forgetting to change it.
2020-06-16 18:19:38 +01:00
Lori Karikari
15c6781083 fixed some small typo in models docs 2020-06-15 21:02:02 +02:00
Gerald Nolan
574387e09f Added IdentityServer4 2020-06-15 17:27:13 +02:00
Jefferson Bledsoe
aa1f29dc53 Documentation typo fixes (#268) 2020-06-15 17:19:05 +02:00
Jefferson Bledsoe
bb9a26d4fc Change all instances of "identifer" to "identifier" (#267) 2020-06-15 17:12:37 +02:00
Iain Collins
4b036d3beb Update credentials.md 2020-06-15 09:28:06 +01:00
Iain Collins
6dd8dc325e Update callbacks.md 2020-06-15 09:26:07 +01:00
Iain Collins
22005c7465 Update callbacks.md 2020-06-15 09:25:23 +01:00
Iain Collins
0686b5ff32 Bump beta version to 78 2020-06-15 08:58:10 +01:00
StefanWerW
f495ecda3a Fixes no adapter error
Fixes #263
2020-06-15 08:56:29 +01:00
Iain Collins
1f4bc91d87 Add limited support for credentials sign in 2020-06-15 04:01:52 +01:00
Iain Collins
e54bf254cb Bump beta version to 76 2020-06-15 01:09:03 +01:00
Iain Collins
fc2d3adc1f Improve built-in pages on mobile
Resolves #231 for now.

I think there is stil room for improvement, but these tweaks will do for now I think.
2020-06-15 01:08:05 +01:00
Iain Collins
18c99616b3 Bump beta version to 75 2020-06-15 00:03:06 +01:00
Iain Collins
70e3ab7e89 Update callbacks and events documentation 2020-06-15 00:03:06 +01:00
Iain Collins
9b25a2d245 Add OAuth profile to JWT callback on sign in 2020-06-15 00:03:06 +01:00
Iain Collins
966aa8245d Wire up supported events so they are triggered 2020-06-15 00:03:06 +01:00
Iain Collins
d220587018 Add description for each event 2020-06-15 00:03:06 +01:00
Iain Collins
c665631191 Refactor callbacks, add events; add debug logging
* New callbacks implemented
* Some events added (not all wired up)
* Documentation for old callbacks removed, new documentation to be added
* All flows seem to be working with all databases.
* If debug is set to true, debug messages can now be easily logged anywhere in the app

The refactoring has been a success - the code is much more maintainable and the flows are better.

This update need further testing to be sure it's all working as intended
2020-06-15 00:03:06 +01:00
Iain Collins
6032a99a90 Add callbacks and events 2020-06-15 00:03:06 +01:00
Iain Collins
d130251b41 Add file for Google Search Console verification
This is so we can control how the site appears in Google.
2020-06-14 15:13:08 +01:00
Iain Collins
4dd8d3160b Merge branch 'main' of github.com:iaincollins/next-auth into main 2020-06-14 14:49:26 +01:00
Iain Collins
667fe8cf50 Update documentation
* Combined options and advanced options into one page
* Improved structure of sidebar
* Other miscellaneous edits
2020-06-14 14:49:20 +01:00
Iain Collins
554c32c6f1 Refactoring naming strategy
Not a breaking change, just a refactor!

* Removes dependency on external library
* Resolves problem of messy logic in models and transform by putting it all in a naming strategy
* No change to table / collection schemas!
2020-06-14 14:15:28 +01:00
Iain Collins
bdc0e8e16f Update documentation 2020-06-14 04:55:56 +01:00
Iain Collins
3b0527add8 Fix typo in docs 2020-06-14 04:39:44 +01:00
Iain Collins
2b494357e5 Update documentation 2020-06-14 04:39:01 +01:00
Iain Collins
7a0624b8db HOTFIX for sqlite schema transform 2020-06-14 04:13:26 +01:00
Iain Collins
bb8a2c94cc Update postgres.md 2020-06-14 04:01:24 +01:00
Iain Collins
f3532ebef2 Update models to use better table/collection names
* Use plural table/collection names
* Use snake_case on SQL
* Use camelCase on Document DB
* Updated docs
2020-06-14 03:50:22 +01:00
Iain Collins
c5fad1b933 Update user model documentation 2020-06-14 03:50:22 +01:00
Iain Collins
5e9f392ba8 Bump beta version to 72 2020-06-14 03:50:22 +01:00
Iain Collins
f1ed5c1e97 Update documentation
This is a larger update to the documentation than I had planned in this PR but I think makes sense to do it together as so much of it is related.
2020-06-14 03:50:22 +01:00
Iain Collins
5946710fe8 Refactor getProfileFromToken to idToken
This is a minor change to an undocumented feature, but makes sense to rename it while I'm documenting it.
2020-06-14 03:50:22 +01:00
Iain Collins
5cf0056e69 Add script to extract schema from databases 2020-06-14 03:50:22 +01:00
Iain Collins
ac12d6a6e2 Add database drivers as devDependencies for testing 2020-06-14 03:50:22 +01:00
Iain Collins
cc0c15e37c Refactor models and schemas 2020-06-14 03:50:22 +01:00
Iain Collins
9a630dcb01 Rename master branch to main 2020-06-13 19:55:19 +01:00
Iain Collins
d30b112d71 Add getProfileFromToken option 2020-06-12 02:08:43 +01:00
Iain Collins
5fded4256d Add issue templates
Trying to create some structure to get external contributors to think about feature requests and/or provide more information when raising issues.

This is not intended for core committers - I'm just trying to wrangle the input we get to reduce signal to noise, especially as I'm also starting to get emails asking for support now.
2020-06-12 01:06:55 +01:00
Iain Collins
d2fdfa7528 Fix typo in adapter docs 2020-06-12 00:23:25 +01:00
Iain Collins
55c3acab9a Fix for unique emails in MongoDB
The previous approach didn't work at enforcing uniqueness, but have double checked that this approach does.
2020-06-11 13:10:59 +01:00
Iain Collins
dc903f8059 Refactor debug messages in adaptor
Format is now consistant with error logs
2020-06-11 13:10:59 +01:00
Iain Collins
156c8e1e97 Make email addresses optional when signin in 2020-06-11 13:10:59 +01:00
terrierscript
78ba85e74d Fix typo 2020-06-10 00:48:20 +01:00
Iain Collins
6d41089d48 Fix typo in introduction.md 2020-06-09 16:41:13 +01:00
Iain Collins
64b23d484d Fix typo in errors.md 2020-06-09 16:39:05 +01:00
StefanWerW
5f65e8c30d Update example.md 2020-06-09 15:50:39 +01:00
StefanWerW
49d560fa24 Update google.md 2020-06-09 10:04:33 +01:00
Iain Collins
36c469660e Update JWT docs 2020-06-09 09:44:35 +01:00
Iain Collins
416785941b Added JWT helper method 2020-06-09 09:44:35 +01:00
Iain Collins
799bd2dfaa Updated error docs and logger 2020-06-09 09:44:35 +01:00
Nico Domino
c0ccbc9274 WIP: Console Errors with URLs (#222)
* test: override console.error

* update: clean up test URL + test docs

* refactor console.error override into own iife

* update: import override into client + typeorm

* chore: organise errors.md page

* fix: lowercase errorCode required for hash

* update: refactor error fn into own fn instead of (ab)using console.error itself

* add: bold docs msg in console

* update: err function rename + prepare docs URL for prod
2020-06-08 17:57:03 +02:00
Nicola Molinari
0918cdbfa0 fix: missing file export jwt.js in npm release 2020-06-08 14:45:01 +01:00
Iain Collins
077f60e7c4 Allow session.get callback to use data from JWT 2020-06-08 12:25:01 +01:00
Iain Collins
96900e77f6 Fix typos in README 2020-06-08 10:56:17 +01:00
Iain Collins
f43343bd2c Reduce line length of comment in example 2020-06-08 10:45:54 +01:00
Iain Collins
8e69940ae6 Fix typos in documentation 2020-06-08 10:42:10 +01:00
Iain Collins
0d825bbc39 Refactor JWT, Sessions and add allowSignin() method (#223)
## Database

- [x] Databases are now optional - useful with OAuth + JWT if you only need access control
- [x] Updated documentation and added example code for custom database adapters

## JWT

- [x] JWT option is now an object that groups JWT related options together (was a boolean)
- [X] Refactored JWT lib and add AES encryption / decryption as well as signing / verification
- [x] Allows JWT encode/decode methods to be overridden as options
- [x] Contents of JWT can easily customised - without needing to use custom encode/decode
- [x] Exported JWT methods so they can be called from custom API routes
- [x] Updated documentation for new JWT options

## Sessions

- [x] All session options (eg. `maxAge`, `updateAge`) now grouped under single `session` option
- [x] Using JWT for sessions is now enabled from session object (`session.jwt: true`)
- [x] All options involving time now use seconds (instead of milliseconds) for consistency
- [x] Added option to customise the Session object that is returned from `/api/auth/session`
- [x] Update documentation for new Session options

## Other improvements

- [x] Added `allowSignin()` option to control what users / accounts are allowed to sign in
- [x] Refactored `callbackUrlHandler()` - this option  is now called `allowCallbackUrl()` 
- [x] Minor improvements to NextAuth.js client API methods
- [x] Minor to NextAuth.js API routes
- [x] Minor improvements to built-in error pages
- [x] Refactored database models
   All tables now include a `created` column for each row which contains the `datetime` of when the row (e.g. User / Account / Session) was created.
  Additionally, sessions now use the name 'expiry' for the expiry `datetime` value for consistency with other models.
2020-06-08 04:01:21 +01:00
Gerald Nolan
35123f005a Fixed Google Link 2020-06-07 11:51:05 +02:00
Lori Karikari
b25730fbd5 Simplified Auth0 config (#219)
* simplified auth0 conf and made domain stuff more consistent across providers that need one

* removed console.log()

* removed too much
2020-06-06 18:27:23 +02:00
Lori Karikari
585be4ce4a Seperate provider docs (#214)
* added page for Okta

* updated oauth section of provider docs

* added docs for each provider

* email provider
2020-06-06 15:20:01 +02:00
Gerald Nolan
3b5d4b6925 Added Link to ServiceID
Updated App Configuration link to Apple developer portal
2020-06-06 08:15:51 +02:00
dependabot[bot]
39471e9bae Bump websocket-extensions from 0.1.3 to 0.1.4 in /www
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-06 03:23:50 +01:00
Iain Collins
50039e5a6b Bump beta version to 62
This includes the OKTA provider and an improved README.
2020-06-06 03:02:10 +01:00
Iain Collins
cb31f9e554 Fix typos in in README.md URLs
🤦
2020-06-06 02:08:49 +01:00
Iain Collins
f21fb0f46d Update introduction.md 2020-06-06 02:05:04 +01:00
Iain Collins
13c6801c45 Update README.md 2020-06-06 02:04:40 +01:00
Iain Collins
1a1b0ffdc6 Add sharing image for Facebook and Twitter 2020-06-06 01:56:44 +01:00
ndo@ndo3
a638e2b27a chore: cleanup 2020-06-06 01:52:36 +01:00
ndo@ndo3
3ce64da78f update: line length to avoid horizontal scroll 2020-06-06 01:52:36 +01:00
ndo@ndo3
5537514b4f update: pull meta info from docusaurus config 2020-06-06 01:52:36 +01:00
ndo@ndo3
42363101f8 update: essential meta tags 2020-06-06 01:52:36 +01:00
ndo@ndo3
bdafc4a2f7 add: meta images / descriptions 2020-06-06 01:52:36 +01:00
Iain Collins
bdb1216119 Fix broken logo src 2020-06-06 01:52:36 +01:00
Iain Collins
e509c28a4f Update docs, mostly for custom pages 2020-06-06 01:52:36 +01:00
Iain Collins
4f5954938a Add logo 2020-06-06 01:52:36 +01:00
mdemin914
267a166895 adding support for Okta as a provider (#213) 2020-06-05 22:55:48 +02:00
Iain Collins
e655e3c550 Update client.md 2020-06-04 23:52:32 +01:00
Iain Collins
6ea00f44dd Update example for session() 2020-06-04 23:10:29 +01:00
Seva Maltsev
843c258dd3 Update client.md (#204)
Added example of using session for api routes
2020-06-04 16:21:46 +02:00
Iain Collins
c6c94b1805 Remove oudated information from CONTRIBUTING.md 2020-06-03 22:06:59 +01:00
Iain Collins
315d75e40b Fix bug parsing hostname from database URL
Resolves #200
2020-06-03 22:01:37 +01:00
Iain Collins
f8bfe0c613 Fix bug linking accounts when using JWT with Mongo
Resolves #198
2020-06-03 18:31:06 +01:00
Iain Collins
50b9743bb6 HOTFIX for incorrect params to createSession
Resolves #197
2020-06-03 09:14:51 +01:00
Iain Collins
9c9abf19a3 Update introduction.md 2020-06-03 05:02:27 +01:00
Iain Collins
51f9f6fe6f Update contributors.md 2020-06-03 04:59:12 +01:00
Iain Collins
0c2c4cab69 Add JWT docs and update other docs 2020-06-03 04:41:43 +01:00
Iain Collins
ceb35cd036 Add JWT session support
* Now has jwt and jwtSecret options
* Set jwt: true to use JWT instead of DB for session
* Enable 'debug: true' to log JWT_SESSION_TOKEN to console if you want to see what it contains
* Magical!
2020-06-03 04:41:43 +01:00
Iain Collins
f9d0719ec4 Fix sessionExpiry on updateSession 2020-06-03 04:41:43 +01:00
Iain Collins
40f36e5ee9 Improve note on client API docs 2020-06-01 18:30:33 +01:00
Iain Collins
e9903d5391 Add documentation about provider client method 2020-06-01 18:22:27 +01:00
Gerald Nolan
a5bc38e61c Updated apple.md (#194)
Convert .k8 to single line to use as environment variable
Added additional comments on how to generate local certificate/edit host file
2020-06-01 19:19:57 +02:00
Iain Collins
6df7322493 Fix bug in Apple provider 2020-06-01 17:48:06 +01:00
Iain Collins
0495057458 Fix bug in VerificationRequest schema 2020-06-01 17:48:06 +01:00
Iain Collins
8a2ee7cbce HOTFIX CSS on signin page in Chome 2020-06-01 13:33:52 +01:00
Iain Collins
a465e2cda8 Improve styling on built-in pages
Improved font usage and button apperance.
2020-06-01 13:24:11 +01:00
Iain Collins
81c22f81ca Bump beta version number 2020-06-01 12:24:45 +01:00
Jibin George
4e4457f3ce Typo fix 2020-06-01 12:23:14 +01:00
Iain Collins
e993bc4f2a Bump beta version number 2020-06-01 04:03:44 +01:00
Iain Collins
c4fe49b0af Update Apple and Email provider docs 2020-06-01 04:03:44 +01:00
Iain Collins
5f211c8d0a Update advanced and basic options docs 2020-06-01 04:03:44 +01:00
Iain Collins
3e41381a52 Update next auth client documentation 2020-06-01 04:03:44 +01:00
Iain Collins
93488846e2 Refactor check-email page as verify-request
This allows the API surface to be more consistent
2020-06-01 04:03:44 +01:00
Iain Collins
9609d44638 Tell ESLint to ignore docs theme files 2020-06-01 04:03:44 +01:00
Iain Collins
59403ec607 Refactor Apple provider to genereate secret dynamically
See #176
2020-06-01 02:20:29 +01:00
Iain Collins
5f0f403b50 Refactor emailVerification to verificationRequest
Changes to verification model mean it can be used for other kinds of verification requests when credentials flow is implemented.

e.g. can support verification via SMS or an app.

Also includes minor update to account model.
2020-06-01 01:54:13 +01:00
Iain Collins
fdae191116 Fix linting and minor bug in client 2020-06-01 01:54:13 +01:00
Iain Collins
15424d2d03 Fix bug with callbackUrls not being passed to custom pages 2020-06-01 01:54:13 +01:00
Iain Collins
9d2d7133a1 Bump beta version number 2020-06-01 01:06:44 +01:00
Iain Collins
f50013899a Preserve callback URL on custom signin and signout pages 2020-06-01 01:06:44 +01:00
Iain Collins
beb2d08260 Add signin and signout methods to client
* Using method preserves current URL on signin / signout
* Reloads browser window once complete to force session update

The force reload behaviour may change to something more graceful in future, but is good for now as ensure page state is correct.
2020-06-01 01:06:44 +01:00
Iain Collins
b39d491df3 Rename tests dir to test (more conventional) 2020-06-01 01:06:44 +01:00
Iain Collins
7560d4ba80 Add new methods to client 2020-06-01 01:06:44 +01:00
Iain Collins
e90244b167 Adding to documentation
* Added REST API docs to sidebar
* Added documentation for the Apple Provider (WIP)
* Added clarity to documentation of options
* Added links to footer
2020-05-31 16:16:28 +01:00
Jibin George
a72aef7a86 clientSecret is required for Auth0
Seems like `clientSecret` is mandatory for Auth0 Provider.
2020-05-31 16:14:47 +01:00
Iain Collins
39e97c3b96 Restructure and extend documentation
Includes some minor tweaks to options to match documentation (non breaking changes).
2020-05-31 05:15:39 +01:00
Jibin George
e7d7a7ccab update 2020-05-30 19:41:25 +01:00
Jibin George
8b173efe96 Fix Provider component import path 2020-05-30 19:41:25 +01:00
Gerald Nolan
f53a7f3b85 Fixed Lint Issues 2020-05-27 14:30:15 +01:00
Gerald Nolan
62f5d7ebe1 Refactor -> Remove oauth-apple 2020-05-27 14:30:15 +01:00
Gerald Nolan
fd6fceb884 Sign In with Apple 2020-05-27 14:30:15 +01:00
Lori Karikari
74a5f459f5 Some cleanup (#173)
* shortened some long urls

* some fixes in text
2020-05-26 22:22:05 +02:00
Nico Domino
7b38af81cf Update README - Fix Typos 2020-05-26 21:48:25 +02:00
Iain Collins
401df2c177 Update Getting Started guide 2020-05-26 19:44:13 +01:00
Iain Collins
ffd9691cd0 Update docs for database and secret options 2020-05-26 19:44:13 +01:00
Iain Collins
e7ae32f618 Update www/docs/configuration.md 2020-05-26 19:39:28 +01:00
ndo@ndo1
97fadb0d9f fix: cleanup code example 2020-05-26 19:39:28 +01:00
ndo@ndo1
86f072bf4b add: docs for custom signin page 2020-05-26 19:39:28 +01:00
Iain Collins
981984b562 Improve database URI handling
* Fix bug in parser (.query -> search)
* Comments to explain what is going on
* Fallback to TypeORM parser
2020-05-26 17:48:04 +01:00
Iain Collins
1e9053d879 Add support for passing URL to 'database' option
* Database configuration now only needs a single line!
* You can still specify options using query string parameters.
* You can still specify an object, so this is not a breaking change.
2020-05-26 13:19:47 +01:00
Iain Collins
cb1ce73c92 Update title of documentation homepage 2020-05-26 13:19:47 +01:00
Iain Collins
93054578c9 Update README.md 2020-05-26 02:08:03 +01:00
Iain Collins
d112800b98 Add custom pages
Now supports 'pages' option, which can be any URL.

If specified, these replace the built in pages.

Example usage:

pages: {
  signin: 'https://example.com/signin',
  signout: 'https://example.com/signout',
  checkEmail: 'https://example.com/check-email',
  error: 'https://example.com/error'
}
2020-05-26 01:02:02 +01:00
Iain Collins
c8bf342d8b Fix sqlite support 2020-05-25 21:43:50 +01:00
Iain Collins
63ceb1a260 Don't lookup session if session token empty 2020-05-25 21:24:45 +01:00
Iain Collins
ca519b69ce Fix schema and queries for mongodb 2020-05-25 21:24:45 +01:00
Iain Collins
2f16d8448d Fix issues with database; make it easier to test
These changes fix compatibility issues with common SQL databases including MySQL, MariaDB and Postgres.

* Fixes #147 - datetime now ANSI SQL timestamp
* Fixes #160 - AccessToken and RefreshToken type change from varchar to text
* Adds Docker Compose files to make it easier to test database integration.

TODO:

* Update documentation with configuration examples and latest compatibility info
* Create DB URI parser (currently only object config works)
* Database table/collection name prefix (will default to `next-auth_`)
* MongoDB support

MongoDB has some issues which mean it will require additional work and refactoring to support (while preserving SQL DB support, which is important).

It's going to take some thinking about to get right; MongoDB support might have to be dropped from 2.0 (and follow in a subsequent release) but I'm going to review options and consider the impact before making a call.
2020-05-25 18:15:33 +01:00
Iain Collins
74b334f7ad Fix default exports in entrypoints
Resolves #157
2020-05-25 12:11:35 +01:00
Lachlan Campbell
d5a231f51b Fix typos: directory is called “pages” (#155) 2020-05-24 17:28:55 +02:00
ndo@ndo3
9b24e216fa fix: import statement typo 2020-05-23 17:31:19 +02:00
Lori Karikari
a944870eb2 changed options to params (#154) 2020-05-23 16:32:44 +02:00
Iain Collins
bc6fd4aa32 Bump version number to beta 43 2020-05-23 03:56:54 +01:00
Iain Collins
4a00d5aca5 Fix error when missing email in profile
* Fixes #145
* See also #131

This doesn't allow signing in without an email address, but it handles it gracefully.
2020-05-23 03:55:41 +01:00
Iain Collins
c55cb526f7 Update about.md 2020-05-23 01:56:31 +01:00
Iain Collins
70a728f15b Update documentation 2020-05-23 01:51:31 +01:00
Iain Collins
e7c9c844dc Update document site (#149) 2020-05-23 00:14:26 +02:00
Iain Collins
cf8e6980be Simplify database configuration
* Now accepts 'database' as an option as an alterantive to 'adapter'.
* If specified, 'database' can be a string or object and will load the default adapter.
* The 'adapter' option is still valid, and overrides the 'database' option.

 If neither option is specified, displays console error and web error page.
2020-05-21 20:51:58 +01:00
Iain Collins
7cd537d58d Customise website theme and homepage 2020-05-21 20:21:56 +01:00
ndo@ndo3
7ad11f73cd fix: quick typo 2020-05-21 12:34:43 +02:00
Nico Domino
82ac943e3e update: docs formatting / spelling / small stuff (#138) 2020-05-20 22:26:38 +02:00
Iain Collins
420bb9a74c Fix default session expiry time
Due to typo, was setting default expiry time to 30 hours, instead of 30 days.

This also made update session  behave incorrectly.
2020-05-20 18:39:28 +01:00
Iain Collins
4c32727b37 Refactor urlPrefix as baseURL
The name baseURL (and basePath) are exposed as options.

As they are more more widely used than urlPrefix and pathPrefix I've renamed them globally.
2020-05-20 18:39:28 +01:00
Iain Collins
339f618685 Refactor urlPrefix as baseURL
The name baseURL (and basePath) are exposed as options.

As they are more more widely used than urlPrefix and pathPrefix I've renamed them globally.
2020-05-20 18:10:37 +01:00
Lori Karikari
2a8337e67c Provider docs cleanup (#135)
* some initial cleanup

* cleaned up the config table

* added FB and fixed some alignments
2020-05-19 20:54:52 +02:00
Iain Collins
bd50714759 Merge branch 'master' of github.com:iaincollins/next-auth 2020-05-19 18:38:43 +01:00
Iain Collins
db9ef09d1d Fix edit URL in docs 2020-05-19 18:38:36 +01:00
Merijn
3bb4e0ca6f Fix parameter params 2020-05-19 14:16:47 +01:00
Iain Collins
b4886295ac Rename compound id field in account model
This constraint provides a cross-platform way of enforcing that a given oAuth account can only be associated with a single user, while allowing a user to link multiple oAuth accounts (and use any account they own to sign in).
2020-05-19 03:37:27 +01:00
Iain Collins
ef455dcf06 Add more information to contributing guide
Added the detailed steps on to set up an environment locally from #105
2020-05-19 02:39:28 +01:00
Iain Collins
5afa4f6e2b Refactor adapter logic
* Refactored adapter, with less redundant logic
* Removed logic from models
* Added email verification expiry support (defaults to 24 hours)
* Refactored session expiry handling and unified it with how email expiry works
* Default session expiry is still 30 days
* Now only updates expiry for a session at most once every 24 hours by default, to reduce writes to database
* Email verification max age, session max age and how often sessions are updated (to reduce database writes) are all simple options now
* Invalid sessionTokens are now deleted from the client
* Email verfication messages are now deleted once used (or when expired)
* Debug output is now an option (set `debug: true` to enable)
* Removed confusing options / callback from default adapter (except for passing in custom models/schemas)
* Adapter can now access all next-auth options, to make configuration easier
2020-05-19 02:08:10 +01:00
Iain Collins
50678d73bd Allow sessionToken cookie options to override defaults
This makes it possible to configure  session tokens to be deleted when the browser window is closed if desired.

Session expiry can now be treated as an optional field (but is always set and enforced by default).
2020-05-18 19:04:36 +01:00
Iain Collins
6d7066e4db Fix bug in session route
Accidentally included set cookie of a conditional it needs to be in.
2020-05-18 18:03:26 +01:00
Iain Collins
52eb11b385 Add session expiry logic
* By default, sessions are 30 day 'rolling sessions' and the timestamp for when they expire is extended when they are accessed to keep them alive.
* When sessions expire (ie after 30 days of inactivity), session object returns empty (as if there is no session) and users must sign in in again.
* Cleaning up old sessions from the database is not currently handled by the default adapter, but I do intend to add some logic to do this (added @TODO).
* The session expiry date can be changed by passing a custom updateSession() callback handler function in the options to the default adapter.

Using a custom `updateSession()` method with the default adapter, it is possible to specify other behaviour:

e.g.

* Disable rolling sessions (e.g. force a new login every X days).
* Create a session expiry date far into the future on initial sign in, so that they effectively never expire.
* Set a decently long max expiry time (e.g. 90+ days) but only actually update the session expiry time if the current expiry time is < 30 days; so that sessions stay valid for 30 days (and at most 90 days of inactivity) so that idle sessions are valid for at least 30 days (and maybe longer) but you don't need to write to your session database as often (useful if slow/expensive).

Note: Adapter options are passed as second option to the default adapter (the first option being the DB connection details). This is probably confusing and might be a design mistake.

const adapter = Adapter.Default({ /* database object * /}, {
  updateSession: async (session, isNewSession) => {
    // 1st arg is the current session (or null) so it's easy to check current
    // expiry date, get user specific info, etc.
    // 2nd arg is true if this is a brand new session.
    //
    // Function should return an ISO date (e.g. toISOString) or false/null to
    // prevent an update from being applied; but should always return a session
    // if isNewSession is set or the sign in will fail.
  }
})

Relying on on Adapter options is a little obtuse / confusing and so I'm considering it an 'advanced option' right now. In future, we might change how session expiry dates and behaviour is set to make it easier.

Note: There are some other updates in this PR, that's just from the linter and some improvements to formatting of contributing guide.
2020-05-18 17:49:32 +01:00
Nico Domino
b176c15405 Docs - Add search (#129) 2020-05-18 15:10:40 +02:00
Iain Collins
021fdbcf1b Update contributing docs 2020-05-18 09:15:02 +01:00
Lori Karikari
d7d9988cd8 Add auth0 (#126)
* added Auth0 and updated docs

* changed to proper Auth0 urls
2020-05-17 23:28:18 +02:00
Nico Domino
e8baee1774 Another Docs Update (#124) 2020-05-17 23:21:14 +02:00
Lori Karikari
79179dad71 added Auth0 and updated docs (#125) 2020-05-17 23:21:02 +02:00
Iain Collins
c8de8a1182 Fix editUrl in docs
I think I broke this earlier by mistake
2020-05-17 22:08:14 +01:00
ndo@ndo3
a2cfcef0aa update: docs site 2020-05-17 22:05:27 +01:00
Lori
28d220a42b added Facebook 2020-05-17 20:57:16 +01:00
Iain Collins
26a8b20459 Bump version number
Debugging issue with deployment of docs site.
2020-05-17 20:42:16 +01:00
Iain Collins
84e0ddf241 Fix issue with docusaurus config on now.sh
Although previous config worked locally, it turns out it isn't compatible with now.sh.

It turns out when deploying from a subdir (like 'www') on now.sh the contents of the parent directory isn't avalible.
2020-05-17 20:30:56 +01:00
Iain Collins
6e3a6ba287 Update docs and website dir structure
* Now has 'www' directory at root level for the website (was 'docs').
* The 'docs' directory now only contains Markdown docs.
* Docusarus config looks in '../docs' for the docs.

This is deployed with now.sh to https://next-auth-docs.now.sh
2020-05-17 20:13:54 +01:00
Nico Domino
d6e7b09ff7 Update docusaurus.js to work with now.sh again 2020-05-17 19:01:50 +02:00
Iain Collins
daca296df4 Remove .vscode dir I commited by mistake 2020-05-17 17:49:24 +01:00
Iain Collins
dbab5a3505 Refactor to remove oauth cruft
Removed unesseary branching for unused oauth code.
2020-05-17 17:45:00 +01:00
Iain Collins
8aa4045651 Force email to lowercase in all flows 2020-05-17 17:45:00 +01:00
ndo@ndo3
eb9561edab chore: cleanup markdown + CNAME 2020-05-17 17:45:00 +01:00
ndo@ndo3
332182a67f add: CNAME 2020-05-17 17:45:00 +01:00
ndo@ndo3
d7a2cde57e update: sidebar labels 2020-05-17 17:45:00 +01:00
ndo@ndo3
bb04645a93 update: package.json 2020-05-17 17:45:00 +01:00
ndo@ndo3
d25493ae79 add: docusaurus docs 2020-05-17 17:45:00 +01:00
Lori
8522628a11 removed incomplete custom email, added some more links and cleanup 2020-05-17 17:45:00 +01:00
Lori
875ecaeb06 first draft 2020-05-17 17:45:00 +01:00
Iain Collins
25c83b2914 Update session.js 2020-05-17 17:45:00 +01:00
Iain Collins
8a516904b8 Force email to lowercase in all flows 2020-05-17 17:45:00 +01:00
Iain Collins
df4c71496b Fix bugs with sign in flow and error handling 2020-05-17 17:45:00 +01:00
Iain Collins
026bef6f60 Improve error handling
* Better error handling, more specific messages.
* Async email option has been removed as was problematic on serverless.
* Refactored email sign in so that sending emails is now handled by the email provider.
* How email configuration works is now more customimzable - and cleanly seperated from  database logic.
* Now possible to define logic for async email (e.g. pass messages to a queue) or use any email provider or API.
2020-05-17 17:45:00 +01:00
Iain Collins
2b168e183b Improve error messages 2020-05-17 17:45:00 +01:00
Iain Collins
c86ea5e9dc Refactor sign in; make async email optional
* Email providers can now set  the option 'async' to 'true' to send emails AFTER displaying confirmation page, or to 'false' send emails BEFORE returning to the user. Defaults to false.

Setting it to true is faster for the user, but is hard to debug as it's not easy to know if it worked or not.

* Fixed bug with unsubscribe option.

* Moved oAuth and Email signin handlers together in `lib` dir.
2020-05-17 17:45:00 +01:00
Iain Collins
966577fc02 Improve email sign in flow 2020-05-17 17:45:00 +01:00
Iain Collins
d0d3af5f12 Bump version to beta 23 2020-05-17 17:45:00 +01:00
Iain Collins
c62617532f Improve email sign in (email, error, options) 2020-05-17 17:45:00 +01:00
Iain Collins
fc28374f88 Add email sign in flow
* Added email verification adapater methods
* Added support on sign in page for email providers
* Added check email page
* Added SMTP transport to send email messages

Includes refactoring of model and handlers for the email verification flow.
2020-05-17 17:45:00 +01:00
Iain Collins
6ec9d8e9d0 Rename deleteUserById to and getUserById
Brings them into line with other methods.

Not refactoring other getUser* methods at this time as may be helpful for them to be explicit about what will be passed.
2020-05-17 17:45:00 +01:00
Iain Collins
26d41d4a2b Refactor session API in adapter
* Renamed 'Session ID' to 'Session Token'.
* Applies to model, functions and default cookie name.
* This avoids confusion by seperating it from 'id' property in session model.
2020-05-17 17:45:00 +01:00
Iain Collins
b6c2befba7 Add verification request methods to adapter 2020-05-17 17:45:00 +01:00
Iain Collins
0d96a7e9e5 Rename Invite model to Verify 2020-05-17 17:45:00 +01:00
Iain Collins
3006161bce Documentation and linting updates
* Updated documentation
* `lint` and `lint:fix` now seperate scripts
* Fixed simple linting issues

Still some linter errors as the email sign up flow is a work in progress.
2020-05-17 17:45:00 +01:00
Lori Karikari
c653a1cc72 Added Mixer, Discord, Slack and Reddit (partially) (#111)
* added a temporary? state param
* added Discord, Mixer, Slack and partial Reddit providers

Co-authored-by: Iain Collins <me@iaincollins.com>
2020-05-17 17:45:00 +01:00
Iain Collins
301f048ce3 Signup bug fixes and enhancements
* Improve CSRF token verification
* Improved access token generation
* Added work in progress code for email signin provider
2020-05-17 17:45:00 +01:00
Iain Collins
3ac6666bee Additional debugging in oAuth callback
Logs provider name and code / token when access token request fails.
2020-05-17 17:45:00 +01:00
Iain Collins
73a5be5d6c Fix for breaking changes in Twitch API
Twitch recently made breaking changes to their oAuth API.

It no longer works like other oAuth 2 providers. The documentation for it is extensive, but poor quality.

This update still has intermittant problems, but as far as I can make out  the problem is the API; they have completed their roll out to 100% but it's still failing sometimes.
2020-05-17 17:45:00 +01:00
Iain Collins
ed6328679a Improve client
* Improve options passing
* Fix bug with cookie parsing
* Remove isomorphic-unfetch (fetch built from Next.js 9.4)
2020-05-17 17:45:00 +01:00
Iain Collins
8eb9c4822e Bump version to beta 16 2020-05-17 17:45:00 +01:00
Iain Collins
8a9e2305c8 Fix typo in comment 2020-05-17 17:45:00 +01:00
Iain Collins
7ef2a2ec93 Add eslint with Standard JS and reformat code
* Run `npm run lint` to find (and where possible, fix) linting issues.
* Includes some minor refactoring, including directory structure for adapters and models, so that code for an adapter and the models for it sit together.

Background:

I've added elint to try and ensure a consistent style and to uncover hidden bugs.

I don't actually care much about what the rules are, it's just helpful to have a baseline.

If it's hard to get code to be compliant, I would rather we just disable a rule in that block of code until we can figure it out and am totally fine with that.

I'd much prefer that than the chore of maintaining a custom set of rules, which is why I just picked Standard JS.

Unfortunately, there is quite a lot that doesn't match the Standard JS format at this point, so this is going to be a big PR.

The file size has gone down in quite a few places, which is nice. I think it may have uncovered potential bugs.

I've run through the flow and everything seems to work as before, though it took some debugging after refactoring.

I have not yet added eslint to a commit hook and am in two minds about that.

This is an open source project and I'd like to make it easy to maintain, but also to have as low a barrier to entry as possible for contributors.

I'm happy to go with encouraging folks to run the linter and try to fix errors they find and to take on the work of wrangling any issues myself.
2020-05-17 17:45:00 +01:00
Fredrik Pettersen
67d49fe483 refactor: Combine useSession and useGlobalSession into one hook 2020-05-17 17:45:00 +01:00
Fredrik Pettersen
cc2753efd5 feat(client): Add useGlobalSession which uses react context 2020-05-17 17:45:00 +01:00
Iain Collins
d0a403e56a Improve auth page CSS 2020-05-17 17:45:00 +01:00
Iain Collins
ab9d1d0a91 Add log and error page to handle signup errors
This error page will be used to handle display all errors to the client.

There will be an option to provide a custom error page URL.

Update includes some tweaks to CSS.
2020-05-17 17:45:00 +01:00
Iain Collins
c85ad74508 Fix bug with session expiry date
* Should database compatability issues with the model.
* Session expiry dates are still not enforced in client.
* All cookies are still sesison cookies and expire when the browser is closed.
* AccessToken expiry has been removed for now.

These are all know issues and intended behaviour for now, and will be addressed before release.
2020-05-17 17:45:00 +01:00
Iain Collins
2dca9308e9 Delete .DS_Store
<<< .DS_Store rage intensifies >>>
2020-05-17 17:45:00 +01:00
Iain Collins
494a267527 Fix typo import twitch provider
This worked locally as local file system is not case sensitive.
2020-05-17 17:45:00 +01:00
Iain Collins
4c163d54ca Fix bug with callback URL triggered on signout
In some flows the signout values was returning 'undefined'.
2020-05-17 17:45:00 +01:00
Iain Collins
b9853b362b Export Twitch provider
I haven't had a chance to test it myself yet, but very happy to include it!
2020-05-17 17:45:00 +01:00
Lori
121e978d76 added Twitch provider 2020-05-17 17:45:00 +01:00
Iain Collins
b9142217a9 Refactor callback URL handler
Less code in one place and less code overall.
2020-05-17 17:45:00 +01:00
Iain Collins
74d67dd801 Refactor callback URL handling
* Logic now centralized to avoid duplicaiton across multiple routes.
* Improved validation of query params.
* Also checks and cookie values as mitigation against cookie hijacking.
2020-05-17 17:45:00 +01:00
Iain Collins
121ed4a58e Add deleteSessionById() so signing out works
Can now securely sign out. Session cookie and entry in session db are deleted.
2020-05-17 17:45:00 +01:00
Iain Collins
cf903ca82e Add route to handle signout POST
* CSRF token is verified first.
* If token doesn't match, redirect client to signout URL to prompt for confirmation.
* `deleteSessionById()` not yet implemented in default adapter, so does not work.
* Identified area for reafactoring around callbackUrl behaviour.
2020-05-17 17:45:00 +01:00
Iain Collins
2f61795697 Add verification of URL prefix cookie
Improves security and defence against bad actors by adding a hash that uses the secret as a salt and checking it on every request (and overriding the cookie with a new secure one if the check fails.)
2020-05-17 17:45:00 +01:00
Iain Collins
d5257fe1db Add signout page
This commit does not include handling of actual signout request.
2020-05-17 17:45:00 +01:00
Iain Collins
822fbee0c4 Fix bugs with server side session handling
* Sets site name + api route now prior to sign in so avalible sooner.
* Improved next-auth/client logic for server side session handling.
* next-auth/client now checks regular and `__Secure-` prefixed cookies.
2020-05-17 17:45:00 +01:00
Iain Collins
937f9cdfda Fix case sensitivy of prefix for secure cookies 2020-05-17 17:45:00 +01:00
Iain Collins
2bb9355933 Improve signin page
* Better contrast
* Displays site name correctly
2020-05-17 17:45:00 +01:00
Iain Collins
57a9021107 Add route to return CSRF token to clients 2020-05-17 17:45:00 +01:00
Iain Collins
71fecfb1f2 Standardize done() call for routes 2020-05-17 17:45:00 +01:00
Iain Collins
1b374817f0 Improve and standardize how responses are returned
Use .status() and .json() where possible.
2020-05-17 17:45:00 +01:00
Iain Collins
eee927a6cd Add CSRF token and improve cookie security
Better default security for cookies, without impacting UX or DX.

Further work to do on CSRF protection, but this is a good start.
2020-05-17 17:45:00 +01:00
Iain Collins
0fabfa4ef9 Add viewport metatag to pages for mobile devices 2020-05-17 17:45:00 +01:00
Iain Collins
57bf54c28d Fix useEffect() warning in client
Technically useEffect should not return a value, or generates a warning.
2020-05-17 17:45:00 +01:00
Iain Collins
9bbc9100ab Update documentation 2020-05-17 17:45:00 +01:00
Iain Collins
e6cd78d71b Bump version number to publish new README 2020-05-17 17:45:00 +01:00
Iain Collins
3d66b90cf8 Update README.md 2020-05-17 17:45:00 +01:00
Iain Collins
ebfb02bd12 Bump version number to update docs 2020-05-17 17:45:00 +01:00
Iain Collins
2032ff1276 Update README 2020-05-17 17:45:00 +01:00
Iain Collins
08582aad83 Fix bugs with parsing options
Some of the logic was wrong following refactoring.
2020-05-17 17:45:00 +01:00
Iain Collins
c9944820c6 Fix bug in client session method
Introduced when refactoring error handling
2020-05-17 17:45:00 +01:00
Iain Collins
0697609dd0 Add beta tag to version
While it is still somewhere between an alpha and a beta am publishing to NPM to facilitate further testing.

The software is not ready for use yet!

While belived to be functional there are no formal tests and only casual manual testing has been done.

Features such as logout and session expiry checks have not yet been implemented.
2020-05-17 17:45:00 +01:00
Iain Collins
39d3689c22 Improve client error handling 2020-05-17 17:45:00 +01:00
Iain Collins
43023293ea Remove install script
No longer needed.
2020-05-17 17:45:00 +01:00
Iain Collins
91f319bc5f Add script to run build after install
This is to temporarily facilitate testing.
2020-05-17 17:45:00 +01:00
Iain Collins
f847488643 Improve error handling in client 2020-05-17 17:45:00 +01:00
Iain Collins
731e227cb6 Update babel config 2020-05-17 17:45:00 +01:00
Iain Collins
f2aafac40c Update formatting in README.md 2020-05-17 17:45:00 +01:00
Iain Collins
5bff4cb07f Add hook, improve client, cookies and docs
* Added React Hook to client.
* NextAuth.session() is now a universal method.
* Improved cookie support, all cookie names and options can be customised (feature request).
* Updated examples in documentation.
2020-05-17 17:45:00 +01:00
Iain Collins
06ef47cc40 Update client and documentation
This is very much a work in progress!
2020-05-17 17:45:00 +01:00
Iain Collins
3e0e4ecb5d Add simple client 2020-05-17 17:45:00 +01:00
Iain Collins
651f3c9887 Improve session and account linking 2020-05-17 17:45:00 +01:00
Iain Collins
cfbe24fc24 Add sessions
While not all signup flows are complete, basic core functionality is now working.

Users can sign in, have their identify verified and session is created for them in a secure manner.

* Added Session model and schema.
* Added createSession and getSessionByID handlers.
 * Added getUserByID handler.
 * Added /api/auth/session endpoint which displays info about the current session.
 * /api/auth/session endpoint is secure as it requires the HTTP only cookie.
 * Remove schema relationship data for now (no value currently and may cause problems if not modeled correctly).
2020-05-17 17:45:00 +01:00
Iain Collins
9432cfda90 Add Preact and PostCSS for pages
We need to be able to return simple server-side rendered pages for authentication (e.g. signin).

Using Preact means we can use JSX in them while keeping depedancy size small.

Ultimately, these pages should be customizable - both by passing in CSS and by specifying custom URL for page.

Additionally, the babel config has been tweaked to reduce bundle size by minifying assets and stripping comments from built assets.
2020-05-17 17:45:00 +01:00
Iain Collins
981adaae24 Update README.md 2020-05-17 17:45:00 +01:00
Iain Collins
ec3da81887 Add option to extend models / use custom models 2020-05-17 17:45:00 +01:00
Iain Collins
d150a7911c Update documentation 2020-05-17 17:45:00 +01:00
Iain Collins
018738bcc0 List files to be published to NPM in package.json 2020-05-17 17:45:00 +01:00
Iain Collins
e37e20faf5 Add babel config for esmodule
This allows next-auth to be imported into Node.js projects.
2020-05-17 17:45:00 +01:00
Iain Collins
4bf13394f1 Add database adapter and models
* Uses typeorm as works with a large number of data stores.
* Compatible with common SQL, document storage & lightweight databases.
* Adapter logic integrated into signup flow but not yet complete.
2020-05-17 17:45:00 +01:00
Iain Collins
3dad0cc849 Apply custom provider options after default
Makes it easier to override options as needed.
2020-05-17 17:45:00 +01:00
Iain Collins
ea69d1e904 Initial commit of signin flow
* Flow not fully complete or tested, but can complete signin journey.
* Does not include source code for adapter (test adapater source currently in client respository while under development).
* Wrapped all calls in promise to avoid early termination of serverless function.
* Callback and Session cookie names and cookie options can now be changed by passing options for them in (feature request from 1.x).
2020-05-17 17:45:00 +01:00
Iain Collins
b666cde7a7 Update README.md and configuration
* Renamed the `serverUrl` configuration variable to `site`.
* Improved cosmetic apperance of place holder sign in page.
2020-05-17 17:45:00 +01:00
Iain Collins
e3784bba9d Initial commit of next-auth 2.0
* Redesigned from the ground up for serverless!
* Doesn't require PassportJS or Express!
* Much simpler configuration!
* Interface exposed via single API endpoint.
* Supports both oAuth 1.x and 2.x services.
* Initial commit includes support for signing in with Twitter, Google and GitHub.

Code is functional, but not useable as adapter support (saving user information) is still in progress.

Still to come:

* Support for Facebook, email, and  credential authentication flows.
* Adapter support - will provide out of the box support for MongoDB, Elasticsearch and SQL with support for writing custom adapters in a similar style to version 1.x.
* Automated User Acceptance Tests for all auth flows.
* Example usage . In a change from version 1.x a demo will exist in a seperate repository to make it easier to get started.
2020-05-17 17:45:00 +01:00
182 changed files with 31727 additions and 12342 deletions

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,32 @@
---
name: Bug report
about: Report a defect with the software
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the defect is.
**To Reproduce**
Steps to reproduce the behavior.
Include example code (or link to public repository) which can be used to reproduce the behaviour.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or error logs**
If applicable, add screenshots or error logs to help explain the problem.
**Additional context**
Add any other context about the problem here.
**Documentation feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful
* [ ] Found documentation but was incomplete
* [ ] Could not find relevant documentation
* [ ] Found the example project helpful
* [ ] Did not find the example project helpful

View File

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

23
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Question
about: Ask for information or support
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.*
**Your question**
A clear and concise question.
**What are you trying to do**
A description of what you are trying to do.
**Documentation feedback**
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
* [ ] Found the documentation helpful
* [ ] Found documentation but was incomplete
* [ ] Could not find relevant documentation
* [ ] Found the example project helpful
* [ ] Did not find the example project helpful

27
.gitignore vendored
View File

@@ -1,3 +1,28 @@
.next
.env
node_modules
.vscode
node_modules
dist
.DS_Store# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Docusaurus
www/build

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting me@iaincollins.com. All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. The project team is obligated to maintain
confidentiality with regard to the reporter of an incident. Further details of
specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

108
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,108 @@
# Contributing guide
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.
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
* 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
## Rebasing
*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!*
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:
1. Clone the repo:
git clone git@github.com:iaincollins/next-auth.git
cd next-auth/
2. Install packages and run the build command:
npm i
npm run build
3. Link React between the repo and the version installed in your project:
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).*
4. Finally link your project back to your local copy of next auth:
cd ../your-application
npm link ../next-auth
That's it!
Notes: You may need to repeat both `npm link` steps if you install / update additional dependancies with `npm i`.
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
### 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.
cd next-auth/
npm run watch
If you are working on `next-auth/src/client/index.js` hot reloading will work as normal in your Next.js app.
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**.
### 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.
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.
When stop the databases, it will reset their contents.
### Testing
Tests can be run with `npm run test`.
Automated tests are currently crude and limited in functionality, but improvements are in development.
Currently to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.

View File

@@ -1,6 +1,6 @@
ISC License
Copyright (c) 2018, Iain Collins
Copyright (c) 2018-2020, 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

289
README.md
View File

@@ -1,221 +1,108 @@
# NextAuth
# NextAuth.js
## About NextAuth
## Overview
NextAuth is an authentication library for Next.js projects.
NextAuth.js is a complete open source authentication solution for [Next.js](http://nextjs.org/) applications.
The NextAuth library uses Express and Passport, the most commonly used authentication library for Node.js, to provide support for signing in with email and with services like Facebook, Google and Twitter.
It is designed from the ground up to support Next.js and Serverless.
NextAuth adds Cross Site Request Forgery (CSRF) tokens and HTTP Only cookies, supports universal rendering and does not require client side JavaScript.
[Follow the examples](https://next-auth.js.org/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
It adds session support without using client side accessible session tokens, providing protection against Cross Site Scripting (XSS) and session hijacking, while leveraging localStorage where available to cache non-critical session state for optimal performance in Single Page Apps.
Install: `npm i next-auth`
The NextAuth comes with a client library, designed to work with React pages powered by Next.js to easily add universal session support to sites.
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
It contains an [example site](https://github.com/iaincollins/next-auth/tree/master/example) that shows how to use it in a simple project. It's also used in the [nextjs-starter.now.sh](https://nextjs-starter.now.sh) project, which provides a more complete example with a live demo.
## Features
Note: As of version 1.5 NextAuth is also compatible non-Next.js React projects, just pass `null` instead of a nextApp instance when calling `nextAuth()`.
### Authentication
You will need to handle setting up routes before and after initialising NextAuth if you are not using Next.js. NextAuth lets you pass an instance of express as 'expressApp' option (and returns it in the response).
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Built-in support for [many popular OAuth sign-in services](https://next-auth.js.org/configuration/providers)
* Supports email / passwordless authentication
* Supports both JSON Web Tokens and database sessions
## Example Client Usage
### Own your own data
````javascript
* 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 for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/database)
* Works great with databases from popular hosting providers
* Can also be used without a database (e.g. OAuth + JWT)
### Secure by default
* Designed to be secure by default and promote best practice for safeguarding user data
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
Security focused features include CSRF protection, use of signed cookies, cookie prefixes, secure cookies, HTTP only, host only and secure only cookies and promoting passwordless sign-in.
## Example
### Add API Route
```javascript
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
site: 'https://example.com'
providers: [
// OAuth authentication providers
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET
}),
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
}),
// Sign in with passwordless email link
Providers.Email({
server: process.env.MAIL_SERVER,
from: '<no-reply@example.com>'
}),
],
// 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 { NextAuth } from 'next-auth/client'
import {
useSession,
signin,
signout
} from 'next-auth/client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req})
}
}
render() {
if (this.props.session.user) {
return(
<div>
<p>You are logged in as {this.props.session.user.name || this.props.session.user.email}.</p>
</div>
)
} else {
return(
<div>
<p>You are not logged in.</p>
</div>
)
}
}
export default () => {
const [ session, loading ] = useSession()
return <p>
{!session && <>
Not signed in <br/>
<button onClick={signin}>Sign in</button>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={signout}>Sign out</button>
</>}
</p>
}
````
```
See [Documentation for the NextAuth Client](https://github.com/iaincollins/next-auth/blob/master/README-CLIENT.md) for more information on how to interact with the client.
## Acknowledgement
## Routes
[NextAuth.js 2.0 is possible thanks to its contributors.](https://next-auth.js.org/contributors)
NextAuth adds a number of routes under `/auth':
## Getting started
* POST `/auth/email/signin` - Request Sign In Token
* GET `/auth/email/signin/:token` - Validate Sign In Token
* POST `/auth/signout` - Sign Out
* GET `/auth/csrf` - CSRF endpoint for Single Page Apps
* GET `/auth/session` - Session endpoint for Single Page Apps
* GET `/auth/linked` - View linked accounts for Single Page Apps
[Follow the examples to get started.](https://next-auth.js.org/getting-started/example)
All POST routes request must include a CSRF token.
## Contributing
CSRF, Session and Linked Account endpoints are provided for Single Page Apps.
Note: Session Tokens are stored in HTTP Only cookies to prevent session hijacking and protect against Cross Site Scripting (XSS) attacks. Only HTTP requests that originate from the original domain are able to read from them.
In addition, it will add the following routes for each oAuth provider currently configured:
* GET `/auth/oauth/${provider}`
* GET `/auth/oauth/${provider}/callback`
* POST `/auth/oauth/${provider}/unlink`
You can see which routes are configured and the callback URLs defined for them via this route:
* GET `/auth/providers`
It will return a JSON object with the current oAuth provider configuration:
````json
{
"Facebook": {
"signin": "/auth/oauth/facebook",
"callback": "/auth/oauth/facebook/callback"
},
"Google": {
"signin": "/auth/oauth/google",
"callback": "/auth/oauth/google/callback"
},
"Twitter": {
"signin": "/auth/oauth/twitter",
"callback": "/auth/oauth/twitter/callback"
}
}
````
Note: The `/auth` prefix is configurable via an option in the module, but it is currently hard coded in the client component, so you probably don't want to change it. It will be configurable in future releases.
## Getting Started
Create an `index.js` file in the root of your Next.js project containing the following:
````javascript
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')
require('dotenv').load()
const nextApp = next({
dir: '.',
dev: (process.env.NODE_ENV === 'development')
})
nextApp.prepare()
.then(async () => {
const nextAuthOptions = await nextAuthConfig()
const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})
````
You can add the following to your `package.json` file to start the project:
````json
"scripts": {
"dev": "NODE_ENV=development node index.js",
"build": "next build",
"start": "node index.js"
}
````
## Pages
You will need to create following pages under `./pages/auth` in your project:
* index.js Sign In and Link/Unlink accounts
* error.js If an authentication error occurs
* check-email.js 'Check your email' messsage
* callback.js Callback page; updates local session on sign in / sign out
You can [find examples of these](https://github.com/iaincollins/next-auth/tree/master/example) included which you can copy and paste into your project.
## Configuration
Configuration can be split across three files to make it easier to understand and manage.
You can copy over the following configuration files into the root of your project to get started:
* [next-auth.config.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.config.js)
* [next-auth.functions.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.functions.js)
* [next-auth.providers.js](https://github.com/iaincollins/next-auth/tree/master/example/next-auth.providers.js)
You can also add a **.env** file to the root of the project as a place to specify configuration options. The provided example files for NextAuth will use one if there is is one.
````
SERVER_URL=http://localhost:3000
MONGO_URI=mongodb://localhost:27017/my-database
FACEBOOK_ID=
FACEBOOK_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_KEY=
TWITTER_SECRET=
EMAIL_FROM=username@gmail.com
EMAIL_SERVER=smtp.gmail.com
EMAIL_PORT=465
EMAIL_USERNAME=username@gmail.com
EMAIL_PASSWORD=
````
### next-auth.config.js
Basic configuration of NextAuth is handled in **next-auth.config.js**.
It is also where the **next-auth.functions.js** and **next-auth.providers.js** files are loaded.
### next-auth.functions.js
Methods for user management and sending email are defined in **next-auth.functions.js**
The example configuration provided is for Mongo DB. By defining the behaviour in these functions you can use NextAuth with any database, including a relational database that uses SQL.
#### Required
* find({id,email,emailToken,provider})
* insert(user, oAuthProfile, providerParams)
* update(user, oAuthProfile, providerParams)
* remove(id)
* serialize(user)
* deserialize(id)
#### Optional
* sendSigninEmail({email, url, req})
* signIn({form, req})
The `sendSigninEmail()` method is used to send an email for email token based sign in (one time use passwords). Omit it or set it to null to disable email based sign in.
The `signIn()` method is used to handle authenticating with custom credentials (e.g. username and password, 2FA token, etc). Omit it or leave it undefined unless you need it.
You can use any combination of authentication methods (email, credentials, oAuth providers).
### next-auth.providers.js
Configuration for oAuth providers are defined in **next-auth.providers.js**
It includes configuration examples for Facebook, Google and Twitter oAuth, which can easily be copied and replicated to add support for signing in other oAuth providers.
For tips on configuring oAuth see [AUTHENTICATION.md](https://github.com/iaincollins/next-auth/tree/master/AUTHENTICATION.md).
----
See the included [example site](https://github.com/iaincollins/next-auth/tree/master/example) and the expanded example at [nextjs-starter.now.sh](https://nextjs-starter.now.sh/examples/authentication).
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).

1
adapters.js Normal file
View File

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

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

52
client.d.ts vendored
View File

@@ -1,52 +0,0 @@
/// <reference path="./index.d.ts" />
import { Store } from "express-session";
import { RequestHandler } from "express";
import { IpcNetConnectOpts } from "net";
declare namespace nextAuth {
interface ILinkedAccounts {
[name: string]: boolean;
}
interface IProviders {
[name: string]: {
signin: string;
callback: string;
};
}
interface NextAuthRequest<SessionType = nextAuth.NextAuthSession>
extends Express.Request {
session: NextAuthSession;
linked: () => Promise<ILinkedAccounts | Error>;
providers: () => Promise<IProviders | Error>;
}
interface IInitOptions {
req?: NextAuthRequest;
force: boolean;
}
interface IClientOptions {
req?: NextAuthRequest;
}
}
declare class Client {
static init(
opts: nextAuth.IInitOptions
): Promise<Partial<nextAuth.INextAuthSessionData>>;
static csrfToken(): Promise<string | Error>;
static linked(
opts: nextAuth.IClientOptions
): Promise<nextAuth.ILinkedAccounts | Error>;
static providers(
opts: nextAuth.IClientOptions
): Promise<nextAuth.IProviders | Error>;
static signin(
params: string | { [k: string]: string }
): Promise<boolean | Error>;
static signout(): Promise<true | Error>;
static _getLocalStore(): Promise<nextAuth.INextAuthSessionData | null>;
static _saveLocalStore(): Promise<boolean>;
static _removeLocalStore(): Promise<boolean>;
}
export = Client;

491
client.js
View File

@@ -1,490 +1 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('babel-polyfill'), require('isomorphic-fetch')) :
typeof define === 'function' && define.amd ? define(['exports', 'babel-polyfill', 'isomorphic-fetch'], factory) :
(factory((global['next-auth-client'] = {}),null,global.fetch));
}(this, (function (exports,babelPolyfill,fetch) { 'use strict';
fetch = fetch && fetch.hasOwnProperty('default') ? fetch['default'] : fetch;
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _class = function () {
function _class() {
_classCallCheck(this, _class);
}
_createClass(_class, null, [{
key: 'init',
/**
* This is an async, isometric method which returns a session object -
* either by looking up the current express session object when run on the
* server, or by using fetch (and optionally caching the result in local
* storage) when run on the client.
*
* Note that actual session tokens are not stored in local storage, they are
* kept in an HTTP Only cookie as protection against session hi-jacking by
* malicious JavaScript.
**/
value: function () {
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
var _this = this;
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref2$req = _ref2.req,
req = _ref2$req === undefined ? null : _ref2$req,
_ref2$force = _ref2.force,
force = _ref2$force === undefined ? false : _ref2$force;
var session;
return regeneratorRuntime.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
session = {};
if (req) {
if (req.session) {
// If running on the server session data should be in the req object
session.csrfToken = req.connection._httpMessage.locals._csrf;
session.expires = req.session.cookie._expires;
// If the user is logged in, add the user to the session object
if (req.user) {
session.user = req.user;
}
}
} else {
// If running in the browser attempt to load session from sessionStore
if (force === true) {
// If force update is set, reset data store
this._removeLocalStore('session');
} else {
session = this._getLocalStore('session');
}
}
// If session data exists, has not expired AND force is not set then
// return the stored session we already have.
if (!(session && Object.keys(session).length > 0 && session.expires && session.expires > Date.now())) {
_context.next = 6;
break;
}
return _context.abrupt('return', new Promise(function (resolve) {
resolve(session);
}));
case 6:
if (!(typeof window === 'undefined')) {
_context.next = 8;
break;
}
return _context.abrupt('return', new Promise(function (resolve) {
resolve({});
}));
case 8:
return _context.abrupt('return', fetch('/auth/session', {
credentials: 'same-origin'
}).then(function (response) {
if (response.ok) {
return response;
} else {
return Promise.reject(Error('HTTP error when trying to get session'));
}
}).then(function (response) {
return response.json();
}).then(function (data) {
// Update session with session info
session = data;
// Set a value we will use to check this client should silently
// revalidate, using the value for revalidateAge returned by the server.
session.expires = Date.now() + session.revalidateAge;
// Save changes to session
_this._saveLocalStore('session', session);
return session;
}).catch(function () {
return Error('Unable to get session');
}));
case 9:
case 'end':
return _context.stop();
}
}
}, _callee, this);
}));
function init() {
return _ref.apply(this, arguments);
}
return init;
}()
/**
* A simple static method to get the CSRF Token is provided for convenience
**/
}, {
key: 'csrfToken',
value: function () {
var _ref3 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee2() {
return regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
return _context2.abrupt('return', fetch('/auth/csrf', {
credentials: 'same-origin'
}).then(function (response) {
if (response.ok) {
return response;
} else {
return Promise.reject(Error('Unexpected response when trying to get CSRF token'));
}
}).then(function (response) {
return response.json();
}).then(function (data) {
return data.csrfToken;
}).catch(function () {
return Error('Unable to get CSRF token');
}));
case 1:
case 'end':
return _context2.stop();
}
}
}, _callee2, this);
}));
function csrfToken() {
return _ref3.apply(this, arguments);
}
return csrfToken;
}()
/**
* A static method to get list of currently linked oAuth accounts
**/
}, {
key: 'linked',
value: function () {
var _ref4 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee3() {
var _ref5 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref5$req = _ref5.req,
req = _ref5$req === undefined ? null : _ref5$req;
return regeneratorRuntime.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
if (!req) {
_context3.next = 2;
break;
}
return _context3.abrupt('return', req.linked());
case 2:
return _context3.abrupt('return', fetch('/auth/linked', {
credentials: 'same-origin'
}).then(function (response) {
if (response.ok) {
return response;
} else {
return Promise.reject(Error('Unexpected response when trying to get linked accounts'));
}
}).then(function (response) {
return response.json();
}).then(function (data) {
return data;
}).catch(function () {
return Error('Unable to get linked accounts');
}));
case 3:
case 'end':
return _context3.stop();
}
}
}, _callee3, this);
}));
function linked() {
return _ref4.apply(this, arguments);
}
return linked;
}()
/**
* A static method to get list of currently configured oAuth providers
**/
}, {
key: 'providers',
value: function () {
var _ref6 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee4() {
var _ref7 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref7$req = _ref7.req,
req = _ref7$req === undefined ? null : _ref7$req;
return regeneratorRuntime.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
if (!req) {
_context4.next = 2;
break;
}
return _context4.abrupt('return', req.providers());
case 2:
return _context4.abrupt('return', fetch('/auth/providers', {
credentials: 'same-origin'
}).then(function (response) {
if (response.ok) {
return response;
} else {
console.log("NextAuth Error Fetching Providers");
return null;
}
}).then(function (response) {
return response.json();
}).then(function (data) {
return data;
}).catch(function (e) {
console.log("NextAuth Error Loading Providers");
console.log(e);
return null;
}));
case 3:
case 'end':
return _context4.stop();
}
}
}, _callee4, this);
}));
function providers() {
return _ref6.apply(this, arguments);
}
return providers;
}()
/*
* Sign in
*
* Will post a form to /auth/signin auth route if an object is passed.
* If the details are valid a session will be created and you should redirect
* to your callback page so the session is loaded in the client.
*
* If just a string containing an email address is specififed will generate a
* a one-time use sign in link and send it via email; you should redirect to a
* page telling the user to check their inbox for an email with the link.
*/
}, {
key: 'signin',
value: function () {
var _ref8 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee6(params) {
var _this2 = this;
var formData, route, encodedForm;
return regeneratorRuntime.wrap(function _callee6$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
// Params can be just string (an email address) or an object (form fields)
formData = typeof params === 'string' ? { email: params } : params;
// Use either the email token generation route or the custom form auth route
route = typeof params === 'string' ? '/auth/email/signin' : '/auth/signin';
// Add latest CSRF Token to request
_context6.next = 4;
return this.csrfToken();
case 4:
formData._csrf = _context6.sent;
// Encoded form parser for sending data in the body
encodedForm = Object.keys(formData).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key]);
}).join('&');
return _context6.abrupt('return', fetch(route, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest' // So Express can detect AJAX post
},
body: encodedForm,
credentials: 'same-origin'
}).then(function () {
var _ref9 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee5(response) {
return regeneratorRuntime.wrap(function _callee5$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
if (!response.ok) {
_context5.next = 6;
break;
}
_context5.next = 3;
return response.json();
case 3:
return _context5.abrupt('return', _context5.sent);
case 6:
throw new Error('HTTP error while attempting to sign in');
case 7:
case 'end':
return _context5.stop();
}
}
}, _callee5, _this2);
}));
return function (_x5) {
return _ref9.apply(this, arguments);
};
}()).then(function (data) {
if (data.success && data.success === true) {
return Promise.resolve(true);
} else {
return Promise.resolve(false);
}
}));
case 7:
case 'end':
return _context6.stop();
}
}
}, _callee6, this);
}));
function signin(_x4) {
return _ref8.apply(this, arguments);
}
return signin;
}()
}, {
key: 'signout',
value: function () {
var _ref10 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee7() {
var csrfToken, formData, encodedForm;
return regeneratorRuntime.wrap(function _callee7$(_context7) {
while (1) {
switch (_context7.prev = _context7.next) {
case 0:
_context7.next = 2;
return this.csrfToken();
case 2:
csrfToken = _context7.sent;
formData = { _csrf: csrfToken
// Encoded form parser for sending data in the body
};
encodedForm = Object.keys(formData).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key]);
}).join('&');
// Remove cached session data
this._removeLocalStore('session');
return _context7.abrupt('return', fetch('/auth/signout', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: encodedForm,
credentials: 'same-origin'
}).then(function () {
return true;
}).catch(function () {
return Error('Unable to sign out');
}));
case 7:
case 'end':
return _context7.stop();
}
}
}, _callee7, this);
}));
function signout() {
return _ref10.apply(this, arguments);
}
return signout;
}()
// The Web Storage API is widely supported, but not always available (e.g.
// it can be restricted in private browsing mode, triggering an exception).
// We handle that silently by just returning null here.
}, {
key: '_getLocalStore',
value: function _getLocalStore(name) {
try {
return JSON.parse(localStorage.getItem(name));
} catch (err) {
return null;
}
}
}, {
key: '_saveLocalStore',
value: function _saveLocalStore(name, data) {
try {
localStorage.setItem(name, JSON.stringify(data));
return true;
} catch (err) {
return false;
}
}
}, {
key: '_removeLocalStore',
value: function _removeLocalStore(name) {
try {
localStorage.removeItem(name);
return true;
} catch (err) {
return false;
}
}
}]);
return _class;
}();
exports.NextAuth = _class;
Object.defineProperty(exports, '__esModule', { value: true });
})));
module.exports = require('./dist/client').default

View File

@@ -1,13 +0,0 @@
SERVER_URL=http://localhost:3000
MONGO_URI=mongodb://localhost:27017/my-database
FACEBOOK_ID=
FACEBOOK_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_KEY=
TWITTER_SECRET=
EMAIL_FROM=username@gmail.com
EMAIL_SERVER=smtp.gmail.com
EMAIL_PORT=465
EMAIL_USERNAME=username@gmail.com
EMAIL_PASSWORD=

3
example/.gitignore vendored
View File

@@ -1,3 +0,0 @@
.env
/.env.production
node_modules

View File

@@ -1,68 +0,0 @@
# NextAuth Example
## About NextAuth Example
This is an example of how to use the [NextAuth](https://www.npmjs.com/package/next-auth) module.
## Getting Started
This project as is run the same way as any Next.js project.
To run it locally, just use:
npm run dev
To run it it production mode, use:
npm build
npm start
## Using NextAuth
NextAuth is included in this project here:
* index.js
## Pages
This example includes the following pages:
* pages/index.js
* pages/auth/index.js
* pages/auth/error.js
* pages/auth/check-email.js
* pages/auth/callback.js
The file `pages/auth/credentials.js` provides an additional example of how to use a custom authentication handler defined in `next-auth.functions.js`.
## Configuration
It also includes the following configuration files:
* next-auth.config.js
* next-auth.functions.js
* next-auth.providers.js
An example **.env** file is provided in **.env.example** which you can copy over to use for simple configuration:
````
SERVER_URL=http://localhost:3000
MONGO_URI=mongodb://localhost:27017/my-database
FACEBOOK_ID=
FACEBOOK_SECRET=
GOOGLE_ID=
GOOGLE_SECRET=
TWITTER_KEY=
TWITTER_SECRET=
EMAIL_FROM=username@gmail.com
EMAIL_SERVER=smtp.gmail.com
EMAIL_PORT=465
EMAIL_USERNAME=username@gmail.com
EMAIL_PASSWORD=
````
If you don't specify a MONGO_URI it will use an in-memory data store for user and session data.
If you don't specify oAuth or SMTP email details you will not be able to log in.
For a more complete example with live demo see [nextjs-starter.now.sh](https://nextjs-starter.now.sh/examples/authentication).

View File

@@ -1,40 +0,0 @@
/**
* An example of how to use the NextAuth module.
*
* To invoke next-auth you will need to define a configuration block for your
* site. You can create a next-auth.config.js file and put all your options
* in it and pass it to next-auth when calling init().
*
* A number of sample configuration files for various databases and
* authentication options are provided.
**/
// Include Next.js, Next Auth and a Next Auth config
const next = require('next')
const nextAuth = require('next-auth')
const nextAuthConfig = require('./next-auth.config')
// Load environment variables from .env
require('dotenv').config({ path: './.env' })
// Initialize Next.js
const nextApp = next({
dir: '.',
dev: (process.env.NODE_ENV === 'development')
})
// Add next-auth to next app
nextApp.prepare()
.then(async () => {
// Load configuration and return config object
const nextAuthOptions = await nextAuthConfig()
// Pass Next.js App instance and NextAuth options to NextAuth
const nextAuthApp = await nextAuth(nextApp, nextAuthOptions)
console.log(`Ready on http://localhost:${process.env.PORT || 3000}`)
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})

View File

@@ -1,80 +0,0 @@
/**
* next-auth.config.js Example
*
* Environment variables for this example:
*
* PORT=3000
* SERVER_URL=http://localhost:3000
* MONGO_URI=mongodb://localhost:27017/my-database
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').config({ path: './.env' })
const nextAuthProviders = require('./next-auth.providers')
const nextAuthFunctions = require('./next-auth.functions')
// If we want to pass a custom session store then we also need to pass an
// instance of Express Session along with it.
const expressSession = require('express-session')
const MongoStore = require('connect-mongo')(expressSession)
// If no store set, NextAuth defaults to using Express Sessions in-memory
// session store (the fallback is intended as fallback for testing only).
let sessionStore
if (process.env.MONGO_URI) {
sessionStore = new MongoStore({
url: process.env.MONGO_URI,
autoRemove: 'interval',
autoRemoveInterval: 10, // Removes expired sessions every 10 minutes
collection: 'sessions',
stringify: false
})
}
module.exports = () => {
// We connect to the User DB before we define our functions.
// next-auth.functions.js returns an async method that does that and returns
// an object with the functions needed for authentication.
return nextAuthFunctions()
.then(functions => {
return new Promise((resolve, reject) => {
// This is the config block we return, ready to be passed to NextAuth
resolve({
// Define a port (if none passed, will not start Express)
port: process.env.PORT || 3000,
// Secret used to encrypt session data on the server.
sessionSecret: 'change-me',
// Maximum Session Age in ms (optional, default is 7 days).
// The expiry time for a session is reset every time a user revisits
// the site or revalidates their session token. This is the maximum
// idle time value.
sessionMaxAge: 60000 * 60 * 24 * 7,
// Session Revalidation in X ms (optional, default is 60 seconds).
// Specifies how often a Single Page App should revalidate a session.
// Does not impact the session life on the server, but causes clients
// to refetch session info (even if it is in a local cache) after N
// seconds has elapsed since it was last checked so they always display
// state correctly.
// If set to 0 will revalidate a session before rendering every page.
sessionRevalidateAge: 60000,
// Canonical URL of the server (optional, but recommended).
// e.g. 'http://localhost:3000' or 'https://www.example.com'
// Used in callbak URLs and email sign in links. It will be auto
// generated if not specified, which may cause problems if your site
// uses multiple aliases (e.g. 'example.com and 'www.examples.com').
serverUrl: process.env.SERVER_URL || null,
// Add an Express Session store.
expressSession: expressSession,
sessionStore: sessionStore,
// Define oAuth Providers
providers: nextAuthProviders(),
// Define functions for manging users and sending email.
functions: functions
})
})
})
}

View File

@@ -1,273 +0,0 @@
/**
* next-auth.functions.js Example
*
* This file defines functions NextAuth to look up, add and update users.
*
* It returns a Promise with the functions matching these signatures:
*
* {
* find: ({
* id,
* email,
* emailToken,
* provider,
* poviderToken
* } = {}) => {},
* update: (user) => {},
* insert: (user) => {},
* remove: (id) => {},
* serialize: (user) => {},
* deserialize: (id) => {}
* }
*
* Each function returns Promise.resolve() - or Promise.reject() on error.
*
* This specific example supports both MongoDB and NeDB, but can be refactored
* to work with any database.
*
* Environment variables for this example:
*
* MONGO_URI=mongodb://localhost:27017/my-database
* EMAIL_FROM=username@gmail.com
* EMAIL_SERVER=smtp.gmail.com
* EMAIL_PORT=465
* EMAIL_USERNAME=username@gmail.com
* EMAIL_PASSWORD=p4ssw0rd
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').config({ path: './.env' })
// This config file uses MongoDB for User accounts, as well as session storage.
// This config includes options for NeDB, which it defaults to if no DB URI
// is specified. NeDB is an in-memory only database intended here for testing.
const MongoClient = require('mongodb').MongoClient
const NeDB = require('nedb')
const MongoObjectId = (process.env.MONGO_URI) ? require('mongodb').ObjectId : (id) => { return id }
// Use Node Mailer for email sign in
const nodemailer = require('nodemailer')
const nodemailerSmtpTransport = require('nodemailer-smtp-transport')
const nodemailerDirectTransport = require('nodemailer-direct-transport')
// Send email direct from localhost if no mail server configured
let nodemailerTransport = nodemailerDirectTransport()
if (process.env.EMAIL_SERVER && process.env.EMAIL_USERNAME && process.env.EMAIL_PASSWORD) {
nodemailerTransport = nodemailerSmtpTransport({
host: process.env.EMAIL_SERVER,
port: process.env.EMAIL_PORT || 25,
secure: process.env.EMAIL_SECURE || true,
auth: {
user: process.env.EMAIL_USERNAME,
pass: process.env.EMAIL_PASSWORD
}
})
}
module.exports = () => {
return new Promise((resolve, reject) => {
if (process.env.MONGO_URI) {
// Connect to MongoDB Database and return user connection
MongoClient.connect(process.env.MONGO_URI, (err, mongoClient) => {
if (err) return reject(err)
const dbName = process.env.MONGO_URI.split('/').pop().split('?').shift()
const db = mongoClient.db(dbName)
return resolve(db.collection('users'))
})
} else {
// If no MongoDB URI string specified, use NeDB, an in-memory work-a-like.
// NeDB is not persistant and is intended for testing only.
let collection = new NeDB({ autoload: true })
collection.loadDatabase(err => {
if (err) return reject(err)
resolve(collection)
})
}
})
.then(usersCollection => {
return Promise.resolve({
// If a user is not found find() should return null (with no error).
find: ({id, email, emailToken, provider} = {}) => {
let query = {}
// Find needs to support looking up a user by ID, Email, Email Token,
// and Provider Name + Users ID for that Provider
if (id) {
query = { _id: MongoObjectId(id) }
} else if (email) {
query = { email: email }
} else if (emailToken) {
query = { emailToken: emailToken }
} else if (provider) {
query = { [`${provider.name}.id`]: provider.id }
}
return new Promise((resolve, reject) => {
usersCollection.findOne(query, (err, user) => {
if (err) return reject(err)
return resolve(user)
})
})
},
// The user parameter contains a basic user object to be added to the DB.
// The oAuthProfile parameter is passed when signing in via oAuth.
//
// The optional oAuthProfile parameter contains all properties associated
// with the users account on the oAuth service they are signing in with.
//
// You can use this to capture profile.avatar, profile.location, etc.
insert: (user, oAuthProfile) => {
return new Promise((resolve, reject) => {
usersCollection.insert(user, (err, response) => {
if (err) return reject(err)
// Mongo Client automatically adds an id to an inserted object, but
// if using a work-a-like we may need to add it from the response.
if (!user._id && response._id) user._id = response._id
return resolve(user)
})
})
},
// The user parameter contains a basic user object to be added to the DB.
// The oAuthProfile parameter is passed when signing in via oAuth.
//
// The optional oAuthProfile parameter contains all properties associated
// with the users account on the oAuth service they are signing in with.
//
// You can use this to capture profile.avatar, profile.location, etc.
update: (user, profile) => {
return new Promise((resolve, reject) => {
usersCollection.update({_id: MongoObjectId(user._id)}, user, {}, (err) => {
if (err) return reject(err)
return resolve(user)
})
})
},
// The remove parameter is passed the ID of a user account to delete.
//
// This method is not used in the current version of next-auth but will
// be in a future release, to provide an endpoint for account deletion.
remove: (id) => {
return new Promise((resolve, reject) => {
usersCollection.remove({_id: MongoObjectId(id)}, (err) => {
if (err) return reject(err)
return resolve(true)
})
})
},
// Seralize turns the value of the ID key from a User object
serialize: (user) => {
// Supports serialization from Mongo Object *and* deserialize() object
if (user.id) {
// Handle responses from deserialize()
return Promise.resolve(user.id)
} else if (user._id) {
// Handle responses from find(), insert(), update()
return Promise.resolve(user._id)
} else {
return Promise.reject(new Error("Unable to serialise user"))
}
},
// Deseralize turns a User ID into a normalized User object that is
// exported to clients. It should not return private/sensitive fields,
// only fields you want to expose via the user interface.
deserialize: (id) => {
return new Promise((resolve, reject) => {
usersCollection.findOne({ _id: MongoObjectId(id) }, (err, user) => {
if (err) return reject(err)
// If user not found (e.g. account deleted) return null object
if (!user) return resolve(null)
return resolve({
id: user._id,
name: user.name,
email: user.email,
emailVerified: user.emailVerified,
admin: user.admin || false
})
})
})
},
// Email Sign In
//
// Accounts are created automatically, as when signing in via oAuth.
// Users are sent one-time use sign in tokens in links. This avoids
// storing user supplied passwords anywhere, preventing password re-use.
//
// To disable this option, do not set sendSignInEmail (or set it to null).
sendSignInEmail: ({email, url, req}) => {
nodemailer
.createTransport(nodemailerTransport)
.sendMail({
to: email,
from: process.env.EMAIL_FROM,
subject: 'Sign in link',
text: `Use the link below to sign in:\n\n${url}\n\n`,
html: `<p>Use the link below to sign in:</p><p>${url}</p>`
}, (err) => {
if (err) {
console.error('Error sending email to ' + email, err)
}
})
if (process.env.NODE_ENV === 'development') {
console.log('Generated sign in link ' + url + ' for ' + email)
}
},
// Credentials Sign In
//
// If you use this you will need to define your own way to validate
// credentials. Unlike with oAuth or Email Sign In, accounts are not
// created automatically so you will need to provide a way to create them.
//
// This feature is intended for strategies like Two Factor Authentication.
//
// To disable this option, do not set signin (or set it to null).
/*
signIn: ({form, req}) => {
return new Promise((resolve, reject) => {
// Should validate credentials (e.g. hash password, compare 2FA token
// etc) and return a valid user object from a database.
return usersCollection.findOne({
email: form.email
}, (err, user) => {
if (err) return reject(err)
if (!user) return resolve(null)
// Check credentials - e.g. compare bcrypt password hashes
if (form.password === "test1234") {
// If valid, return user object - e.g. { id, name, email }
return resolve(user)
} else {
// If invalid, return null
return resolve(null)
}
})
})
},
*/
// Session Object (optional)
//
// The session object that gets returned to the client. You don't need to
// specify this function here unless you want to override or extend the
// default (e.g. with any other properties you have added to req.session)
//
// Note: The object returned will be stored in localStorage and visible
// client side so do not return data you would not want the user to see.
/*
session: (session, req) => {
if (req.session && req.session.someCustomProperty)
session.someCustomProperty = req.session.someCustomProperty
session.someOtherCustomProperty = "Example custom property"
return session
}
*/
})
})
}

View File

@@ -1,108 +0,0 @@
/**
* next-auth.providers.js Example
*
* This file returns a simple array of oAuth Provider objects for NextAuth.
*
* This example returns an array based on what environment variables are set,
* with explicit support for Facebook, Google and Twitter, but it can be used
* to add strategies for other oAuth providers.
*
* Environment variables for this example:
*
* FACEBOOK_ID=
* FACEBOOK_SECRET=
* GOOGLE_ID=
* GOOGLE_SECRET=
* TWITTER_KEY=
* TWITTER_SECRET=
*
* If you wish, you can put these in a `.env` to seperate your environment
* specific configuration from your code.
**/
// Load environment variables from a .env file if one exists
require('dotenv').config({ path: './.env' })
module.exports = () => {
let providers = []
if (process.env.FACEBOOK_ID && process.env.FACEBOOK_SECRET) {
providers.push({
providerName: 'Facebook',
providerOptions: {
scope: ['email', 'public_profile']
},
Strategy: require('passport-facebook').Strategy,
strategyOptions: {
clientID: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
profileFields: ['id', 'displayName', 'email', 'link']
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: profile._json.email
}
}
})
}
if (process.env.GOOGLE_ID && process.env.GOOGLE_SECRET) {
providers.push({
providerName: 'Google',
providerOptions: {
scope: ['profile', 'email']
},
Strategy: require('passport-google-oauth').OAuth2Strategy,
strategyOptions: {
clientID: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: profile.emails[0].value
}
}
})
}
/**
* Note: Twitter doesn't expose emails by default.
* If we don't get one, Passport-stategies.js will create a placeholder.
*
*
* To have your Twitter oAuth return emails go to apps.twitter.com and add
* links to your Terms and Conditions and Privacy Policy under the "Settings"
* tab, then check the "Request email addresses" from users box under the
* "Permissions" tab.
**/
if (process.env.TWITTER_KEY && process.env.TWITTER_SECRET) {
providers.push({
providerName: 'Twitter',
providerOptions: {
scope: []
},
Strategy: require('passport-twitter').Strategy,
strategyOptions: {
consumerKey: process.env.TWITTER_KEY,
consumerSecret: process.env.TWITTER_SECRET,
userProfileURL: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true'
},
getProfile(profile) {
// Normalize profile into one with {id, name, email} keys
return {
id: profile.id,
name: profile.displayName,
email: (profile.emails && profile.emails[0].value) ? profile.emails[0].value : ''
}
}
})
}
return providers
}

6888
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +0,0 @@
{
"name": "next-auth-examples",
"version": "1.12.1",
"description": "An example project for next-auth",
"repository": "https://github.com/iaincollins/next-auth.git",
"main": "",
"scripts": {
"dev": "NODE_ENV=development node index.js",
"build": "next build",
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"connect-mongo": "^2.0.3",
"dotenv": "^6.1.0",
"express-session": "^1.15.6",
"mongodb": "^3.1.10",
"nedb": "^1.8.0",
"next": "^7.0.2",
"next-auth": "^1.12.1",
"nodemailer": "^4.7.0",
"nodemailer-direct-transport": "^3.3.2",
"nodemailer-smtp-transport": "^2.7.4",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
"passport-twitter": "^1.0.4",
"react": "^16.6.3",
"react-dom": "^16.6.3"
},
"now": {
"name": "next-auth-demo",
"alias": "next-auth-demo.now.sh"
}
}

View File

@@ -1,22 +0,0 @@
import Document, { Head, Main, NextScript } from 'next/document'
export default class DefaultDocument extends Document {
static async getInitialProps(props) {
return await Document.getInitialProps(props)
}
render() {
return (
<html lang={this.props.__NEXT_DATA__.props.lang || 'en'}>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossOrigin="anonymous"/>
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
)
}
}

View File

@@ -1,84 +0,0 @@
import React from 'react'
import Link from 'next/link'
import Router from 'next/router'
import { NextAuth } from 'next-auth/client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({force: true, req: req})
}
}
async componentDidMount() {
// Get latest session data after rendering on client then redirect.
// The ensures client state is always updated after signing in or out.
const session = await NextAuth.init({force: true})
Router.push('/')
}
render() {
// Provide a link for clients without JavaScript as a fallback.
return (
<React.Fragment>
<style jsx global>{`
body{
background-color: #fff;
}
.circle-loader {
position: absolute;
top: 50%;
left: 50%;
width: 50%;
z-index: 100;
text-align: center;
transform: translate(-50%, -50%);
}
.circle-loader .circle {
fill: transparent;
stroke: rgba(0,0,0,0.2);
stroke-width: 4px;
animation: dash 2s ease infinite, rotate 2s linear infinite;
}
@keyframes dash {
0% {
stroke-dasharray: 1,95;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 85,95;
stroke-dashoffset: -25;
}
100% {
stroke-dasharray: 85,95;
stroke-dashoffset: -93;
}
}
@keyframes rotate {
0% {transform: rotate(0deg); }
100% {transform: rotate(360deg); }
}
`}</style>
<noscript>
<style>{`
svg {
display: none;
}
a {
font-weight: bold;
}
`}</style>
</noscript>
<a href="/" className="circle-loader">
<svg className="circle" width="60" height="60" version="1.1" xmlns="http://www.w3.org/2000/svg">
<circle cx="30" cy="30" r="15"/>
</svg>
<noscript>
Click here to continue
</noscript>
</a>
</React.Fragment>
)
}
}

View File

@@ -1,27 +0,0 @@
import React from 'react'
import Link from 'next/link'
export default class extends React.Component {
static async getInitialProps({query}) {
return {
email: query.email
}
}
render() {
return(
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-5 mb-3">Check your email</h1>
<p className="lead">
A sign in link has been sent to { (this.props.email) ? <span className="font-weight-bold">{this.props.email}</span> : <span>your inbox</span> }.
</p>
<p>
<Link href="/"><a>Home</a></Link>
</p>
</div>
</div>
)
}
}

View File

@@ -1,109 +0,0 @@
import React from 'react'
import Router from 'next/router'
import Link from 'next/link'
import { NextAuth } from 'next-auth/client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req}),
linkedAccounts: await NextAuth.linked({req}),
providers: await NextAuth.providers({req})
}
}
constructor(props) {
super(props)
this.state = {
email: '',
password: '',
session: this.props.session
}
this.handleEmailChange = this.handleEmailChange.bind(this)
this.handlePasswordChange = this.handlePasswordChange.bind(this)
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
}
async componentDidMount() {
if (this.props.session.user) {
Router.push(`/auth/`)
}
}
handleEmailChange(event) {
this.setState({
email: event.target.value
})
}
handlePasswordChange(event) {
this.setState({
password: event.target.value
})
}
handleSignInSubmit(event) {
event.preventDefault()
// An object passed NextAuth.signin will be passed to your signin() function
NextAuth.signin({
email: this.state.email,
password: this.state.password
})
.then(authenticated => {
Router.push(`/auth/callback`)
})
.catch(() => {
alert("Authentication failed.")
})
}
render() {
if (this.props.session.user) {
return null
} else {
return (
<div className="container">
<div className="text-center">
<h1 className="display-5 mt-4 mb-2">NextAuth - Custom Sign In</h1>
</div>
<div className="row">
<div className="col-sm-12 col-md-10 col-lg-8 col-xl-7 mr-auto ml-auto">
<p className="mt-3 mb-4 text-center">
If you want to support password based sign in, two factor authentication
or another sign in method, define a signin() function
in <strong>next-auth.functions.js</strong>.
</p>
<div className="card mt-3 mb-3">
<h4 className="card-header">Sign In</h4>
<div className="card-body pb-0">
<form id="signin" method="post" action="/auth/signin" onSubmit={this.handleSignInSubmit}>
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
<p>
<label htmlFor="email">Email address</label><br/>
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
</p>
<p>
<label htmlFor="password">Password</label><br/>
<input name="password" type="password" placeholder="" id="password" className="form-control" value={this.state.password} onChange={this.handlePasswordChange}/>
</p>
<p className="text-right">
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in</button>
</p>
</form>
</div>
</div>
<p className="text-italic text-muted text-center small">
For this to work, you will need enable the signin() function in <strong>next-auth.functions.js</strong>
</p>
</div>
</div>
<p className="text-center">
<Link href="/auth"><a>Back</a></Link>
</p>
</div>
)
}
}
}

View File

@@ -1,70 +0,0 @@
import React from 'react'
import Link from 'next/link'
export default class extends React.Component {
static async getInitialProps({query}) {
return {
action: query.action || null,
type: query.type || null,
service: query.service || null
}
}
render() {
if (this.props.action == 'signin' && this.props.type == 'oauth') {
return(
<div className="container">
<div className="text-center mb-5">
<h1 className="display-4 mt-5 mb-3">Unable to sign in</h1>
<p className="lead">An account associated with your email address already exists.</p>
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
</div>
<div className="row">
<div className="col-sm-8 mr-auto ml-auto mb-5 mt-5">
<div className="text-muted">
<h4 className="mb-2">Why am I seeing this?</h4>
<p className="mb-3">
It looks like you might have already signed up using another service to sign in.
</p>
<p className="mb-3">
If you have previously signed up using another service you must link accounts before you
can use a different service to sign in.
</p>
<p className="mb-5">
This is to prevent people from signing up to another service using your email address
to try and access your account.
</p>
<h4 className="mb-2">How do I fix this?</h4>
<p className="mb-0">
First sign in using your email address then link your account to the service you want
to use to sign in with in future. You only need to do this once.
</p>
</div>
</div>
</div>
</div>
)
} else if (this.props.action == 'signin' && this.props.type == 'token-invalid') {
return(
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-5 mb-2">Link not valid</h1>
<p className="lead">This sign in link is no longer valid.</p>
<p className="lead"><Link href="/auth"><a>Get a new sign in link</a></Link></p>
</div>
</div>
)
} else {
return(
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-5">Error signing in</h1>
<p className="lead">An error occured while trying to sign in.</p>
<p className="lead"><Link href="/auth"><a>Sign in with email or another service</a></Link></p>
</div>
</div>
)
}
}
}

View File

@@ -1,165 +0,0 @@
import React from 'react'
import Router from 'next/router'
import Link from 'next/link'
import { NextAuth } from 'next-auth/client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req}),
linkedAccounts: await NextAuth.linked({req}),
providers: await NextAuth.providers({req})
}
}
constructor(props) {
super(props)
this.state = {
email: '',
session: this.props.session
}
this.handleEmailChange = this.handleEmailChange.bind(this)
this.handleSignInSubmit = this.handleSignInSubmit.bind(this)
}
handleEmailChange(event) {
this.setState({
email: event.target.value
})
}
handleSignInSubmit(event) {
event.preventDefault()
if (!this.state.email) return
NextAuth.signin(this.state.email)
.then(() => {
Router.push(`/auth/check-email?email=${this.state.email}`)
})
.catch(() => {
Router.push(`/auth/error?action=signin&type=email&email=${this.state.email}`)
})
}
render() {
if (this.props.session.user) {
return (
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-3">NextAuth Example</h1>
<p className="lead mt-3 mb-1">You are signed in as <span className="font-weight-bold">{this.props.session.user.email}</span>.</p>
</div>
<div className="row">
<div className="col-sm-5 mr-auto ml-auto">
<LinkAccounts
session={this.props.session}
linkedAccounts={this.props.linkedAccounts}
/>
</div>
</div>
<p className="text-center">
<Link href="/"><a>Home</a></Link>
</p>
</div>
)
} else {
return (
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
</div>
<div className="row">
<div className="col-sm-6 mr-auto ml-auto">
<div className="card mt-3 mb-3">
<h4 className="card-header">Sign In</h4>
<div className="card-body pb-0">
<SignInButtons providers={this.props.providers}/>
<form id="signin" method="post" action="/auth/email/signin" onSubmit={this.handleSignInSubmit}>
<input name="_csrf" type="hidden" value={this.state.session.csrfToken}/>
<p>
<label htmlFor="email">Email address</label><br/>
<input name="email" type="text" placeholder="j.smith@example.com" id="email" className="form-control" value={this.state.email} onChange={this.handleEmailChange}/>
</p>
<p className="text-right">
<button id="submitButton" type="submit" className="btn btn-outline-primary">Sign in with email</button>
</p>
</form>
</div>
</div>
</div>
</div>
<p className="text-center small">
<Link href="/auth/credentials"><a>Sign in with credentials</a></Link>
</p>
<p className="text-center">
<Link href="/"><a>Home</a></Link>
</p>
</div>
)
}
}
}
export class LinkAccounts extends React.Component {
render() {
return (
<div className="card mt-3 mb-3">
<h4 className="card-header">Link Accounts</h4>
<div className="card-body pb-0">
{
Object.keys(this.props.linkedAccounts).map((provider, i) => {
return <LinkAccount key={i} provider={provider} session={this.props.session} linked={this.props.linkedAccounts[provider]}/>
})
}
</div>
</div>
)
}
}
export class LinkAccount extends React.Component {
render() {
if (this.props.linked === true) {
return (
<form method="post" action={`/auth/oauth/${this.props.provider.toLowerCase()}/unlink`}>
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
<p>
<button className="btn btn-block btn-outline-danger" type="submit">
Unlink from {this.props.provider}
</button>
</p>
</form>
)
} else {
return (
<p>
<a className="btn btn-block btn-outline-primary" href={`/auth/oauth/${this.props.provider.toLowerCase()}`}>
Link with {this.props.provider}
</a>
</p>
)
}
}
}
export class SignInButtons extends React.Component {
render() {
return (
<React.Fragment>
{
Object.keys(this.props.providers).map((provider, i) => {
return (
<p key={i}>
<a className="btn btn-block btn-outline-secondary" href={this.props.providers[provider].signin}>
Sign in with {provider}
</a>
</p>
)
})
}
</React.Fragment>
)
}
}

View File

@@ -1,62 +0,0 @@
import React from 'react'
import Router from 'next/router'
import Link from 'next/link'
import { NextAuth } from 'next-auth/client'
export default class extends React.Component {
static async getInitialProps({req}) {
return {
session: await NextAuth.init({req})
}
}
constructor(props) {
super(props)
this.handleSignOutSubmit = this.handleSignOutSubmit.bind(this)
}
handleSignOutSubmit(event) {
event.preventDefault()
NextAuth.signout()
.then(() => {
Router.push('/auth/callback')
})
.catch(err => {
Router.push('/auth/error?action=signout')
})
}
render() {
return (
<div className="container">
<div className="text-center">
<h1 className="display-4 mt-3 mb-3">NextAuth Example</h1>
<p className="lead mt-3 mb-3">An example of how to use the <a href="https://www.npmjs.com/package/next-auth">NextAuth</a> module.</p>
<SignInMessage {...this.props}/>
</div>
</div>
)
}
}
export class SignInMessage extends React.Component {
render() {
if (this.props.session.user) {
return (
<React.Fragment>
<p><Link href="/auth"><a className="btn btn-secondary">Manage Account</a></Link></p>
<form id="signout" method="post" action="/auth/signout" onSubmit={this.handleSignOutSubmit}>
<input name="_csrf" type="hidden" value={this.props.session.csrfToken}/>
<button type="submit" className="btn btn-outline-secondary">Sign out</button>
</form>
</React.Fragment>
)
} else {
return (
<React.Fragment>
<p><Link href="/auth"><a className="btn btn-primary">Sign in</a></Link></p>
</React.Fragment>
)
}
}
}

84
index.d.ts vendored
View File

@@ -1,84 +0,0 @@
import { Store } from "express-session";
import { RequestHandler } from "express";
import { IpcNetConnectOpts } from "net";
declare namespace nextAuth {
interface IOptions {
bodyParser: boolean;
csrf: boolean;
pathPrefix: string;
expressApp?: Express.Application;
expressSession: RequestHandler;
sessionSecret: string;
sessionStore: Store;
sessionMaxAge: number;
sessionRevalidateAge: number;
sessionResave: boolean;
sessionRolling: boolean;
sessionSaveUninitialized: boolean;
serverUrl?: string;
trustProxy: boolean;
providers: any[];
port?: number;
functions: IFunctions;
}
interface IUserProvider {
name: string;
id: string;
}
interface ISendSignInEmailOpts {
email?: string;
url?: string;
req?: Express.Request;
}
interface ISignInOpts {
email?: string;
password?: string;
}
interface INextAuthSessionData<UserType = {}> extends Session {
maxAge: number;
revalidateAge: number;
csrfToken: string;
user?: UserType;
expires?: number;
}
interface IFunctions<
UserType = {},
IDType = string,
SessionType extends INextAuthSessionData = INextAuthSessionData
> {
find(
id: IDType,
email: string,
emailToken: string,
provider: IUserProvider
): Promise<UserType>;
update: (user: UserType, profile: any) => Promise<UserType>;
insert: (user: UserType, profile: any) => Promise<UserType>;
remove: (id: IDType) => Promise<boolean>;
serialize: (user: UserType) => Promise<IDType>;
deserialize: (id: IDType) => Promise<UserType>;
session?: (
session: INextAuthSessionData,
req: Express.Request
) => SessionType;
sendSignInEmail?: (opts: ISendSignInEmailOpts) => Promise<boolean>;
signIn?: (opts: ISignInOpts) => Promise<UserType>;
}
interface INextAuthResult {
next?: nextApp;
express: Express;
expressApp: Express.Application;
function: IFunctions;
providers: any;
port?: number;
}
}
declare function NextAuth(
nextApp?: NextApp,
options?: nextAuth.IOptions
): Promise<nextAuth.INextAuthResult>;
export = NextAuth;

479
index.js
View File

@@ -1,478 +1 @@
'use strict'
const BodyParser = require('body-parser')
const Express = require('express')
const ExpressSession = require('express-session')
const lusca = require('lusca')
const passportStrategies = require('./src/passport-strategies')
const uuid = require('uuid/v4')
module.exports = (nextApp, {
bodyParser = true,
// Optional, allows you to set e.g. 'limit' (maximum request body size, default 100kb)
bodyParserJsonOptions = {},
bodyParserUrlEncodedOptions = { extended: true },
csrf = true,
// URL base path for authentication routes (optional).
// Note: The prefix value of '/auth' is currently hard coded in
// next-auth-client so you should not change this unless you also modify it.
pathPrefix = '/auth',
// Express Server (optional).
expressApp = null,
// Express Session (optional).
expressSession = ExpressSession,
// Secret used to encrypt session data on the server.
sessionSecret = 'change-me',
// Session store for express-session.
// Defaults to an in memory store, which is not recommended for production.
sessionStore = expressSession.MemoryStore(),
// The name of the session ID cookie to set in the response (and read from in
// the request). The default value is 'connect.sid'.
sessionCookie = 'connect.sid',
// Maximum Session Age in ms (optional, default is 7 days).
// The expiry time for a session is reset every time a user revisits the site
// or revalidates their session token - this is the maximum idle time value.
sessionMaxAge = 60000 * 60 * 24 * 7,
// The session cookie name. Useful for adding cookie prefixes. E.g. setting
// '__HOST-' and '__SECURE-' prefixes on cookie names prevents them from being
// overwritten by insecure origins.
sessionName = null,
// Session Revalidation in X ms (optional, default is 60 seconds).
// Specifies how often a Single Page App should revalidate a session.
// Does not impact the session life on the server, but causes clients to
// refetch session info (even if it is in a local cache) after N seconds has
// elapsed since it was last checked so they always display state correctly.
// If set to 0 will revalidate a session before rendering every page.
sessionRevalidateAge = 60000,
// Force the session to be saved back to the session store, even if the
// session was not modified during the request.
// Note: If this is false, session expiry will not rotate and will expire
// after sessionMaxAge unless you write you own code to rotate the session.
// This is an option exposed for advanced use cases on people with specific
// databases that have session store drivers that do not work well with
// the express-session resave option.
// https://www.npmjs.com/package/express-session#resave
sessionResave = true,
// Force a session identifier cookie to be set on every response. The expire
// time is reset to the original maxAge, resetting the expiration time.
// Note When this option is set to true but the saveUninitialized option
// is set to false, the cookie will not be set on a response with an
// uninitialized session https://www.npmjs.com/package/express-session#rolling
sessionRolling = true,
// Prevent cookies from being sent cross-site, protecting against CSRF
// attacks.
sessionSameSite = null,
// Forces a session that is "uninitialized" to be saved to the store.
// A session is uninitialized when it is new but not modified. Choosing false
// is useful for implementing login sessions, reducing server storage usage,
// or complying with laws that require permission before setting a cookie.
//
// Choosing false will also help with race conditions where a client makes
// multiple parallel requests without a session.
//
// Note that if the build in CSRF protection is enabled (the default) then
// sessions will ALWAYS be 'initialized' as it saves to the session.
// https://www.npmjs.com/package/express-session#saveuninitialized
sessionSaveUninitialized = false,
// Canonical URL of the server (optional, but recommended).
// e.g. 'http://localhost:3000' or 'https://www.example.com'
// Used in callbak URLs and email sign in links. It will be auto generated
// if not specified, which may cause problems if your site uses multiple
// aliases (e.g. 'example.com and 'www.examples.com').
serverUrl = null,
// If we are behind a proxy server and it says we are running SSL, trust it.
// All this does is make sure we use HTTPS for callback URLs and email links.
// You should never need to turn this off.
trustProxy = true,
// An array of oAuth Provider config blocks (optional).
providers = [],
// Port to start listening on
port = null,
// Functions for find, update, insert, serialize and deserialize methods.
// They should all return a Promise with resolve() or reject().
// find() should return resolve(null) if no matching user found.
functions = {
find: ({
id,
email,
emailToken,
provider // provider = { name: 'twitter', id: '123456' }
} = {}) => { Promise.resolve(user) },
update: (user, profile) => { Promise.resolve(user) },
insert: (user, profile) => { Promise.resolve(user) },
remove: (id) => { Promise.resolve(id) },
serialize: (user) => { Promise.resolve(id) },
deserialize: (id) => { Promise.resolve(user) },
session: null,
sendSignInEmail: null, /* ({
email = null,
url = null,
req = null
} = {}) => { Promise.resolve(true) }
*/
signIn: null /* ({
email = null,
password = null
} = {}) => { Promise.resolve(user) }
*/
}
} = {}) => {
if (typeof(functions) !== 'object') {
throw new Error('functions must be a an object')
}
if (expressApp === null) {
expressApp = Express()
}
// If an instance of nextApp was passed, let all requests to /_next/* pass
// to it *before* Express Session and other middleware is called.
if (nextApp) {
expressApp.all('/_next/*', (req, res) => {
let nextRequestHandler = nextApp.getRequestHandler()
return nextRequestHandler(req, res)
})
}
/*
* Set up body parsing, express sessions and add CSRF tokens.
*
* You can set bodyParser to false and pass an Express instance if you want
* to customise how you invoke Body Parser.
*/
if (bodyParser === true) {
expressApp.use(BodyParser.json(bodyParserJsonOptions))
expressApp.use(BodyParser.urlencoded(bodyParserUrlEncodedOptions))
}
expressApp.use(expressSession({
name: sessionName,
secret: sessionSecret,
store: sessionStore,
resave: sessionResave,
rolling: sessionRolling,
saveUninitialized: sessionSaveUninitialized,
cookie: {
name: sessionCookie,
httpOnly: true,
secure: 'auto',
maxAge: sessionMaxAge,
sameSite: sessionSameSite,
}
}))
if (csrf === true) {
// If csrf is true (default) apply to all routes
expressApp.use(lusca.csrf())
} else if (csrf !== false) {
// If csrf is anything else (except false) then pass it as a config option
expressApp.use(lusca.csrf(csrf))
} // if csrf is explicitly set to false then doesn't apply CSRF at all
if (trustProxy === true) {
expressApp.set('trust proxy', 1)
}
/*
* With sessions configured we need to configure Passport and trigger
* passport.initialize() before we add any other routes.
*/
passportStrategies({
expressApp: expressApp,
serverUrl: serverUrl,
providers: providers,
functions: functions
})
/*
* Add route to get CSRF token via AJAX
*/
expressApp.get(`${pathPrefix}/csrf`, (req, res) => {
return res.json({
csrfToken: res.locals._csrf
})
})
/*
* Return session info to client
*
* Will be stored in local storage, so should not return sensitive data that
* could be captured in a Cross Site Scripting attack (i.e. so not the session
* token) or anything you don't want users to see (like private IDs) but is
* is okay to return things like access tokens for acessing remote services.
*/
expressApp.get(`${pathPrefix}/session`, (req, res) => {
let session = {
maxAge: sessionMaxAge,
revalidateAge: sessionRevalidateAge,
csrfToken: res.locals._csrf
}
if (req.user)
session.user = req.user
if (functions.session)
session = functions.session(session, req)
return res.json(session)
})
// Server side function for returning list of accounts user has linked.
// Called when pages are rendered in on the server (instead of /auth/linked).
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
expressApp.use((req, res, next) => {
req.linked = () => {
return new Promise((resolve, reject) => {
if (!req.user) return resolve({})
functions.serialize(req.user)
.then(id => {
if (!id) throw new Error("Unable to serialize user")
return functions.find({ id: id })
})
.then(user => {
if (!user) return resolve({})
let linkedAccounts = {}
providers.forEach(provider => {
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
})
return resolve(linkedAccounts)
})
.catch(err => {
return reject(err)
})
})
}
next()
})
// Client side REST endpoint for returning list of accounts user has linked.
// Called when pages are rendered in the browser (instead of req.linked()).
// Returns all accounts the user has linked (e.g. Twitter, Facebook, Google…)
expressApp.get(`${pathPrefix}/linked`, (req, res) => {
if (!req.user) return res.json({})
// First get the User ID from the User, then look up the user details.
// Note: We don't use the User object in req.user directly as it is a
// a simplified set of properties set by functions.deserialize().
functions.serialize(req.user)
.then(id => {
return functions.find({ id: id })
})
.then(user => {
if (!user) return res.json({})
let linkedAccounts = {}
providers.forEach(provider => {
linkedAccounts[provider.providerName] = (user[provider.providerName.toLowerCase()]) ? true : false
})
return res.json(linkedAccounts)
})
.catch(err => {
return res.status(500).end()
})
})
/*
* Return list of configured oAuth Providers
*
* We define this both as a server side function and a RESTful endpoint so
* that it can be used rendering a page both server side and client side.
*/
// Server side function
expressApp.use((req, res, next) => {
req.providers = () => {
return new Promise((resolve, reject) => {
let configuredProviders = {}
providers.forEach(provider => {
configuredProviders[provider.providerName] = {
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
}
})
return resolve(configuredProviders)
})
}
next()
})
// RESTful endpoint
expressApp.get(`${pathPrefix}/providers`, (req, res) => {
return new Promise((resolve, reject) => {
let configuredProviders = {}
providers.forEach(provider => {
configuredProviders[provider.providerName] = {
signin: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}`,
callback: (serverUrl || '') + `${pathPrefix}/oauth/${provider.providerName.toLowerCase()}/callback`
}
})
return res.json(configuredProviders)
})
})
/*
* Enable /auth/signin routes if signIn() function is passed
*/
if (functions.signIn) {
expressApp.post(`${pathPrefix}/signin`, (req, res) => {
// Passes all supplied credentials to the signIn function
functions.signIn({
form: req.body,
req: req
})
.then(user => {
if (user) {
// If signIn() returns a user, sign in as them
req.logIn(user, (err) => {
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
if (req.xhr) {
// If AJAX request (from client with JS), return JSON response
return res.json({success: true})
} else {
// If normal form POST (from client without JS) return redirect
return res.redirect(`${pathPrefix}/callback?action=signin&service=credentials`)
}
})
} else {
// If no user object is returned, bounce back to the sign in page
return res.redirect(`${pathPrefix}`)
}
})
.catch(err => {
return res.redirect(`${pathPrefix}/error?action=signin&type=credentials`)
})
})
}
/*
* Enable /auth/email/signin routes if sendSignInEmail() function is passed
*/
if (functions.sendSignInEmail) {
/*
* Generate a one time use sign in link and email it to the user
*/
expressApp.post(`${pathPrefix}/email/signin`, (req, res) => {
const email = req.body.email || null
if (!email || email.trim() === '') {
res.redirect(`${pathPrefix}`)
}
const token = uuid()
const url = (serverUrl || `${req.protocol}://${req.headers.host}`) + `${pathPrefix}/email/signin/${token}`
// Create verification token save it to database
functions.find({ email: email })
.then(user => {
if (user) {
// If a user with that email address exists already, update token.
user.emailToken = token
return functions.update(user)
} else {
// If the user does not exist, create a new account with the token.
return functions.insert({
email: email,
emailToken: token
})
}
})
.then(user => {
functions.sendSignInEmail({
email: user.email,
url: url,
req: req
})
if (req.xhr) {
// If AJAX request (from client with JS), return JSON response
return res.json({success: true})
} else {
// If normal form POST (from client without JS) return redirect
return res.redirect(`${pathPrefix}/check-email?email=${email}`)
}
})
.catch(err => {
return res.redirect(`${pathPrefix}/error?action=signin&type=email&email=${email}`)
})
})
/*
* Verify token in callback URL for email sign in
*/
expressApp.get(`${pathPrefix}/email/signin/:token`, (req, res) => {
if (!req.params.token) {
return res.redirect(`${pathPrefix}/error?action=signin&type=token-missing`)
}
functions.find({ emailToken: req.params.token })
.then(user => {
if (user) {
// Delete current token so it cannot be used again
delete user.emailToken
// Mark email as verified now we know they have access to it
user.emailVerified = true
return functions.update(user, null, { delete: 'emailToken' })
} else {
return Promise.reject(new Error("Token not valid"))
}
})
.then(user => {
// If the user object is valid, sign the user in
req.logIn(user, (err) => {
if (err) return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
if (req.xhr) {
// If AJAX request (from client with JS), return JSON response
return res.json({success: true})
} else {
// If normal form POST (from client without JS) return redirect
return res.redirect(`${pathPrefix}/callback?action=signin&service=email`)
}
})
})
.catch(err => {
return res.redirect(`${pathPrefix}/error?action=signin&type=token-invalid`)
})
})
}
/*
* Sign a user out
*/
expressApp.post(`${pathPrefix}/signout`, (req, res) => {
// Log user out with Passport and remove their Express session
req.logout()
req.session.destroy(() => {
return res.redirect(`${pathPrefix}/callback?action=signout`)
})
})
return new Promise((resolve, reject) => {
let response = {
next: nextApp,
express: Express,
expressApp: expressApp,
functions: functions,
providers: providers,
port: port
}
// If no port specified, don't start Express automatically
if (!port) return resolve(response)
// If an instance of nextApp was passed, have it handle all other routes
if (nextApp) {
expressApp.all('*', (req, res) => {
let nextRequestHandler = nextApp.getRequestHandler()
return nextRequestHandler(req, res)
})
}
// Start Express
return expressApp.listen(port, err => {
if (err) reject(err)
return resolve(response)
})
})
}
module.exports = require('./dist/server')

1
jwt.js Normal file
View File

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

9424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,66 @@
{
"name": "next-auth",
"version": "1.13.0",
"version": "2.0.0",
"description": "An authentication library for Next.js",
"repository": "https://github.com/iaincollins/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"scripts": {
"build": "rollup --config",
"prepare": "rollup --config"
"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",
"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 test:db",
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
"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:stop": "docker-compose -f test/docker/docker-compose.yml down",
"prepublishOnly": "npm run build",
"publish:beta": "npm publish --tag beta",
"publish:canary": "npm publish --tag canary",
"lint": "standard",
"lint:fix": "standard --fix"
},
"author": "",
"files": [
"dist",
"index.js",
"providers.js",
"adapters.js",
"client.js",
"jwt.js"
],
"license": "ISC",
"sideEffects": false,
"dependencies": {
"babel-polyfill": "^6.26.0",
"body-parser": "^1.18.2",
"express": "^4.16.3",
"express-session": "^1.15.6",
"isomorphic-fetch": "^2.2.1",
"lusca": "^1.6.0",
"passport": "^0.4.0",
"uuid": "^3.2.1"
"crypto-js": "^4.0.0",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^2.2.0",
"nodemailer": "^6.4.6",
"oauth": "^0.9.15",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.7",
"querystring": "^0.2.0",
"typeorm": "^0.2.24"
},
"peerDependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"babel-core": "^6.26.3",
"rollup-plugin-babel": "^3.0.7",
"babel-plugin-external-helpers": "^6.22.0",
"babel-preset-env": "^1.7.0",
"bl": "^2.1.2",
"rollup": "^0.67.4",
"rollup-plugin-commonjs": "^9.1.3",
"rollup-plugin-json": "^3.0.0",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-resolve": "^3.3.0",
"semver": "^5.6.0"
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/preset-env": "^7.9.6",
"autoprefixer": "^9.7.6",
"babel-preset-preact": "^2.0.0",
"cssnano": "^4.1.10",
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"pg": "^8.2.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"standard": "^14.3.3"
}
}

7
postcss.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-nested'),
require('cssnano')({ preset: 'default' })
]
}

1
providers.js Normal file
View File

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

View File

@@ -1,21 +0,0 @@
import babel from 'rollup-plugin-babel'
export default {
input: 'src/client/index.js',
output: {
name: 'next-auth-client',
file: 'client.js',
format: 'umd',
globals: {
'fetch': 'isomorphic-fetch'
}
},
plugins: [
babel({
babelrc: false,
exclude: [ 'node_modules/**' ],
presets: [['env', { modules: false }]]
})
],
}

18
scripts/wrap-css.js Normal file
View File

@@ -0,0 +1,18 @@
// Serverless target in Next.js does 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
//
// 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
// a function that returns the CSS as a string.
const fs = require('fs')
const path = require('path')
const pathToCssJs = path.join(__dirname, '../dist/css/index.js')
const pathToCss = path.join(__dirname, '../dist/css/index.css')
const css = fs.readFileSync(pathToCss, 'utf8')
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
fs.writeFileSync(pathToCssJs, js)

View File

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

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

@@ -0,0 +1,6 @@
import TypeORM from './typeorm'
export default {
Default: TypeORM.Adapter,
TypeORM
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,175 @@
// Perform transforms on SQL models so they can be used with other databases
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
const postgres = (models, options) => {
// Apply snake case naming strategy for Postgres databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// Only transforms models that are not custom models
const { models: customModels = {} } = options
// For Postgres we need to use the `timestamp with time zone` type
// aka `timestamptz` to store timestamps correctly in UTC.
if (!customModels.User) {
for (const column in models.User.schema.columns) {
if (models.User.schema.columns[column].type === 'timestamp') {
models.User.schema.columns[column].type = 'timestamptz'
}
}
}
if (!customModels.Account) {
for (const column in models.Account.schema.columns) {
if (models.Account.schema.columns[column].type === 'timestamp') {
models.Account.schema.columns[column].type = 'timestamptz'
}
}
}
if (!customModels.Session) {
for (const column in models.Session.schema.columns) {
if (models.Session.schema.columns[column].type === 'timestamp') {
models.Session.schema.columns[column].type = 'timestamptz'
}
}
}
if (!customModels.VerificationRequest) {
for (const column in models.VerificationRequest.schema.columns) {
if (models.VerificationRequest.schema.columns[column].type === 'timestamp') {
models.VerificationRequest.schema.columns[column].type = 'timestamptz'
}
}
}
}
const mongodb = (models, options) => {
// A CamelCase naming strategy is used for all document databases
if (!options.namingStrategy) {
options.namingStrategy = new CamelCaseNamingStrategy()
}
// Important!
//
// 1. You must set 'objectId: true' on one property on a model in MongoDB.
//
// 'objectId' MUST be set on the primary ID field. This overrides other
// values on that object in TypeORM (e.g. type: 'int' or 'primary').
//
// 2. Other properties that are Object IDs in the same model MUST be set to
// type: 'objectId' (and should not be set to `objectId: true`).
//
// If you set 'objectId: true' on multiple properties on a model you will
// see the result of queries like find() is wrong. You will see the same
// Object ID in every property of type Object ID in the result (but the
// database will look fine); so use `type: 'objectId'` for them instead.
// Only transforms models that are not custom models
const { models: customModels = {} } = options
if (!customModels.User) {
delete models.User.schema.columns.id.type
models.User.schema.columns.id.objectId = true
// The options `unique: true` and `nullable: true` don't work the same
// with MongoDB as they do with SQL databases like MySQL and Postgres,
// we also to add sparce to the index. This still doesn't allow multiple
// *null* values, but does allow some records to omit the property.
delete models.User.schema.columns.email.unique
models.User.schema.indices = [
{
name: 'email',
unique: true,
sparse: true,
columns: ['email']
}
]
}
if (!customModels.Account) {
delete models.Account.schema.columns.id.type
models.Account.schema.columns.id.objectId = true
models.Account.schema.columns.userId.type = 'objectId'
}
if (!customModels.Session) {
delete models.Session.schema.columns.id.type
models.Session.schema.columns.id.objectId = true
models.Session.schema.columns.userId.type = 'objectId'
}
if (!customModels.VerificationRequest) {
delete models.VerificationRequest.schema.columns.id.type
models.VerificationRequest.schema.columns.id.objectId = true
}
}
const sqlite = (models, options) => {
// Apply snake case naming strategy for SQLite databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// Only transforms models that are not custom models
const { models: customModels = {} } = options
// SQLite does not support `timestamp` fields so we remap them to `datetime`
// in all models.
//
// `timestamp` is an ANSI SQL specification and widely supported by other
// databases so this transform is a specific workaround required for SQLite.
//
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
// specific to SQLite and so we ignore that behaviour.
if (!customModels.User) {
for (const column in models.User.schema.columns) {
if (models.User.schema.columns[column].type === 'timestamp') {
models.User.schema.columns[column].type = 'datetime'
}
}
}
if (!customModels.Account) {
for (const column in models.Account.schema.columns) {
if (models.Account.schema.columns[column].type === 'timestamp') {
models.Account.schema.columns[column].type = 'datetime'
}
}
}
if (!customModels.Session) {
for (const column in models.Session.schema.columns) {
if (models.Session.schema.columns[column].type === 'timestamp') {
models.Session.schema.columns[column].type = 'datetime'
}
}
}
if (!customModels.VerificationRequest) {
for (const column in models.VerificationRequest.schema.columns) {
if (models.VerificationRequest.schema.columns[column].type === 'timestamp') {
models.VerificationRequest.schema.columns[column].type = 'datetime'
}
}
}
}
export default (config, models, options) => {
if ((config.type && config.type.startsWith('mongodb')) ||
(config.url && config.url.startsWith('mongodb'))) {
mongodb(models, options)
} else if ((config.type && config.type.startsWith('postgres')) ||
(config.url && config.url.startsWith('postgres'))) {
postgres(models, options)
} else if ((config.type && config.type.startsWith('sqlite')) ||
(config.url && config.url.startsWith('sqlite'))) {
sqlite(models, options)
} else {
// Apply snake case naming strategy by default for SQL databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,250 @@
'use strict'
// fetch() is built in to Next.js 9.4 (you can use a polyfill if using an older version)
/* global fetch:false */
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'
import "babel-polyfill"
import NextAuth from './next-auth-client'
// Note: In calls to fetch() from universal methods, all cookies are passed
// through from the browser, when the server makes the HTTP request, so that
// it can authenticate as the browser.
export {
NextAuth
// These can be overridden with NEXTAUTH_ env vars in next.config.js
// e.g. process.env.NEXTAUTH_SITE
const NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME = 'next-auth.base-url'
const NEXTAUTH_DEFAULT_SITE = ''
const NEXTAUTH_DEFAULT_BASE_PATH = '/api/auth'
const NEXTAUTH_DEFAULT_CLIENT_MAXAGE = 0 // e.g. 0 == disabled, 60 == 60 seconds
let NEXTAUTH_EVENT_LISTENER_ADDED = false
// Universal method (client + server)
const getSession = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, options)
_sendMessage({ event: 'session', data: { triggeredBy: 'getSession' } })
return session
}
// Universal method (client + server)
const getProviders = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
return _fetchData(`${baseUrl}/providers`, options)
}
// Universal method (client + server)
const getCsrfToken = async ({ req } = {}) => {
const baseUrl = _baseUrl({ req })
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`, options)
return data.csrfToken
}
// Context to store session data globally
const SessionContext = createContext()
// Client side method
// Hook to access the session data stored in the context
const useSession = (session) => {
const value = useContext(SessionContext)
// If we have no Provider in the tree we call the actual hook for fetching the session
if (value === undefined) {
return useSessionData(session)
}
return value
}
// Internal hook for getting session from the api.
const useSessionData = (session) => {
const clientMaxAge = (process.env.NEXTAUTH_CLIENT_MAXAGE || NEXTAUTH_DEFAULT_CLIENT_MAXAGE) * 1000
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const _getSession = async (sendEvent = true) => {
try {
setData(await getSession())
setLoading(false)
// Send event to trigger other tabs to update (unless sendEvent is false)
if (sendEvent) {
_sendMessage({ event: 'session', data: { triggeredBy: 'useSessionData' } })
}
if (typeof window !== 'undefined' && NEXTAUTH_EVENT_LISTENER_ADDED === false) {
NEXTAUTH_EVENT_LISTENER_ADDED = true
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message.event && message.event === 'session' && message.data) {
// Fetch new session data but tell it not to fire an event to
// avoid an infinate loop.
//
// Note: We could pass session data through and do something like
// `setData(message.data)` but that causes problems depending on
// how the session object is being used and may expose session
// data to 3rd party scripts, it's safer to update the session
// this way.
await _getSession(false)
}
}
})
}
// If CLIENT_MAXAGE is greater than zero, trigger auto re-fetching session
if (clientMaxAge > 0) {
setTimeout(async (session) => {
await _getSession()
}, clientMaxAge)
}
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
}
}
useEffect(() => { _getSession() }, [])
return [data, loading]
}
// Client side method
const signin = async (provider, args) => {
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
if (!provider) {
// Redirect to sign in page if no provider specified
const baseUrl = _baseUrl()
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
return
}
const providers = await getProviders()
if (!providers[provider]) {
// If Provider not recognized, redirect to sign in page
const baseUrl = _baseUrl()
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
} else if (providers[provider].type === 'oauth') {
// If is an OAuth provider, redirect to providers[provider].signinUrl
window.location = `${providers[provider].signinUrl}?callbackUrl=${encodeURIComponent(callbackUrl)}`
} else {
// If is any other provider type, POST to providers[provider].signinUrl (with CSRF Token)
const options = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: _encodedForm({
csrfToken: await getCsrfToken(),
callbackUrl: callbackUrl,
...args
})
}
const res = await fetch(providers[provider].signinUrl, options)
window.location = res.url ? res.url : callbackUrl
}
}
// Client side method
const signout = async (args) => {
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
const baseUrl = _baseUrl()
const options = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: _encodedForm({
csrfToken: await getCsrfToken(),
callbackUrl: callbackUrl
})
}
const res = await fetch(`${baseUrl}/signout`, options)
_sendMessage({ event: 'session', data: { triggeredBy: 'signout' } })
window.location = res.url ? res.url : callbackUrl
}
// Provider to wrap the app in to make session data available globally
const Provider = ({ children, session }) => {
const value = useSession(session)
return createElement(SessionContext.Provider, { value }, children)
}
const _fetchData = async (url, options) => {
try {
const res = await fetch(url, options)
const data = await res.json()
return Promise.resolve(Object.keys(data).length > 0 ? data : null) // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', url, error)
return Promise.resolve(null)
}
}
const _baseUrl = ({ req } = {}) => {
if (req) {
// Server Side
// If we have a 'req' object are running sever side, so we should grab the
// base URL from cookie that is set by the API route - which is how config
// is shared automatically between the API route and the client.
const cookies = req ? _parseCookies(req.headers.cookie) : null
const baseUrlCookieName = process.env.NEXTAUTH_BASE_URL_COOKIE_NAME || NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME
const cookieValue = cookies[`__Secure-${baseUrlCookieName}`] || cookies[baseUrlCookieName]
const [baseUrl] = cookieValue ? cookieValue.split('|') : [null]
return baseUrl
} else {
// Client Side
// Note: 'site' is empty by default; URL is normally relative.
const site = process.env.NEXTAUTH_SITE || NEXTAUTH_DEFAULT_SITE
const basePath = process.env.NEXTAUTH_BASE_PATH || NEXTAUTH_DEFAULT_BASE_PATH
return `${site}${basePath}`
}
}
// Adapted from https://github.com/felixfong227/simple-cookie-parser/blob/master/index.js
const _parseCookies = (string) => {
if (!string) { return {} }
try {
const object = {}
const a = string.split(';')
for (let i = 0; i < a.length; i++) {
const b = a[i].split('=')
if (b[0].length > 1 && b[1]) {
object[b[0].trim()] = decodeURIComponent(b[1])
}
}
return object
} catch (error) {
logger.error('CLIENT_COOKIE_PARSE_ERROR', error)
return {}
}
}
const _encodedForm = (formData) => {
return Object.keys(formData).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
}).join('&')
}
const _sendMessage = (message) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('nextauth.message', JSON.stringify(message)) // eslint-disable-line
}
}
export default {
// Some methods are exported with more than one name. This provides
// flexibility over how they can be invoked and compatibility with earlier
// releases (going back to v1 and earlier v2 beta releases).
// e.g. NextAuth.session() or const { getSession } from 'next-auth/client'
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
getSession,
getProviders,
getCsrfToken,
useSession,
Provider,
signin,
signout
}

View File

@@ -1,267 +0,0 @@
'use strict'
import fetch from 'isomorphic-fetch'
export default class {
/**
* This is an async, isometric method which returns a session object -
* either by looking up the current express session object when run on the
* server, or by using fetch (and optionally caching the result in local
* storage) when run on the client.
*
* Note that actual session tokens are not stored in local storage, they are
* kept in an HTTP Only cookie as protection against session hi-jacking by
* malicious JavaScript.
**/
static async init({
req = null,
force = false
} = {}) {
let session = {}
if (req) {
if (req.session) {
// If running on the server session data should be in the req object
session.csrfToken = req.connection._httpMessage.locals._csrf
session.expires = req.session.cookie._expires
// If the user is logged in, add the user to the session object
if (req.user) {
session.user = req.user
}
}
} else {
// If running in the browser attempt to load session from sessionStore
if (force === true) {
// If force update is set, reset data store
this._removeLocalStore('session')
} else {
session = this._getLocalStore('session')
}
}
// If session data exists, has not expired AND force is not set then
// return the stored session we already have.
if (session && Object.keys(session).length > 0 && session.expires && session.expires > Date.now()) {
return new Promise(resolve => {
resolve(session)
})
} else {
// If running on server, but session has expired return empty object
// (no valid session)
if (typeof window === 'undefined') {
return new Promise(resolve => {
resolve({})
})
}
}
// If we don't have session data, or it's expired, or force is set
// to true then revalidate it by fetching it again from the server.
return fetch('/auth/session', {
credentials: 'same-origin'
})
.then(response => {
if (response.ok) {
return response
} else {
return Promise.reject(Error('HTTP error when trying to get session'))
}
})
.then(response => response.json())
.then(data => {
// Update session with session info
session = data
// Set a value we will use to check this client should silently
// revalidate, using the value for revalidateAge returned by the server.
session.expires = Date.now() + session.revalidateAge
// Save changes to session
this._saveLocalStore('session', session)
return session
})
.catch(() => Error('Unable to get session'))
}
/**
* A simple static method to get the CSRF Token is provided for convenience
**/
static async csrfToken() {
return fetch('/auth/csrf', {
credentials: 'same-origin'
})
.then(response => {
if (response.ok) {
return response
} else {
return Promise.reject(Error('Unexpected response when trying to get CSRF token'))
}
})
.then(response => response.json())
.then(data => data.csrfToken)
.catch(() => Error('Unable to get CSRF token'))
}
/**
* A static method to get list of currently linked oAuth accounts
**/
static async linked({
req = null
} = {}) {
// If running server side, uses server side method
if (req) return req.linked()
// If running client side, use RESTful endpoint
return fetch('/auth/linked', {
credentials: 'same-origin'
})
.then(response => {
if (response.ok) {
return response
} else {
return Promise.reject(Error('Unexpected response when trying to get linked accounts'))
}
})
.then(response => response.json())
.then(data => data)
.catch(() => Error('Unable to get linked accounts'))
}
/**
* A static method to get list of currently configured oAuth providers
**/
static async providers({
req = null
} = {}) {
// If running server side, uses server side method
if (req) return req.providers()
// If running client side, use RESTful endpoint
return fetch('/auth/providers', {
credentials: 'same-origin'
})
.then(response => {
if (response.ok) {
return response
} else {
console.log("NextAuth Error Fetching Providers")
return null
}
})
.then(response => response.json())
.then(data => data)
.catch((e) => {
console.log("NextAuth Error Loading Providers")
console.log(e)
return null
})
}
/*
* Sign in
*
* Will post a form to /auth/signin auth route if an object is passed.
* If the details are valid a session will be created and you should redirect
* to your callback page so the session is loaded in the client.
*
* If just a string containing an email address is specififed will generate a
* a one-time use sign in link and send it via email; you should redirect to a
* page telling the user to check their inbox for an email with the link.
*/
static async signin(params) {
// Params can be just string (an email address) or an object (form fields)
const formData = (typeof params === 'string') ? { email: params } : params
// Use either the email token generation route or the custom form auth route
const route = (typeof params === 'string') ? '/auth/email/signin' : '/auth/signin'
// Add latest CSRF Token to request
formData._csrf = await this.csrfToken()
// Encoded form parser for sending data in the body
const encodedForm = Object.keys(formData).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
}).join('&')
return fetch(route, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest' // So Express can detect AJAX post
},
body: encodedForm,
credentials: 'same-origin'
})
.then(async response => {
if (response.ok) {
return await response.json()
} else {
throw new Error('HTTP error while attempting to sign in')
}
})
.then(data => {
if (data.success && data.success === true) {
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
})
}
static async signout() {
// Signout from the server
const csrfToken = await this.csrfToken()
const formData = { _csrf: csrfToken }
// Encoded form parser for sending data in the body
const encodedForm = Object.keys(formData).map((key) => {
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
}).join('&')
// Remove cached session data
this._removeLocalStore('session')
return fetch('/auth/signout', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: encodedForm,
credentials: 'same-origin'
})
.then(() => {
return true
})
.catch(() => Error('Unable to sign out'))
}
// The Web Storage API is widely supported, but not always available (e.g.
// it can be restricted in private browsing mode, triggering an exception).
// We handle that silently by just returning null here.
static _getLocalStore(name) {
try {
return JSON.parse(localStorage.getItem(name))
} catch (err) {
return null
}
}
static _saveLocalStore(name, data) {
try {
localStorage.setItem(name, JSON.stringify(data))
return true
} catch (err) {
return false
}
}
static _removeLocalStore(name) {
try {
localStorage.removeItem(name)
return true
} catch (err) {
return false
}
}
}

171
src/css/index.css Normal file
View File

@@ -0,0 +1,171 @@
:root {
--color-primary: #444;
--color-control-border: #bbb;
--color-button-hover-background: #f9f9f9;
--color-button-active-background: #f5f5f5;
--color-button-active-border: #aaa;
--border-width: 1px;
--border-radius: .3rem;
}
body {
margin: 0;
padding: 0;
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';
}
h1 {
font-weight: 400;
margin-bottom: 1.5rem;
padding: 0 1rem;
}
form {
margin: 0;
padding: 0;
}
label {
font-weight: 500;
text-align: left;
margin-bottom: 0.25rem;
display: block;
color: #666;
}
input[type] {
box-sizing: border-box;
display: block;
width: 100%;
padding: .5rem 1rem;
border: var(--border-width) solid var(--color-control-border);
background: #fff;
font-size: 1rem;
border-radius: var(--border-radius);
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
&:focus {
box-shadow: none;
}
}
p {
margin: 0 0 1.5rem 0;
padding: 0 1rem;
font-size: 1.1rem;
line-height: 2rem;
}
a.button {
text-decoration: none;
line-height: 1rem;
&:link,
&:visited {
background-color: #fff;
color: var(--color-primary);
}
}
button,
a.button {
margin: 0 0 .75rem 0;
padding: .75rem 1rem;
border: var(--border-width) solid var(--color-control-border);
color: var(--color-primary);
background-color: #fff;
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 #fff, inset 0 -.1rem .1rem rgba(0,0,0,.05);
font-weight: 500;
position: relative;
&:hover {
background-color: var(--color-button-hover-background);
cursor: pointer;
}
&:active {
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem #fff, 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;
}
}
a.site {
color: var(--color-primary);
text-decoration: none;
font-size: 1rem;
line-height: 2rem;
&:hover {
text-decoration: underline;
}
}
.page {
position: absolute;
width: 100%;
height: 100%;
display: table;
margin: 0;
padding: 0;
> div {
display: table-cell;
vertical-align: middle;
text-align: center;
padding: 0.5rem;
}
}
.error {
a.button {
display: inline-block;
padding-left: 2rem;
padding-right: 2rem;
margin-top: .5rem;
}
.message {
margin-bottom: 1.5rem;
}
}
.signin {
button,
a.button,
input[type="text"] {
margin-left: auto;
margin-right: auto;
display: block;
}
hr {
display: block;
border: 0;
border-top: 1px solid #ccc;
margin: 1.5em auto 0 auto;
overflow: visible;
&::before {
content: "or";
background: #fff;
color: #888;
padding: 0 .4rem;
position: relative;
top: -.6rem;
}
}
> div,
form {
display: block;
margin: 0 auto 0.5rem auto;
input[type] {
margin-bottom: 0.5rem;
}
button {
width: 100%;
}
max-width: 300px;
}
}

10
src/css/index.js Normal file
View File

@@ -0,0 +1,10 @@
// To support serverless targets (which don't work if you try to read in things
// like CSS files at run time) this file is replaced in production builds with
// a function that returns compiled CSS (embedded as a string in the function).
import fs from 'fs'
import path from 'path'
const pathToCss = path.join(__dirname, '/index.css')
const css = fs.readFileSync(pathToCss, 'utf8')
export default () => css

41
src/lib/errors.js Normal file
View File

@@ -0,0 +1,41 @@
class UnknownError extends Error {
constructor (message) {
super(message)
this.name = 'UnknownError'
this.message = message
}
toJSON () {
return {
error: {
name: this.name,
message: this.message
// stack: this.stack
}
}
}
}
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 {
constructor (message) {
super(message)
this.name = 'AccountNotLinkedError'
this.message = message
}
}
module.exports = {
UnknownError,
CreateUserError,
AccountNotLinkedError
}

44
src/lib/jwt.js Normal file
View File

@@ -0,0 +1,44 @@
import jwt from 'jsonwebtoken'
import CryptoJS from 'crypto-js'
const encode = async ({ secret, key = secret, token = {}, maxAge }) => {
// If maxAge is set remove any existing created/expiry dates and replace them
if (maxAge) {
if (token.iat) { delete token.iat }
if (token.exp) { delete token.exp }
}
const signedToken = jwt.sign(token, secret, { expiresIn: maxAge })
const encryptedToken = CryptoJS.AES.encrypt(signedToken, key).toString()
return encryptedToken
}
const decode = async ({ secret, key = secret, token, maxAge }) => {
if (!token) return null
const decryptedBytes = CryptoJS.AES.decrypt(token, key)
const decryptedToken = decryptedBytes.toString(CryptoJS.enc.Utf8)
const verifiedToken = jwt.verify(decryptedToken, secret, { maxAge })
return verifiedToken
}
// This is a simple helper method to make it easier to use JWT from an API route
const getJwt = async ({ req, secret, cookieName }) => {
if (!req || !secret) throw new Error('Must pass { req, secret } to getJWT()')
const secureCookieName = '__Secure-next-auth.session-token'
const insecureCookieName = 'next-auth.session-token'
const cookieValue = cookieName ? req.cookies[cookieName] : req.cookies[secureCookieName] || req.cookies[insecureCookieName]
if (!cookieValue) { return null }
try {
return await decode({ secret, token: cookieValue })
} catch (error) {
return null
}
}
export default {
encode,
decode,
getJwt
}

23
src/lib/logger.js Normal file
View File

@@ -0,0 +1,23 @@
const logger = {
error: (errorCode, ...text) => {
if (console) {
!text
? console.error(errorCode)
: console.error(
`[next-auth][error][${errorCode}]`,
text,
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
)
}
},
debug: (debugCode, ...text) => {
if (process && process.env && process.env._NEXT_AUTH_DEBUG) {
console.log(
`[next-auth][debug][${debugCode}]`,
text
)
}
}
}
export default logger

View File

@@ -1,347 +0,0 @@
/*
* Configures Passport Strategies
*/
'use strict'
const passport = require('passport')
module.exports = ({
expressApp = null, // Express Server
pathPrefix = '/auth', // URL base path for authentication routes
providers = [],
serverUrl = null,
functions = {
find: ({
id,
email,
emailToken,
provider
} = {}) => {},
update: (user, profile) => {},
insert: (user, profile) => {},
serialize: (user) => {},
deserialize: (id) => {}
}
} = {}) => {
if (expressApp === null) {
throw new Error('expressApp must be an instance of an express server')
}
if (typeof(functions) !== 'object') {
throw new Error('functions must be a an object')
}
/*
* Return functions ID property from a functions object
*/
passport.serializeUser((user, next) => {
functions.serialize(user)
.then(id => {
next(null, id)
})
.catch(err => {
next(err, false)
})
})
/*
* Return functions from a functions ID
*/
passport.deserializeUser((id, next) => {
functions.deserialize(id)
.then(user => {
if (!user) return next(null, false)
next(null, user)
})
.catch(err => {
next(err, false)
})
})
// Define a Passport strategy for provider
providers.forEach(({
providerName,
Strategy,
strategyOptions,
getProfile
}) => {
strategyOptions.callbackURL = (strategyOptions.callbackURL || (serverUrl || '') + `${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`)
strategyOptions.passReqToCallback = true
passport.use(providerName, new Strategy(strategyOptions, (req, accessToken, refreshToken, _params, _profile, next) => {
try {
// Normalise the provider specific profile into a standard basic
// profile object with just { id, name, email } properties.
let profile = getProfile(_profile)
const linkToExistingAccount = user => {
const nextUser = Object.assign({}, user)
// If we don't already have a name for the user, use value the
// name value specfied in their profile on the remote service.
nextUser.name = user.name || profile.name
// If we don't have a real email address for the user, use the
// email value specified in their profile on the remote service.
if (
user.email &&
user.email.match(/.*@localhost\.localdomain$/) &&
profile.email &&
!profile.email.match(/.*@localhost\.localdomain$/)
) {
nextUser.emailVerified = false
nextUser.email = profile.email
}
// Save Profile ID, Access Token and Refresh Token values
// to the users local account, which links the accounts.
nextUser[providerName.toLowerCase()] = {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
};
// Update details for the new provider for this user.
return functions.update(nextUser, _profile, _params)
}
// Save the Access Token to the current session.
req.session[providerName.toLowerCase()] = {
accessToken: accessToken
}
// If we didn't get an email address from the oAuth provider then
// generate a unique one as placeholder, using Provider name and ID.
//
// If you want users to specify a valid email address after signing in,
// you can check for email addresses ending "@localhost.localdomain"
// and prompt those users to supply a valid address.
if (!profile.email) {
profile.email = `${providerName.toLowerCase()}-${profile.id}@localhost.localdomain`
}
// Look for a user in the database associated with this account.
functions.find({
provider: {
name: providerName.toLowerCase(),
id: profile.id
}
})
.then(user => {
if (req.user) {
// This section handles scenarios when a user is already signed in.
if (user) {
// This section handles if the user is already logged in
if (req.user.id === user.id) {
// This section handles if the user is already logged in and is
// already linked to local account they are signed in with.
// If they are, all we need to do is update the Refresh Token
// value if we got one.
if (refreshToken) {
user[providerName.toLowerCase()] = {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
}
functions.update(user, _profile, _params)
.then(user => {
return next(null, user)
})
.catch(err => {
next(err)
})
} else {
return next(null, user)
}
} else {
// This section handles if a user is logged in but the oAuth
// account they are trying to link to is already linked to a
// different local account.
// This prevents users from linking an oAuth account to more
// than one local account at the same time.
return next(null, false)
}
} else {
// This secion handles if a user is already logged in and is
// trying to link a new account to the existing local account.
// First get the User ID from the User, then look up the user
// details. Note: We don't use the User object in req.user
// directly as it is a simplified set of properties set by
// functions.deserialize().
return functions.serialize(req.user)
.then(id => {
if (!id) throw new Error("Unable to serialize user")
return functions.find({ id: id })
})
.then(user => {
// This error should not happen, unless the currently signed in
// user has been deleted deleted from the database since
// signing in (or there is a problem talking to the database).
if (!user) return next(new Error("Unable to look up account for current user"), false);
return linkToExistingAccount(user)
})
.then(user => {
next(null, user)
})
.catch(err => next(err, false));
}
} else {
// This section handles scenarios when a user is not logged in.
if (user) {
// This section handles senarios where the user is not logged in
// but they seem to have an account already, so we sign them in
// as that user.
// Update Access and Refresh Tokens for the user if we got them.
if (accessToken || refreshToken) {
if (accessToken) user[providerName.toLowerCase()].accessToken = accessToken
if (refreshToken) user[providerName.toLowerCase()].refreshToken = refreshToken
return functions.update(user, _profile, _params)
.then(user => {
return next(null, user)
})
.catch(err => {
return next(err, false)
})
} else {
return next(null, user)
}
} else {
// This section handles senarios where the user is not logged in
// and they don't have a local account already.
// First we check to see if a local account with the same email
// address as the one associated with their oAuth profile exists.
//
// This is so they can't accidentally end up with two accounts
// linked to the same email address.
return functions.find({email: profile.email})
.then(user => {
if (user) {
// We already have a local account associated with their
// email address.
// If the provider is trustworthy and doesn't allow to create accounts
// for another user emails, then we allow to link this new account to
// the corresponding local account.
//
// Note: By default, we treat all providers as not trustworthy. Enable
// the `trustedIdentity` flag at your own risk and only to those providers,
// who you are sure will guarantee that identities can not be spoofed.
const provider = providers.find(provider => provider.providerName === providerName)
if (provider && provider.trustedIdentity) {
return linkToExistingAccount(user)
.then(user => {
next(null, user)
})
.catch(err => next(err, false))
}
// Otherwise, the user should sign in with his/her local account first -
// and then they can link accounts if they wish.
//
// Note: Automatically linking insufficiently trusted accounts here could
// expose a potential security exploit allowing someone to pre-register
// or create an account elsewhere for another users email
// address then trying to sign in from it, so don't do that.
return next(null, false)
}
// If an account does not exist, create one for them and return
// a user object to passport, which will sign them in.
return functions.insert({
name: profile.name,
email: profile.email,
[providerName.toLowerCase()]: {
id: profile.id,
accessToken: accessToken,
refreshToken: refreshToken
}
}, _profile, _params)
.then(user => {
return next(null, user)
})
.catch(err => {
return next(err, false)
})
})
}
}
})
.catch(err => {
next(err, false)
})
} catch (err) {
return next(err, false)
}
}))
})
// Initialise Passport
expressApp.use(passport.initialize())
expressApp.use(passport.session())
// Add routes for each provider
providers.forEach(({
providerName,
providerOptions
}) => {
// Route to start sign in
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}`, passport.authenticate(providerName, providerOptions))
// Route to call back to after signing in
expressApp.get(`${pathPrefix}/oauth/${providerName.toLowerCase()}/callback`,
passport.authenticate(providerName, {
successRedirect: `${pathPrefix}/callback?action=signin&service=${providerName}`,
failureRedirect: `${pathPrefix}/error?action=signin&type=oauth&service=${providerName}`
})
)
// Route to post to unlink accounts
expressApp.post(`${pathPrefix}/oauth/${providerName.toLowerCase()}/unlink`, (req, res, next) => {
if (!req.user) {
return next(new Error('Not signed in'))
}
// First get the User ID from the User, then look up the user details.
// Note: We don't use the User object in req.user directly as it is a
// a simplified set of properties set by functions.deserialize().
functions.serialize(req.user)
.then(id => {
if (!id) throw new Error("Unable to serialize user")
return functions.find({ id: id })
})
.then(user => {
if (!user) return next(new Error('Unable to look up account for current user'))
// Remove connection between user account and oauth provider
if (user[providerName.toLowerCase()]) {
delete user[providerName.toLowerCase()]
}
return functions.update(user, null, { delete: providerName.toLowerCase() })
.then(user => {
return res.redirect(`${pathPrefix}/callback?action=unlink&service=${providerName.toLowerCase()}`)
})
.catch(err => {
return next(err, false)
})
})
})
})
// A catch all for providers that are not configured
expressApp.get(`${pathPrefix}/oauth/:provider`, (req, res) => {
return res.redirect(`${pathPrefix}/error?action=signin&type=unsupported`)
})
}

48
src/providers/apple.js Normal file
View File

@@ -0,0 +1,48 @@
import jwt from 'jsonwebtoken'
export default (options) => {
return {
id: 'apple',
name: 'Apple',
type: 'oauth',
version: '2.0',
scope: 'name email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://appleid.apple.com/auth/token',
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
profileUrl: null,
idToken: true,
profile: (profile) => {
return {
id: profile.sub,
name: profile.name == null ? profile.sub : profile.name,
email: profile.email
}
},
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
},
privateKey,
{
algorithm: 'ES256',
keyid: keyId
}
)
return Promise.resolve(response)
},
...options
}
}

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

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

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

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

View File

@@ -0,0 +1,10 @@
export default (options) => {
return {
id: 'credentials',
name: 'Credentials',
type: 'credentials',
authorize: null,
credentials: null,
...options
}
}

23
src/providers/discord.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'discord',
name: 'Discord',
type: 'oauth',
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',
profile: (profile) => {
return {
id: profile.id,
name: profile.username,
image: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
email: profile.email
}
},
...options
}
}

73
src/providers/email.js Normal file
View File

@@ -0,0 +1,73 @@
import nodemailer from 'nodemailer'
import logger from '../lib/logger'
export default (options) => {
return {
id: 'email',
type: 'email',
name: 'Email',
// Server can be an SMTP connection string or a nodemailer config object
server: {
host: 'localhost',
port: 25,
auth: {
user: '',
pass: ''
}
},
from: 'NextAuth <no-reply@example.com>',
maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
sendVerificationRequest,
...options
}
}
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
const siteName = site.replace(/^https?:\/\//, '')
nodemailer
.createTransport(server)
.sendMail({
to: emailAddress,
from,
subject: `Sign in to ${siteName}`,
text: text({ url, siteName }),
html: html({ url, siteName })
}, (error) => {
if (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
}
return resolve()
})
})
}
// Email HTML body
const html = ({ url, siteName }) => {
const buttonBackgroundColor = '#444444'
const buttonTextColor = '#ffffff'
return `
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 8px 0; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #888888;">
${siteName}
</td>
</tr>
<tr>
<td align="center" style="padding: 16px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 3px; padding: 12px 18px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>
</tr>
</table>
`
}
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
const text = ({ url, siteName }) => `Sign in to ${siteName}\n${url}\n\n`

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

@@ -0,0 +1,21 @@
export default (options) => {
return {
id: 'facebook',
name: 'Facebook',
type: 'oauth',
version: '2.0',
scope: 'email',
accessTokenUrl: 'https://graph.facebook.com/oauth/access_token',
authorizationUrl: 'https://www.facebook.com/v7.0/dialog/oauth?response_type=code',
profileUrl: 'https://graph.facebook.com/me?fields=email,name,picture',
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.picture.data.url
}
},
...options
}
}

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

@@ -0,0 +1,21 @@
export default (options) => {
return {
id: 'github',
name: 'GitHub',
type: 'oauth',
version: '2.0',
scope: 'user',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
authorizationUrl: 'https://github.com/login/oauth/authorize',
profileUrl: 'https://api.github.com/user',
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.avatar_url
}
},
...options
}
}

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

@@ -0,0 +1,22 @@
export default (options) => {
return {
id: 'gitlab',
name: 'GitLab',
type: 'oauth',
version: '2.0',
scope: 'read_user',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://gitlab.com/oauth/token',
authorizationUrl: 'https://gitlab.com/oauth/authorize?response_type=code',
profileUrl: 'https://gitlab.com/api/v4/user',
profile: (profile) => {
return {
id: profile.id,
name: profile.username,
email: profile.email,
image: profile.avatar_url
}
},
...options
}
}

23
src/providers/google.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'google',
name: 'Google',
type: 'oauth',
version: '2.0',
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
requestTokenUrl: 'https://accounts.google.com/o/oauth2/auth',
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth?response_type=code',
profileUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.picture
}
},
...options
}
}

View File

@@ -0,0 +1,18 @@
export default (options) => {
return {
id: 'identity-server4',
name: 'IdentityServer4',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: `https://${options.domain}/connect/token`,
authorizationUrl: `https://${options.domain}/connect/authorize?response_type=code`,
profileUrl: `https://${options.domain}/connect/userinfo`,
profile: (profile) => {
return { ...profile, id: profile.sub }
},
setGetAccessTokenAuthHeader: false,
...options
}
}

37
src/providers/index.js Normal file
View File

@@ -0,0 +1,37 @@
import Auth0 from './auth0'
import Apple from './apple'
import Box from './box'
import Credentials from './credentials'
import Discord from './discord'
import Email from './email'
import Facebook from './facebook' // @TODO
import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
import IdentityServer4 from './identity-server4'
import Mixer from './mixer'
import Okta from './okta'
import Slack from './slack'
import Twitch from './twitch'
import Twitter from './twitter'
import Yandex from './yandex'
export default {
Auth0,
Apple,
Box,
Credentials,
Discord,
Email,
Facebook,
GitHub,
GitLab,
Google,
IdentityServer4,
Mixer,
Okta,
Slack,
Twitter,
Twitch,
Yandex
}

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

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

23
src/providers/okta.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'okta',
name: 'Okta',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
params: {
grant_type: 'authorization_code',
client_id: options.clientId,
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/`,
profile: (profile) => {
return { ...profile, id: profile.sub }
},
setGetAccessTokenAuthHeader: false,
...options
}
}

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

@@ -0,0 +1,24 @@
// Logging in works but trying to retrieve the profile results in 401 unauthorized
export default (options) => {
return {
id: 'reddit',
name: 'Reddit',
type: 'oauth',
version: '2.0',
scope: 'identity',
params: { grant_type: 'authorization_code' },
accessTokenUrl: ' https://www.reddit.com/api/v1/access_token',
authorizationUrl:
'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,
// };
},
...options
}
}

23
src/providers/slack.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'slack',
name: 'Slack',
type: 'oauth',
version: '2.0',
scope: 'identity.basic identity.email identity.avatar',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://slack.com/api/oauth.access',
authorizationUrl: 'https://slack.com/oauth/authorize?response_type=code',
profileUrl: 'https://slack.com/api/users.identity',
profile: (profile) => {
const { user } = profile
return {
id: user.id,
name: user.name,
image: user.image_512,
email: user.email
}
},
...options
}
}

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

@@ -0,0 +1,24 @@
export default (options) => {
return {
id: 'twitch',
name: 'Twitch',
type: 'oauth',
version: '2.0',
scope: 'user:read:email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://id.twitch.tv/oauth2/token',
authorizationUrl:
'https://id.twitch.tv/oauth2/authorize?response_type=code',
profileUrl: 'https://api.twitch.tv/helix/users',
profile: (profile) => {
const data = profile.data[0]
return {
id: data.id,
name: data.display_name,
image: data.profile_image_url,
email: data.email
}
},
...options
}
}

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

@@ -0,0 +1,22 @@
export default (options) => {
return {
id: 'twitter',
name: 'Twitter',
type: 'oauth',
version: '1.0A',
scope: '',
accessTokenUrl: 'https://api.twitter.com/oauth/access_token',
requestTokenUrl: 'https://api.twitter.com/oauth/request_token',
authorizationUrl: 'https://api.twitter.com/oauth/authenticate',
profileUrl: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, '.jpg')
}
},
...options
}
}

23
src/providers/yandex.js Normal file
View File

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'yandex',
name: 'Yandex',
type: 'oauth',
version: '2.0',
scope: 'login:email login:info',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://oauth.yandex.ru/token',
requestTokenUrl: 'https://oauth.yandex.ru/token',
authorizationUrl: 'https://oauth.yandex.ru/authorize?response_type=code',
profileUrl: 'https://login.yandex.ru/info?format=json',
profile: (profile) => {
return {
id: profile.id,
name: profile.real_name,
email: profile.default_email,
image: null
}
},
...options
}
}

317
src/server/index.js Normal file
View File

@@ -0,0 +1,317 @@
import { createHash, randomBytes } from 'crypto'
import jwt from '../lib/jwt'
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'
const DEFAULT_SITE = 'http://localhost:3000'
const DEFAULT_BASE_PATH = '/api/auth'
export default async (req, res, userSuppliedOptions) => {
// 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
const { url, query, body } = req
const {
nextauth,
action = nextauth[0],
provider = nextauth[1],
error
} = query
const {
csrfToken: csrfTokenFromPost
} = body
// Allow site name, path prefix to be overriden
const site = userSuppliedOptions.site || DEFAULT_SITE
const basePath = userSuppliedOptions.basePath || DEFAULT_BASE_PATH
const baseUrl = `${site}${basePath}`
// 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(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
}
},
baseUrl: {
name: `${cookiePrefix}next-auth.base-url`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
csrfToken: {
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: useSecureCookies
}
},
// Allow user cookie options to override any cookie settings above
...userSuppliedOptions.cookies
}
// Session options
const sessionOption = {
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,
key: secret,
encode: jwt.encode,
decode: jwt.decode,
...userSuppliedOptions.jwt
}
// If no adapter specified, force use of JSON Web Tokens (stateless)
if (!adapter) {
sessionOption.jwt = true
}
// Event messages
const eventsOption = {
...events,
...userSuppliedOptions.events
}
// Callback functions
const callbacksOption = {
...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)
}
// Set canonical site name + API route in a cookie to facilitate passing configuration
// to the NextAuth client. There are potential security considerations around this
// relating to trying to prevent attackers from exploiting this by setting this cookie
// on the client first if they can get control of a sub domain or exploit a XSS
// vulnerability, but this approach attempts to mitgate that by always verifying
// the cookie and updating it if fails the verification check.
let setUrlPrefixCookie = true
if (req.cookies[cookies.baseUrl.name]) {
const [baseUrlValue, baseUrlHash] = req.cookies[cookies.baseUrl.name].split('|')
// If the hash on the cookie is verified, then we must have set the cookie and don't need to update it
if (baseUrlValue === baseUrl && baseUrlHash === createHash('sha256').update(`${baseUrlValue}${secret}`).digest('hex')) { setUrlPrefixCookie = false }
}
// If the cookie is not set already (or if it is set, but failed verification) set header to update the cookie
if (setUrlPrefixCookie) {
const newUrlPrefixCookie = `${baseUrl}|${createHash('sha256').update(`${baseUrl}${secret}`).digest('hex')}`
cookie.set(res, cookies.baseUrl.name, newUrlPrefixCookie, cookies.baseUrl.options)
}
// 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)
// Custom options override defaults
...userSuppliedOptions,
// These computed settings can values in userSuppliedOptions but override them
// and are request-specific.
adapter,
site,
basePath,
baseUrl,
action,
provider,
cookies,
secret,
csrfToken,
csrfTokenVerified,
providers: parseProviders(userSuppliedOptions.providers, baseUrl),
session: sessionOption,
jwt: jwtOptions,
events: eventsOption,
callbacks: callbacksOption,
callbackUrl: site
}
// If debug enabled, set ENV VAR so that logger logs debug messages
if (options.debug === true) { process.env._NEXT_AUTH_DEBUG = true }
// Get / Set callback URL based on query param / cookie + validation
options.callbackUrl = await callbackUrlHandler(req, res, options)
const redirect = (redirectUrl) => {
res.status(302).setHeader('Location', redirectUrl)
res.end()
return done()
}
if (req.method === 'GET') {
switch (action) {
case 'providers':
providers(req, res, options, done)
break
case 'session':
session(req, res, options, done)
break
case 'csrf':
res.json({ csrfToken })
return done()
case 'signin':
if (provider && options.providers[provider]) {
signin(req, res, options, done)
} else {
if (options.pages.signin) { return redirect(`${options.pages.signin}${options.pages.signin.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
pages.render(req, res, 'signin', { site, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
}
break
case 'signout':
if (options.pages.signout) { return redirect(`${options.pages.signout}${options.pages.signout.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
pages.render(req, res, 'signout', { site, baseUrl, csrfToken, callbackUrl: options.callbackUrl }, done)
break
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()
}
break
case 'verify-request':
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
pages.render(req, res, 'verify-request', { site }, done)
break
case 'error':
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
pages.render(req, res, 'error', { site, error, baseUrl }, done)
break
default:
res.status(404).end()
return done()
}
} else if (req.method === 'POST') {
switch (action) {
case 'signin':
// Signin POST requests are used for email sign in
if (provider && options.providers[provider]) {
signin(req, res, options, done)
break
}
break
case 'signout':
signout(req, res, options, done)
break
case 'callback':
if (provider && options.providers[provider]) {
callback(req, res, options, done)
} else {
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
return done()
}
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()
}
})
}

View File

@@ -0,0 +1,224 @@
// 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') }
const { adapter, jwt, events } = options
const useJwtSession = options.session.jwt
const sessionMaxAge = options.session.maxAge
// 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 {
createUser,
updateUser,
getUser,
getUserByProviderAccountId,
getUserByEmail,
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
let session = null
let user = null
let isSignedIn = null
let isNewUser = false
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
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)
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)
}
}
// Update emailVerified property on the user object
const currentDate = new Date()
userByEmail.emailVerified = currentDate
user = await updateUser(userByEmail)
await dispatchEvent(events.updateUser, user)
} else {
// Create user account if there isn't one for the email address already
user = await createUser({ ...profile, emailVerified: true })
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
}
} 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

@@ -0,0 +1,27 @@
import cookie from '../lib/cookie'
export default async (req, res, options) => {
const { query } = req
const { body } = req
const { cookies, site, defaultCallbackUrl, callbacks } = options
// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || site
// 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
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect(callbackUrlParamValue, site)
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, site)
}
// 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) }
return Promise.resolve(callbackUrl)
}

View File

@@ -0,0 +1,75 @@
/**
* Use the signin callback to control if a user is allowed to sign in or not.
*
* This is triggered before sign in flow completes, so the user profile may be
* a user object (with an ID) or it may be just their name and email address,
* depending on the sign in flow and if they have an account already.
*
* When using email sign in, this method is triggered both when the user
* 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
*/
const signin = async (profile, account, metadata) => {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return Promise.resolve(true)
} else {
return Promise.resolve(false)
}
}
/**
* Redirect is called anytime the user is redirected on signin or signout.
* By default, for security, only Callback URLs on the same URL as the site
* are allowed, you can use this callback to customise that behaviour.
*
* @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
*/
const redirect = async (url, baseUrl) => {
return url.startsWith(baseUrl)
? Promise.resolve(url)
: Promise.resolve(baseUrl)
}
/**
* The session callback is called whenever a session is checked.
* e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* @param {object} session Session object
* @param {object} token JSON Web Token (if enabled)
* @return {object} Session that will be returned to the client
*/
const session = async (session, token) => {
return Promise.resolve(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
* 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
*/
const jwt = async (token, oAuthProfile) => {
return Promise.resolve(token)
}
export default {
signin,
redirect,
session,
jwt
}

102
src/server/lib/cookie.js Normal file
View File

@@ -0,0 +1,102 @@
// 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 = {}) => {
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
if ('maxAge' in options) {
options.expires = new Date(Date.now() + options.maxAge)
options.maxAge /= 1000
}
// Preserve any existing cookies that have already been set in the same session
const setCookieHeader = res.getHeader('Set-Cookie') || []
setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader('Set-Cookie', setCookieHeader)
}
function _serialize (name, val, options) {
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
const opt = options || {}
const enc = opt.encode || encodeURIComponent
if (typeof enc !== 'function') { throw new TypeError('option encode is invalid') }
if (!fieldContentRegExp.test(name)) { throw new TypeError('argument name is invalid') }
const value = enc(val)
if (value && !fieldContentRegExp.test(value)) { throw new TypeError('argument val is invalid') }
let str = name + '=' + value
if (opt.maxAge != null) {
const maxAge = opt.maxAge - 0
if (isNaN(maxAge) || !isFinite(maxAge)) { throw new TypeError('option maxAge is invalid') }
str += '; Max-Age=' + Math.floor(maxAge)
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) { throw new TypeError('option domain is invalid') }
str += '; Domain=' + opt.domain
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) { throw new TypeError('option path is invalid') }
str += '; Path=' + opt.path
} else {
str += '; Path=/'
}
if (opt.expires) {
let expires = opt.expires
if (typeof opt.expires.toUTCString === 'function') {
expires = opt.expires.toUTCString()
} else {
const dateExpires = new Date(opt.expires)
expires = dateExpires.toUTCString()
}
str += '; Expires=' + expires
}
if (opt.httpOnly) { str += '; HttpOnly' }
if (opt.secure) { str += '; Secure' }
if (opt.sameSite) {
const sameSite = typeof opt.sameSite === 'string' ? opt.sameSite.toLowerCase() : opt.sameSite
switch (sameSite) {
case true:
str += '; SameSite=Strict'
break
case 'lax':
str += '; SameSite=Lax'
break
case 'strict':
str += '; SameSite=Strict'
break
case 'none':
str += '; SameSite=None'
break
default:
throw new TypeError('option sameSite is invalid')
}
}
return str
}
export default {
set
}

View File

@@ -0,0 +1,9 @@
import logger from '../../lib/logger'
export default async (event, message) => {
try {
await event(message)
} catch (e) {
logger.error('EVENT_ERROR', e)
}
}

38
src/server/lib/events.js Normal file
View File

@@ -0,0 +1,38 @@
const signin = async (message) => {
// Event triggered on successful sign in
}
const signout = async (message) => {
// Event triggered on signout
}
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,210 @@
import oAuthClient from './client'
import querystring from 'querystring'
import jwtDecode from 'jwt-decode'
import logger from '../../../lib/logger'
// @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.
export default async (req, provider, callback) => {
let { oauth_token, oauth_verifier, code } = req.query // eslint-disable-line camelcase
const client = oAuthClient(provider)
if (provider.version && provider.version.startsWith('2.')) {
if (req.method === 'POST') {
// Get the CODE from Body
const body = JSON.parse(JSON.stringify(req.body))
code = body.code
}
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
} else {
client.useAuthorizationHeaderforGET(true)
}
// Use custom getOAuthAccessToken() method for oAuth2 flows
client.getOAuthAccessToken = _getOAuthAccessToken
await client.getOAuthAccessToken(
code,
provider,
(error, accessToken, refreshToken, results) => {
// @TODO Handle error
if (error || results.error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, results, provider.id, code)
}
if (provider.idToken) {
// Support services that use OpenID ID Tokens to encode profile data
_decodeToken(
provider,
accessToken,
refreshToken,
results.id_token,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
)
} else {
// Use custom get() method for oAuth2 flows
client.get = _get
client.get(
provider,
accessToken,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
)
}
}
)
} 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,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
)
}
)
}
}
async function _getProfile (error, profileData, accessToken, refreshToken, provider) {
// @TODO Handle error
if (error) {
logger.error('OAUTH_GET_PROFILE_ERROR', error)
}
let profile = {}
try {
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) { profileData = JSON.parse(profileData) }
profile = await provider.profile(profileData)
} catch (exception) {
// @TODO Handle parsing error
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception)
throw new Error('Failed to get OAuth profile')
}
// 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

@@ -0,0 +1,31 @@
// @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'
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
const accessTokenPath = new URL(provider.accessTokenUrl).pathname
return 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')
)
}
}

View File

@@ -0,0 +1,14 @@
export default (_providers, baseUrl) => {
const providers = {}
_providers.forEach(provider => {
const providerId = provider.id
providers[providerId] = {
...provider,
signinUrl: `${baseUrl}/signin/${providerId}`,
callbackUrl: `${baseUrl}/callback/${providerId}`
}
})
return providers
}

View File

@@ -0,0 +1,26 @@
import { randomBytes } from 'crypto'
export default async (email, provider, options) => {
try {
const { baseUrl, adapter } = options
const { createVerificationRequest } = await adapter.getAdapter(options)
// Prefer provider specific secret, but use default secret if none specified
const secret = provider.secret || options.secret
// Generate token
const token = randomBytes(32).toString('hex')
// Send email with link containing token (the unhashed version)
const url = `${baseUrl}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
// @TODO Create invite (send secret so can be hashed)
await createVerificationRequest(email, url, token, secret, provider, options)
// Return promise
return Promise.resolve()
} catch (error) {
return Promise.reject(error)
}
}

View File

@@ -0,0 +1,40 @@
import oAuthClient from '../oauth/client'
import crypto from 'crypto'
import logger from '../../../lib/logger'
export default (provider, callback) => {
const { callbackUrl } = provider
const client = oAuthClient(provider)
if (provider.version && provider.version.startsWith('2.')) {
// Handle oAuth v2.x
let url = client.getAuthorizeUrl({
redirect_uri: provider.callbackUrl,
scope: provider.scope,
state: crypto.randomBytes(64).toString('hex')
})
// If the authorizationUrl specified in the config has query parameters on it
// make sure they are included in the URL we return.
//
// This is a fix for an open issue with the oAuthClient library we are using
// which inadvertantly strips them.
//
// https://github.com/ciaranj/node-oauth/pull/193
if (provider.authorizationUrl.includes('?')) {
const parseUrl = new URL(provider.authorizationUrl)
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
url = url.replace(baseUrl, provider.authorizationUrl + '&')
}
callback(null, url)
} else {
// Handle oAuth v1.x
client.getOAuthRequestToken((error, oAuthToken) => {
if (error) {
logger.error('GET_AUTHORISATION_URL_ERROR', error)
}
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
callback(error, url)
}, callbackUrl)
}
}

102
src/server/pages/error.js Normal file
View File

@@ -0,0 +1,102 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ site, error, baseUrl }) => {
const signinPageUrl = `${baseUrl}/signin` // @TODO Make sign in URL configurable
let heading = <h1>Error</h1>
let message = <p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
switch (error) {
case 'Signin':
case 'OAuthSignin':
case 'OAuthCallback':
case 'OAuthCreateAccount':
case 'EmailCreateAccount':
case 'Callback':
heading = <h1>Sign in failed</h1>
message =
<div>
<div className='message'>
<p>Try signing with a different account.</p>
</div>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
break
case 'OAuthAccountNotLinked':
heading = <h1>Sign in failed</h1>
message =
<div>
<div className='message'>
<p>An account associated with your email address already exists.</p>
<p>Sign in with the same account you used originally to confirm your identity.</p>
</div>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
// @TODO Add this text when account linking is complete
// <p>Once you are signed in, you can link your accounts.</p>
// @TODO Display email sign in option if an email provider is configured
break
case 'EmailSignin':
heading = <h1>Sign in failed</h1>
message =
<div>
<div className='message'>
<p>Unable to send email.</p>
</div>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
break
case 'CredentialsSignin':
heading = <h1>Sign in failed</h1>
message =
<div>
<div className='message'>
<p>Check the details you provided are correct.</p>
</div>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
break
case 'Configuration':
heading = <h1>Server error</h1>
message =
<div>
<div className='message'>
<p>There is a problem with the server configuration.</p>
<p>Check the server logs for more information.</p>
</div>
</div>
break
case 'AccessDenied':
heading = <h1>Access Denied</h1>
message =
<div>
<div className='message'>
<p>You do not have permission to sign in.</p>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
</div>
break
case 'Verification':
// @TODO Check if user is signed in already with the same email address.
// If they are, no need to display this message, can just direct to callbackUrl
heading = <h1>Unable to sign in</h1>
message =
<div>
<div className='message'>
<p>The sign in link is no longer valid.</p>
<p>It may have be used already or it may have expired.</p>
</div>
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
</div>
break
default:
}
return render(
<div className='error'>
{heading}
{message}
</div>
)
}

36
src/server/pages/index.js Normal file
View File

@@ -0,0 +1,36 @@
import fs from 'fs'
import path from 'path'
import signin from './signin'
import signout from './signout'
import verifyRequest from './verify-request'
import error from './error'
import css from '../../css'
function render (req, res, page, props, done) {
let html = ''
switch (page) {
case 'signin':
html = signin({ ...props, req })
break
case 'signout':
html = signout(props)
break
case 'verify-request':
html = verifyRequest(props)
break
case 'error':
html = error(props)
break
default:
html = error(props)
return
}
res.setHeader('Content-Type', 'text/html')
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div class="page">${html}</div></body></html>`)
done()
}
export default {
render
}

View File

@@ -0,0 +1,66 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ req, csrfToken, providers, callbackUrl }) => {
const withCallbackUrl = callbackUrl ? `?callbackUrl=${callbackUrl}` : ''
const { email } = req.query
// We only want to render providers
const providersToRender = providers.filter(provider => {
if (provider.type === 'oauth' || provider.type === 'email') {
// Always render oauth and email type providers
return true
} else if (provider.type === 'credentials' && provider.credentials) {
// Only render credentials type provider if credentials are defined
return true
} else {
// Don't render other provider types
return false
}
})
return render(
<div className='signin'>
{providersToRender.map((provider, i) =>
<div key={provider.id} className='provider'>
{provider.type === 'oauth' &&
<a className='button' data-provider={provider.id} href={`${provider.signinUrl}${withCallbackUrl}`}>Sign in with {provider.name}</a>}
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
<hr />}
{provider.type === 'email' &&
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
<label for={`input-email-for-${provider.id}-provider`}>Email</label>
<input id={`input-email-for-${provider.id}-provider`} autoFocus type='text' name='email' value={email} placeholder='email@example.com' />
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{provider.type === 'credentials' &&
<form action={provider.callbackUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{Object.keys(provider.credentials).map(credential => {
return (
<div key={`input-group-${provider.id}`}>
<label
for={`input-${credential}-for-${provider.id}-provider`}
>{provider.credentials[credential].label || credential}
</label>
<input
name={credential}
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type || 'text'}
value={provider.credentials[credential].value || ''}
placeholder={provider.credentials[credential].placeholder || ''}
/>
</div>
)
})}
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && ((i + 1) < providersToRender.length) &&
<hr />}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,14 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ baseUrl, csrfToken }) => {
return render(
<div className='signout'>
<h1>Are you sure you want to sign out?</h1>
<form action={`${baseUrl}/signout`} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
<button type='submit'>Sign out</button>
</form>
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ site }) => {
return render(
<div className='verify-request'>
<h1>Check your email</h1>
<p>A sign in link has been sent to your email address.</p>
<p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
</div>
)
}

View File

@@ -0,0 +1,267 @@
// Handle callbacks from login services
import oAuthCallback from '../lib/oauth/callback'
import callbackHandler from '../lib/callback-handler'
import cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
export default async (req, res, options, done) => {
const {
provider: providerName,
providers,
adapter,
site,
secret,
baseUrl,
cookies,
callbackUrl,
pages,
jwt,
events,
callbacks
} = options
const provider = providers[providerName]
const { type } = provider
const useJwtSession = options.session.jwt
const sessionMaxAge = options.session.maxAge
// Get session ID (if set)
const sessionToken = req.cookies ? req.cookies[cookies.sessionToken.name] : null
if (type === 'oauth') {
oAuthCallback(req, provider, async (error, oauthAccount) => {
if (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=oAuthCallback`)
res.end()
return done()
}
try {
const { profile, account, oAuthProfile } = await oauthAccount
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account, oAuthProfile)
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
if (useJwtSession) {
const defaultJwtPayload = { user, account, isNewUser }
const jwtPayload = await callbacks.jwt(defaultJwtPayload, oAuthProfile)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
}
await dispatchEvent(events.signin, { user, account, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
res.status(302).setHeader('Location', pages.newUser)
res.end()
return done()
}
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
// If the email on the account is already linked, but nto with this oAuth account
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthCreateAccount`)
} else {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
}
res.end()
return done()
}
// Callback URL is already verified at this point, so safe to use if specified
if (callbackUrl) {
res.status(302).setHeader('Location', callbackUrl)
res.end()
} else {
res.status(302).setHeader('Location', site)
res.end()
}
return done()
})
} else if (type === 'email') {
try {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
}
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(options)
const verificationToken = req.query.token
const email = req.query.email
// Verify email and verification token exist in database
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
if (!invite) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=Verification`)
res.end()
return done()
}
// If verification token is valid, delete verification request token from
// the database so it cannot be used again
await deleteVerificationRequest(email, verificationToken, secret, provider)
// If is an existing user return a user object (otherwise use placeholder)
const profile = await getUserByEmail(email) || { email }
const account = { id: provider.id, type: 'email', providerAccountId: email }
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account, null)
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
if (useJwtSession) {
const defaultJwtPayload = { user, account, isNewUser }
const jwtPayload = await callbacks.jwt(defaultJwtPayload)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
} else {
console.log('debug.cookies', cookies)
console.log('debug.session', session)
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
}
await dispatchEvent(events.signin, { user, account, isNewUser })
// Handle first logins on new accounts
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
res.status(302).setHeader('Location', pages.newUser)
res.end()
return done()
}
// Callback URL is already verified at this point, so safe to use if specified
if (callbackUrl) {
res.status(302).setHeader('Location', callbackUrl)
res.end()
} else {
res.status(302).setHeader('Location', site)
res.end()
}
return done()
} catch (error) {
if (error.name === 'CreateUserError') {
res.status(302).setHeader('Location', `${baseUrl}/error?error=EmailCreateAccount`)
} else {
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
logger.error('CALLBACK_EMAIL_ERROR', error)
}
res.end()
return done()
}
} else if (type === 'credentials' && req.method === 'POST') {
if (!useJwtSession) {
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
}
if (!provider.authorize) {
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
}
const credentials = req.body
// If promise is rejected / throws error then display Configuration error
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
} catch (error) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
}
const user = userObjectReturnedFromAuthorizeHandler
const account = { id: provider.id, type: 'credentials' }
// If no user is returned, credentials are not valid
if (!user) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
res.end()
return done()
}
const signinCallbackResponse = await callbacks.signin(user, account, credentials)
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
}
const defaultJwtPayload = { user, account }
const jwtPayload = await callbacks.jwt(defaultJwtPayload)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
await dispatchEvent(events.signin, { user, account })
if (callbackUrl) {
res.status(302).setHeader('Location', callbackUrl)
res.end()
} else {
res.status(302).setHeader('Location', site)
res.end()
}
return done()
} else {
res.status(500).end(`Error: Callback for provider type ${type} not supported`)
return done()
}
}

View File

@@ -0,0 +1,21 @@
// Return a JSON object with a list of all outh providers currently configured
// and their signin and callback URLs. This makes it possible to automatically
// generate buttons for all providers when rendering client side.
export default (req, res, options, done) => {
const { providers } = options
const result = {}
Object.entries(providers).map(([provider, providerConfig]) => {
result[provider] = {
id: provider,
name: providerConfig.name,
type: providerConfig.type,
signinUrl: providerConfig.signinUrl,
callbackUrl: providerConfig.callbackUrl
}
})
res.setHeader('Content-Type', 'application/json')
res.json(result)
return done()
}

View File

@@ -0,0 +1,104 @@
// Return a session object (without any private fields) for Single Page App clients
import cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
export default async (req, res, options, done) => {
const { cookies, adapter, jwt, events, callbacks } = options
const useJwtSession = options.session.jwt
const sessionMaxAge = options.session.maxAge
const sessionToken = req.cookies[cookies.sessionToken.name]
if (!sessionToken) {
res.setHeader('Content-Type', 'application/json')
res.json({})
return done()
}
let response = {}
if (useJwtSession) {
try {
// Decrypt and verify token
const decodedJwt = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
// Generate new session expiry date
const sessionExpiresDate = new Date()
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
const sessionExpires = sessionExpiresDate.toISOString()
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSessionPayload = {
user: {
name: decodedJwt.user && decodedJwt.user.name ? decodedJwt.user.name : null,
email: decodedJwt.user && decodedJwt.user.email ? decodedJwt.user.email : null,
image: decodedJwt.user && decodedJwt.user.image ? decodedJwt.user.image : null
},
expires: sessionExpires
}
// Pass Session and JSON Web Token through to the session callback
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
// Return session payload as response
response = sessionPayload
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error('JWT_SESSION_ERROR', error)
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
}
} else {
try {
const { getUser, getSession, updateSession } = await adapter.getAdapter(options)
const session = await getSession(sessionToken)
if (session) {
// Trigger update to session object to update session expiry
await updateSession(session)
const user = await getUser(session.userId)
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSessionPayload = {
user: {
name: user.name,
email: user.email,
image: user.image
},
accessToken: session.accessToken,
expires: session.expires
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(defaultSessionPayload)
// Return session payload as response
response = sessionPayload
// Set cookie again to update expiry
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
await dispatchEvent(events.session, { session: sessionPayload })
} else if (sessionToken) {
// If sessionToken was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
}
} catch (error) {
logger.error('SESSION_ERROR', error)
}
}
res.setHeader('Content-Type', 'application/json')
res.json(response)
return done()
}

111
src/server/routes/signin.js Normal file
View File

@@ -0,0 +1,111 @@
// Handle requests to /api/auth/signin
import oAuthSignin from '../lib/signin/oauth'
import emailSignin from '../lib/signin/email'
import logger from '../../lib/logger'
export default async (req, res, options, done) => {
const {
provider: providerName,
providers,
baseUrl,
csrfTokenVerified,
adapter,
callbacks
} = options
const provider = providers[providerName]
const { type } = provider
if (!type) {
res.status(500).end(`Error: Type not specified for ${provider}`)
return done()
}
if (type === 'oauth') {
oAuthSignin(provider, (error, oAuthSigninUrl) => {
if (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
res
.status(302)
.setHeader('Location', `${baseUrl}/error?error=oAuthSignin`)
res.end()
return done()
}
res.status(302).setHeader('Location', oAuthSigninUrl)
res.end()
return done()
})
} else if (type === 'email' && req.method === 'POST') {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
res
.status(302)
.setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
}
const { getUserByEmail } = await adapter.getAdapter(options)
// Note: Technically the part of the email address local mailbox element
// (everything before the @ symbol) should be treated as 'case sensitive'
// according to RFC 2821, but in practice this causes more problems than
// it solves. We treat email addresses as all lower case. If anyone
// complains about this we can make strict RFC 2821 compliance an option.
const email = req.body.email ? req.body.email.toLowerCase() : null
// If is an existing user return a user object (otherwise use placeholder)
const profile = await getUserByEmail(email) || { email }
const account = { id: provider.id, type: 'email', providerAccountId: email }
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account)
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
}
// If CSRF token not verified, send the user to sign in page, which will
// display a new form with a valid token so that submitting it should work.
//
// Note: Adds ?csrf=true query string param to URL for debugging/tracking
if (!csrfTokenVerified) {
res
.status(302)
.setHeader(
'Location',
`${baseUrl}/signin?email=${encodeURIComponent(email)}&csrf=true`
)
res.end()
return done()
}
try {
await emailSignin(email, provider, options)
} catch (error) {
logger.error('SIGNIN_EMAIL_ERROR', error)
res
.status(302)
.setHeader('Location', `${baseUrl}/error?error=EmailSignin`)
res.end()
return done()
}
res
.status(302)
.setHeader(
'Location',
`${baseUrl}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`
)
res.end()
return done()
} else {
// If provider not supported, redirect to sign in page
res.status(302).setHeader('Location', `${baseUrl}/signin`)
res.end()
return done()
}
}

View File

@@ -0,0 +1,62 @@
// Handle requests to /api/auth/signout
import cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
export default async (req, res, options, done) => {
const { adapter, cookies, events, jwt, callbackUrl, csrfTokenVerified, baseUrl } = options
const sessionMaxAge = options.session.maxAge
const useJwtSession = options.session.jwt
const sessionToken = req.cookies[cookies.sessionToken.name]
if (!csrfTokenVerified) {
// If a csrfToken was not verified with this request, send the user to
// the signout page, as they should have a valid one now and clicking
// the signout button should work.
//
// Note: Adds ?csrf=true query string param to URL for debugging/tracking.
// @TODO Add support for custom signin URLs
res.status(302).setHeader('Location', `${baseUrl}/signout?csrf=true`)
res.end()
return done()
}
if (useJwtSession) {
// Dispatch signout event
try {
const decodedJwt = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
await dispatchEvent(events.signout, decodedJwt)
} catch (error) {
// Do nothing if decoding the JWT fails
}
} else {
// Get session from database
const { getSession, deleteSession } = await adapter.getAdapter(options)
try {
// Dispatch signout event
const session = await getSession(sessionToken)
await dispatchEvent(events.signout, session)
} catch (error) {
// Do nothing if looking up the session fails
}
try {
// Remove session from database
await deleteSession(sessionToken)
} catch (error) {
// If error, log it but continue
logger.error('SIGNOUT_ERROR', error)
}
}
// Remove Session Token
cookie.set(res, cookies.sessionToken.name, '', {
...cookies.sessionToken.options,
maxAge: 0
})
res.status(302).setHeader('Location', callbackUrl)
res.end()
return done()
}

View File

@@ -0,0 +1,21 @@
# Start Mongo, MySQL and Postgres databases
# Though other databases will be supported, these are the initial targets.
# Uses Docker Compose v2 as v3 doesn't support extends
version: '2'
services:
mongo:
extends:
file: mongo.yml
service: mongo
mysql:
extends:
file: mysql.yml
service: mysql
postgres:
extends:
file: postgres.yml
service: postgres

13
test/docker/mongo.yml Normal file
View File

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

15
test/docker/mysql.yml Normal file
View File

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

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