Compare commits

...

189 Commits
v2.0 ... v3.0.1

Author SHA1 Message Date
Iain Collins
3474d3e250 Add object type to updateSession save
This isn't technically required (it is working fine currently) but if we specify the type explicitly it should help avoid any problems in future that might be introduced with refactoring.
2020-07-28 09:41:06 +01:00
Iain Collins
a35c3a424c Fix problem updating user in TypeORM adapter
Resolves #493
2020-07-28 09:41:06 +01:00
Brady Caspar
6e65ba87a6 Removing localhost from link 2020-07-27 19:53:34 +01:00
Iain Collins
ae7247f14f Update options.md 2020-07-27 06:28:58 +01:00
Iain Collins
12a5d6b1f4 Fix linter errors 2020-07-27 05:32:01 +01:00
Iain Collins
19da066b04 Bump version number to 3.0.0 2020-07-27 05:22:15 +01:00
Iain Collins
8115a7c66c Add option to get raw JWT from getToken helper 2020-07-27 05:20:34 +01:00
Ben Fox
1ab029c60a Update local environment setup steps to fix npm link problems per issue #472 2020-07-27 05:20:34 +01:00
Iain Collins
d0dbacfc4b Display some error messages on the sign in page
Improves the UX by displaying some error messages on the sign in page
2020-07-27 05:20:34 +01:00
Diego Borges
f6b7e0aad9 Clarify script's intent 2020-07-27 05:20:34 +01:00
Iain Collins
4a23f88180 Add option to reject signIn/authorize callbacks 2020-07-27 05:20:34 +01:00
Iain Collins
f8dbd67a16 Improve homepage 2020-07-27 05:20:34 +01:00
Iain Collins
9406f8b332 Improve callbacks by adding User object to calls 2020-07-27 05:20:34 +01:00
Iain Collins
b0410ed9d4 Improvements to documentation 2020-07-27 05:20:34 +01:00
Iain Collins
8c0d0c4dea Update contributors page 2020-07-27 05:20:34 +01:00
Iain Collins
9446c26419 Improve documentation
* CSS Refactor to make it easier to maintain and UI more consistant.
* Misc small updates to docs
* Split off tutorials into a seperate menu item

I would like to add more tutorials and explainers, including those people have written on other sites.

This is a starting point for that.
2020-07-27 05:20:34 +01:00
Iain Collins
cdfa6008c7 Improve contast on links in docs 2020-07-27 05:20:34 +01:00
Iain Collins
b4bb8bda26 Hotfix for email_verified bug
Not being saved by default on sign in. Discovered in #477
2020-07-27 05:20:34 +01:00
Iain Collins
2c32504cc9 Bump version to 3.0.0-beta.21
Resolves #477 by fixing issue with last build being screwy
2020-07-27 05:20:34 +01:00
Iain Collins
bd188ff410 Improve examples and documentation 2020-07-27 05:20:34 +01:00
Iain Collins
89aedb1285 Fix contrast issues with sidebar menu 2020-07-27 05:20:34 +01:00
Iain Collins
db8c0820b6 Fix typo in docs 2020-07-27 05:20:34 +01:00
Iain Collins
d86464c822 Improve adapter documentation 2020-07-27 05:20:34 +01:00
Iain Collins
fcfeb0ce88 Update database documentation 2020-07-27 05:20:34 +01:00
Iain Collins
01e472912e Update MongoDB test (minor update) 2020-07-27 05:20:34 +01:00
Iain Collins
bbfeac408e Document SQL for MySQL and Postgres
These match up exactly with the models generated by TypeORM in v3 and are suitable for use with Prisma.
2020-07-27 05:20:34 +01:00
Iain Collins
364de1fc6c Update MySQL Model in TypeORM to use TIMESTAMP(6)
An issue with the defaults for MySQL used by TypeORM Adapter has been highlighted during testing parity with the Prisma Adapter.

This change ensures *all* TIMESTAMP columns use TIMESTAMP(6) to store six digits of precision after the number of seconds.

While this is level of precision is not required everywhere it ensures all timestamps in the default models use the same configuration (instead of a mix of values) and is consisitant with the level of precision on timestamps used Postgres.
2020-07-27 05:20:34 +01:00
Iain Collins
52af06cd33 Add Prisma client to optional peer dependencies
This doesn't technically do anything (except for the mongodb peer dependancy, which is invoked when a mongodb is being used) but it provides a way for us to indicate and track the last known good versions of database clients for NextAuth.js.
2020-07-27 05:20:34 +01:00
Iain Collins
8f472c5987 Prisma adapter refactor 2020-07-27 05:20:34 +01:00
Iain Collins
dcbd7a6703 Improve TypeORM adapter (#460)
* Uses `require_optional` and `peerOptionalDependencies` instead of dynamic import to resolve issue some users have experience with using using compliers/bundlers (especially on starter projects) that don't handle dynamic imports well.

This should (hopefully) also make it easier to support older versions of Internet Explorer by avoiding bundlers that choke on dynamic imports unless MongoDB is included as a dependancy (even though it's not code they need to compile).

We use `require_optional` to load `ObjectID` conditionally, if NextAuth.js is using MongoDB. This is also exactly how the MongoDB driver itself loads the ObjectID from the `bson/bson-ext` module.

Should resolve #251
    
* The default name for the TypeORM connection is now 'nextauth' instead of 'default'.

This should help people avoid problems with connection re-use when not using serverless (including in local development), especially if they are doing things with their default connection that differ from whats expected by NextAuth.js (like not using UTF-8 for encoding or UTC timezones).

* Now uses connection manager object from the connection, to allow a custom TypeORM connection name to be specified (resolves #459).
2020-07-27 05:20:34 +01:00
Iain Collins
e6fd4c2edc Improve sidebar apperance 2020-07-27 05:20:34 +01:00
Iain Collins
e19ca19a82 Add tips to provider documentation 2020-07-27 05:20:34 +01:00
Iain Collins
7b1b68e1c4 Fix typos in tutorial code 2020-07-27 05:20:34 +01:00
Iain Collins
56d848c868 Fix return type of sign in callback in docs 2020-07-27 05:20:34 +01:00
Timo Mämecke
100eece7a2 docs: Fix typos and wording in Client API (#455)
While reading through the new v3 docs, I spotted a few typos and some convoluted wording. Hence I directly fixed them.
2020-07-27 05:20:34 +01:00
Ty Lange-Smith
278ecc1e48 Explicitly set expires property for session on updateSession 2020-07-27 05:20:34 +01:00
Fredrik Pettersen
a3d379554b fix(prisma): Explicitly set fields to use when updating user (#449) 2020-07-27 05:20:34 +01:00
Iain Collins
983dd98a66 Fix typo in docs 2020-07-27 05:20:34 +01:00
Iain Collins
ca3f26b8d2 Update configuration docs 2020-07-27 05:20:34 +01:00
Iain Collins
d2a2352e9a Update callback docs
Addresses issue raised in comments on #429
2020-07-27 05:20:34 +01:00
Iain Collins
3043a9525a Update documentation for client methods 2020-07-27 05:20:34 +01:00
Iain Collins
e1c6632b6f Fix typo on homepage 2020-07-27 05:20:34 +01:00
Iain Collins
56e64e322e Move help menu on mobile (again)
So many UX issue with this. Hard to wrangle it given where it is semantically.

Hopefully Docusuarus beta will improve on it in a future release.
2020-07-27 05:20:34 +01:00
Iain Collins
cbd056f225 Fix typo in tutorial 2020-07-27 05:20:34 +01:00
Iain Collins
22ab66f9d8 Cosmetic improvements to docs 2020-07-27 05:20:34 +01:00
Iain Collins
3597733dae Improve FAQ documentation 2020-07-27 05:20:34 +01:00
Iain Collins
cb9ce69ba3 Update JWT questions in FAQ 2020-07-27 05:20:34 +01:00
Iain Collins
c19a79cbca Update database docs 2020-07-27 05:20:34 +01:00
Iain Collins
e97e090b65 Improve heading formatting on docs 2020-07-27 05:20:34 +01:00
Iain Collins
eda4a6d18b Add tutorial showing how to protect pages & routes 2020-07-27 05:20:34 +01:00
Iain Collins
94f66b60d8 Update documentation 2020-07-27 05:20:34 +01:00
Iain Collins
9a85e27c0c Update README 2020-07-27 05:20:34 +01:00
Iain Collins
7fb7e3d1bc Update documentation 2020-07-27 05:20:34 +01:00
Iain Collins
90066fdbec Update homepage copy and package description 2020-07-27 05:20:34 +01:00
Iain Collins
475f0e7b51 Update documentation 2020-07-27 05:20:34 +01:00
Iain Collins
a9131724d6 Update copy on homepage 2020-07-27 05:20:34 +01:00
Iain Collins
55bfb6d9dc Update docs 2020-07-27 05:20:34 +01:00
Iain Collins
af3da3abf8 Fix linting errors 2020-07-27 05:20:34 +01:00
Iain Collins
339d9f2d03 CSS tweaks 2020-07-27 05:20:34 +01:00
Iain Collins
a24fb8b380 Update JWT documentation and FAQs 2020-07-27 05:20:34 +01:00
Iain Collins
65319e3927 Update JWT defaults
* Set encryption: false  by default
 * Use 64 bit input for generated signing key
2020-07-27 05:20:34 +01:00
Iain Collins
19917972ef Review JWT comments; enable zip encoding 2020-07-27 05:20:34 +01:00
Iain Collins
c1b412814a WIP refactor JWT based on feedback 2020-07-27 05:20:34 +01:00
Iain Collins
53ea8407ea Remove default iss check (makes it optional) 2020-07-27 05:20:34 +01:00
Iain Collins
66f46e8cc7 Use URL to ensure secret is unique per instance 2020-07-27 05:20:34 +01:00
Iain Collins
fec69a21be Refactor JWT payload to use claims
Resovles #224
2020-07-27 05:20:34 +01:00
Iain Collins
505ebb8ae1 Clean up cruft in JWT class; add comments 2020-07-27 05:20:34 +01:00
Iain Collins
fb4381d8eb Implement JWE 2020-07-27 05:20:34 +01:00
Iain Collins
4772f5b571 WIP evaluating JWE solutions 2020-07-27 05:20:34 +01:00
Iain Collins
481db425d6 WIP Add JWE
Working implementation (with limited key length and no exp check) using node-jose from Cisco.

I want to compare it panva/jose which has more features before building it out.
2020-07-27 05:20:34 +01:00
Iain Collins
b886729bb8 Update version to 3.0.0-beta.18 2020-07-27 05:20:34 +01:00
Iain Collins
3a21a9c9f1 Enforce HMAC-256 on JWT
Now that we are going to expose the option to disable encryption on tokens we need to enforce the algorithm is valid (e.g. not 'None' or 'RSA') to prevent vultrabilties being exploited by tampering with the token.

Custom encode/decode routines can be specified if someone needs to use another algorithm.
2020-07-27 05:20:34 +01:00
Iain Collins
9e4a6fec59 Update JWT and session docs 2020-07-27 05:20:34 +01:00
Iain Collins
86921022dc Refactor JWT support
* Adds option to disable encryption
* Easy to add custom helper
* Removed getJWT helper
* Added getToken helper
* Helper does not fallback to trying non-prefixed cookie on HTTPS sites
* Supports bearer tokens in HTTP header on helper #397
2020-07-27 05:20:34 +01:00
Iain Collins
f57f11e6ff Bump version to 3.0.0-beta.17 2020-07-27 05:20:34 +01:00
Iain Collins
77ad6bd97e Update FAQ 2020-07-27 05:20:34 +01:00
Iain Collins
78c7041b3f Improve docs site on mobile 2020-07-27 05:20:34 +01:00
Iain Collins
99edead0f2 Add FAQ 2020-07-27 05:20:34 +01:00
Iain Collins
b0b3dbc0fc Add provider icons to homepage 2020-07-27 05:20:34 +01:00
Iain Collins
8b5af54e1c Update documentation 2020-07-27 05:20:34 +01:00
Iain Collins
0b5b04a22f Apply datetime transforms on properties in custom models
It makes sense to change this behaviour now we have a tutorial and have been testing this functionality.

Docs are being updated to reflect this change.
2020-07-27 05:20:34 +01:00
Iain Collins
890be1de0d Update email provider 2020-07-27 05:20:34 +01:00
Iain Collins
40ae747bc1 Add support for passing appContext to getCsrfToken
Requested in #345

getSession() already does this so seems reasonable to support it in getCsrfToken too.
2020-07-27 05:20:34 +01:00
Iain Collins
5a8022e9a2 Update homepage and refactor CSS
Making an attempt to clean up some of crusty CSS I've added.
2020-07-27 05:20:34 +01:00
Iain Collins
3e512b5cf5 Tweak CSS on homepage 2020-07-27 05:20:34 +01:00
Iain Collins
81071d7776 Update adapters documentation 2020-07-27 05:20:34 +01:00
Iain Collins
fc05140c1f Improve homepage 2020-07-27 05:20:34 +01:00
Iain Collins
faec6824ba Disable use of state on Apple provider
It is not supported by Apple ID.
2020-07-27 05:20:34 +01:00
Iain Collins
b91bfef16d Refactor and document state provider option 2020-07-27 05:20:34 +01:00
Iain Collins
ba9dc17e44 Update homepage 2020-07-27 05:20:34 +01:00
Iain Collins
c220bcc57e Update version to 3.0.0-beta.13 2020-07-27 05:20:34 +01:00
Iain Collins
f8a4808aa7 Fix bug with NEXTAUTH_URL parsing 2020-07-27 05:20:34 +01:00
ndo@$(hostname)
495d0a47db fix: marquee icons 2020-07-27 05:20:34 +01:00
Iain Collins
8cda627fe6 Update adapter documentation 2020-07-27 05:20:34 +01:00
Iain Collins
d0a0ccc6bc Update TypeORM tutorial 2020-07-27 05:20:34 +01:00
Iain Collins
999222cd97 Refactor to simplify site URL configuration
Includes some linter fixes
2020-07-27 05:20:34 +01:00
Iain Collins
72eb7fda3f Fix error merging branches for v3
Accidentally squashed a couple of lines in OAuth callback.
2020-07-27 05:20:34 +01:00
Iain Collins
3c94940ae6 Respect existing cookies on a request object
Unproven, but should fix #395 and improve middleware compatibility.
2020-07-27 05:20:34 +01:00
Iain Collins
1a8ed2aec1 Update version to 3.0.0-beta.9 2020-07-27 05:20:34 +01:00
Iain Collins
0e2321dc14 Update pages documentation 2020-07-27 05:20:34 +01:00
Iain Collins
78d1983f9a Update version to 3.0.0-beta.8 2020-07-27 05:20:34 +01:00
Iain Collins
5435df110c Fix linter errors 2020-07-27 05:20:34 +01:00
Iain Collins
32853b8d1e Update events, callbacks & pages to use camelCase
* This is a breaking change in v3
* Includes updated documentation
2020-07-27 05:20:34 +01:00
Iain Collins
9737b4c6ab Only invoke setTimeout client side
This should never be called server side, but just in case someone calls setOptions server side this prevents it from being invoked at all.
2020-07-27 05:20:34 +01:00
Iain Collins
e9bdd5c355 Improve client event handling
Improves how well syncing client state is handled and how well caching is leveraged.

Reduces network load, cpu load and memory footprint.
2020-07-27 05:20:34 +01:00
Iain Collins
9728567296 Improve client state syncing
* clientMaxAge now passive
* clientPollInterval added (works like old clientMaxAge)
* poll intervals uses timer (more efficent)
* updates state on window focus/blur
2020-07-27 05:20:34 +01:00
Iain Collins
ef6579a7ee Refactor redirect handling (WIP)
Passing a redirect function like this is a bit horrible, but is less horrible than before.
2020-07-27 05:20:34 +01:00
Iain Collins
8e810aa765 Fix linting errors and bug in getCsrfToken 2020-07-27 05:20:34 +01:00
Iain Collins
37596edf2b Improve CSRF security for all routes
Includes breaking changes for v3 and updates to documentation.

If using the client, the only required change should be setting the NEXTAUTH_URL environment variable.
2020-07-27 05:20:34 +01:00
tmayr@tmayr.com
229a3e430e Add tutorial on how to use custom typeorm models 2020-07-27 05:20:34 +01:00
Nico Domino
1d80f595c5 Add provider Vercel-style marquee to docs
* add: marquee provider section
* fix: lint
* update: adjust node sizes
* fix: window undefined SSR
* fix: path to imgs

Co-authored-by: Iain Collins <me@iaincollins.com>
2020-07-27 05:20:34 +01:00
Iain Collins
189a2c8e0e Fix for reading private key in Apple provider 2020-07-27 05:20:34 +01:00
Iain Collins
97096fb811 Fix linter errors and add comments 2020-07-27 05:20:34 +01:00
Gerald Nolan
e8b75e40b1 feat: Added UserData to ProfileData after return from Apple to get user name on first sign in 2020-07-27 05:20:34 +01:00
Iain Collins
d41c38e002 Add support for hitting cancel if using token id
When using a provider that uses Token ID option (like Apple) a user hitting cancel with no longer cause the app to crash.

Users who do this will now be taken back to the sign in page.

This was already working for other providers that didn't use this option but wasn't supported for providers that did use it.
2020-07-27 05:20:34 +01:00
Fredrik Pettersen
966bc7b433 docs(prisma): Add note about model names and set email to optional 2020-07-27 05:20:34 +01:00
Fredrik Pettersen
e7b06d3362 fix(prisma): Make sure provider id is a string 2020-07-27 05:20:34 +01:00
Fredrik Pettersen
d5d8eb8d7c feat(adapter): Add opinionated prisma adapter 2020-07-27 05:20:34 +01:00
nyedidikeke
8ec07f0224 Add LinkedIn provider 2020-07-27 03:22:43 +01:00
dan-kwiat
558536db1e docs(options): remove duplicate arrow 2020-07-23 10:30:50 +01:00
Lori Karikari
0c2fe054d1 [Docs] fix small typo 2020-07-20 17:46:48 +02:00
dependabot[bot]
b5a69fd787 Bump lodash from 4.17.15 to 4.17.19 in /www
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 12:31:11 +01:00
dependabot[bot]
9b29ed347d Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-17 12:06:12 +01:00
Jibin George
c5c4ff4d51 Fix typo 2020-07-15 13:30:01 +01:00
styxlab
008b1a9f8d fallback if user.name is null (#424)
Co-authored-by: cws <cws@home.fritz.box>
2020-07-12 18:52:58 +02:00
Iain Collins
4a6f153aa6 Update question.md 2020-07-10 16:14:10 +01:00
Iain Collins
9eccc78e3a Update feature_request.md 2020-07-10 16:13:12 +01:00
Iain Collins
09938cc368 Update bug_report.md 2020-07-10 16:12:59 +01:00
Jake Harding
5db05e1031 Use id_str for reading in Twitter user ID 2020-07-09 11:07:18 +01:00
Ryuichi Okumura
f6ba72b4fa Fix wrong syntax in Apple provider example
It fixes a syntax error in Apple provider example code.
2020-07-07 19:38:44 +01:00
Jonathan Dean
bf7e555cfa Fix typo: curly brace should be square bracket 2020-07-07 19:35:36 +01:00
William Duplenne
26abc70a99 Add Spotify provider
Add Spotify to the sidebar
2020-07-07 08:35:55 +01:00
tmayr@tmayr.com
d38cd54dee Fix using merged models var instead of user provided models in props
Merged models were being overwritten by user provided models which
might come incomplete
2020-07-06 20:51:36 +01:00
felipe muner
200690ad6c Update pages.md (#389) 2020-07-05 10:58:09 +02:00
Nico Domino
52b69a6d68 Add: testing with cypress docs (#357)
* add: testing with cypress docs

* update: add tutorials group
2020-07-04 21:47:00 +02:00
Theo Gravity
f319b2af05 Fix reading of req in getSession() 2020-07-04 01:27:03 +01:00
Iain Collins
b80a005733 Update callbacks.md 2020-07-01 17:16:15 +01:00
Arunoda Susiripala
34936aecc0 Pass maxAge to the 'jwt.getJwt' function
With that it can pass that to the `jwt.decode` method. This will be useful, if we are using a custom `maxAge` value.
2020-07-01 10:46:35 +01:00
Tom Astley
b021f26f03 Update client.md (#370)
Fixed small syntax error on line 219 in the signout example. Added a '('
2020-06-30 11:56:47 +02:00
Iain Collins
fcf7197120 Fix indentation in example (trivial) 2020-06-30 10:47:29 +01:00
Iain Collins
bec8d8dff1 Update callbacks.md 2020-06-30 09:30:01 +01:00
Iain Collins
781c63e966 Update pages.md 2020-06-30 08:52:58 +01:00
Iain Collins
2da1883726 Fix typo in email.md 2020-06-30 08:52:18 +01:00
Thibaut Patel
83ffac7cd2 Fix missing closing tag in docs 2020-06-28 21:09:11 +01:00
Iain Collins
6198903cdf Update copy on homepage 2020-06-28 14:49:30 +01:00
Iain Collins
bd98f8188c Update introduction.md 2020-06-28 14:40:29 +01:00
Iain Collins
73ea402b1c Compress images
* Forgot to do this when I added them.
* May add a script to compress them on commit.
* Adding new binary blobs bloats repo over time, we can always purge old assets if it's a problem.
* The social card is slightly too bright to compensate for PNG gamma interpretation not being consistant between browsers (too bright in some looks better than too dark in others). Maybe it should be a JPEG.
2020-06-28 14:03:23 +01:00
Iain Collins
4284684a3b Improve apperance of documentation on mobile 2020-06-28 10:51:51 +01:00
Iain Collins
b5d522410a Update CSS
Additional changes following testing on mobile.
2020-06-28 10:51:51 +01:00
Iain Collins
284cb8e2a7 Improve website CSS on mobile and dark mode 2020-06-28 10:51:51 +01:00
Iain Collins
079aab2315 Improve mobile documentation secondary menu
Still not great, but somewhat better.
2020-06-28 10:51:51 +01:00
Iain Collins
645ee382cf Improve documentation structure / sidebar 2020-06-28 10:51:51 +01:00
Iain Collins
e947a772ce Website cosmetic refresh
Addressing quality issues with assets and layout
2020-06-28 10:51:51 +01:00
Iain Collins
5d63adf7df Update errors.md 2020-06-26 17:22:18 +01:00
Iain Collins
f1a872f861 Update errors.md 2020-06-26 16:39:48 +01:00
Iain Collins
02b1d02f09 Update cognito.md 2020-06-26 12:56:53 +01:00
Iain Collins
a3479b3503 Bump version to 2.2.0 2020-06-25 22:50:50 +01:00
Iain Collins
740535a8f2 Add support for mongodb+srv:// URLs 2020-06-25 22:49:56 +01:00
Iain Collins
19ed684a52 Add HTTP status codes to error pages 2020-06-25 22:47:08 +01:00
Iain Collins
bd72949fa7 Fix typos in docs 2020-06-25 22:44:31 +01:00
Iain Collins
a277cd5b0c Fix linter errors 2020-06-25 22:38:07 +01:00
Iain Collins
fd6e7e94df Update version to 2.2.0-beta.0
*  New email template
*  New callback error handling

I anticipate adding more changes and a new beta before we release 2.2.0 but wanted to test these changes.
2020-06-25 18:04:35 +01:00
Iain Collins
2f6403478d Improve apperance of sign in email
* Prevents links from being turned into hyperlinks by email clients
* Improve UI with a primary action button and better font sizing and spacing in the template
* Adds email address to body to clear indicate who they will be signing in as

While not exactly a bug in NextAuth.js it does resolve #331
2020-06-25 17:43:35 +01:00
Iain Collins
a4372ffc61 Handle OAuth sign in cancellations gracefully
Currently if a user hits a cancel button after selecting the option to sign in with an OAuth provider an error is displayed.

This error is only triggered in production.

This update refactors error handling so that in both dev and prod modes, the user is directed back to the sign in page.

Not all OAuth providers have a cancel button on their sign in page (e.g. Twitter does, Google doesn't).

The oAuthCallback has been slightly refactored to make debugging easier. It is still pretty horrible, but i don't want to do major refactoring of it until we have tests we trust in place.
2020-06-25 17:37:17 +01:00
Iain Collins
d6ce92811e Update documentation for credentials provider
There was a typo in the documentation and some of the documentation was outdated.
2020-06-25 02:35:43 +01:00
Sreetam Das
e5aecdf315 fix link in client API docs (#323) 2020-06-24 12:33:27 +02:00
Iain Collins
6d1c457a75 Add action to run npm test on new pull requests 2020-06-24 02:23:48 +01:00
Iain Collins
6e16aec6d3 Update test to run linter
The action to publish to NPM fails as it can't run the DB test yet so removing that.

Changing the test to run the linter instead so it does something (e.g. catch the worst syntax errors).
2020-06-24 02:13:02 +01:00
Iain Collins
f899d7bb04 Update version to 2.1.0 2020-06-24 01:57:28 +01:00
Iain Collins
e36646ce7f Update docs formatting 2020-06-24 01:53:49 +01:00
Iain Collins
f3d36a74c9 Update provider documentation 2020-06-24 01:53:49 +01:00
Iain Collins
4e11c9c36e Fix linter errors 2020-06-24 01:26:07 +01:00
Iain Collins
0a7ac36584 Update client documentation 2020-06-24 01:26:07 +01:00
Iain Collins
fc4850f354 Update documentation for client and options 2020-06-24 01:26:07 +01:00
Iain Collins
6e9a8d2074 Update beta version 2020-06-24 01:26:07 +01:00
Iain Collins
c712d7da07 Remove base URL cookie; no longer needed 2020-06-24 01:26:07 +01:00
Iain Collins
5183181d1c Refactor client to allow provider to be passed options 2020-06-24 01:26:07 +01:00
Iain Collins
b024f89ba8 Update client max age documentation 2020-06-24 01:26:07 +01:00
Iain Collins
fbbe516b9a Refactor client to take configuraiton from _app.js 2020-06-24 01:26:07 +01:00
Iain Collins
d48a3fd948 Rename internal debug env var for consistancy
This is a naughty global used server side to more easily facilitate debugging.
2020-06-24 01:26:07 +01:00
ndo@$(hostname)
86f0c53bd3 add: npm publish workflow 2020-06-24 01:19:51 +01:00
Onur
7f6cc2048b Minor typo fix 2020-06-24 00:00:09 +01:00
Lori Karikari
b2829f6384 added missing comma to homepage example 2020-06-23 18:33:59 +02:00
Iain Collins
67c5041860 Remove console.log from auth0 provider 2020-06-23 13:00:25 +01:00
Lori Karikari
33df9e3132 updated Cognito (#307) 2020-06-23 11:40:18 +02:00
Lori Karikari
602bc28a45 Add new OAuth providers (#221) 2020-06-23 11:31:18 +02:00
jose-donato
5a7a494701 typo 2020-06-23 00:34:44 +01:00
Iain Collins
9fa82cedbd Fix Auth0 provider
Resolves #301
2020-06-23 00:13:55 +01:00
Iain Collins
b0408284b8 Tweak homepage CSS 2020-06-21 17:30:43 +01:00
143 changed files with 5152 additions and 2409 deletions

View File

@@ -1,6 +1,6 @@
---
name: Bug report
about: Report a defect with the software
about: Report a defect with NextAuth.js
labels: bug
assignees: ''
---

View File

@@ -1,6 +1,6 @@
---
name: Feature request
about: Suggest an idea for this project
about: Suggest an idea for NextAuth.js
labels: enhancement
assignees: ''
---

View File

@@ -1,6 +1,6 @@
---
name: Question
about: Ask for information or support
about: Ask a question about NextAuth.js or for help using it
labels: question
assignees: ''
---

29
.github/workflows/node.js.yml vendored Normal file
View File

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

36
.github/workflows/npm-publish.yml vendored Normal file
View File

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

7
.gitignore vendored
View File

@@ -25,4 +25,9 @@ yarn-debug.log*
yarn-error.log*
# Docusaurus
www/build
www/build
#VS
/.vs/slnx.sqlite-journal
/.vs/slnx.sqlite
/.vs

View File

@@ -57,17 +57,18 @@ A quick and dirty guide on how to setup *next-auth* locally to work on it and te
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:
3. Link your project back to your local copy of next auth:
cd ../your-application
npm link ../next-auth
4. Finally link React between the repo and the version installed in your project:
cd ../next-auth
npm link ../your-application/node_modules/react
*This is an annoying step and not obvious, but is needed because of how React has been written (otherwise React crashes when you try to use the `useSession()` hook in your project).*
That's it!
Notes: You may need to repeat both `npm link` steps if you install / update additional dependancies with `npm i`.

View File

@@ -14,27 +14,38 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
## Features
### Authentication
### Flexible and easy to use
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Built-in support for [many popular OAuth sign-in services](https://next-auth.js.org/configuration/providers)
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
* Supports email / passwordless authentication
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
* Supports both JSON Web Tokens and database sessions
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/database)
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used without a database (e.g. OAuth + JWT)
* 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
* Promotes the use of passwordless sign in mechanisms
* Designed to be secure by default and encourage best practice for safeguarding user data
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
* Auto-generates symmetric signing and encryption keys for developer convenience
* Features tab/window syncing and keepalive messages to support short lived sessions
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
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.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
## Example
@@ -45,7 +56,6 @@ import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
site: 'https://example.com'
providers: [
// OAuth authentication providers
Providers.Apple({
@@ -97,7 +107,7 @@ export default () => {
## Acknowledgement
[NextAuth.js 2.0 is possible thanks to its contributors.](https://next-auth.js.org/contributors)
[NextAuth.js is possible thanks to its contributors.](https://next-auth.js.org/contributors)
## Getting started

96
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "2.0.0",
"version": "3.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1041,6 +1041,11 @@
"fastq": "^1.6.0"
}
},
"@panva/asn1.js": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
},
"@types/color-name": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
@@ -1334,6 +1339,11 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
},
"base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
},
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
@@ -1413,6 +1423,14 @@
}
}
},
"browserify-zlib": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
"requires": {
"pako": "~1.0.5"
}
},
"browserslist": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz",
@@ -2253,6 +2271,11 @@
"is-symbol": "^1.0.2"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -3573,6 +3596,11 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true
},
"futoin-hkdf": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.2.tgz",
"integrity": "sha512-3EVi3ETTyJg5PSXlxLCaUVVn0pSbDf62L3Gwxne7Uq+d8adOSNWQAad4gg7WToHkcgnCJb3Wlb1P8r4Evj4GPw=="
},
"gensync": {
"version": "1.0.0-beta.1",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
@@ -4187,6 +4215,14 @@
"dev": true,
"optional": true
},
"jose": {
"version": "1.27.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-1.27.2.tgz",
"integrity": "sha512-zLIwnMa8dh5A2jFo56KvhiXCaW0hFjdNvG0I5GScL8Wro+/r/SnyIYTbnX3fYztPNSfgQp56sDMHUuS9c3e6bw==",
"requires": {
"@panva/asn1.js": "^1.0.0"
}
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@@ -4360,10 +4396,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash.includes": {
"version": "4.3.0",
@@ -4421,6 +4456,11 @@
"chalk": "^2.0.1"
}
},
"long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4642,6 +4682,28 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
"dev": true
},
"node-forge": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
"integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
},
"node-jose": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-1.1.4.tgz",
"integrity": "sha512-L31IFwL3pWWcMHxxidCY51ezqrDXMkvlT/5pLTfNw5sXmmOLJuN6ug7txzF/iuZN55cRpyOmoJrotwBQIoo5Lw==",
"requires": {
"base64url": "^3.0.1",
"browserify-zlib": "^0.2.0",
"buffer": "^5.5.0",
"es6-promise": "^4.2.8",
"lodash": "^4.17.15",
"long": "^4.0.0",
"node-forge": "^0.8.5",
"process": "^0.11.10",
"react-zlib-js": "^1.0.4",
"uuid": "^3.3.3"
}
},
"node-releases": {
"version": "1.1.53",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
@@ -4896,6 +4958,11 @@
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
"dev": true
},
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -6061,6 +6128,11 @@
"integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
"dev": true
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -6107,6 +6179,11 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
},
"react-zlib-js": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/react-zlib-js/-/react-zlib-js-1.0.4.tgz",
"integrity": "sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A=="
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -6367,7 +6444,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
"dev": true,
"requires": {
"resolve-from": "^2.0.0",
"semver": "^5.1.0"
@@ -6376,8 +6452,7 @@
"resolve-from": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
"dev": true
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
}
}
},
@@ -7384,6 +7459,11 @@
"object.getownpropertydescriptors": "^2.1.0"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"v8-compile-cache": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",

View File

@@ -1,8 +1,9 @@
{
"name": "next-auth",
"version": "2.0.0",
"description": "An authentication library for Next.js",
"repository": "https://github.com/iaincollins/next-auth.git",
"version": "3.0.1",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
"author": "Iain Collins <me@iaincollins.com>",
"main": "index.js",
"scripts": {
@@ -12,13 +13,19 @@
"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": "npm run lint",
"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:start:mongo": "docker-compose -f test/docker/mongo.yml up -d",
"db:start:mysql": "docker-compose -f test/docker/mysql.yml up -d",
"db:start:postgres": "docker-compose -f test/docker/postgres.yml up -d",
"db:stop": "docker-compose -f test/docker/docker-compose.yml down",
"db:stop:mongo": "docker-compose -f test/docker/mongo.yml down",
"db:stop:mysql": "docker-compose -f test/docker/mysql.yml down",
"db:stop:postgres": "docker-compose -f test/docker/postgres.yml down",
"prepublishOnly": "npm run build",
"publish:beta": "npm publish --tag beta",
"publish:canary": "npm publish --tag canary",
@@ -36,19 +43,29 @@
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"jwt-decode": "^2.2.0",
"node-jose": "^1.1.4",
"nodemailer": "^6.4.6",
"oauth": "^0.9.15",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.7",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.24"
},
"peerDependencies": {
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"pg": "^8.2.1",
"@prisma/client": "^2.3.0"
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",

View File

@@ -1,4 +1,4 @@
// Serverless target in Next.js does work if you try to read in files at runtime
// Serverless target in Next.js does not work if you try to read in files at runtime
// that are not JavaScript or JSON (e.g. CSS files).
// https://github.com/iaincollins/next-auth/issues/281
//
@@ -15,4 +15,4 @@ const css = fs.readFileSync(pathToCss, 'utf8')
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
fs.writeFileSync(pathToCssJs, js)
fs.writeFileSync(pathToCssJs, js)

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import { createConnection, getConnection, getManager } from 'typeorm'
import { createConnection, getConnection } from 'typeorm'
import { createHash } from 'crypto'
import require_optional from 'require_optional' // eslint-disable-line camelcase
import { CreateUserError } from '../../lib/errors'
import adapterConfig from './lib/config'
@@ -28,7 +29,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
// anything to do them). This function updates arguments by reference.
adapterTransform(typeOrmConfigObject, models, options)
const config = adapterConfig.loadConfig(typeOrmConfigObject, { models, ...options })
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
// Create objects from models that can be consumed by functions in the adapter
const User = models.User.model
@@ -67,12 +68,14 @@ const Adapter = (typeOrmConfig, options = {}) => {
await _connect()
}
// Get manager from connection object
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
const { manager } = connection
// 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)
}
function debug (debugCode, ...args) {
logger.debug(`TYPEORM_${debugCode}`, ...args)
}
// The models are primarily designed for ANSI SQL database, but some
@@ -86,7 +89,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
let ObjectId
if (config.type === 'mongodb') {
idKey = '_id'
const mongodb = await import('mongodb')
// Using a dynamic import causes problems for some compilers/bundlers
// that don't handle dynamic imports. To try and work around this we are
// using the same method mongodb uses to load Object ID type, which is to
// use the require_optional loader.
const mongodb = require_optional('mongodb')
ObjectId = mongodb.ObjectId
}
@@ -96,7 +103,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
// 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')
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
}
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
@@ -107,11 +114,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
: 0
async function createUser (profile) {
debugMessage('CREATE_USER', profile)
debug('CREATE_USER', profile)
try {
// Create user account
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
return await getManager().save(user)
return await manager.save(user)
} catch (error) {
logger.error('CREATE_USER_ERROR', error)
return Promise.reject(new CreateUserError(error))
@@ -119,7 +126,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function getUser (id) {
debugMessage('GET_USER', id)
debug('GET_USER', id)
// In the very specific case of both using JWT for storing session data
// and using MongoDB to store user data, the ID is a string rather than
@@ -132,7 +139,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
try {
return connection.getRepository(User).findOne({ [idKey]: id })
return manager.findOne(User, { [idKey]: id })
} catch (error) {
logger.error('GET_USER_BY_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
@@ -140,10 +147,10 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function getUserByEmail (email) {
debugMessage('GET_USER_BY_EMAIL', email)
debug('GET_USER_BY_EMAIL', email)
try {
if (!email) { return Promise.resolve(null) }
return connection.getRepository(User).findOne({ email })
return manager.findOne(User, { email })
} catch (error) {
logger.error('GET_USER_BY_EMAIL_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
@@ -151,11 +158,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function getUserByProviderAccountId (providerId, providerAccountId) {
debugMessage('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
try {
const account = await connection.getRepository(Account).findOne({ providerId, providerAccountId })
const account = await manager.findOne(Account, { providerId, providerAccountId })
if (!account) { return null }
return connection.getRepository(User).findOne({ [idKey]: account.userId })
return manager.findOne(User, { [idKey]: account.userId })
} catch (error) {
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
@@ -163,22 +170,22 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function updateUser (user) {
debugMessage('UPDATE_USER', user)
return getManager().save(user)
debug('UPDATE_USER', user)
return manager.save(User, user)
}
async function deleteUser (userId) {
debugMessage('DELETE_USER', userId)
debug('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)
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
try {
// Create provider account linked to user
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
return getManager().save(account)
return manager.save(account)
} catch (error) {
logger.error('LINK_ACCOUNT_ERROR', error)
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
@@ -186,7 +193,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function unlinkAccount (userId, providerId, providerAccountId) {
debugMessage('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
// @TODO Get current user from DB
// @TODO Delete [provider] object from user object
// @TODO Save changes to user object in DB
@@ -194,7 +201,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function createSession (user) {
debugMessage('CREATE_SESSION', user)
debug('CREATE_SESSION', user)
try {
let expires = null
if (sessionMaxAge) {
@@ -205,7 +212,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
const session = new Session(user.id, expires)
return getManager().save(session)
return manager.save(session)
} catch (error) {
logger.error('CREATE_SESSION_ERROR', error)
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
@@ -213,9 +220,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function getSession (sessionToken) {
debugMessage('GET_SESSION', sessionToken)
debug('GET_SESSION', sessionToken)
try {
const session = await connection.getRepository(Session).findOne({ sessionToken })
const session = await manager.findOne(Session, { sessionToken })
// Check session has not expired (do not return it if it has)
if (session && session.expires && new Date() > new Date(session.expires)) {
@@ -231,7 +238,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function updateSession (session, force) {
debugMessage('UPDATE_SESSION', session)
debug('UPDATE_SESSION', session)
try {
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
// Calculate last updated date, to throttle write updates to database
@@ -259,7 +266,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
if (!force) { return null }
}
return getManager().save(session)
return manager.save(Session, session)
} catch (error) {
logger.error('UPDATE_SESSION_ERROR', error)
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
@@ -267,9 +274,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function deleteSession (sessionToken) {
debugMessage('DELETE_SESSION', sessionToken)
debug('DELETE_SESSION', sessionToken)
try {
return await connection.getRepository(Session).delete({ sessionToken })
return await manager.delete(Session, { sessionToken })
} catch (error) {
logger.error('DELETE_SESSION_ERROR', error)
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
@@ -277,9 +284,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function createVerificationRequest (identifier, url, token, secret, provider) {
debugMessage('CREATE_VERIFICATION_REQUEST', identifier)
debug('CREATE_VERIFICATION_REQUEST', identifier)
try {
const { site } = appOptions
const { baseUrl } = appOptions
const { sendVerificationRequest, maxAge } = provider
// Store hashed token (using secret as salt) so that tokens cannot be exploited
@@ -296,11 +303,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
// Save to database
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
const verificationRequest = await getManager().save(newVerificationRequest)
const verificationRequest = await manager.save(newVerificationRequest)
// With the verificationCallback on a provider, you can send an email, or queue
// an email to be sent, or perform some other action (e.g. send a text message)
await sendVerificationRequest({ identifier, url, token, site, provider })
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
return verificationRequest
} catch (error) {
@@ -310,16 +317,16 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function getVerificationRequest (identifier, token, secret, provider) {
debugMessage('GET_VERIFICATION_REQUEST', identifier, token)
debug('GET_VERIFICATION_REQUEST', identifier, token)
try {
// Hash token provided with secret before trying to match it with database
// @TODO Use bcrypt instead of salted SHA-256 hash for token
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
const verificationRequest = await connection.getRepository(VerificationRequest).findOne({ identifier, token: hashedToken })
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
// Delete verification entry so it cannot be used again
await connection.getRepository(VerificationRequest).delete({ token: hashedToken })
await manager.delete(VerificationRequest, { token: hashedToken })
return null
}
@@ -331,11 +338,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
}
async function deleteVerificationRequest (identifier, token, secret, provider) {
debugMessage('DELETE_VERIFICATION', identifier, token)
debug('DELETE_VERIFICATION', identifier, token)
try {
// Delete verification entry so it cannot be used again
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
await connection.getRepository(VerificationRequest).delete({ token: hashedToken })
await manager.delete(VerificationRequest, { token: hashedToken })
} catch (error) {
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))

View File

@@ -7,19 +7,30 @@ const parseConnectionString = (configString) => {
// 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..
// not for all databases (e.g. SQLite) or for all options, so we handle
// parsing it in this function.
try {
const parsedUrl = new URL(configString)
const config = {}
// 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.protocol.startsWith('mongodb+srv')) {
// Special case handling is required for mongodb+srv with TypeORM
config.type = 'mongodb'
config.url = configString.replace(/\?(.*)$/, '')
config.useNewUrlParser = true
} else {
config.type = parsedUrl.protocol.replace(/:$/, '')
config.host = parsedUrl.hostname
config.port = Number(parsedUrl.port)
config.username = parsedUrl.username
config.password = parsedUrl.password
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
}
// This option is recommended by mongodb
if (config.type === 'mongodb') {
config.useUnifiedTopology = true
}
if (parsedUrl.search) {
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
@@ -42,7 +53,7 @@ const parseConnectionString = (configString) => {
const loadConfig = (config, { models, namingStrategy }) => {
const defaultConfig = {
name: 'default',
name: 'nextauth',
autoLoadEntities: true,
entities: [
new EntitySchema(models.User.schema),

View File

@@ -1,51 +1,45 @@
// Perform transforms on SQL models so they can be used with other databases
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
const postgres = (models, options) => {
const postgresTransform = (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'
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
models[model].schema.columns[column].type = 'timestamptz'
}
}
}
}
const mongodb = (models, options) => {
const mysqlTransform = (models, options) => {
// Apply snake case naming strategy for MySQL databases
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}
// For MySQL we default milisecond precision of all timestamps to 6 digits.
// This ensures all timestamp fields use the same precision (unless explictly
// configured otherwise) and that values in MySQL match those Postgress.
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
// If precision explictly set (including to null) don't change it
if (typeof models[model].schema.columns[column].precision === 'undefined') {
models[model].schema.columns[column].precision = 6
}
}
}
}
}
const mongodbTransform = (models, options) => {
// A CamelCase naming strategy is used for all document databases
if (!options.namingStrategy) {
options.namingStrategy = new CamelCaseNamingStrategy()
@@ -65,56 +59,37 @@ const mongodb = (models, options) => {
// 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']
}
]
for (const model in models) {
delete models[model].schema.columns.id.type
models[model].schema.columns.id.objectId = true
}
if (!customModels.Account) {
delete models.Account.schema.columns.id.type
models.Account.schema.columns.id.objectId = true
models.Account.schema.columns.userId.type = 'objectId'
}
// Ensure reference to User ID in other models are Object IDs
// This needs to done for any properties that reference another entity by ID
models.Account.schema.columns.userId.type = 'objectId'
models.Session.schema.columns.userId.type = 'objectId'
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
}
// The options `unique: true` and `nullable: true` don't work the same
// with MongoDB as they do with SQL databases like MySQL and Postgres,
// we need to create a sparse index to only allow unique values, while
// still allowing multiple entires to omit the email address.
delete models.User.schema.columns.email.unique
models.User.schema.indices = [
{
name: 'email',
unique: true,
sparse: true,
columns: ['email']
}
]
}
const sqlite = (models, options) => {
const sqliteTransform = (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.
//
@@ -123,51 +98,32 @@ const sqlite = (models, options) => {
//
// 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'
for (const model in models) {
for (const column in models[model].schema.columns) {
if (models[model].schema.columns[column].type === 'timestamp') {
models[model].schema.columns[column].type = 'datetime'
}
}
}
}
export default (config, models, options) => {
// @TODO Refactor into switch statement
if ((config.type && config.type.startsWith('mongodb')) ||
(config.url && config.url.startsWith('mongodb'))) {
mongodb(models, options)
mongodbTransform(models, options)
} else if ((config.type && config.type.startsWith('postgres')) ||
(config.url && config.url.startsWith('postgres'))) {
postgres(models, options)
postgresTransform(models, options)
} else if ((config.type && config.type.startsWith('mysql')) ||
(config.url && config.url.startsWith('mysql'))) {
mysqlTransform(models, options)
} else if ((config.type && config.type.startsWith('sqlite')) ||
(config.url && config.url.startsWith('sqlite'))) {
sqlite(models, options)
sqliteTransform(models, options)
} else {
// Apply snake case naming strategy by default for SQL databases
// For all other SQL databases (e.g. MySQL) apply snake case naming
// strategy, but otherwise use the models and schemas as they are.
if (!options.namingStrategy) {
options.namingStrategy = new SnakeCaseNamingStrategy()
}

View File

@@ -1,176 +1,289 @@
// fetch() is built in to Next.js 9.4 (you can use a polyfill if using an older version)
/// Note: fetch() is built in to Next.js 9.4
//
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
/* global fetch:false */
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import logger from '../lib/logger'
import parseUrl from '../lib/parse-url'
// 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.
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0, // used for timestamp since last sycned (in seconds)
_clientSyncTimer: null, // stores timer for poll interval
_eventListenersAdded: false, // tracks if event listeners have been added,
_clientSession: undefined, // stores last session response from hook,
// Generate a unique ID to make it possible to identify when a message
// was sent from this tab/window so it can be ignored to avoid event loops.
_clientId: Math.random().toString(36).substring(2) + Date.now().toString(36),
// Used to store to function export by getSession() hook
_getSession: () => {}
}
// 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
// Add event listners on load
if (typeof window !== 'undefined') {
if (__NEXTAUTH._eventListenersAdded === false) {
__NEXTAUTH._eventListenersAdded = true
let NEXTAUTH_EVENT_LISTENER_ADDED = false
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
window.addEventListener('storage', async (event) => {
if (event.key === 'nextauth.message') {
const message = JSON.parse(event.newValue)
if (message.event && message.event === 'session' && message.data) {
// Ignore storage events fired from the same window that created them
if (__NEXTAUTH._clientId === message.clientId) {
return
}
// Fetch new session data but pass 'true' to it not to fire an event to
// avoid an infinite loop.
//
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
await __NEXTAUTH._getSession({ event: 'storage' })
}
}
})
// Listen for window focus/blur events
window.addEventListener('focus', async (event) => __NEXTAUTH._getSession({ event: 'focus' }))
window.addEventListener('blur', async (event) => __NEXTAUTH._getSession({ event: 'blur' }))
}
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
const setOptions = ({
baseUrl,
basePath,
clientMaxAge,
keepAlive
} = {}) => {
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl }
if (basePath) { __NEXTAUTH.basePath = basePath }
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window !== 'undefined' && keepAlive > 0) {
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) { clearTimeout(__NEXTAUTH._clientSyncTimer) }
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (__NEXTAUTH._clientSession) {
await __NEXTAUTH._getSession({ event: 'timer' })
}
}, keepAlive * 1000)
}
}
}
// 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' } })
const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getSession() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
if (!req && ctx && ctx.req) { req = ctx.req }
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
if (triggerEvent) {
_sendMessage({ event: 'session', data: { trigger: '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)
const getCsrfToken = async ({ req, ctx } = {}) => {
// If passed 'appContext' via getInitialProps() in _app.js then get the req
// object from ctx and use that for the req value to allow getCsrfToken() to
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
if (!req && ctx && ctx.req) { req = ctx.req }
const baseUrl = _apiBaseUrl()
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
return data && data.csrfToken ? data.csrfToken : null
}
// 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
// Universal method (client + server); does not require request headers
const getProviders = async () => {
const baseUrl = _apiBaseUrl()
return _fetchData(`${baseUrl}/providers`)
}
// 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) => {
// Try to use context if we can
const value = useContext(SessionContext)
// If we have no Provider in the tree we call the actual hook for fetching the session
// If we have no Provider in the tree, call the actual hook
if (value === undefined) {
return useSessionData(session)
return _useSessionHook(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 _useSessionHook = (session) => {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(true)
const _getSession = async (sendEvent = true) => {
const _getSession = async ({ event = null } = {}) => {
try {
setData(await getSession())
const triggredByEvent = (event !== null)
const triggeredByStorageEvent = !!((event && event === 'storage'))
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = Math.floor(new Date().getTime() / 1000)
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (triggeredByStorageEvent === false && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const triggerEvent = (triggeredByStorageEvent === false)
const newClientSessionData = await getSession({ triggerEvent })
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
// 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() }, [])
__NEXTAUTH._getSession = _getSession
useEffect(() => {
_getSession()
})
return [data, loading]
}
// Client side method
const signin = async (provider, args) => {
const signIn = async (provider, args = {}) => {
const baseUrl = _apiBaseUrl()
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]) {
// Redirect to sign in page if no valid provider specified
if (!provider || !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 = {
const signInUrl = (providers[provider].type === 'credentials')
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
// If is any other provider type, POST to provider URL with CSRF Token,
// callback URL and any other parameters supplied.
const fetchOptions = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: _encodedForm({
...args,
csrfToken: await getCsrfToken(),
callbackUrl: callbackUrl,
...args
json: true
})
}
const res = await fetch(providers[provider].signinUrl, options)
window.location = res.url ? res.url : callbackUrl
const res = await fetch(signInUrl, fetchOptions)
const data = await res.json()
window.location = data.url ? data.url : callbackUrl
}
}
// Client side method
const signout = async (args) => {
const signOut = async (args = {}) => {
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
const baseUrl = _baseUrl()
const options = {
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: _encodedForm({
csrfToken: await getCsrfToken(),
callbackUrl: callbackUrl
callbackUrl: callbackUrl,
json: true
})
}
const res = await fetch(`${baseUrl}/signout`, options)
_sendMessage({ event: 'session', data: { triggeredBy: 'signout' } })
window.location = res.url ? res.url : callbackUrl
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
window.location = data.url ? data.url : callbackUrl
}
// 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 Provider = ({ children, session, options }) => {
setOptions(options)
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
}
const _fetchData = async (url, options) => {
const _fetchData = async (url, options = {}) => {
try {
const res = await fetch(url, options)
const data = await res.json()
@@ -181,42 +294,16 @@ const _fetchData = async (url, options) => {
}
}
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}`
}
}
const _apiBaseUrl = () => {
if (typeof window === 'undefined') {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }
// 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 {}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
} else {
// Return relative path when called client side
return __NEXTAUTH.basePath
}
}
@@ -228,23 +315,29 @@ const _encodedForm = (formData) => {
const _sendMessage = (message) => {
if (typeof localStorage !== 'undefined') {
localStorage.setItem('nextauth.message', JSON.stringify(message)) // eslint-disable-line
const timestamp = Math.floor(new Date().getTime() / 1000)
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // 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'
getSession,
getCsrfToken,
getProviders,
useSession,
signIn,
signOut,
Provider,
/* Deprecated / unsupported features below this line */
// Use setOptions() set options globally in the app.
setOptions,
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases.
options: setOptions,
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
getSession,
getProviders,
getCsrfToken,
useSession,
Provider,
signin,
signout
signin: signIn,
signout: signOut
}

View File

@@ -1,14 +1,18 @@
:root {
--color-background: #fff;
--color-primary: #444;
--color-control-border: #bbb;
--color-button-hover-background: #f9f9f9;
--color-button-active-background: #f5f5f5;
--color-button-active-background: #f9f9f9;
--color-button-active-border: #aaa;
--border-width: 1px;
--border-radius: .3rem;
--color-error: #c94b4b;
--color-info: #157efb;
--color-seperator: #ccc;
}
body {
background-color: var(--color-background);
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';
@@ -39,7 +43,7 @@ input[type] {
width: 100%;
padding: .5rem 1rem;
border: var(--border-width) solid var(--color-control-border);
background: #fff;
background: var(--color-background);
font-size: 1rem;
border-radius: var(--border-radius);
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
@@ -61,7 +65,7 @@ a.button {
line-height: 1rem;
&:link,
&:visited {
background-color: #fff;
background-color: var(--color-background);
color: var(--color-primary);
}
}
@@ -72,21 +76,20 @@ a.button {
padding: .75rem 1rem;
border: var(--border-width) solid var(--color-control-border);
color: var(--color-primary);
background-color: #fff;
background-color: var(--color-background);
font-size: 1rem;
border-radius: var(--border-radius);
transition: all .1s ease-in-out;
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem #fff, inset 0 -.1rem .1rem rgba(0,0,0,.05);
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.05);
font-weight: 500;
position: relative;
&:hover {
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);
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.1);
background-color: var(--color-button-active-background);
border-color: var(--color-button-active-border);
cursor: pointer;
@@ -143,19 +146,34 @@ a.site {
hr {
display: block;
border: 0;
border-top: 1px solid #ccc;
border-top: 1px solid var(--color-seperator);
margin: 1.5em auto 0 auto;
overflow: visible;
&::before {
content: "or";
background: #fff;
background: var(--color-background);
color: #888;
padding: 0 .4rem;
position: relative;
top: -.6rem;
}
}
.error {
background: #f5f5f5;
font-weight: 500;
border-radius: 0.3rem;
background: var(--color-info);
color: #fff;
p {
text-align: left;
padding: 0.5rem 1rem;
font-size: 0.9rem;
line-height: 1.2rem;
}
}
> div,
form {
display: block;

View File

@@ -7,4 +7,4 @@ import path from 'path'
const pathToCss = path.join(__dirname, '/index.css')
const css = fs.readFileSync(pathToCss, 'utf8')
export default () => css
export default () => css

View File

@@ -1,44 +1,157 @@
import jwt from 'jsonwebtoken'
import CryptoJS from 'crypto-js'
import jose from 'jose'
import hkdf from 'futoin-hkdf'
import logger from './logger'
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 }
// Set default algorithm to use for auto-generated signing key
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
// Set default algorithm for auto-generated symmetric encryption key
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
// Use encryption or not by default
const DEFAULT_ENCRYPTION_ENABLED = false
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
const encode = async ({
token = {},
maxAge = DEFAULT_MAX_AGE,
secret,
signingKey,
signingOptions = {
expiresIn: `${maxAge}s`
},
encryptionKey,
encryptionOptions = {
alg: 'dir',
enc: DEFAULT_ENCRYPTION_ALGORITHM,
zip: 'DEF'
},
encryption = DEFAULT_ENCRYPTION_ENABLED
} = {}) => {
// Signing Key
const _signingKey = (signingKey)
? jose.JWK.asKey(JSON.parse(signingKey))
: getDerivedSigningKey(secret)
// Sign token
const signedToken = jose.JWT.sign(token, _signingKey, signingOptions)
if (encryption) {
// Encryption Key
const _encryptionKey = (encryptionKey)
? jose.JWK.asKey(JSON.parse(encryptionKey))
: getDerivedEncryptionKey(secret)
// Encrypt token
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
} else {
return signedToken
}
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 }) => {
const decode = async ({
secret,
token,
maxAge = DEFAULT_MAX_AGE,
signingKey,
verificationKey = signingKey, // Optional (defaults to encryptionKey)
verificationOptions = {
maxTokenAge: `${maxAge}s`,
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
},
encryptionKey,
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
decryptionOptions = {
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
},
encryption = DEFAULT_ENCRYPTION_ENABLED
} = {}) => {
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
let tokenToVerify = token
if (encryption) {
// Encryption Key
const _encryptionKey = (decryptionKey)
? jose.JWK.asKey(JSON.parse(decryptionKey))
: getDerivedEncryptionKey(secret)
// Decrypt token
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
tokenToVerify = decryptedToken.toString('utf8')
}
// Signing Key
const _signingKey = (verificationKey)
? jose.JWK.asKey(JSON.parse(verificationKey))
: getDerivedSigningKey(secret)
// Verify token
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
}
// 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 getToken = async (args) => {
const {
req,
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
// or not set (e.g. development or test instance) case use unprefixed name
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
raw = false
} = args
if (!req) throw new Error('Must pass `req` to JWT getToken()')
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]
// Try to get token from cookie
let token = req.cookies[cookieName]
if (!cookieValue) { return null }
// If cookie not found in cookie look for bearer token in authorization header.
// This allows clients that pass through tokens in headers rather than as
// cookies to use this helper function.
if (!token && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
const urlEncodedToken = req.headers.authorization.split(' ')[1]
token = decodeURIComponent(urlEncodedToken)
}
if (raw) {
return token
}
try {
return await decode({ secret, token: cookieValue })
return await decode({ token, ...args })
} catch (error) {
return null
}
}
// Generate warning (but only once at startup) when auto-generated keys are used
let DERIVED_SIGNING_KEY_WARNING = false
let DERIVED_ENCRYPTION_KEY_WARNING = false
const getDerivedSigningKey = (secret) => {
if (!DERIVED_SIGNING_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
DERIVED_SIGNING_KEY_WARNING = true
}
const buffer = hkdf(secret, 64, { info: 'NextAuth.js Generated Signing Key', hash: 'SHA-256' })
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
return key
}
const getDerivedEncryptionKey = (secret) => {
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
DERIVED_ENCRYPTION_KEY_WARNING = true
}
const buffer = hkdf(secret, 32, { info: 'NextAuth.js Generated Encryption Key', hash: 'SHA-256' })
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
return key
}
export default {
encode,
decode,
getJwt
getToken
}

View File

@@ -1,19 +1,28 @@
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()}`
)
}
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
console.error(
`[next-auth][error][${errorCode.toLowerCase()}]`,
text,
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
)
},
warn: (warnCode, ...text) => {
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
console.warn(
`[next-auth][warn][${warnCode.toLowerCase()}]`,
text,
`\nhttps://next-auth.js.org/warning#${warnCode.toLowerCase()}`
)
},
debug: (debugCode, ...text) => {
if (process && process.env && process.env._NEXT_AUTH_DEBUG) {
if (!console) { return }
if (text && text.length <= 1) { text = text[0] || '' }
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
console.log(
`[next-auth][debug][${debugCode}]`,
`[next-auth][debug][${debugCode.toLowerCase()}]`,
text
)
}

27
src/lib/parse-url.js Normal file
View File

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

View File

@@ -12,10 +12,12 @@ export default (options) => {
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
profileUrl: null,
idToken: true,
state: false, // Apple doesn't support state verfication
profile: (profile) => {
// The name of the user will only return on first login
return {
id: profile.sub,
name: profile.name == null ? profile.sub : profile.name,
name: profile.user != null ? profile.user.name.firstName + ' ' + profile.user.name.lastName : null,
email: profile.email
}
},
@@ -35,7 +37,9 @@ export default (options) => {
aud: 'https://appleid.apple.com',
sub: appleId
},
privateKey,
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, '\n'),
{
algorithm: 'ES256',
keyid: keyId

View File

@@ -4,11 +4,11 @@ export default (options) => {
name: 'Auth0',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code', response_type: 'code' },
params: { grant_type: 'authorization_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`,
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
profileUrl: `https://${options.domain}/userinfo`,
profile: (profile) => {
return {
id: profile.sub,

View File

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

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

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

View File

@@ -22,22 +22,23 @@ export default (options) => {
}
}
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
const sendVerificationRequest = ({ identifier: email, url, baseUrl, provider }) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
const siteName = site.replace(/^https?:\/\//, '')
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
nodemailer
.createTransport(server)
.sendMail({
to: emailAddress,
to: email,
from,
subject: `Sign in to ${siteName}`,
text: text({ url, siteName }),
html: html({ url, siteName })
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
if (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
}
return resolve()
@@ -46,28 +47,55 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
}
// Email HTML body
const html = ({ url, siteName }) => {
const buttonBackgroundColor = '#444444'
const html = ({ url, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, '&#8203;.')}`
const escapedSite = `${site.replace(/\./g, '&#8203;.')}`
// Some simple styling options
const backgroundColor = '#f9f9f9'
const textColor = '#444444'
const mainBackgroundColor = '#ffffff'
const buttonBackgroundColor = '#346df1'
const buttonBorderColor = '#346df1'
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>
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong>
</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 5px;" 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: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// 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`
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`

View File

@@ -11,7 +11,7 @@ export default (options) => {
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url
}

View File

@@ -2,16 +2,20 @@ import Auth0 from './auth0'
import Apple from './apple'
import Box from './box'
import Credentials from './credentials'
import BattleNet from './battlenet'
import Cognito from './cognito'
import Discord from './discord'
import Email from './email'
import Facebook from './facebook' // @TODO
import Facebook from './facebook'
import GitHub from './github'
import GitLab from './gitlab'
import Google from './google'
import IdentityServer4 from './identity-server4'
import LinkedIn from './linkedin'
import Mixer from './mixer'
import Okta from './okta'
import Slack from './slack'
import Spotify from './spotify'
import Twitch from './twitch'
import Twitter from './twitter'
import Yandex from './yandex'
@@ -21,6 +25,8 @@ export default {
Apple,
Box,
Credentials,
BattleNet,
Cognito,
Discord,
Email,
Facebook,
@@ -28,9 +34,11 @@ export default {
GitLab,
Google,
IdentityServer4,
LinkedIn,
Mixer,
Okta,
Slack,
Spotify,
Twitter,
Twitch,
Yandex

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

@@ -0,0 +1,26 @@
export default (options) => {
return {
id: 'linkedin',
name: 'LinkedIn',
type: 'oauth',
version: '2.0',
scope: 'r_liteprofile',
params: {
grant_type: 'authorization_code',
client_id: options.clientId,
client_secret: options.clientSecret
},
accessTokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization?response_type=code',
profileUrl: 'https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName)',
profile: (profile) => {
return {
id: profile.id,
name: profile.localizedFirstName + ' ' + profile.localizedLastName,
email: null,
image: null
}
},
...options
}
}

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

@@ -0,0 +1,23 @@
export default (options) => {
return {
id: 'spotify',
name: 'Spotify',
type: 'oauth',
version: '2.0',
scope: 'user-read-email',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://accounts.spotify.com/api/token',
authorizationUrl:
'https://accounts.spotify.com/authorize?response_type=code',
profileUrl: 'https://api.spotify.com/v1/me',
profile: (profile) => {
return {
id: profile.id,
name: profile.display_name,
email: profile.email,
image: profile.images[0].url
}
},
...options
}
}

View File

@@ -8,10 +8,11 @@ export default (options) => {
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',
profileUrl:
'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
profile: (profile) => {
return {
id: profile.id,
id: profile.id_str,
name: profile.name,
email: profile.email,
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, '.jpg')

View File

@@ -1,5 +1,6 @@
import { createHash, randomBytes } from 'crypto'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import cookie from './lib/cookie'
import callbackUrlHandler from './lib/callback-url-handler'
import parseProviders from './lib/providers'
@@ -12,9 +13,13 @@ import callback from './routes/callback'
import session from './routes/session'
import pages from './pages'
import adapters from '../adapters'
import logger from '../lib/logger'
const DEFAULT_SITE = 'http://localhost:3000'
const DEFAULT_BASE_PATH = '/api/auth'
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}
export default async (req, res, userSuppliedOptions) => {
// To the best of my knowledge, we need to return a promise here
@@ -32,17 +37,17 @@ export default async (req, res, userSuppliedOptions) => {
nextauth,
action = nextauth[0],
provider = nextauth[1],
error
error = nextauth[1]
} = 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}`
// @todo refactor all existing references to site, baseUrl and basePath
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
const baseUrl = parsedUrl.baseUrl
const basePath = parsedUrl.basePath
// Parse database / adapter
let adapter
@@ -58,7 +63,7 @@ export default async (req, res, userSuppliedOptions) => {
// 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')
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
// Use secure cookies if the site uses HTTPS
// This being conditional allows cookies to work non-HTTPS development URLs
@@ -89,15 +94,6 @@ export default async (req, res, userSuppliedOptions) => {
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.
@@ -114,7 +110,7 @@ export default async (req, res, userSuppliedOptions) => {
}
// Session options
const sessionOption = {
const sessionOptions = {
jwt: false,
maxAge: 30 * 24 * 60 * 60, // Sessions expire after 30 days of being idle
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
@@ -123,8 +119,8 @@ export default async (req, res, userSuppliedOptions) => {
// JWT options
const jwtOptions = {
secret,
key: secret,
secret, // Use application secret if no keys specified
maxAge: sessionOptions.maxAge, // maxAge is dereived from session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userSuppliedOptions.jwt
@@ -132,17 +128,17 @@ export default async (req, res, userSuppliedOptions) => {
// If no adapter specified, force use of JSON Web Tokens (stateless)
if (!adapter) {
sessionOption.jwt = true
sessionOptions.jwt = true
}
// Event messages
const eventsOption = {
const eventsOptions = {
...events,
...userSuppliedOptions.events
}
// Callback functions
const callbacksOption = {
const callbacksOptions = {
...callbacks,
...userSuppliedOptions.callbacks
}
@@ -180,22 +176,18 @@ export default async (req, res, userSuppliedOptions) => {
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)
// Helper method for handling redirects, this is passed to all routes
// @TODO Refactor into a lib instead of passing as an option
// e.g. and call as redirect(req, res, url)
const redirect = (redirectUrl) => {
const reponseAsJson = !!((req.body && req.body.json === 'true'))
if (reponseAsJson) {
res.json({ url: redirectUrl })
} else {
res.status(302).setHeader('Location', redirectUrl)
res.end()
}
return done()
}
// User provided options are overriden by other options,
@@ -209,35 +201,28 @@ export default async (req, res, userSuppliedOptions) => {
// These computed settings can values in userSuppliedOptions but override them
// and are request-specific.
adapter,
site,
basePath,
baseUrl,
basePath,
action,
provider,
cookies,
secret,
csrfToken,
csrfTokenVerified,
providers: parseProviders(userSuppliedOptions.providers, baseUrl),
session: sessionOption,
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
session: sessionOptions,
jwt: jwtOptions,
events: eventsOption,
callbacks: callbacksOption,
callbackUrl: site
events: eventsOptions,
callbacks: callbacksOptions,
callbackUrl: baseUrl,
redirect
}
// If debug enabled, set ENV VAR so that logger logs debug messages
if (options.debug === true) { process.env._NEXT_AUTH_DEBUG = true }
if (options.debug === true) { process.env._NEXTAUTH_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':
@@ -250,18 +235,18 @@ export default async (req, res, userSuppliedOptions) => {
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)
if (options.pages.signIn) {
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
return redirect(redirectUrl)
}
pages.render(req, res, 'signin', { baseUrl, basePath, 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}`) }
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }
pages.render(req, res, 'signout', { site, baseUrl, csrfToken, callbackUrl: options.callbackUrl }, done)
pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
break
case 'callback':
if (provider && options.providers[provider]) {
@@ -274,12 +259,12 @@ export default async (req, res, userSuppliedOptions) => {
case 'verify-request':
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
pages.render(req, res, 'verify-request', { site }, done)
pages.render(req, res, 'verify-request', { baseUrl }, 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)
pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
break
default:
res.status(404).end()
@@ -288,17 +273,30 @@ export default async (req, res, userSuppliedOptions) => {
} else if (req.method === 'POST') {
switch (action) {
case 'signin':
// Signin POST requests are used for email sign in
// Verified CSRF Token required for all sign in routes
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
if (provider && options.providers[provider]) {
signin(req, res, options, done)
break
}
break
case 'signout':
// Verified CSRF Token required for signout
if (!csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
}
signout(req, res, options, done)
break
case 'callback':
if (provider && options.providers[provider]) {
// Verified CSRF Token required for credentials providers only
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
callback(req, res, options, done)
} else {
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)

View File

@@ -20,7 +20,6 @@ export default async (sessionToken, profile, providerAccount, options) => {
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.
@@ -52,7 +51,7 @@ export default async (sessionToken, profile, providerAccount, options) => {
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session && session.user) {
user = await getUser(session.user.id)
isSignedIn = !!user
@@ -86,12 +85,12 @@ export default async (sessionToken, profile, providerAccount, options) => {
// Update emailVerified property on the user object
const currentDate = new Date()
userByEmail.emailVerified = currentDate
user = await updateUser(userByEmail)
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
await dispatchEvent(events.updateUser, user)
} else {
// Create user account if there isn't one for the email address already
user = await createUser({ ...profile, emailVerified: true })
const currentDate = new Date()
user = await createUser({ ...profile, emailVerified: currentDate })
await dispatchEvent(events.createUser, user)
isNewUser = true
}

View File

@@ -3,21 +3,21 @@ import cookie from '../lib/cookie'
export default async (req, res, options) => {
const { query } = req
const { body } = req
const { cookies, site, defaultCallbackUrl, callbacks } = options
const { cookies, baseUrl, 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
let callbackUrl = defaultCallbackUrl || baseUrl
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect(callbackUrlParamValue, site)
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, site)
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
}
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow

View File

@@ -1,5 +1,5 @@
/**
* Use the signin callback to control if a user is allowed to sign in or not.
* 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,
@@ -15,7 +15,7 @@
* @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 signIn = async (profile, account, metadata) => {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return Promise.resolve(true)
@@ -68,7 +68,7 @@ const jwt = async (token, oAuthProfile) => {
}
export default {
signin,
signIn,
redirect,
session,
jwt

View File

@@ -15,7 +15,9 @@ const set = (res, name, value, options = {}) => {
}
// Preserve any existing cookies that have already been set in the same session
const setCookieHeader = res.getHeader('Set-Cookie') || []
let setCookieHeader = res.getHeader('Set-Cookie') || []
// If not an array (i.e. a string with a single cookie) convert it into an array
if (!Array.isArray(setCookieHeader)) { setCookieHeader = [setCookieHeader] }
setCookieHeader.push(_serialize(name, String(stringValue), options))
res.setHeader('Set-Cookie', setCookieHeader)
}

View File

@@ -1,9 +1,9 @@
const signin = async (message) => {
const signIn = async (message) => {
// Event triggered on successful sign in
}
const signout = async (message) => {
// Event triggered on signout
const signOut = async (message) => {
// Event triggered on sign out
}
const createUser = async (message) => {
@@ -28,8 +28,8 @@ const error = async (message) => {
}
export default {
signin,
signout,
signIn,
signOut,
createUser,
updateUser,
linkAccount,

View File

@@ -1,6 +1,8 @@
import oAuthClient from './client'
import { createHash } from 'crypto'
import querystring from 'querystring'
import jwtDecode from 'jwt-decode'
import oAuthClient from './client'
import logger from '../../../lib/logger'
// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
@@ -9,15 +11,38 @@ import logger from '../../../lib/logger'
// 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
// @TODO Refactor to use promises and not callbacks
// @TODO Refactor to use jsonwebtoken instead of jwt-decode & remove dependancy
export default async (req, provider, csrfToken, callback) => {
// The "user" object is specific to apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { oauth_token, oauth_verifier, code, user, state } = req.query // eslint-disable-line camelcase
const client = oAuthClient(provider)
if (provider.version && provider.version.startsWith('2.')) {
// For OAuth 2.0 flows, check state returned and matches expected value
// (a hash of the NextAuth.js CSRF token).
//
// This check can be disabled for providers that do not support it by
// setting `state: false` as a option on the provider (defaults to true).
if (!Object.prototype.hasOwnProperty.call(provider, 'state') || provider.state === true) {
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
if (state !== expectedState) {
return callback(new Error('Invalid state returned from oAuth provider'))
}
}
if (req.method === 'POST') {
// Get the CODE from Body
const body = JSON.parse(JSON.stringify(req.body))
code = body.code
try {
const body = JSON.parse(JSON.stringify(req.body))
if (body.error) { throw new Error(body.error) }
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (e) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', e, req.body, provider.id, code)
return callback()
}
}
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
@@ -34,19 +59,32 @@ export default async (req, provider, callback) => {
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)
return callback(error || results.error)
}
if (provider.idToken) {
// If we don't have an ID Token most likely the user hit a cancel
// button when signing in (or the provider is misconfigured).
//
// Unfortunately, we can't tell which, so we can't treat it as an
// error, so instead we just returning nothing, which will cause the
// user to be redirected back to the sign in page.
if (!results || !results.id_token) {
return callback()
}
// Support services that use OpenID ID Tokens to encode profile data
_decodeToken(
provider,
accessToken,
refreshToken,
results.id_token,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider, user)
callback(error, profile, account, OAuthProfile)
}
)
} else {
// Use custom get() method for oAuth2 flows
@@ -55,7 +93,10 @@ export default async (req, provider, callback) => {
client.get(
provider,
accessToken,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
}
@@ -76,14 +117,21 @@ export default async (req, provider, callback) => {
provider.profileUrl,
accessToken,
refreshToken,
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
async (error, profileData) => {
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
callback(error, profile, account, OAuthProfile)
}
)
}
)
}
}
async function _getProfile (error, profileData, accessToken, refreshToken, provider) {
/**
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
* Returns profile, raw profile and auth provider details
*/
async function _getProfile (error, profileData, accessToken, refreshToken, provider, userData) {
// @TODO Handle error
if (error) {
logger.error('OAUTH_GET_PROFILE_ERROR', error)
@@ -94,15 +142,32 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) { profileData = JSON.parse(profileData) }
// If a user object is supplied (e.g. Apple provider) add it to the profile object
if (userData != null) {
profileData.user = userData
}
logger.debug('PROFILE_DATA', 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')
// If we didn't get a response either there was a problem with the provider
// response *or* the user cancelled the action with the provider.
//
// Unfortuately, we can't tell which - at least not in a way that works for
// all providers, so we return an empty object; the user should then be
// redirected back to the sign up page. We log the error to help developers
// who might be trying to debug this when configuring a new provider.
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
return {
profile: null,
account: null,
OAuthProfile: profileData
}
}
// Return profile, raw profile and auth provider details
return ({
return {
profile: {
name: profile.name,
email: profile.email ? profile.email.toLowerCase() : null,
@@ -116,8 +181,8 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
accessToken,
accessTokenExpires: null
},
oAuthProfile: profileData
})
OAuthProfile: profileData
}
}
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js

View File

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

View File

@@ -2,7 +2,7 @@ import { randomBytes } from 'crypto'
export default async (email, provider, options) => {
try {
const { baseUrl, adapter } = options
const { baseUrl, basePath, adapter } = options
const { createVerificationRequest } = await adapter.getAdapter(options)
@@ -13,7 +13,7 @@ export default async (email, provider, options) => {
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)}`
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
// @TODO Create invite (send secret so can be hashed)
await createVerificationRequest(email, url, token, secret, provider, options)

View File

@@ -1,8 +1,8 @@
import oAuthClient from '../oauth/client'
import crypto from 'crypto'
import { createHash } from 'crypto'
import logger from '../../../lib/logger'
export default (provider, callback) => {
export default (provider, csrfToken, callback) => {
const { callbackUrl } = provider
const client = oAuthClient(provider)
if (provider.version && provider.version.startsWith('2.')) {
@@ -10,7 +10,8 @@ export default (provider, callback) => {
let url = client.getAuthorizeUrl({
redirect_uri: provider.callbackUrl,
scope: provider.scope,
state: crypto.randomBytes(64).toString('hex')
// A hash of the NextAuth.js CSRF token is used as the state
state: createHash('sha256').update(csrfToken).digest('hex')
})
// If the authorizationUrl specified in the config has query parameters on it

View File

@@ -1,11 +1,12 @@
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
export default ({ baseUrl, basePath, error, res }) => {
const signinPageUrl = `${baseUrl}${basePath}/signin`
let statusCode = 200
let heading = <h1>Error</h1>
let message = <p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
let message = <p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
switch (error) {
case 'Signin':
@@ -14,50 +15,15 @@ export default ({ site, error, baseUrl }) => {
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
// These messages are displayed in line on the sign in page
res.status(302).setHeader('Location', `${signinPageUrl}?error=${error}`)
res.end()
return false
case 'Configuration':
statusCode = 500
heading = <h1>Server error</h1>
message =
<div>
@@ -68,6 +34,7 @@ export default ({ site, error, baseUrl }) => {
</div>
break
case 'AccessDenied':
statusCode = 403
heading = <h1>Access Denied</h1>
message =
<div>
@@ -80,6 +47,7 @@ export default ({ site, error, baseUrl }) => {
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
statusCode = 403
heading = <h1>Unable to sign in</h1>
message =
<div>
@@ -93,6 +61,8 @@ export default ({ site, error, baseUrl }) => {
default:
}
res.status(statusCode)
return render(
<div className='error'>
{heading}

View File

@@ -1,5 +1,3 @@
import fs from 'fs'
import path from 'path'
import signin from './signin'
import signout from './signout'
import verifyRequest from './verify-request'
@@ -19,7 +17,8 @@ function render (req, res, page, props, done) {
html = verifyRequest(props)
break
case 'error':
html = error(props)
html = error({ ...props, res })
if (html === false) return done()
break
default:
html = error(props)

View File

@@ -2,8 +2,7 @@ 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
const { email, error } = req.query
// We only want to render providers
const providersToRender = providers.filter(provider => {
@@ -19,12 +18,46 @@ export default ({ req, csrfToken, providers, callbackUrl }) => {
}
})
let errorMessage
if (error) {
switch (error) {
case 'Signin':
case 'OAuthSignin':
case 'OAuthCallback':
case 'OAuthCreateAccount':
case 'EmailCreateAccount':
case 'Callback':
errorMessage = <p>Try signing with a different account.</p>
break
case 'OAuthAccountNotLinked':
errorMessage = <p>To confirm your identity, sign in with the same account you used originally.</p>
break
case 'EmailSignin':
errorMessage = <p>Check your email address.</p>
break
case 'CredentialsSignin':
errorMessage = <p>Sign in failed. Check the details you provided are correct.</p>
break
default:
errorMessage = <p>Unable to sign in.</p>
break
}
}
return render(
<div className='signin'>
{errorMessage &&
<div className='error'>
{errorMessage}
</div>}
{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>}
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
<button type='submit' className='button'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
<hr />}

View File

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

View File

@@ -1,12 +1,12 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import render from 'preact-render-to-string'
export default ({ site }) => {
export default ({ baseUrl }) => {
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>
<p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
</div>
)
}

View File

@@ -10,15 +10,17 @@ export default async (req, res, options, done) => {
provider: providerName,
providers,
adapter,
site,
secret,
baseUrl,
basePath,
secret,
cookies,
callbackUrl,
pages,
jwt,
events,
callbacks
callbacks,
csrfToken,
redirect
} = options
const provider = providers[providerName]
const { type } = provider
@@ -29,86 +31,111 @@ export default async (req, res, options, done) => {
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
try {
oAuthCallback(req, provider, csrfToken, async (error, profile, account, OAuthProfile) => {
try {
if (error) {
logger.error('CALLBACK_OAUTH_ERROR', error)
return redirect(`${baseUrl}${basePath}/error?error=oAuthCallback`)
}
// Check if user is allowed to sign in
const signinCallbackResponse = await callbacks.signin(profile, account, oAuthProfile)
// Make it easier to debug when adding a new provider
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
if (signinCallbackResponse === false) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
res.end()
return done()
// If we don't have a profile object then either something went wrong
// or the user cancelled signin in. We don't know which, so we just
// direct the user to the signup page for now. We could do something
// else in future.
//
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile) {
return redirect(`${baseUrl}${basePath}/signin`)
}
// Check if user is allowed to sign in
// Attempt to get Profile from OAuth provider details before invoking
// signIn callback - but if no user object is returned, that is fine
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByProviderAccountId } = await adapter.getAdapter(options)
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
if (userFromProviderAccountId) {
userOrProfile = userFromProviderAccountId
}
}
try {
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
if (signInCallbackResponse === false) {
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
} else {
return redirect(error)
}
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
if (useJwtSession) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
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) {
return redirect(pages.newUser)
}
// Callback URL is already verified at this point, so safe to use if specified
return redirect(callbackUrl || baseUrl)
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
// If the email on the account is already linked, but nto with this oAuth account
return redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
return redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
} else {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
}
// 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()
})
})
} catch (error) {
logger.error('OAUTH_CALLBACK_ERROR', error)
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} 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()
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(options)
@@ -118,9 +145,7 @@ export default async (req, res, options, done) => {
// 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()
return redirect(`${baseUrl}${basePath}/error?error=Verification`)
}
// If verification token is valid, delete verification request token from
@@ -132,23 +157,32 @@ export default async (req, res, options, done) => {
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()
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
if (signInCallbackResponse === false) {
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
} else {
return redirect(error)
}
}
// 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)
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -156,92 +190,85 @@ export default async (req, res, options, done) => {
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 })
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()
return redirect(pages.newUser)
}
// Callback URL is already verified at this point, so safe to use if specified
if (callbackUrl) {
res.status(302).setHeader('Location', callbackUrl)
res.end()
return redirect(callbackUrl)
} else {
res.status(302).setHeader('Location', site)
res.end()
return redirect(baseUrl)
}
return done()
} catch (error) {
if (error.name === 'CreateUserError') {
res.status(302).setHeader('Location', `${baseUrl}/error?error=EmailCreateAccount`)
return redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
} else {
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
logger.error('CALLBACK_EMAIL_ERROR', error)
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
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()
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
if (!provider.authorize) {
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const credentials = req.body
// If promise is rejected / throws error then display Configuration error
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
if (!userObjectReturnedFromAuthorizeHandler) {
return redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
}
} catch (error) {
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
res.end()
return done()
if (error instanceof Error) {
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
} else {
return redirect(error)
}
}
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()
try {
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
if (signInCallbackResponse === false) {
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
} else {
return redirect(error)
}
}
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 = {
name: user.name,
email: user.email,
picture: user.image
}
const defaultJwtPayload = { user, account }
const jwtPayload = await callbacks.jwt(defaultJwtPayload)
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -249,17 +276,9 @@ export default async (req, res, options, done) => {
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
await dispatchEvent(events.signin, { user, account })
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()
return redirect(callbackUrl || baseUrl)
} else {
res.status(500).end(`Error: Callback for provider type ${type} not supported`)
return done()

View File

@@ -19,7 +19,7 @@ export default async (req, res, options, done) => {
if (useJwtSession) {
try {
// Decrypt and verify token
const decodedJwt = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
// Generate new session expiry date
const sessionExpiresDate = new Date()
@@ -30,9 +30,9 @@ export default async (req, res, options, done) => {
// 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
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null
},
expires: sessionExpires
}
@@ -45,7 +45,7 @@ export default async (req, res, options, done) => {
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 })
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
@@ -79,7 +79,7 @@ export default async (req, res, options, done) => {
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(defaultSessionPayload)
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
// Return session payload as response
response = sessionPayload

View File

@@ -8,9 +8,11 @@ export default async (req, res, options, done) => {
provider: providerName,
providers,
baseUrl,
csrfTokenVerified,
basePath,
adapter,
callbacks
callbacks,
csrfToken,
redirect
} = options
const provider = providers[providerName]
const { type } = provider
@@ -20,29 +22,19 @@ export default async (req, res, options, done) => {
return done()
}
if (type === 'oauth') {
oAuthSignin(provider, (error, oAuthSigninUrl) => {
if (type === 'oauth' && req.method === 'POST') {
oAuthSignin(provider, csrfToken, (error, oAuthSigninUrl) => {
if (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
res
.status(302)
.setHeader('Location', `${baseUrl}/error?error=oAuthSignin`)
res.end()
return done()
return redirect(`${baseUrl}${basePath}/error?error=oAuthSignin`)
}
res.status(302).setHeader('Location', oAuthSigninUrl)
res.end()
return done()
return redirect(oAuthSigninUrl)
})
} 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()
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getUserByEmail } = await adapter.getAdapter(options)
@@ -58,54 +50,30 @@ export default async (req, res, options, done) => {
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 {
const signinCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
if (signinCallbackResponse === false) {
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
} else {
return redirect(error)
}
}
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()
return redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
res
.status(302)
.setHeader(
'Location',
`${baseUrl}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`
)
res.end()
return done()
return redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`)
} else {
// If provider not supported, redirect to sign in page
res.status(302).setHeader('Location', `${baseUrl}/signin`)
res.end()
return done()
return redirect(`${baseUrl}${basePath}/signin`)
}
}

View File

@@ -4,28 +4,15 @@ 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 { adapter, cookies, events, jwt, callbackUrl, redirect } = options
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)
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
await dispatchEvent(events.signOut, decodedJwt)
} catch (error) {
// Do nothing if decoding the JWT fails
}
@@ -36,7 +23,7 @@ export default async (req, res, options, done) => {
try {
// Dispatch signout event
const session = await getSession(sessionToken)
await dispatchEvent(events.signout, session)
await dispatchEvent(events.signOut, session)
} catch (error) {
// Do nothing if looking up the session fails
}
@@ -56,7 +43,5 @@ export default async (req, res, options, done) => {
maxAge: 0
})
res.status(302).setHeader('Location', callbackUrl)
res.end()
return done()
return redirect(callbackUrl)
}

View File

@@ -15,7 +15,7 @@
"default": null
},
"email_verified": {
"type": "timestamp",
"type": "timestamp(6)",
"nullable": true,
"default": null
},
@@ -69,7 +69,7 @@
"default": null
},
"access_token_expires": {
"type": "timestamp",
"type": "timestamp(6)",
"nullable": true,
"default": null
},
@@ -92,7 +92,7 @@
"nullable": false
},
"expires": {
"type": "timestamp",
"type": "timestamp(6)",
"nullable": false
},
"session_token": {
@@ -126,7 +126,7 @@
"nullable": false
},
"expires": {
"type": "timestamp",
"type": "timestamp(6)",
"nullable": false
},
"created_at": {

74
test/fixtures/sql/mysql.sql vendored Normal file
View File

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

74
test/fixtures/sql/postgres.sql vendored Normal file
View File

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

View File

@@ -4,15 +4,17 @@ const Adapters = require('../adapters')
;(async () => {
try {
// @FIXME Does not actally bail on connection error, adapter *should* throw exception
// Before that is addressed, we should make sure all routes in the app that
// call getAdapter() or rely on methods that use getAdapter() will handle
// a connection error gracefully.
const adapter1 = Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1:27017/nextauth?ssl=false&retryWrites=true')
await adapter1.getAdapter()
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
const adapter2 = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
await adapter2.getAdapter()
// Connect to local MongoDB instance
// Note: MongoDB doesn't thrown a connection error right away if is a
// problem with the credentials or host configuration, but after a few
// seconds it throws a Timeout error (which is caught by the adapter).
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
await adapter.getAdapter()
// @TODO create objects in database, check format of objects returned
console.log('MongoDB loaded ok')
process.exit()

View File

@@ -7,113 +7,177 @@ Callbacks are asynchronous functions you can use to control what happens when an
Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as they allow you to implement access controls without a database and to integrate with external databases or APIs.
:::tip
If you want to pass data such as an Access Token or User ID to the browser when using JSON Web Tokens, you can persist the data in the token when the `jwt` callback is called, then pass the data through to the browser in the `session` callback.
:::
You can specify a handler for any of the callbacks below.
#### How to use the callback option
```js
callbacks: {
signin: async (profile, account, metadata) => { },
redirect: async (url, baseUrl) => { },
session: async (session, token) => { },
jwt: async (token) => { }
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
signIn: async (user, account, profile) => {
return Promise.resolve(true)
},
redirect: async (url, baseUrl) => {
return Promise.resolve(baseUrl)
},
session: async (session, user) => {
return Promise.resolve(session)
},
jwt: async (token, user, account, profile, isNewUser) => {
return Promise.resolve(token)
}
...
}
```
The documentation below shows how to implement each callback and their default behaviour.
The documentation below shows how to implement each callback, their default behaviour and an example of what the response for each callback should be. Note that configuration options and authentication providers you are using can impact the values passed to the callbacks.
## Signin
## Sign in callback
Use the signin callback to control if a user is allowed to sign in or not.
Use the `signIn()` callback to control if a user is allowed to sign in.
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.
```js
/**
* @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)
```js title="pages/api/auth/[...nextauth.js]"
callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @return {boolean} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
signIn: async (user, account, profile) => {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return Promise.resolve(true)
} else {
// Return false to display a default error message
return Promise.resolve(false)
// You can also Reject this callback with an Error or with a URL:
// return Promise.reject(new Error('error message')) // Redirect to error page
// return Promise.reject('/path/to/redirect') // Redirect to a URL
}
}
}
```
## Redirect
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
The redirect callback is called anytime the user is redirected to a callback URL
(e.g. on signin or signout).
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include an property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
By default, for security, only Callback URLs on the same URL as the site are
allowed, you can use the redirect callback to customise that behaviour.
* When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `profile` object is the raw body of the `HTTP POST` submission.
```js
/**
* @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)
:::note
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
:::
:::tip
If you only want to allow users who already have accounts in the database to sign in, you can check for the existance of a `user.id` property and reject any sign in attempts from accounts that do not have one.
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
:::
## Redirect callback
The redirect callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
By default only URLs on the same URL as the site are allowed, you can use the redirect callback to customise that behaviour.
```js title="pages/api/auth/[...nextauth.js]"
callbacks: {
/**
* @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
*/
redirect: async (url, baseUrl) => {
return url.startsWith(baseUrl)
? Promise.resolve(url)
: Promise.resolve(baseUrl)
}
}
```
## Session
:::note
The redirect callback may be invoked more than once in the same flow.
:::
## Session callback
The session callback is called whenever a session is checked.
e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
e.g. `getSession()`, `useSession()`, `/api/auth/session`
If JSON Web Tokens are enabled, you can also access the decrypted token and use
this method to pass information from the encoded token back to the client.
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
The JWT callback is invoked before session is called, so anything you add to the
JWT will be immediately available in the session callback.
```js
/**
* @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)
```js title="pages/api/auth/[...nextauth.js]"
callbacks: {
/**
* @param {object} session Session object
* @param {object} user User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
*/
session: async (session, user, sessionToken) => {
session.foo = 'bar' // Add property to session
return Promise.resolve(session)
}
}
```
## JWT
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback.
:::
This JSON Web Token callback is called whenever a JSON Web Token is created or updated.
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session` (etc)
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
On initial sign in with an OAuth provider, the raw oAuthProfile returned by the
provider is also passed as a parameter - it is not available on subsequent calls.
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
:::
You can take advantage of this to persist additional data you need from their
raw profile to the encoded JWT.
## JWT callback
```js
/**
* @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)
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accesed in the client).
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
* As with database session expiry times, token expiry time is extended whenever a session is active.
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
```js title="pages/api/auth/[...nextauth.js]"
callbacks: {
/**
* @param {object} token Decrypted JSON Web Token
* @param {object} user User object (only available on sign in)
* @param {object} account Provider account (only available on sign in)
* @param {object} profile Provider profile (only available on sign in)
* @param {boolean} isNewUser True if new user (only available on sign in)
* @return {object} JSON Web Token that will be saved
*/
jwt: async (token, user, account, profile, isNewUser) => {
const isSignIn = (user) ? true : false
// Add auth_time to token on signin in
if (isSignIn) { token.auth_time = new Date().toISOString() }
return Promise.resolve(token)
}
}
```
:::warning
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain commonly imposed by browsers.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
:::

View File

@@ -1,160 +0,0 @@
---
id: database
title: Database Configuration
---
Specifying a database is optional if you don't need to persist user data or support email sign in.
If you want to do either of these things you will need to specify a database.
If you don't specify a database then JSON Web Tokens will be enabled and used to store session data. If you do specify a database then database sessions will be enabled, unless you explicitly enable JSON Web Tokens for sessions by passing the option `sessions { jwt: true }`.
## How to specify a database
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
The following approaches are exactly equivalent:
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
```js
database: {
type: 'mysql',
host: "127.0.0.1",
port: 3306,
username: "nextauth",
password: "password",
database: "nextauth",
synchronize: true
}
```
:::note
See the [TypeORM configuration documentation](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) for all the supported database options.
:::
## Setting up a database
NextAuth.js will configure your database with tables / collections automatically if `synchronize: true` is set.
If you are having problems connecting to your database, try enabling debug message with the `debug: true` option when initializing NextAuth.js.
:::warning
The option **?synchronize=true** automatically synchronizes the database schema with what NextAuth.js expects. It is useful to create the tables you need in the database on first run against a test database but it should not be enabled in production as it may result in data loss.
:::
:::tip
If you want to set a prefix for all table names you can use the TypeORM **entityPrefix** option.
*For example:*
```js
'mysql://username:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
```
*…or as a database configuration object:*
```js
database: {
type: 'mysql',
host: "127.0.0.1",
port: 3306,
username: "nextauth",
password: "password",
database: "nextauth",
synchronize: true,
entityPrefix: 'nextauth_'
}
```
:::
## Supported databases
NextAuth.js uses TypeORM as the default database adapter, but only some databases are supported.
:::tip
When configuring your database you also need to install an appropriate **node module** for your database.
:::
### SQLite
*SQLite is intended only for development / testing and not for production use.*
Install module:
`npm i sqlite3`
#### Example
```js
database: 'sqlite://localhost/:memory:?synchronize=true'
```
### MySQL
Install module:
`npm i mysql`
#### Example
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
### MariaDB
Install module:
`npm i mariadb`
#### Example
```js
database: 'mariadb://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
### Postgres
Install module:
`npm i pg`
#### Example
```js
database: 'postgres://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
### MongoDB
Install module:
`npm i mongodb`
#### Example
```js
database: 'mongodb://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
## Unsupported databases
The following additional databases are supported by TypeORM (which the default adapter uses) and *may* work with NextAuth.js but have not been tested:
* cordova
* expo
* mssql
* oracle
* sqljs
* react-native
Any database that supports ANSI SQL *should* work out of the box.
:::tip
You can customize, extend or replace the models, you can do this by using the 'adapters' option and passing passing additional options to **Adapters.Default()**. How to do this is not yet documented.
:::
:::note
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with any database.
:::

View File

@@ -0,0 +1,178 @@
---
id: databases
title: Databases
---
NextAuth.js comes with multiple ways of connecting to a database:
* **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, Postgres, SQLite and MongoDB databases._
* **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
* **Custom Adapter**<br/>
_A custom Adapter can be used to connect to any database._
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
---
## How to use a database
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
The following approaches are exactly equivalent:
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name'
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'nextauth'
}
```
:::tip
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
```js
'mysql://username:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
```
*…or as a database configuration object:*
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'nextauth'
entityPrefix: 'nextauth_'
}
```
:::
---
## Setting up a database
Using SQL to create tables and columns is the recommended way to set up an SQL database for NextAuth.js.
Check out the links below for SQL you can run to set up a database for NextAuth.js.
* [MySQL Schema](/schemas/mysql)
* [Postgres Schema](/schemas/postgres)
_If you are running SQLite, MongoDB or a Document database you can skip this step._
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'nextauth',
synchronize: true
}
```
:::warning
**The `synchronize` option should not be used against production databases.**
It is useful to create the tables you need when setting up a database for the first time, but it should not be enabled against production databases as it may result in data loss if there is a difference between the schema that found in the database and the schema that the version of NextAuth.js being used is expecting.
:::
---
## Supported databases
The default database adapter is TypeORM, but only some databases supported by TypeORM are supported by NextAuth.js as custom logic needs to be handled by NextAuth.js.
Databases compatible with MySQL, Postgres and MongoDB should work out of the box with NextAuth.js. When used with any other database, NextAuth.js will assume an ANSI SQL compatible database.
:::tip
When configuring your database you also need to install an appropriate **node module** for your database.
:::
### MySQL
Install module:
`npm i mysql`
#### Example
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name'
```
### MariaDB
Install module:
`npm i mariadb`
#### Example
```js
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
```
### Postgres
Install module:
`npm i pg`
#### Example
```js
database: 'postgres://username:password@127.0.0.1:3306/database_name'
```
### MongoDB
Install module:
`npm i mongodb`
#### Example
```js
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
```
### SQLite
*SQLite is intended only for development / testing and not for production use.*
Install module:
`npm i sqlite3`
#### Example
```js
database: 'sqlite://localhost/:memory:'
```
---
## Other databases
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).

View File

@@ -0,0 +1,23 @@
---
id: events
title: Events
---
Events are asynchronous functions that do not return a response, they are useful for audit logs / reporting.
You can specify a handler for any of these events below, for debugging or for an audit log.
```js title="pages/api/auth/[...nextauth].js"
...
events: {
signIn: async (message) => { /* on successful sign in */ },
signOut: async (message) => { /* on signout */ },
createUser: async (message) => { /* user created */ },
linkAccount: async (message) => { /* account linked to a user */ },
session: async (message) => { /* session is active */ },
error: async (message) => { /* error in authentication flow */ }
}
...
```
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc) but typically contains a user object and/or contents of the JSON Web Token and other information relevent to the event.

View File

@@ -1,28 +1,31 @@
---
id: options
title: NextAuth.js Options
title: Options
---
Options are passed to NextAuth.js when initializing it in an API route.
## Environment Variables
:::note
The only *required* options are **site** and **providers**.
### NEXTAUTH_URL
When deploying to production, set the `NEXTAUTH_URL` environment variable to the canonical URL of your site.
```
NEXTAUTH_URL=https://example.com
```
If your Next.js application uses a custom base path, specify the route to the API endpoint in full.
_e.g. `NEXTAUTH_URL=https://example.com/custom-route/api/auth`_
:::tip
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
:::
---
## Options
### site
* **Default value**: `empty string`
* **Required**: *Yes*
#### Description
The fully qualified URL for the root of your site.
e.g. `http://localhost:3000` or `https://www.example.com`
---
Options are passed to NextAuth.js when initializing it in an API route.
### providers
@@ -40,36 +43,26 @@ See the [providers documentation](/configuration/providers) for a list of suppor
### database
* **Default value**: `null`
* **Required**: *No* (Except by Email provider)
* **Required**: *No (unless using email provider)*
#### Description
A database connection string or [TypeORM](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) configuration object.
NextAuth.js has built in support for MySQL, MariaDB, Postgress, MongoDB and SQLite databases.
The default database provider is also compatible with other ANSI SQL compatible databases.
NextAuth.js can can be used with any database by specifying a custom `adapter` option.
:::tip
The Email provider currently requires a database to be configured.
:::
[A database connection string or configuration object.](/configuration/databases)
---
### secret
* **Default value**: `string` (*SHA hash of the "options" object*)
* **Required**: *No* (but strongly recommended)
* **Required**: *No - but strongly recommended!*
#### Description
A random string used to hash tokens and sign cookies (e.g. SHA hash).
A random string used to hash tokens, sign cookies and generate crytographic keys.
If not provided will be auto-generated based on hash of all your provided options.
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
The default behaviour is secure, but volatile, and it is strongly recommended you explicitly specify a value for secret to avoid invalidating any tokens when the automatically generated hash changes.
The default behaviour is volatile, and it is strongly recommended you explicitly specify a value to avoid invalidating end user sessions when configuration changes are deployed.
---
@@ -110,73 +103,87 @@ session: {
#### Description
JSON Web Tokens are only used if JWT sessions are enabled with `session: { jwt: true }` (see `session` documentation).
JSON Web Tokens are can be used for session tokens if enabled with `session: { jwt: true }` option. JSON Web Tokens enabled by default if you have not specified a database.
The `jwt` object and all properties on it are optional.
By default JSON Web Tokens tokens are signed (JWS) but not encrypted (JWE), as JWT encryption adds additional overhead and comes with some caveats. You can enable encryption by setting `encryption: true`.
When enabled, JSON Web Tokens is signed with `HMAC SHA256` and encrypted with symmetric `AES`.
Using JWT to store sessions is often faster, cheaper and more scaleable relying on a database.
Default values for this option are shown below:
#### JSON Web Token Options
```js
jwt: {
// secret: 'my-secret-123', // Secret auto-generated if not specified.
// A secret to use for key generation - you should set this explicitly
// Defaults to NextAuth.js secret if not explicitly specified.
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// By default the JSON Web Token is signed with SHA256 and encrypted with AES.
//
// You can define your own encode/decode functions for signing + encryption if
// you want to override the default behaviour (or to add/remove information
// from the JWT when it is encoded).
// encode: async ({ secret, key, token, maxAge }) => {},
// decode: async ({ secret, key, token, maxAge }) => {},
// Set to true to use encryption. Defaults to false (signing only).
// encryption: true,
// You can define your own encode/decode functions for signing and encryption
// if you want to override the default behaviour.
// encode: async ({ secret, token, maxAge }) => {},
// decode: async ({ secret, token, maxAge }) => {},
}
```
An example JSON WebToken contains an encrypted payload like this:
An example JSON Web Token contains a payload like this:
```js
{
user: {
name: 'Iain Collins',
email: 'me@iaincollins.com',
image: 'https://example.com/image.jpg',
id: 1 // User ID will note be specified if used without a database
},
// The account object stores details for the authentication provider account
// that was used to sign in. It only contains exactly one account, even the
// user is linked to multiple provider accounts in a database.
account: {
provider: 'google',
type: 'oauth',
id: 3218529,
refreshToken: 'cc0d32d79145091cd6cd8979f0a6d6b67d490899',
accessToken: '931400799b4a980715bb55af1bb8e01d92316956',
accessTokenExpires: null
},
isNewUser: true, // Is set to true if is first sign in
iat: 1591150735, // Issued at
exp: 4183150735 // Expires in
name: 'Iain Collins',
email: 'me@iaincollins.com',
picture: 'https://example.com/image.jpg',
"iat": 1594601838,
"exp": 1597193838
}
```
You can use the built-in `getJwt()` helper method to read the token, like this:
#### JWT Helper
You can use the built-in `getToken()` helper method to verify and decrypt the token, like this:
```js
import jwt from 'next-auth/jwt'
const secret = process.env.JWT_SECRET
export default async (req, res) => {
// Automatically decrypts and verifies JWT
const token = await jwt.getJwt({ req, secret })
res.end(JSON.stringify(token, null, 2))
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
console.log('JSON Web Token', token)
res.end()
}
```
_For convenience, this helper function is also able to read and decode tokens passed in an HTTP Bearer header._
**Required**
The getToken() helper requires the following options:
* `req` - (object) Request object
* `secret` - (string) JWT Secret
You must also pass *any options configured on the `jwt` option* to the helper.
e.g. Including custom session `maxAge` and custom signing and/or encryption keys or options
**Optional**
It also supports the following options:
* `secureCookie` - (boolean) Use secure prefixed cookie name
By default, the helper function will attempt to determine if it should use the secure prefixed cookie (e.g. `true` in production and `false` in development, unless NEXTAUTH_URL contains an HTTPS URL).
* `cookieName` - (string) Session token cookie name
The `secureCookie` option is ignored if `cookieName` is explcitly specified.
* `raw` - (boolean) Get raw token (not decoded)
If set to `true` returns the raw token without decrypting or verifying it.
:::note
The JWT is stored in the Session Token cookie the same cookie used for database sessions.
The JWT is stored in the Session Token cookie, the same cookie used for tokens with database sessions.
:::
---
@@ -196,8 +203,8 @@ Pages specified will override the corresponding built-in page.
```js
pages: {
signin: '/auth/signin',
signout: '/auth/signout',
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
@@ -223,10 +230,10 @@ You can specify a handler for any of the callbacks below.
```js
callbacks: {
signin: async (profile, account, metadata) => { },
signIn: async (profile, account, metadata) => { },
redirect: async (url, baseUrl) => { },
session: async (session, token) => { },
jwt: async (token) => => { }
jwt: async (token, profile) => => { }
}
```
@@ -249,8 +256,8 @@ The content of the message object varies depending on the flow (e.g. OAuth or Em
```js
events: {
signin: async (message) => { /* on successful sign in */ },
signout: async (message) => { /* on signout */ },
signIn: async (message) => { /* on successful sign in */ },
signOut: async (message) => { /* on signout */ },
createUser: async (message) => { /* user created */ },
linkAccount: async (message) => { /* account linked to a user */ },
session: async (message) => { /* session is active */ },
@@ -260,6 +267,25 @@ events: {
---
### adapter
* **Default value**: *Adapater.Default()*
* **Required**: *No*
#### Description
By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases. An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
See the [adapter documentation](/schemas/adapters) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
:::
---
### debug
* **Default value**: `false`
@@ -273,52 +299,7 @@ Set debug to `true` to enable debug messages for authentication and database ope
## Advanced Options
:::warning
Advanced options are passed the same way as basic options, but may have complex implications or side effects. You should try to avoid using advanced options unless you are very comfortable using them.
:::
---
### basePath
* **Default value**: `/api/auth`
* **Required**: *No*
#### Description
This option allows you to specify a different base path if you don't want to use `/api/auth` for some reason.
If you set this option you **must** also specify the same value in the `NEXTAUTH_BASE_PATH` environment variable in `next.config.js` so that the client knows how to contact the server:
```js title="next.config.js"
module.exports = {
env: {
NEXTAUTH_BASE_PATH: '/api/my-custom-auth-route',
},
}
```
This is required because the NextAuth.js API route is a separate codepath to the NextAuth.js Client.
As long as you also specify this option in an environment variable, the client will be able to pick up any subsequent configuration from the server, but if you do not set in both it the NextAuth.js Client will not work.
---
### adapter
* **Default value**: *Adapater.Default()*
* **Required**: *No*
#### Description
A custom provider is an advanced option intended for use only you need to use NextAuth.js with a database configuration that is not supported by the default `database` adapter.
See the [adapter documentation](/schemas/adapters) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option.
:::
---
@@ -340,7 +321,7 @@ Properties on any custom `cookies` that are specified override this option.
:::
:::warning
Setting this option to *false* in production is a security risk and may allow sessions to hijacked.
Setting this option to *false* in production is a security risk and may allow sessions to hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
:::
---
@@ -356,9 +337,15 @@ You can override the default cookie names and options for any of the cookies use
This is an advanced option and using it is not recommended as you may break authentication or introduce security flaws into your application.
You can specify one or more cookies with custom properties, but if you specify custom options for a cookie you must provided all the options for it. You will also likely want to create conditional behaviour to support local development (e.g. setting `secure: false` and not using cookie prefixes on localhost URLs).
You can specify one or more cookies with custom properties, but if you specify custom options for a cookie you must provided all the options for that cookie.
**For example:**
If you use this feature, you will likely want to create conditional behaviour to support setting different cookies policies in development and production builds, as you will be opting out of the built-in dynamic policy.
:::tip
An example of a use case for this option is to support sharing session tokens across subdomains.
:::
#### Example
```js
cookies: {
@@ -379,15 +366,6 @@ cookies: {
secure: true
}
},
baseUrl: {
name: `__Secure-next-auth.base-url`,
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true
}
},
csrfToken: {
name: `__Host-next-auth.csrf-token`,
options: {
@@ -401,40 +379,5 @@ cookies: {
```
:::warning
Changing the cookie options may introduce security flaws into your application and may break NextAuth.js integration now or in a future update. Using this option is not recommended.
Using a custom cookie policy may introduce security flaws into your application and is intended as an option for advanced users who understand the implications. Using this option is not recommended.
:::
---
### Client Max Age
* **Default value**: `0`
* **Required**: *No*
#### Description
By default the NextAuth.js client will use whatever cached session object it has and will not not re-check the current session if using the `useSession()` hook.
You can change this behaviour and force it to periodically sync the session state by setting a `NEXTAUTH_CLIENT_MAXAGE` environment variable.
```js title="next.config.js"
module.exports = {
env: {
NEXTAUTH_CLIENT_MAXAGE: 60, // Will re-check session every 60 seconds
},
}
```
If set to `0` (the default) sessions are not re-checked automatically, only when a new window or tab is opened or when `getSession()` is called.
If set to any other value, specifies how many seconds the window or tab should poll the server to update the session data.
When a session is checked this way (or using `getSession()`) it is active and extends the life of the current session.
It can be useful to use this option to prevent sessions from timing out if your application has a short session expiry time.
This option usually has cost implications as checking session status triggers a call to a server side route and/or a database.
:::note
In NextAuth.js session state is automatically synchronized across all open windows and tabs in the same browser. If you have session expiry times of 30 days or more (the default) you probably don't need to use this option, or can set it to a high value (e.g. every 24 hours).
:::

View File

@@ -1,89 +1,66 @@
---
id: pages
title: Custom Pages
title: Pages
---
NextAuth.js automatically creates simple, unbranded authentication pages for handling Sign in, Sign out, Email Verification and displaying error messages.
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
## Using custom pages
To add a custom login page, for example. You can use the `pages` option:
To add a custom login page, for example. You can us the `pages` option:
```javascript title="/pages/api/auth/[...nextauth].js"
...
```javascript title="pages/api/auth/[...nextauth].js"
...
pages: {
signin: '/auth/signin',
signout: '/auth/signout',
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
}
...
...
```
## Sign in
## Examples
### OAuth sign in page
### OAuth Sign in
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="/pages/auth/signin"
```jsx title="pages/auth/signin"
import React from 'react'
import { providers, signin } from 'next-auth/client'
import { providers, signIn } from 'next-auth/client'
export default ({ providers }) => {
return (
<>
{Object.values(providers).map(provider => (
<p key={provider.name}>
<a href={provider.signinUrl} onClick={(e) => e.preventDefault()}>
<button onClick={() => signin(provider.id)}>Sign in with {provider.name}</button>
</a>
</p>
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
</div>
))}
</>
)
}
export async function getServerSideProps (context) {
export async function getInitalProps(context) {
return {
props: {
providers: await providers(context)
}
providers: await providers(context)
}
}
```
:::tip
The **signin()** method automatically sets the callback URL to the current page. Using a link as a fallback means it sign in can work even without client side JavaScript.
:::
### Email sign in page
### Email Sign in
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
This is easier of if you use the build in `signin()` function, as it sets the CSRF automatically.
:::tip
To create a sign in page that works on clients with and without client side JavaScript, you can use both the **signin()** method and the **csrfToken()** method
:::
```jsx title="/pages/auth/email-signin"
```jsx title="pages/auth/email-signin"
import React from 'react'
import { csrfToken, signin } from 'next-auth/client'
import { csrfToken } from 'next-auth/client'
export default ({ csrfToken }) => {
return (
<form
method='post'
action='/api/auth/signin/email'
onSubmit={(e) => {
e.preventDefault()
signin('email', { email: document.getElementById('email').value })
}}
>
<form method='post' action='/api/auth/signin/email'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Email address
@@ -94,11 +71,53 @@ export default ({ csrfToken }) => {
)
}
export async function getServerSideProps (context) {
export async function getInitalProps(context) {
return {
props: {
csrfToken: await csrfToken(context)
}
csrfToken: await csrfToken(context)
}
}
```
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('email', { email: 'jsmith@example.com' })
```
### Credentials Sign in
If you create a sign in form for credentials based authenticaiton, you will needt to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
```jsx title="pages/auth/credentials-signin"
import React from 'react'
import { csrfToken } from 'next-auth/client'
export default ({ csrfToken }) => {
return (
<form method='post' action='/api/auth/callback/credentials'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<label>
Username
<input name='username' type='text'/>
</label>
<label>
Password
<input name='password' type='text'/>
</label>
<button type='submit'>Sign in</button>
</form>
)
}
export async function getInitalProps(context) {
return {
csrfToken: await csrfToken(context)
}
}
```
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('credentials', { username: 'jsmith', password: '1234' })
```

View File

@@ -1,19 +1,9 @@
---
id: providers
title: Authentication Providers
title: Providers
---
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<img alt={alt} src={src} />
</div>
)
Authentication Providers in NextAuth.js are how you define services can be used to sign in.
NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0 and has built-in support for many popular OAuth sign-in services. It also supports email / passwordless authentication.
@@ -23,16 +13,20 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
* [Apple](/providers/apple)
* [Auth0](/providers/auth0)
* [Battle.net](/providers/battlenet)
* [Box](/providers/box)
* [Amazon Cognito](/providers/cognito)
* [Discord](/providers/discord)
* [Facebook](/providers/facebook)
* [GitHub](/providers/github)
* [GitLab](/providers/gitlab)
* [Google](/providers/google)
* [IdentityServer4](/providers/identity-server4)
* [LinkedIn](/providers/LinkedIn)
* [Mixer](/providers/Mixer)
* [Okta](/providers/Okta)
* [Slack](/providers/slack)
* [Spotify](/providers/spotify)
* [Twitch](/providers/Twitch)
* [Twitter](/providers/twitter)
* [Yandex](/providers/yandex)
@@ -56,9 +50,9 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
```
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
...
providers: [
Providers.Twitter({
@@ -108,7 +102,7 @@ As an example of what this looks like, this is the the provider object returned
```
You can replace all the options in this JSON object with the ones from your custom provider  be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
...
providers: [
Providers.Twitter({
@@ -127,15 +121,15 @@ providers: [
...
```
#### Options
### OAuth provider options
| Name | Description | Required |
| :--------------: | :-------------------------------------------------: | :------: |
| id | An unique ID for your custom provider | Yes |
| name | An unique name for your custom provider | Yes |
| id | Unique ID for the provider | Yes |
| name | Descriptive name for the provider | Yes |
| type | Type of provider, in this case it should be `oauth` | Yes |
| version | OAuth version. | Yes |
| scope | OAuth access scopes | No |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | Yes |
| scope | OAuth access scopes (expects array or string) | No |
| params | Additional authorization URL parameters | No |
| accessTokenUrl | Endpoint to retrieve an access token | Yes |
| requestTokenUrl | Endpoint to retrieve a request token | No |
@@ -145,6 +139,7 @@ providers: [
| clientId | Client ID of the OAuth provider | Yes |
| clientSecret | Client Secret of the OAuth provider | No |
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | No |
| state | Set to `false` for services that do not support `state` verfication | No |
:::note
Feel free to open a PR for your custom configuration if you've created one for a provider that others may be interested in so we can add it to the list of built-in OAuth providers!
@@ -158,10 +153,10 @@ Adding support for signing in via email in addition to one or more OAuth service
Configuration is similar to other providers, but the options are different:
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
// maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
}),
@@ -181,7 +176,7 @@ The Credentials provider allows you to handle signing in with arbitrary credenti
It is intended to support use cases where you have an existing system you need to authenticate users against.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [
@@ -220,3 +215,16 @@ See the [Credentials provider documentation](/providers/credentials) for more in
:::note
The Credentials provider can only be used if JSON Web Tokens are enabled for sessions. Users authenticated with the Credentials provider are not persisted in the database.
:::
<!-- React Image Component -->
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<img alt={alt} src={src} />
</div>
)

View File

@@ -3,43 +3,26 @@ id: contributors
title: Contributors
---
NextAuth.js was initially created by <a href="https://github.com/iaincollins">Iain Collins</a> in 2016.
NextAuth.js 2.0 has been possible thanks to the work of many individual contributors.
Special thanks to Lori Karikari for creating most of the OAuth providers and to Nico Domino for creating this site.
### Collaborators (Core Team)
## Core Team
* <a href="https://github.com/iaincollins">Iain Collins</a>
* <a href="https://github.com/LoriKarikari">Lori Karikari</a>
* <a href="https://github.com/ndom91">Nico Domino</a>
* <a href="https://github.com/Fumler">Fredrik Pettersen</a>
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
### Contributors
* <a href="https://github.com/lluia">Lluis Agusti</a>
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
*Displayed in alphabetical order (by username).*
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._
* <a href="https://github.com/ajaymathur">Ajay Narain Mathur</a>
* <a href="https://github.com/aol-nnov">Andrey (aol-nnov)</a>
* <a href="https://github.com/cross19xx">Kenneth Kwakye-Gyamfi</a>
* <a href="https://github.com/c100k">Chafik (c100k)</a>
* <a href="https://github.com/dmitriyK1">Dmitriy (dmitriyK1)</a>
* <a href="https://github.com/drudv">Dmitry Druganov</a>
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
* <a href="https://github.com/jenssogaard">Jens Soegaard</a>
* <a href="https://github.com/joshuar500">Josh (joshuar500)</a>
* <a href="https://github.com/khuezy">khuezy</a>
* <a href="https://github.com/langovoi">Mark Langovoi</a>
* <a href="https://github.com/lifehome">lifehome</a>
* <a href="https://github.com/mebrein">Merijn (mebrein)</a>
* <a href="https://github.com/NickBolles">Nick Bolles</a>
* <a href="https://github.com/rahls7">Rahul (rahls7)</a>
* <a href="https://github.com/rmcalvert">Ryan Calvert</a>
* <a href="https://github.com/rxl881">Richard Lewis</a>
* <a href="https://github.com/sponte">Stanislaw Wozniak</a>
* <a href="https://github.com/9oelM">Joel M (9oelM)</a>
## Other Contributors
*Last updated: 29 May 2020.*
NextAuth.js as it exists today has been possible thanks to the work of many individual contributors.
Thank you to the [dozens of individual contributors](https://github.com/iaincollins/next-auth/graphs/contributors) who have help shaped NextAuth.js.
## History
NextAuth.js was originally developed by <a href="https://github.com/iaincollins">Iain Collins</a> in 2016.
In 2020, NextAuth.js was rebuilt from the ground up to support Serverless, with support for MySQL, Postgres and MongoDB, JSON Web Tokens and built in support for over a dozen authentication providers.

View File

@@ -17,9 +17,11 @@ These errors are returned from the client. As the client is [Universal JavaScrip
#### CLIENT_USE_SESSION_ERROR
This error occurs when the `useSession()` React Hook has a problem fetching session data.
#### CLIENT_FETCH_ERROR
#### CLIENT_COOKIE_PARSE_ERROR
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` envionment variable.
---

213
www/docs/faq.md Normal file
View File

@@ -0,0 +1,213 @@
---
id: faq
title: Frequently Asked Questions
---
## About NextAuth.js
### Is NextAuth.js commercial software?
NextAuth.js is an open source project built by individual contributors.
It is not commercial software and is not associated with a commercial organization.
---
## Compatibility
### What databases does NextAuth.js support?
You can use NextAuth.js with MySQL, MariaDB, Postgres, MongoDB and SQLite or without a database.
You can use also NextAuth.js with any database using a custom database adapter, or by using a custom credentials authentication provider - e.g. to support signing in with a username and password stored in an existing database.
### What authentication services does NextAuth.js support?
NextAuth.js includes built-in support for signing in with Apple, Auth0, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Okta, Slack, Spotify, Twitch, Twitter and Yandex.
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
You can also use a custom based provider to support signing in with a username and password stored in an external database and/or using two factor authentication.
### Does NextAuth.js support signing in with a username and password?
NextAuth.js is designed to avoid the need to store passwords for user accounts.
If you have an existing database of usernames and passwords, you can use a custom credentials provider to allow signing in with a username and password stored in an existing database.
_If you use a custom credentials provider user accounts will not be persisted in a database by NextAuth.js (even if one is configured). The option to use JSON Web Tokens for session tokens (which allow sign in without using a session database) must be enabled to use a custom credentials provider._
### Can I use NextAuth.js with a website that does not use Next.js?
NextAuth.js is designed for use with Next.js and Serverless.
You can create a website that handles sign in with Next.js and then access those sessions on a website that does not use Next.js as long as the websites are on the same domain.
If they are on a different subdomain you may need to set a custom cookie policy. NextAuth.js does not supporting signing into sites on different domains using the same service.
### Can I use NextAuth.js with React Native?
NextAuth.js is designed to handle sign in a Next.js web application.
It is designed as secure, confidental OAuth2 client with server side authentication flow, which allows it to do things public clients (which store embedded secrets) and browser-only clients cannot do.
It is not intended to be used in native applications on desktop or mobile applications, which typically use public clients (e.g. with client / secrets embedded in the application).
---
## Databases
### What databases are supported by NextAuth.js?
NextAuth.js can be used with MySQL, Postgres, MongoDB, SQLite and compatible databases (e.g. MariaDB, Amazon Aurora, Amazon DocumentDB…) or with no database.
It also provides an Adapter API which allows you to connect it to any database.
### What does NextAuth.js use databases for?
Databases in NextAuth.js are used for persisting users, oauth accounts, email sign in tokens and sessions.
Specifying a database is optional if you don't need to persist user data or support email sign in. If you don't specify a database then JSON Web Tokens will be enabled for session storage and used to store session data.
If you are using a database with NextAuth.js, you can still explicitly enable JSON Web Tokens for sessions (instead of using database sessions).
### Should I use a database?
* Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
* Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
### What database should I use?
Managed database solutions for MySQL, Postgres and MongoDB (and compatible databases) are well supported from cloud providers such as Amazon, Google, Microsoft and Atlas.
If you are deploying directly to a particular cloud platform you may also want to consider serverless database offerings they have (e.g. [Amazon Aurora Serverless on AWS](https://aws.amazon.com/rds/aurora/serverless/)).
---
## Security
### I think I've found a security problem, what should I do?
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
### What is the disclosure policy for NextAuth.js?
We practice responsible disclosure.
If you contact us regarding a potentially serious issue, we will endeavor to get back to you within 72 hours and to publish a fix within 30 days. We will responsibly disclose the issue (and credit you with your consent) once a fix to resolve the issue has been released - or after 90 days, which ever is sooner.
### How do I get Refresh Tokens and Access Tokens for an OAuth account?
_This is not currently supported, but is something we would like to have support for in future._
NextAuth.js provides a solution for authentication, session management and user account creation.
NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by the provider) and it will save them (along with the User ID, Provider and Provider Account ID) to either:
1. A database - if a database connection string is provided
2. A JSON Web Token - if JWT sessions are enabled (e.g. if no database specified)
However, NextAuth.js does not also handle Access Token rotation for you. If this is something you need, currently you will need to write the logic to handle that yourself.
---
## Feature Requests
### Why doesn't NextAuth.js support [a particular feature]?
NextAuth.js is an open source project built by individual contributors who are volunteers who writing code and providing support to help folks out in their spare time.
If you would like NextAuth.js to support a particular feature the best way to help make that happen is to raise a feature request describing the feature and to offer to work with other contributors to develop and test it.
If you are not able to develop a feature yourself, you can offer sponsor someone to work on it.
### I disagree with a design decision, how can I change your mind?
Product design decisions on NextAuth.js are made by core team members.
You can raise suggestions as feature requests / requests for enhancement.
Requests that provide the detail requested in the template and follow the format requested may be more likely to be supported, as additional detail prompted in the templates often provides important context.
Ultimately if your request is not accepted or is not actively in development, you are always free to fork the project under the terms of the ISC License.
---
## JSON Web Tokens
### Does NextAuth.js use JSON Web Tokens?
NextAuth.js supports both database session tokens and JWT session tokens.
* If a database is specified, database session tokens will be used by default.
* If no database is specified, JWT session tokens will be used by default.
You can also choose to use JSON Web Tokens as session tokens with using a database, by explictly setting the `session: { jwt: true }` option.
### What are the advantages of JSON Web Tokens?
JSON Web Tokens can be used for session tokens, but are also used for lots of other things, such as sending signed objects between services in authentication flows.
* Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
* JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
* You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in an server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
### What are the disadvantages of JSON Web Tokens?
* You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
Shorter session expiry times are used when using JSON Web Tokens as session tokens to allow sessions to be invalidated sooner and simplify this problem.
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expirying if tehre is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
Even if appropriately configured, information stored in an encrypted JWT should not be assumed to be impossible to decrypt at some point - e.g. due to the discovery of a defect or advances in technology.
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
* If you do not explictly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
### Are JSON Web Tokens secure?
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space avalible to store data (total cookie size for a domain is limited to 4KB).
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
NextAuth.js will generate keys for you, but this will generate a warning at start up.
Using explict public/private keys for signing is strongly recommended.
### What signing and encryption standards does NextAuth.js support?
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
* [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
* [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
* [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
* [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
* [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
This incorporates support for:
* [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
* [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
* [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)

View File

@@ -5,46 +5,7 @@ title: Client API
The NextAuth.js client library makes it easy to interact with sessions from React applications.
Some of the methods can be called both client side and server side.
:::note
When using any of the client API methods server side, [context](https://nextjs.org/docs/api-reference/data-fetching/getInitialProps#context-object) must be passed as an argument. The documentation for **getSession()** has an example.
:::
## useSession()
* Client Side: **Yes**
* Server Side: No
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
It works best when used with NextAuth.js `<Provider>` is added to `pages/_app.js` (see [provider](#provider)).
```jsx
import { useSession } from 'next-auth/client'
export default () => {
const [ session, loading ] = useSession()
return <>
{session && <p>Signed in as {session.user.email}</p>}
{!session && <p><a href="/api/auth/signin">Sign in</p>}
</>
}
```
---
## getSession()
* Client Side: **Yes**
* Server Side: **Yes**
NextAuth.js also provides a `getSession()` method which can be called client or server side to return a session.
It calls `/api/auth/session` and returns a promise with a session object, or null if no session exists.
A session object looks like this:
#### Example Session Object
```js
{
@@ -58,69 +19,108 @@ A session object looks like this:
}
```
You can call `getSession()` inside a function to check if a user is signed in, or use it for server side rendered pages that supporting signing in without requiring client side JavaScript.
:::tip
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
:::info
Note that because it exposed to the client it does not contain sensitive information such as the Session Token or OAuth service related tokens. It includes enough information (e.g name, email) to display information on a page about the user who is signed in, and an Access Token that can be used to identify the session without exposing the Session Token itself.
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
:::
Because it is a Universal method, you can use `getSession()` in both client and server side functions, such as `getInitialProps()` in Next.js:
---
```jsx title="/pages/index.js"
import { getSession } from 'next-auth/client'
## useSession()
const Page = ({ session }) => (<p>
{!session && <>
Not signed in <br/>
<a href="/api/auth/signin">Sign in</a>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<a href="/api/auth/signout">Sign out</a>
</>}
</p>)
* Client Side: **Yes**
* Server Side: No
Page.getInitialProps = async (context) => {
return {
session: await getSession(context)
}
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
#### Example
```jsx
import { useSession } from 'next-auth/client'
export default () => {
const [ session, loading ] = useSession()
return <>
{session && <p>Signed in as {session.user.email}</p>}
{!session && <p><a href="/api/auth/signin">Sign in</a></p>}
</>
}
export default Page
```
#### Using getSession() in API routes
---
You can also get the session object in Next.js API routes:
## getSession()
* Client Side: **Yes**
* Server Side: **Yes**
NextAuth.js provides a `getSession()` method which can be called client or server side to return a session.
It calls `/api/auth/session` and returns a promise with a session object, or null if no session exists.
#### Client Side Example
```js
async function myFunction() {
const session = await getSession()
/* ... */
}
```
#### Server Side Example
```js
import { getSession } from 'next-auth/client'
export default (req, res) => {
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
// Signed in
const { accessToken } = session.user
// Do something with accessToken (e.g. look up user in DB)
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({ /* data */ }))
} else {
// Not signed in
res.status(302).setHeader('Location', pages.newUser)
res.end()
}
/* ... */
res.end()
}
```
:::note
When calling `getSession()` server side, you must pass the request object - e.g. `getSession({req})` - or you can the pass entire `context` object as it contains the `req` object.
When calling `getSession()` server side, you need to pass `{req}` or `context` object.
:::
The tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-routes) shows how to use `getSession()` in server side calls.
---
## getCsrfToken()
* Client Side: **Yes**
* Server Side: **Yes**
The `getCsrfToken()` method returns the current Cross Site Request Forgery Token (CSRF Token) required to make POST requests (e.g. for signing in and signing out).
You likely only need to use this if you are not using the built-in `signIn()` and `signOut()` methods.
#### Client Side Example
```js
async function myFunction() {
const csrfToken = await getCsrfToken()
/* ... */
}
```
#### Server Side Example
```js
import { getCsrfToken } from 'next-auth/client'
export default async (req, res) => {
const csrfToken = await getCsrfToken({ req })
/* ... */
res.end()
}
```
---
## getProviders()
@@ -130,49 +130,52 @@ When calling `getSession()` server side, you must pass the request object - e.g.
The `getProviders()` method returns the list of providers currently configured for sign in.
It calls `/api/auth/providers` and returns a with a list of the currently configured authentication providers.
It calls `/api/auth/providers` and returns a list of the currently configured authentication providers.
It can be use useful if you are creating a dynamic custom sign in page.
---
## getCsrfToken()
#### API Route
* Client Side: **Yes**
* Server Side: **Yes**
```jsx title="pages/api/example.js"
import { getSession } from 'next-auth/client'
The `getCsrfToken()` method returns the current Cross Site Request Forgery (CSRF Token) required to make POST requests (e.g. for signing in and signing out). It calls `/api/auth/csrf`.
You likely only need to use this if you are not using the built-in `signin()` and `signout()` methods.
export default async (req, res) => {
const session = await getSession({ req })
console.log('Session', session)
res.end()
}
```
---
## signin()
## signIn()
* Client Side: **Yes**
* Server Side: No
Using the `signin()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF tokens for you automatically when signing in with email.
Using the `signIn()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF Tokens for you automatically when signing in with email.
The `signin()` method can be called from the client in different ways, as shown below.
The `signIn()` method can be called from the client in different ways, as shown below.
#### Redirects to sign in page when clicked
```js
import { signin } from 'next-auth/client'
import { signIn } from 'next-auth/client'
export default () => (
<button onClick={signin}>Sign in</button>
<button onClick={signIn}>Sign in</button>
)
```
#### Starts Google OAuth sign-in flow when clicked
```js
import { signin } from 'next-auth/client'
import { signIn } from 'next-auth/client'
export default () => (
<button onClick={() => signin('google')}>Sign in with Google</button>
<button onClick={() => signIn('google')}>Sign in with Google</button>
)
```
@@ -181,57 +184,53 @@ export default () => (
When using it with the email flow, pass the target `email` as an option.
```js
import { signin } from 'next-auth/client'
import { signIn } from 'next-auth/client'
export default ({ email }) => (
<button onClick={() => signin('email', { email })}>Sign in with Email</button>
<button onClick={() => signIn('email', { email })}>Sign in with Email</button>
)
```
#### Specifying a callbackUrl
By default, the URL of page the client is on when they sign in is used as the `callbackUrl` and that is the URL the client will be redirected to after signing in.
The `callbackUrl` specifies to which URL the user will be redirected after signing in. It defaults to the current URL of a user.
You can specify a different URL as the `callbackUrl` parameter by passing it in the second argument to `signin()`. This works for all calls to `signin()`.
You can specify a different `callbackUrl` by specifying it as the second argument of `signIn()`. This works for all providers.
e.g.
* `signin(null, { callbackUrl: 'http://localhost:3000/foo' })`
* `signin('google', { callbackUrl: 'http://localhost:3000/foo' })`
* `signin('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
* `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
* `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
* `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
:::tip
To also support signing in from clients that do not have client side JavaScript, use a regular link, add an onClick handler to it and call **e.preventDefault()** before calling the **signin()** method.
:::
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own redirect callback to allow other URLs, including supporting relative URLs.
---
## signout()
## signOut()
* Client Side: **Yes**
* Server Side: No
Using the `signout()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
```js
import { signout } from 'next-auth/client'
import { signOut } from 'next-auth/client'
export default () => (
<button onClick={signout}>Sign out</button>
<button onClick={signOut}>Sign out</button>
)
```
#### Specifying a callbackUrl
As with the `signin()` function, you can specify a `callbackUrl` parameter by passing it as an option.
As with the `signIn()` function, you can specify a `callbackUrl` parameter by passing it as an option.
e.g. `signout{ callbackUrl: 'http://localhost:3000/foo' })`
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
---
@@ -239,17 +238,14 @@ The URL must be considered valid by the [redirect callback handler](http://local
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
This improves performance, reduces network calls and avoids page flicker when rendering.
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
It is highly recommended and can be easily added to all pages in Next.js apps by using `/pages/_app.js`.
```jsx title="/pages/_app.js"
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default ({ Component, pageProps }) => {
const { session } = pageProps
export default function App ({ Component, pageProps }) => {
return (
<Provider session={session}>
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
)
@@ -258,6 +254,61 @@ export default ({ Component, pageProps }) => {
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
### Options
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default function App ({ Component, pageProps }) => {
return (
<Provider session={pageProps.session}
options={{
clientMaxAge: 60 // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60 // Send keepAlive message every 5 minutes
}}
>
<Component {...pageProps} />
</Provider>
)
}
```
:::note
**These options have no effect on clients that are not signed in.**
Every tab/window maintains it's own copy of the local session state; the session it is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
:::
#### Client Max Age
The `clientMaxAge` option is the maximum age a session data can be on the client before it is considerd stale.
When `clientMaxAge` is set to `0` (the default) the cache will always be used when useSession is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
If set to any value other than zero, it specifies in seconds the maxium age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
The value for `clientMaxAge` should always be lower than the value of the session `maxAge` option.
#### Keep Alive
The `keepAlive` option is how often the client should contact the server to avoid a session expirying.
When `keepAlive` is set to `0` (the default) it will not send a keep alive message.
If set to any value other than zero, it specifies in seconds how often the client should contact the server to update the session state. If the session state has expired when it is triggered, all open tabs/windows will be updated to reflect this.
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
:::

View File

@@ -3,33 +3,32 @@ id: example
title: Example
---
## Example with live demo
### Check out the example project
The easiest way to get started is to clone the example Next.js application from the [next-auth-example](https://github.com/iaincollins/next-auth-example) repository and to the instructions in the [README](https://github.com/iaincollins/next-auth-example/blob/main/README.md).
The easiest way to get started is to clone the [example application](https://github.com/iaincollins/next-auth-example) and follow the instructions in the [README](https://github.com/iaincollins/next-auth-example/blob/main/README.md).
You can find a live demo of the example project at [next-auth-example.now.sh](https://next-auth-example.now.sh)
## How to use NextAuth.js
## Add to an existing project
*The examples below show how to add authentication with NextAuth.js to an existing Next.js project.*
*The example code below shows how to add authentication to an existing Next.js project.*
### Add API route
To add NextAuth.js to a project, first create a file called `[...nextauth].js` in `pages/api/auth`.
To add NextAuth.js to a project create a file called `[...nextauth].js` in `pages/api/auth`.
```javascript title="/pages/api/auth/[...nextauth].js"
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
site: process.env.SITE || 'http://localhost:3000',
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
}),
// ...add more providers here
],
// A database is optional, but required to persist accounts in a database
@@ -49,28 +48,60 @@ See the [options documentation](/configuration/options) for how to configure pro
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```jsx title="/pages/index.js"
```jsx title="pages/index.js"
import React from 'react'
import { useSession } from 'next-auth/client'
import { signIn, signOut, useSession } from 'next-auth/client'
export default () => {
export default function Page() {
const [ session, loading ] = useSession()
return <p>
return <>
{!session && <>
Not signed in <br/>
<a href="/api/auth/signin">Sign in</a>
<button onClick={signIn}>Sign in</button>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<a href="/api/auth/signout">Sign out</a>
<button onClick={signOut}>Sign out</button>
</>}
</p>
</>
}
```
*That's all the code you need to add authentication to a project!*
***That's all the code you need to add authentication with NextAuth.js to a project!***
:::tip
You can use the `useSession` hook from anywhere in your application (e.g. in a header component).
:::
### Add to all pages
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `pages/_app.js`.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default function App ({ Component, pageProps }) => {
return (
<Provider session={pageProps.session}>
<Component {...pageProps} />
</Provider>
)
}
```
:::tip
Check out the [client documentation](/getting-started/client) to see how you can improve the user experience and page performance by using the NextAuth.js client.
:::
### Deploying
When deploying your site set the `NEXTAUTH_URL` environment variable to the canonical URL of the website.
```
NEXTAUTH_URL=https://example.com
```
:::tip
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
:::

View File

@@ -9,17 +9,16 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
It is designed from the ground up to support Next.js and Serverless.
[Follow the examples](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
[Check out the example code](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
## Features
### Easy authentication
### Flexible and easy to use
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Supports both JSON Web Tokens and database sessions
* Built-in support for [many popular OAuth sign-in services](/configuration/providers)
* Built-in support for [many popular sign-in services](/configuration/providers)
* Supports email / passwordless authentication
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
* Supports both JSON Web Tokens and database sessions
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
@@ -27,31 +26,30 @@ NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/database)
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)
*Note: Email sign in requires a database to store verification tokens - though if you are not too concered about when sign in emails expire, this can be an in-memory database like SQLite.*
*Note: Email sign in requires a database to be configured to store single-use verification tokens.*
### Secure by default
Security focused features include CSRF protection, use of signed cookies, cookie prefixes, secure cookies, HTTP only, host only and secure only cookies, secure URL redirection handling, and promoting passwordless sign-in.
* Designed to be secure by default and promote best practice for safeguarding user data
* Promotes the use of passwordless sign in mechanisms
* Designed to be secure by default and encourage best practice for safeguarding user data
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
* JSON Web Tokens are signed and encrypted (HMAC+AES) and only accessible server side
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
* Auto-generates symmetric signing and encryption keys for developer convenience
* Features tab/window syncing and keepalive messages to support short lived sessions
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
To keep your site secure while still making it easy to share data between the backend and front end securely [callback methods](/configuration/callbacks) are provided.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
The callbacks you send information to the client without having to handle session validation or JSON Web Token encryption /decryption yourself - just read and write JSON objects, and the rest is handled for you.
## Credits
Advanced options allow you to define your own routines for signin and decoding JSON Web Tokens and to set custom cookie security policies and access controls, so you can control who is able to sign in and how often sessions have to be re-validated.
NextAuth.js is an open source project that is only possible [thanks to contributors](/contributors).
## Acknowledgement
## Getting Started
[NextAuth.js 2.0 is possible thanks to its contributors.](/contributors)
## Getting started
[Follow the examples to get started.](/getting-started/example)
[Check out the example code](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.

View File

@@ -5,50 +5,58 @@ title: REST API
NextAuth.js exposes a REST API which is used by the NextAuth.js client.
:::note
The default base path is `/api/auth` but it is configurable with the `basePath` option.
:::
### GET /api/auth/signin
#### GET /api/auth/signin
Displays the sign in page.
### GET /api/auth/signin/:provider
#### POST /api/auth/signin/:provider
Starts an OAuth signin flow for the specified provider.
### POST /api/auth/signin/:provider
A POST submission is required for email sign in.
The POST submission requires CSRF token from `/api/auth/csrf`.
### GET /api/auth/callback/:provider
#### GET /api/auth/callback/:provider
Handles retuning requests from OAuth services during sign in.
### GET /api/auth/signout
For OAuth 2.0 providers that support the `state` option, the value of the `state` parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in.
#### GET /api/auth/signout
Displays the sign out page.
### POST /api/auth/signout
#### POST /api/auth/signout
Handles signing out - this is a POST submission to prevent malicious links from triggering signing a user out without their consent.
The POST submission requires CSRF token from `/api/auth/csrf`.
### GET /api/auth/session
#### GET /api/auth/session
Returns client-safe session object - or an empty object if there is no session.
### GET /api/auth/csrf
The contents of the session object that is returned is configurable with the session callback.
#### GET /api/auth/csrf
Returns object containing CSRF token. In NextAuth.js, CSRF protection is present on all authentication routes. It uses the "double submit cookie method", which uses a signed HttpOnly, host-only cookie.
The CSRF token returned by this endpoint must be passed as form variable named `csrfToken` in all POST submissions to any API endpoint.
### GET /api/auth/providers
#### GET /api/auth/providers
Returns a list of configured OAuth services and the configuration (e.g. sign in and callback URLs) for each service.
Returns a list of configured OAuth services and details (e.g. sign in and callback URLs) for each service.
It can be used to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.
It can be used to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.
---
:::note
The default base path is `/api/auth` but it is configurable by specyfing a custom path in `NEXTAUTH_URL`
e.g.
`NEXTAUTH_URL=https://example.com/myapp/api/authentication`
`/api/auth/signin` -> `/myapp/api/authentication/signin`
:::

View File

@@ -3,16 +3,15 @@ id: apple
title: Apple
---
## API Documentation
## Documentation
https://developer.apple.com/sign-in-with-apple/get-started/
## App Configuration
## Configuration
https://developer.apple.com/account/resources/identifiers/list/serviceId
## Usage
## Example
There are two ways you can use the Sign in with Apple provider.
@@ -39,16 +38,17 @@ providers: [
:::tip
Mac
Convert your apple key to a single line to use as a environment variable.
```bash
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
```
You can convert your Apple key to a single line to use it in a environment variable.
**Mac**
```bash
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
```
Windows
**Windows**
```powershell
```powershell
$k8file = "AuthKey_ID.k8"
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
```
@@ -127,7 +127,8 @@ openssl req -x509 -out localhost.crt -keyout localhost.key \
```
:::tip
On Windows?
**Windows**
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if its not already done.
@@ -170,7 +171,7 @@ app.prepare().then(() => {
if (err) throw err
console.log('> Ready on https://localhost:3000')
})
}
})
```
### Example JWT code

View File

@@ -3,15 +3,19 @@ id: auth0
title: Auth0
---
## API Documentation
## Documentation
https://auth0.com/docs/api/authentication#authorize-application
## App Configuration
## Configuration
https://manage.auth0.com/dashboard
## Usage
:::tip
Configure your application in Auth0 as a 'Regular Web Application' (not a 'Single Page App').
:::
## Example
```js
import Providers from `next-auth/providers`
@@ -20,8 +24,12 @@ providers: [
Providers.Auth0({
clientId: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
subdomain: process.env.AUTH0_SUBDOMAIN
domain: process.env.AUTH0_DOMAIN
})
}
...
```
```
:::note
`domain` should be the fully qualified domain  e.g. `dev-s6clz2lv.eu.auth0.com`
:::

View File

@@ -0,0 +1,27 @@
---
id: battle.net
title: Battle.net
---
## Documentation
https://develop.battle.net/documentation/guides/using-oauth
## Configuration
https://develop.battle.net/access/clients
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.BattleNet({
clientId: process.env.BATTLENET_CLIENT_ID,
clientSecret: process.env.BATTLENET_CLIENT_SECRET,
region: process.env.BATTLENET_REGION
})
}
...
```

View File

@@ -3,15 +3,15 @@ id: box
title: Box
---
## API Documentation
## Documentation
https://developer.box.com/reference/
## App Configuration
## Configuration
https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -0,0 +1,35 @@
---
id: cognito
title: Amazon Cognito
---
## Documentation
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html
## Configuration
https://console.aws.amazon.com/cognito/users/
You need to select your AWS region to go the the Cognito dashboard.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Cognito({
clientId: process.env.COGNITO_CLIENT_ID,
clientSecret: process.env.COGNITO_CLIENT_SECRET,
domain: process.env.COGNITO_DOMAIN,
})
}
...
```
:::warning
Make sure you select all the appropriate client settings or the OAuth flow will not work.
:::
![cognito](https://user-images.githubusercontent.com/7902980/83951604-cd096e80-a832-11ea-8bd2-c496ec9a16cb.PNG)

View File

@@ -3,39 +3,35 @@ id: credentials
title: Credentials
---
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<img alt={alt} src={src} />
</div>
)
## Overview
The Credentials provider allows you to handle signing in with arbitrary credentials, such as a username and password, domain, or two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
It is intended to support use cases where you have an existing system you need to authenticate users against.
It comes with the constraint that users authenticated in this manner are not persisted in the database, and that the Credentials provider can only be used if JSON Web Tokens are enabled for sessions.
It comes with the constraint that users authenticated in this manner are not persisted in the database, and consequently that the Credentials provider can only be used if JSON Web Tokens are enabled for sessions.
:::note
The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them and the additional complexity associated with supporting usernames and passwords.
:::
## Usage
## Example
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials input and returns either a `user` object or `false`.
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials submitted via HTTP POST as input and returns either:
If you return an object it will be persisted to the JSON Web Token and the user will be signed in.
1. A `user` object, which indicates the credentials are valid.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
```js title="/pages/api/auth/[...nextauth].js"
2. Either `false` or `null`, which indicates failure.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
3. `Promise.Rejected()` with an Error or a URL.
If you reject the promise with an Error, the user will be sent to the error page with the error message as a query parameter. If you reject the promise with a URL (a string), the user will be redirected to the URL.
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [
@@ -50,54 +46,36 @@ providers: [
password: { label: "Password", type: "password" }
},
authorize: async (credentials) => {
const user = (credentials) => {
// You need to provide your own logic here that takes the credentials
// submitted and returns either a object representing a user or value
// that is false/null if the credentials are invalid.
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
return null
}
// Add logic here to look up the user from the credentials supplied
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
if (user) {
// Any user object returned here will be saved in the JSON Web Token
// Any object returned will be saved in `user` property of the JWT
return Promise.resolve(user)
} else {
// If you return null or false then the credentials will be rejected
return Promise.resolve(null)
// You can also Reject this callback with an Error or with a URL:
// return Promise.reject(new Error('error message')) // Redirect to error page
// return Promise.reject('/path/to/redirect') // Redirect to a URL
}
}
})
}
]
...
```
To use your new credentials provider, you will need to create a form that posts back to `/api/auth/callback/credentials`.
See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
All form parameters submitted will be passed as `credentials` to your `authorize` callback.
## Multiple providers
```js title="/pages/signin"
import React from 'react'
export default () => {
return (
<form method='post' action='/api/auth/callback/credentials'>
<input name='email' type='text' defaultValue='' />
<input name='password' type='password' defaultValue='' />
<button type='submit'>Sign in</button>
</form>
)
}
```
As the JSON Web Token is encrypted, you can safely store user credentials in it and revalidate them whenever an action is performed. See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
## With multiple providers
### Usage
### Example code
You can specify more than one credentials provider by specifying a unique `id` for each one.
You can also use them in conjuction with other provider options.
As with all providers, the order you specify them in, is the order they are displayed on the sign in page.
As with all providers, the order you specify them is the order they are displayed on the sign in page.
```js
providers: [
@@ -105,7 +83,7 @@ As with all providers, the order you specify them in, is the order they are disp
id: 'domain-login',
name: "Domain Account",
authorize: async (credentials) => {
const user = (credentials) => { /* add function to get user */ }
const user = { /* add function to get user */ }
return Promise.resolve(user)
},
credentials: {
@@ -118,7 +96,7 @@ As with all providers, the order you specify them in, is the order they are disp
id: 'intranet-credentials',
name: "Two Factor Auth",
authorize: async (credentials) => {
const user = (credentials) => { /* add function to get user */ }
const user = { /* add function to get user */ }
return Promise.resolve(user)
},
credentials: {
@@ -130,6 +108,22 @@ As with all providers, the order you specify them in, is the order they are disp
]
```
### Sign in
### Example UI
This example below shows a complex configuration is rendered by the built in sign in page.
You can also [use a custom sign in page](/configuration/pages#credentials-sign-in) if you want to provide a custom user experience.
<Image src="/img/signin-complex.png"/>
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',
width: '100%',
display: 'flex',
justifyContent: 'center'
}}>
<img alt={alt} src={src} />
</div>
)

View File

@@ -3,15 +3,15 @@ id: discord
title: Discord
---
## API Documentation
## Documentation
https://discord.com/developers/docs/topics/oauth2
## App Configuration
## Configuration
https://discord.com/developers/applications
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -15,7 +15,7 @@ The Email provider can be used in conjunction with (or instead of) one or more O
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
If someone provides the email address of an *existing account* when signin in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
If someone provides the email address of an *existing account* when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
:::tip
@@ -39,7 +39,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
Now you can add the email provider like this:
```js {3} title="/pages/api/auth/[...nextauth].js"
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -61,7 +61,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
```
Now you can add the provider settings to the NextAuth options object in the Email Provider.
```js title="/pages/api/auth/[...nextauth].js"
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: {
@@ -84,26 +84,40 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
You can fully customise the sign in email that is sent by passing a custom function as the `sendVerificationRequest` option to `Providers.Email()`.
The following example shows the complete source for the built-in `sendVerificationRequest()` method.
e.g.
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest: ({ identifier: email, url, token, site, provider }) => { /* your function */ }
})
]
```
The following code shows the complete source for the built-in `sendVerificationRequest()` method:
```js
import nodemailer from 'nodemailer'
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
const sendVerificationRequest = ({ identifier: email, url, token, baseUrl, provider }) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
const siteName = site.replace(/^https?:\/\//, '')
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
nodemailer
.createTransport(server)
.sendMail({
to: emailAddress,
to: email,
from,
subject: `Sign in to ${siteName}`,
text: text({ url, siteName }),
html: html({ url, siteName })
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
if (error) {
console.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
}
return resolve()
@@ -112,33 +126,61 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
}
// Email HTML body
const html = ({ url, siteName }) => {
const buttonBackgroundColor = '#444444'
const html = ({ url, site, email }) => {
// Insert invisible space into domains and email address to prevent both the
// email address and the domain from being turned into a hyperlink by email
// clients like Outlook and Apple mail, as this is confusing because it seems
// like they are supposed to click on their email address to sign in.
const escapedEmail = `${email.replace(/\./g, '&#8203;.')}`
const escapedSite = `${site.replace(/\./g, '&#8203;.')}`
// Some simple styling options
const backgroundColor = '#f9f9f9'
const textColor = '#444444'
const mainBackgroundColor = '#ffffff'
const buttonBackgroundColor = '#346df1'
const buttonBorderColor = '#346df1'
const buttonTextColor = '#ffffff'
// Uses tables for layout and inline CSS due to email client limitations
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>
<body style="background: ${backgroundColor};">
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
<strong>${escapedSite}</strong>
</td>
</tr>
</table>
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
<tr>
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
Sign in as <strong>${escapedEmail}</strong>
</td>
</tr>
<tr>
<td align="center" style="padding: 20px 0;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 5px;" 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: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
If you did not request this email you can safely ignore it.
</td>
</tr>
</table>
</body>
`
}
// 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`
// Email text body  fallback for email clients that don't render HTML
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
```
:::tip
If you want to generate email-client compatible HTML from React, check out https://mjml.io
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
:::

View File

@@ -3,15 +3,15 @@ id: facebook
title: Facebook
---
## API Documentation
## Documentation
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
## App Configuration
## Configuration
https://developers.facebook.com/apps/
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -27,4 +27,8 @@ providers: [
:::tip
Production applications cannot use localhost URLs to sign in with Facebook. You need to use a dedicated development application in Facebook to use **localhost** callback URLs.
:::
:::tip
Email address may not be returned for accounts created on mobile.
:::

View File

@@ -3,15 +3,15 @@ id: github
title: GitHub
---
## API Documentation
## Documentation
https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
## App Configuration
## Configuration
https://github.com/settings/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -23,7 +23,12 @@ providers: [
})
}
...
```
:::warning
Only allows one callback URL. May not return email address if privacy enabled.
Only allows one callback URL per Client ID / Client Secret.
:::
:::tip
Email address is not returned if privacy settings are enabled.
:::

View File

@@ -3,15 +3,15 @@ id: gitlab
title: GitLab
---
## API Documentation
## Documentation
https://docs.gitlab.com/ee/api/oauth2.html
## App Configuration
## Configuration
https://gitlab.com/profile/applications
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -24,6 +24,7 @@ providers: [
}
...
```
:::tip
Enable the *"read_user"* option in scope if you want to save the users email address on sign up.
:::

View File

@@ -3,15 +3,15 @@ id: google
title: Google
---
## API Documentation
## Documentation
https://developers.google.com/identity/protocols/oauth2
## App Configuration
## Configuration
https://console.developers.google.com/apis/credentials
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -21,5 +21,14 @@ providers: [
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
})
}
]
...
```
:::warning
Unlike most other providers, Google only Provide the Refresh Token on first sign in.
:::
:::tip
Google also return an `email_verified` boolean property in the OAuth profile.
:::

View File

@@ -3,15 +3,12 @@ id: identity-server4
title: IdentityServer4
---
## API Documentation
## Documentation
https://identityserver4.readthedocs.io/en/latest/
## App Configuration
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -29,12 +26,13 @@ providers: [
...
```
## Demo IdentityServer
## Example using Demo IdentityServer
https://demo.identityserver.io/
The configuration below is for the demo server at https://demo.identityserver.io/
**The demo below is using a live server! You can copy and paste the values below and it will work out of the box.
This intended as an example only. (use either <b>bob/bob</b>, <b>alice/alice</b>)
If you want to try it out, you can copy and paste the configuration below.
You can sign in to the demo service with either <b>bob/bob</b> or <b>alice/alice</b>.
```js
import Providers from `next-auth/providers`

View File

@@ -0,0 +1,25 @@
---
id: linkedin
title: LinkedIn
---
## Documentation
https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow
## Configuration
https://www.linkedin.com/developers/apps/
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.LinkedIn({
clientId: process.env.LINKEDIN_CLIENT_ID,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET
})
]
...

View File

@@ -3,15 +3,15 @@ id: mixer
title: Mixer
---
## API Documentation
## Documentation
https://dev.mixer.com/reference/oauth
## App Configuration
## Configuration
https://mixer.com/lab/oauth
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,11 @@ id: okta
title: Okta
---
## API Documentation
## Documentation
https://developer.okta.com/docs/reference/api/oidc
## App Configuration
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: slack
title: Slack
---
## API Documentation
## Documentation
https://api.slack.com
## App Configuration
## Configuration
https://api.slack.com/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -0,0 +1,26 @@
---
id: spotify
title: Spotify
---
## Documentation
https://developer.spotify.com/documentation
## Configuration
https://developer.spotify.com/dashboard/applications
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Spotify({
clientId: process.env.SPOTIFY_CLIENT_ID,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET
})
}
...
```

View File

@@ -3,15 +3,15 @@ id: twitch
title: Twitch
---
## API Documentation
## Documentation
https://dev.twitch.tv/docs/authentication
## App Configuration
## Configuration
https://dev.twitch.tv/console/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,15 +3,15 @@ id: twitter
title: Twitter
---
## API Documentation
## Documentation
https://developer.twitter.com
## App Configuration
## Configuration
https://developer.twitter.com/en/apps
## Usage
## Example
```js
import Providers from `next-auth/providers`
@@ -26,7 +26,7 @@ providers: [
```
:::tip
Enable the *"Request email address from users"* option in your app permissions if you want to save the users email address on sign up.
You must enable the *"Request email address from users"* option in your app permissions if you want to obtain the users email address.
:::
![twitter](https://user-images.githubusercontent.com/7902980/83944068-1640ca80-a801-11ea-959c-0e744e2144f7.PNG)

View File

@@ -3,15 +3,15 @@ id: yandex
title: Yandex
---
## API Documentation
## Documentation
https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/
## App Configuration
## Configuration
https://oauth.yandex.com/client/new
## Usage
## Example
```js
import Providers from `next-auth/providers`

View File

@@ -3,23 +3,20 @@ id: adapters
title: Database Adapters
---
An "*Adapter*" in NextAuth.js is the thing that connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
You do not need to specify an adapter explicltly unless you want to use advanced options such as custom models or schemas, or if you are creating a custom adapter to connect to a database that is not one of the supported datatabases.
You do not need to specify an adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma adapter instead of the default TypeORM adapter, or if you are creating a custom adapter to connect to a database that is not one of the supported databases.
:::tip
*The **adapter** option is currently considered advanced usage intended for use by NextAuth.js contributors.*
:::
### Database Schemas
## TypeORM (default adapter)
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
NextAuth.js comes with a default adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the database driver you want to use to your project and tell NextAuth.js to use it.
* [MySQL Schema](/schemas/mysql)
* [Postgres Schema](/schemas/postgres)
The default adapter comes with predefined models for **Users**, **Sessions**, **Account Linking** and **Verification Emails**. You can extend or replace the default models and schemas, or even provide your adapter to handle reading/writing from the database (or from multiple databases).
## TypeORM Adapter
If you have an existing database / user management system or want to use a database that isn't supported out of the box you can create and pass your own adapter to handle actions like `createAccount`, `deleteAccount`, (etc) and do not have to use the built in models.
If you are using a database that is not supported out of the box, or if you want to use NextAuth.js with an existing database, or have a more complex setup with accounts and sessions spread across different systems then you can pass your own methods to be called for user and session creation / deletion (etc).
NextAuth.js comes with a default adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
The default adapter is the TypeORM adapter, the following configuration options are exactly equivalent.
@@ -47,178 +44,188 @@ adapter: Adapters.TypeORM.Adapter({
})
```
## Custom adapters
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM adapter. You can use these models in your own code.
Using a custom adapter you can connect to any database backend or even several different databases.
:::tip
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
Creating a custom adapter is considerable undertaking and will require some trial and error and some reverse engineering as it is not currently well documented. The hope and expectation is to grow both the number of included (and third party) adapters over time.
However, it should not be enabled against production databases as may cause dataloss if the configured schema does not match the expected schema!
:::
An adapter in NextAuth.js is a function which returns an async `getAdapter()` method, which in turn returns a Promise with a list of functions used to handle operations such as creating user, linking a user and an OAuth account or handling reading and writing sessions.
## Prisma Adapter
It uses this approach to allow database connection logic to live in the `getAdapter()` method. By calling the function just before an action needs to happen, it is possible to check database connection status and handle connecting / reconnecting to a database as required.
You can also use NextAuth.js with [Prisma](https://www.prisma.io/docs/).
### Required methods
To use this adapter, you need to install Prisma Client and Prisma CLI:
These methods are required for all sign in flows:
```
npm i @prisma/client
npm add -D @prisma/cli
```
* createUser
* getUser
* getUserByEmail
* getUserByProviderAccountId
* linkAccount
* createSession
* getSession
* updateSession
* deleteSession
Configure your NextAuth.js to use the Prisma adapter:
These methods are required to support email / passwordless sign in:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import Adapters from 'next-auth/adapters'
import { PrismaClient } from '@prisma/client'
* createVerificationRequest
* getVerificationRequest
* deleteVerificationRequest
const prisma = new PrismaClient()
### Unimplemented methods
These methods will be required in a future release, but are not yet invoked:
* getUserByCredentials
* updateUser
* deleteUser
* unlinkAccount
### Example code
An example of adapter structure is shown below:
```js
const Adapter = (config, options = {}) => {
async function getAdapter (appOptions) {
async function createUser (profile) {
return null
}
async function getUser (id) {
return null
}
async function getUserByEmail (email) {
return null
}
async function getUserByProviderAccountId (
providerId,
providerAccountId
) {
return null
}
async function getUserByCredentials (credentials) {
return null
}
async function updateUser (user) {
return null
}
async function deleteUser (userId) {
return null
}
async function linkAccount (
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires
) {
return null
}
async function unlinkAccount (
userId,
providerId,
providerAccountId
) {
return null
}
async function createSession (user) {
return null
}
async function getSession (sessionToken) {
return null
}
async function updateSession (
session,
force
) {
return null
}
async function deleteSession (sessionToken) {
return null
}
async function createVerificationRequest (
identifier,
url,
token,
secret,
provider
) {
return null
}
async function getVerificationRequest (
identifier,
token,
secret,
provider
) {
return null
}
async function deleteVerificationRequest (
identifier,
token,
secret,
provider
) {
return null
}
return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
getUserByCredentials,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
const options = {
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
})
}
return {
getAdapter
}
],
adapter: Adapters.Prisma.Adapter({ prisma }),
}
export default {
Adapter
export default (req, res) => NextAuth(req, res, options)
```
:::tip
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
:::
### Prisma Schema
Create a `schema.prisma` file similar to this one:
```json title="schema.prisma"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Account {
id Int @default(autoincrement()) @id
compoundId String @unique @map(name: "compound_id")
userId Int @map(name: "user_id")
providerType String @map(name: "provider_type")
providerId String @map(name: "provider_id")
providerAccountId String @map(name: "provider_account_id")
refreshToken String? @map(name: "refresh_token")
accessToken String? @map(name: "access_token")
accessTokenExpires DateTime? @map(name: "access_token_expires")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")
@@map(name: "accounts")
}
model Session {
id Int @default(autoincrement()) @id
userId Int @map(name: "user_id")
expires DateTime
sessionToken String @unique @map(name: "session_token")
accessToken String @unique @map(name: "access_token")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "sessions")
}
model User {
id Int @default(autoincrement()) @id
name String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
image String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "users")
}
model VerificationRequest {
id Int @default(autoincrement()) @id
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}
```
:::note
Set the `datasource` option appropriately for your database:
```json title="schema.prisma"
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
```
```json title="schema.prisma"
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
```
:::
### Generate Client
Once you have saved your schema, you can run the Prisma CLI to generate the Prisma Client:
```
npx @prisma/cli generate
```
### Custom Models
You can add properties to the schema and map them to any database colum names you wish, but you should not change the base properties or types defined in the example schema.
The model names themselves can be changed with a configuration option, and the datasource can be changed to anything supported by Prisma.
You can use custom model names by using the `modelMapping` option (shown here with default values).
```javascript title="pages/api/auth/[...nextauth].js"
...
adapter: Adapters.Prisma.Adapter({
prisma,
modelMapping: {
User: 'user',
Account: 'account',
Session: 'session',
VerificationRequest: 'verificationRequest'
}
})
...
```
:::tip
If you experience issues with Prisma opening too many database connections opening in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
```javascript title="pages/api/auth/[...nextauth].js"
let prisma
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
```
:::
## Custom Adapter
See the tutorial for [creating a database adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom adapter.

View File

@@ -3,19 +3,18 @@ id: models
title: Models
---
## Overview
Models in NextAuth.js are built for ANSI SQL but are polymorphic and are transformed to adapt to the database being used; there is some variance in specific data types (e.g. for datetime, text fields, etc) but they are functionally the same with as much parity in behaviour as possible.
This a description of the models and data structure used by NextAuth.js default database adapter.
All table/collection names in the built in models are are plural, and all table names and column names use `snake_case` when used with an SQL database and `camelCase` when used with Document database.
You are free to define your own models and schemas if you want to use a custom database adapter.
In NextAuth.js all table/collection names are plural, and all table names and column names use snake_case when used with an SQL database and camelCase when used with Document database. Indexes are declared on properties where appropriate.
:::tip
Models in NextAuth.js are built for ANSI SQL but are polymorphic and are transformed to adapt to the database being used; there is some variance in specific data types (e.g. for datetime, text fields, etc).
:::note
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
:::
### User
---
## User
Table: `users`
@@ -31,7 +30,7 @@ If a user first signs in with OAuth then their email address is automatically po
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured).
:::
### Account
## Account
Table: `accounts`
@@ -41,7 +40,7 @@ The Account model is for information about OAuth accounts associated with a User
A single User can have multiple Accounts, each Account can only have one User.
### Session
## Session
Table: `sessions`
@@ -51,7 +50,7 @@ The Session model is used for database sessions. It is not used if JSON Web Toke
A single User can have multiple Sessions, each Session can only have one User.
### Verification Request
## Verification Request
Table: `verification_requests`
@@ -61,29 +60,4 @@ The Verification Request model is used to store tokens for passwordless sign in
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
:::note
See `src/adapters/typeorm/models` for the source for the current models and schemas.
:::
---
## Schemas
### MySQL
* See [MySQL schema documentation](/schemas/mysql) for details.
### Postgres
* See [Postgres schema documentation](/schemas/postgres) for details.
### MongoDB
MongoDB does not use schemas in the same way as most RDBMS databases, but the objects stored in MongoDB use similar datatypes to SQL, with some differences:
* ID fields are of type `ObjectID` rather than `int`
* By convention all collection names and object properties are `camelCase` rather than `snake_case`
* A sparse index is used on the User `email` property to allow it to not be specified, while enforcing uniqueness if it is - this ensures it is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property
* All timestamps are stored as `ISODate()` in MongoDB, all timestamps on all models are stored in UTC (aka Zulu time)
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).

View File

@@ -0,0 +1,22 @@
---
id: mongodb
title: MongoDB
---
MongoDB is a document database and does not use schemas in the same way as most RDBMS databases.
**In MongoDB as collections and indexes are created automatically.**
## Objects in MonogDB
Objects stored in MongoDB use similar datatypes to SQL, with some differences:
1. ID fields are of type `ObjectID` rather than type `int`.
2. All collection names and property names use `camelCase` rather than `snake_case`.
3. All timestamps are stored as `ISODate()` in MongoDB and all date/time values are stored in UTC.
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.

View File

@@ -1,169 +1,87 @@
---
id: mysql
title: MySQL Schema
title: MySQL
---
The schema generated for a MySQL database when using the built-in models.
Schema for a MySQL database.
:::note
When using MySQL the timezone is set to `Z` (aka Zulu time / UTC / GMT) in the adapter and all timestamps on all models are stored in UTC.
When using a MySQL database with the default adapter (TypeORM) all timestamp columns use 6 digits of precision (unless another value for `precision` is specified in the schema) and the timezone is set to `Z` (aka Zulu Time / UTC) and all timestamps are stored in UTC.
:::
## User
```sql
CREATE TABLE accounts
(
id INT NOT NULL AUTO_INCREMENT,
compound_id VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
provider_type VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT,
access_token_expires TIMESTAMP(6),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
```json
"users": {
"id": {
"type": "int",
"nullable": false
},
"name": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"email": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"email_verified": {
"type": "timestamp",
"nullable": true,
"default": null
},
"image": {
"type": "varchar(255)",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
}
```
CREATE TABLE sessions
(
id INT NOT NULL AUTO_INCREMENT,
user_id INTEGER NOT NULL,
expires TIMESTAMP(6) NOT NULL,
session_token VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
## Account
CREATE TABLE users
(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255),
email VARCHAR(255),
email_verified TIMESTAMP(6),
image VARCHAR(255),
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
```json
"accounts": {
"id": {
"type": "int",
"nullable": false
},
"compound_id": {
"type": "varchar(255)",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"provider_type": {
"type": "varchar(255)",
"nullable": false
},
"provider_id": {
"type": "varchar(255)",
"nullable": false
},
"provider_account_id": {
"type": "varchar(255)",
"nullable": false
},
"refresh_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token_expires": {
"type": "timestamp",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
}
```
CREATE TABLE verification_requests
(
id INT NOT NULL AUTO_INCREMENT,
identifier VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
expires TIMESTAMP(6) NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (id)
);
## Session
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
```json
"sessions": {
"id": {
"type": "int",
"nullable": false
},
"user_id": {
"type": "int",
"nullable": false
},
"expires": {
"type": "timestamp",
"nullable": false
},
"session_token": {
"type": "varchar(255)",
"nullable": false
},
"access_token": {
"type": "varchar(255)",
"nullable": false
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
}
```
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
## Verification Request
CREATE INDEX provider_id
ON accounts(provider_id);
```json
"verification_requests": {
"id": {
"type": "int",
"nullable": false
},
"identifier": {
"type": "varchar(255)",
"nullable": false
},
"token": {
"type": "varchar(255)",
"nullable": false
},
"expires": {
"type": "timestamp",
"nullable": false
},
"created_at": {
"type": "timestamp(6)",
"nullable": false
},
"updated_at": {
"type": "timestamp(6)",
"nullable": false
}
}
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```

View File

@@ -1,169 +1,90 @@
---
id: postgres
title: Postgres Schema
title: Postgres
---
The schema generated for a Postgres database when using the built-in models.
Schema for a Postgres database.
:::note
When using Postgres all timestamps on all models use the type `timestamp with time zone` (aka `timestamptz`) to store timestamps in UTC.
When using a Postgres database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `timestamp with time zone`/`timestamptz` and all timestamps are stored in UTC.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
## User
```sql
CREATE TABLE accounts
(
id SERIAL,
compound_id VARCHAR(255) NOT NULL,
user_id INTEGER NOT NULL,
provider_type VARCHAR(255) NOT NULL,
provider_id VARCHAR(255) NOT NULL,
provider_account_id VARCHAR(255) NOT NULL,
refresh_token TEXT,
access_token TEXT,
access_token_expires TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
```json
"users": {
"id": {
"type": "integer",
"nullable": false
},
"name": {
"type": "character varying",
"nullable": true,
"default": null
},
"email": {
"type": "character varying",
"nullable": true,
"default": null
},
"email_verified": {
"type": "timestamp with time zone",
"nullable": true,
"default": null
},
"image": {
"type": "character varying",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
}
```
CREATE TABLE sessions
(
id SERIAL,
user_id INTEGER NOT NULL,
expires TIMESTAMPTZ NOT NULL,
session_token VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
## Account
CREATE TABLE users
(
id SERIAL,
name VARCHAR(255),
email VARCHAR(255),
email_verified TIMESTAMPTZ,
image VARCHAR(255),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
```json
"accounts": {
"id": {
"type": "integer",
"nullable": false
},
"compound_id": {
"type": "character varying",
"nullable": false
},
"user_id": {
"type": "integer",
"nullable": false
},
"provider_type": {
"type": "character varying",
"nullable": false
},
"provider_id": {
"type": "character varying",
"nullable": false
},
"provider_account_id": {
"type": "character varying",
"nullable": false
},
"refresh_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token": {
"type": "text",
"nullable": true,
"default": null
},
"access_token_expires": {
"type": "timestamp with time zone",
"nullable": true,
"default": null
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
}
```
CREATE TABLE verification_requests
(
id SERIAL,
identifier VARCHAR(255) NOT NULL,
token VARCHAR(255) NOT NULL,
expires TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
## Session
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
```json
"sessions": {
"id": {
"type": "integer",
"nullable": false
},
"user_id": {
"type": "integer",
"nullable": false
},
"expires": {
"type": "timestamp with time zone",
"nullable": false
},
"session_token": {
"type": "character varying",
"nullable": false
},
"access_token": {
"type": "character varying",
"nullable": false
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
}
```
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
## Verification Request
CREATE INDEX provider_id
ON accounts(provider_id);
```json
"verification_requests": {
"id": {
"type": "integer",
"nullable": false
},
"identifier": {
"type": "character varying",
"nullable": false
},
"token": {
"type": "character varying",
"nullable": false
},
"expires": {
"type": "timestamp with time zone",
"nullable": false
},
"created_at": {
"type": "timestamp with time zone",
"nullable": false
},
"updated_at": {
"type": "timestamp with time zone",
"nullable": false
}
}
```
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```

23
www/docs/tutorials.md Normal file
View File

@@ -0,0 +1,23 @@
---
id: tutorials
title: Tutorials and Explainers
---
### How to restrict access
* [Securing pages and API routes](tutorials/securing-pages-and-api-routes)
### How to use custom models
* [Custom models with TypeORM](tutorials/typeorm-custom-models)
### How to use custom adapters
* [Creating a database adapter](tutorials/creating-a-database-adapter)
### How to write tests
* [Testing with Cypress](tutorials/testing-with-cypress)

View File

@@ -0,0 +1,180 @@
---
id: creating-a-database-adapter
title: Creating a database adapter
---
Using a custom adapter you can connect to any database backend or even several different databases.
Creating a custom adapter can be considerable undertaking and will require some trial and error and some reverse engineering using the built-in adapters for reference.
## How to create an adapter
From an implementation perspective, an adapter in NextAuth.js is a function which returns an async `getAdapter()` method, which in turn returns a Promise with a list of functions used to handle operations such as creating user, linking a user and an OAuth account or handling reading and writing sessions.
It uses this approach to allow database connection logic to live in the `getAdapter()` method. By calling the function just before an action needs to happen, it is possible to check database connection status and handle connecting / reconnecting to a database as required.
_See the code below for practical example._
### Required methods
These methods are required for all sign in flows:
* createUser
* getUser
* getUserByEmail
* getUserByProviderAccountId
* linkAccount
* createSession
* getSession
* updateSession
* deleteSession
* updateUser
These methods are required to support email / passwordless sign in:
* createVerificationRequest
* getVerificationRequest
* deleteVerificationRequest
### Unimplemented methods
These methods will be required in a future release, but are not yet invoked:
* getUserByCredentials
* deleteUser
* unlinkAccount
### Example code
```js
const Adapter = (config, options = {}) => {
async function getAdapter (appOptions) {
async function createUser (profile) {
return null
}
async function getUser (id) {
return null
}
async function getUserByEmail (email) {
return null
}
async function getUserByProviderAccountId (
providerId,
providerAccountId
) {
return null
}
async function getUserByCredentials (credentials) {
return null
}
async function updateUser (user) {
return null
}
async function deleteUser (userId) {
return null
}
async function linkAccount (
userId,
providerId,
providerType,
providerAccountId,
refreshToken,
accessToken,
accessTokenExpires
) {
return null
}
async function unlinkAccount (
userId,
providerId,
providerAccountId
) {
return null
}
async function createSession (user) {
return null
}
async function getSession (sessionToken) {
return null
}
async function updateSession (
session,
force
) {
return null
}
async function deleteSession (sessionToken) {
return null
}
async function createVerificationRequest (
identifier,
url,
token,
secret,
provider
) {
return null
}
async function getVerificationRequest (
identifier,
token,
secret,
provider
) {
return null
}
async function deleteVerificationRequest (
identifier,
token,
secret,
provider
) {
return null
}
return Promise.resolve({
createUser,
getUser,
getUserByEmail,
getUserByProviderAccountId,
getUserByCredentials,
updateUser,
deleteUser,
linkAccount,
unlinkAccount,
createSession,
getSession,
updateSession,
deleteSession,
createVerificationRequest,
getVerificationRequest,
deleteVerificationRequest
})
}
return {
getAdapter
}
}
export default {
Adapter
}
```

View File

@@ -0,0 +1,138 @@
---
id: securing-pages-and-api-routes
title: Securing pages and API routes
---
You can easily protect client and server side side rendered pages and API routes with NextAuth.js.
_You can find working examples of the approaches shown below in the [example project](https://github.com/iaincollins/next-auth-example/)._
:::tip
The methods `getSession()` and `getToken()` both return an `object` if a session is valid and `null` if a session is invalid or has expired.
:::
## Securing Pages
### Client Side
If data on a page is fetched using calls to secure API routes - i.e. routes which use `getSession()` or `getToken()` to access the session - you can use the `useSession` React Hook to secure pages.
```js title="pages/client-side-example.js"
import { useSession, getSession } from 'next-auth/client'
export default function Page() {
const [ session, loading ] = useSession()
if (loading) return null
if (!loading && !session) return <p>Access Denied</p>
return (
<>
<h1>Protected Page</h1>
<p>You can view this page because you are signed in.</p>
</>
)
}
```
### Server Side
You can protect server side rendered pages using the `getSession()` method.
```js title="pages/server-side-example.js"
import { useSession, getSession } from 'next-auth/client'
export default function Page() {
const [ session, loading ] = useSession()
if (typeof window !== 'undefined' && loading) return null
if (!session) return <p>Access Denied</p>
return (
<>
<h1>Protected Page</h1>
<p>You can view this page because you are signed in.</p>
</>
)
}
export async function getServerSideProps(context) {
const session = await getSession(context)
return {
props: { session }
}
}
```
:::tip
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately avalible on page load to `useSession`.
```js title="pages/_app.js"
import { Provider } from 'next-auth/client'
export default ({ Component, pageProps }) => {
return (
<Provider session={pageProps.session} >
<Component {...pageProps} />
</Provider>
)
}
```
:::
## Securing API Routes
### Using getSession()
You can protect API routes using the `getSession()` method.
```js title="pages/api/get-session-example.js"
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
// Signed in
console.log('Session', JSON.stringify(session, null, 2))
} else {
// Not Signed in
res.status(401)
}
res.end()
}
```
### Using getToken()
If you are using JSON Web Tokens you can use the `getToken()` helper to access the contents of the JWT without having to handle JWT decryption / verification yourself. This method can only be used server side.
```js title="pages/api/get-token-example.js"
// This is an example of how to read a JSON Web Token from an API route
import jwt from 'next-auth/jwt'
const secret = process.env.SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
if (token) {
// Signed in
console.log('JSON Web Token', JSON.stringify(token, null, 2))
} else {
// Not Signed in
res.status(401)
}
res.end()
}
```
:::tip
You can use the `getToken()` helper function in any application as long as you set the `NEXTAUTH_URL` environment variable and the application is able to read the JWT cookie (e.g. is on the same domain).
:::
:::note
Pass `getToken` the same value for `secret` as specified in `pages/api/auth/[...nextauth].js`.
See [the documentation for the JWT option](/configuration/options#jwt) for more information.
:::

View File

@@ -0,0 +1,124 @@
---
id: testing-with-cypress
title: Testing with Cypress
---
To test an implementation of NextAuth.js, we encourage you to use [Cypress](https://cypress.io).
## Setting up Cypress
To get started, install the dependencies:
`npm install --save-dev cypress cypress-social-login @testing-library/cypress`
:::note
If you are using username/password based login, you will not need the `cypress-social-login` dependency.
:::
Cypress will install and initialize the folder structure with example integration tests, a folder for plugins, etc.
Next you will have to create some configuration files for Cypress.
First, the primary cypress config:
```js title="cypress.json"
{
"baseUrl": "http://localhost:3000",
"chromeWebSecurity": false
}
```
This initial Cypress config will tell Cypress where to find your site on initial launch as well as allow it to open up URLs at domains that aren't your page, for example to be able to login to a social provider.
Second, a cypress file for environment variables. These can be defined in `cypress.json` under the key `env` as well, however since we're storing username / passwords in here we should keep those in a separate file and only commit `cypress.json` to version control, not `cypress.env.json`.
```js title="cypress.env.json"
{
"GOOGLE_USER": "username@company.com",
"GOOGLE_PW": "password",
"COOKIE_NAME": "__Secure-next-auth.session-token",
"SITE_NAME": "http://localhost:3000"
}
```
You must change the login credentials you want to use, but you can also redefine the name of the `GOOGLE_*` variables if you're using a different provider. `COOKIE_NAME`, however, must be set to that value for NextAuth.js.
Third, if you're using the `cypress-social-login` plugin, you must add this to your `/cypress/plugins/index.js` file like so:
```js title="cypress/plugins/index.js"
const { GoogleSocialLogin } = require('cypress-social-logins').plugins
module.exports = (on, config) => {
on('task', {
GoogleSocialLogin: GoogleSocialLogin,
})
}
```
Finally, you can also add the following npm scripts to your `package.json`:
```json
"test:e2e:open": "cypress open",
"test:e2e:run": "cypress run"
```
## Writing a test
Once we've got all that configuration out of the way, we can begin writing tests to login using NextAuth.js.
The basic login test looks like this:
```js title="cypress/integration/login.js"
describe('Login page', () => {
before(() => {
cy.log(`Visiting https://company.tld`)
cy.visit('/')
})
it('Login with Google', () => {
const username = Cypress.env('GOOGLE_USER')
const password = Cypress.env('GOOGLE_PW')
const loginUrl = Cypress.env('SITE_NAME')
const cookieName = Cypress.env('COOKIE_NAME')
const socialLoginOptions = {
username,
password,
loginUrl,
headless: true,
logs: false,
isPopup: true,
loginSelector: `a[href="${Cypress.env(
'SITE_NAME'
)}/api/auth/signin/google"]`,
postLoginSelector: '.unread-count',
}
return cy
.task('GoogleSocialLogin', socialLoginOptions)
.then(({ cookies }) => {
cy.clearCookies()
const cookie = cookies
.filter(cookie => cookie.name === cookieName)
.pop()
if (cookie) {
cy.setCookie(cookie.name, cookie.value, {
domain: cookie.domain,
expiry: cookie.expires,
httpOnly: cookie.httpOnly,
path: cookie.path,
secure: cookie.secure,
})
Cypress.Cookies.defaults({
whitelist: cookieName,
})
cy.visit('/api/auth/signout')
cy.get('form').submit()
}
})
})
})
```
Things to note here include, that you must adjust the CSS selector defined under `postLoginSelector` to match a selector found on your page after the user is logged in. This is how Cypress knows whether it succeeded or not. Also, if you're using another provider, you will have to adjust the `loginSelector` URL.

View File

@@ -0,0 +1,85 @@
---
id: typeorm-custom-models
title: Custom models with TypeORM
---
NextAuth.js provides a set of [models and schemas](/schemas/models) for the built-in TypeORM adapter that you can easily extend.
You can use these models with MySQL, MariaDB, Postgres, MongoDB and SQLite.
## Creating custom models
```js title="models/User.js"
import Adapters from "next-auth/adapters"
// Extend the built-in models using class inheritance
export default class User extends Adapters.TypeORM.Models.User.model {
// You can extend the options in a model but you should not remove the base
// properties or change the order of the built-in options on the constructor
constructor(name, email, image, emailVerified) {
super(name, email, image, emailVerified)
}
}
export const UserSchema = {
name: "User",
target: User,
columns: {
...Adapters.TypeORM.Models.User.schema.columns,
// Adds a phoneNumber to the User schema
phoneNumber: {
type: "varchar",
nullable: true,
},
},
}
```
```js title="models/index.js"
// To make importing them easier, you can export all models from single file
import User, { UserSchema } from "./User"
export default {
User: {
model: User,
schema: UserSchema,
},
}
```
:::note
[View source for built-in TypeORM models and schemas](https://github.com/iaincollins/next-auth/tree/main/src/adapters/typeorm/models)
:::
## Using custom models
You can use custom models by specifying the TypeORM adapter explicitly and passing them as an option.
```js title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import Adapters from "next-auth/adapters"
import Models from "../../../models"
const options = {
providers: [
// Your providers
],
adapter: Adapters.TypeORM.Adapter(
// The first argument should be a database connection string or TypeORM config object
"mysql://username:password@127.0.0.1:3306/database_name",
// The second argument can be used to pass custom models and schemas
{
models: {
User: Models.User,
},
}
),
}
export default (req, res) => NextAuth(req, res, options)
```

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