Compare commits

...

18 Commits

Author SHA1 Message Date
Balázs Orbán
86baefdd9d feat(adapter): take away error handling from adapters (#1871) 2021-05-05 19:45:11 +02:00
Manish Chiniwalar
332e237c3e feat(provider): add Dropbox (#1756)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Adam Bergman <adam@fransvilhelm.com>
2021-05-05 19:42:55 +02:00
Mathis Møller
2fce08c0b5 docs(provider): Update IdentityServer 4 demo configuration (#1932) 2021-05-05 15:17:22 +02:00
Ernie Miranda
adf3fb669f docs(www): fix typo (#1922) 2021-05-04 19:34:06 +02:00
Anubisoft
5323be3594 fix(page): don't pass params to custom signout page (#1912)
* For the custom signout page addressed two issues with the query params being added to the signout url. A conditional check on the error value is now made before adding it as a query param. Also added a conditional check on the callbackUrl and if present that then gets appended as a query param to the signout api call.

* Changed fix for bug #192 to have no querystring params in the custom signout page url.

Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-03 22:43:38 +02:00
Balázs Orbán
6df0d04a1e fix(ts): tweak Adapter related types (#1914)
Contains the following squashed commits:

* fix(ts): make first adapter parameter non-optional
* fix(ts): make defaulted values non-optional internally
* test(ts): fix linting
2021-05-03 21:24:19 +02:00
Kristóf Poduszló
aa9c1e7c96 fix(ts): unset generics defaults for overriding (#1891)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-03 14:31:56 +02:00
Lluis Agusti
66473054f5 docs(provider): update providers documentation (#1900)
* docs(providers): update providers documentation

- delineate clearly the 3 provider types (oauth, email, credentials)
- make each section structure consistent
- update the option list for every provider type
- use emojis

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

To stay consistent with the rest of our documentation, for now we should not emojis on the sections of our documentation pages.

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

* docs(providers): clarify `params` option usage

* docs(providers): make names list inline

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-02 22:07:08 +02:00
Balázs Orbán
e8ddbc5c11 fix(build): export aliases from client (#1909) 2021-05-02 12:11:11 +02:00
Ernie Miranda
dfe4620056 docs(www): fix minor typo. (#1902)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-01 11:09:01 +02:00
leeoocca
848224e2c5 fix(ts): optional variables for custom provider options (#1876)
Contains the following squashed commits:

* fix optional variables for custom provider options
* revert some types for custom provider
* docs: client secret required in provider options
* Revert "docs: client secret required in provider options"
2021-05-01 10:46:04 +02:00
dependabot[bot]
aee376cc57 chore(deps): bump ssri from 6.0.1 to 6.0.2 in /www (#1901)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-30 21:17:23 +02:00
Amir Ali
0d2a81cd39 docs(www): syntax error on JWT_SESSION_ERROR code example (#1899) 2021-04-30 16:51:02 +02:00
Balázs Orbán
61e99c9489 fix(ts): wrap adapter option in ReturnType (#1887)
* fix(ts): wrap adapter option in ReturnType

* test(ts): fix adapter tests
2021-04-29 19:43:34 +02:00
Balázs Orbán
0eb4159737 fix(ts): fix updateSession return type 2021-04-28 22:23:13 +02:00
Balázs Orbán
9f0008375f fix(ts): fix createVerificationRequest type (#1877) 2021-04-28 22:16:09 +02:00
leeoocca
0cf1823e70 docs: fix typos in custom provider page (#1875)
* fix typo on custom provider options table

* fix typo in custom provider code example
2021-04-28 20:49:27 +02:00
Mohamed Ouyizme
7f39669053 feat(provider): add 42 School provider (#1872)
* feat(provider): add 42 School provider

* fix(docs): fix provider import

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

* fix(provider): change provider id

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

* fix(provider): change provider id

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

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-04-28 19:10:05 +02:00
30 changed files with 1192 additions and 303 deletions

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

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

View File

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

395
package-lock.json generated
View File

@@ -489,6 +489,227 @@
}
}
},
"@babel/helper-define-polyfill-provider": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.0.tgz",
"integrity": "sha512-JT8tHuFjKBo8NnaUbblz7mIu1nnvUDiHVjXXkulZULyidvo/7P6TY7+YqpV37IfF+KUFxmlK04elKtGKXaiVgw==",
"dev": true,
"requires": {
"@babel/helper-compilation-targets": "^7.13.0",
"@babel/helper-module-imports": "^7.12.13",
"@babel/helper-plugin-utils": "^7.13.0",
"@babel/traverse": "^7.13.0",
"debug": "^4.1.1",
"lodash.debounce": "^4.0.8",
"resolve": "^1.14.2",
"semver": "^6.1.2"
},
"dependencies": {
"@babel/code-frame": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
"dev": true,
"requires": {
"@babel/highlight": "^7.12.13"
}
},
"@babel/compat-data": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz",
"integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==",
"dev": true
},
"@babel/generator": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.0.tgz",
"integrity": "sha512-C6u00HbmsrNPug6A+CiNl8rEys7TsdcXwg12BHi2ca5rUfAs3+UwZsuDQSXnc+wCElCXMB8gMaJ3YXDdh8fAlg==",
"dev": true,
"requires": {
"@babel/types": "^7.14.0",
"jsesc": "^2.5.1",
"source-map": "^0.5.0"
}
},
"@babel/helper-compilation-targets": {
"version": "7.13.16",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz",
"integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.13.15",
"@babel/helper-validator-option": "^7.12.17",
"browserslist": "^4.14.5",
"semver": "^6.3.0"
}
},
"@babel/helper-function-name": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz",
"integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.12.13",
"@babel/template": "^7.12.13",
"@babel/types": "^7.12.13"
}
},
"@babel/helper-get-function-arity": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz",
"integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
}
},
"@babel/helper-module-imports": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz",
"integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==",
"dev": true,
"requires": {
"@babel/types": "^7.13.12"
}
},
"@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
},
"@babel/helper-split-export-declaration": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz",
"integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==",
"dev": true,
"requires": {
"@babel/types": "^7.12.13"
}
},
"@babel/helper-validator-identifier": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
"dev": true
},
"@babel/helper-validator-option": {
"version": "7.12.17",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz",
"integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==",
"dev": true
},
"@babel/highlight": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz",
"integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@babel/parser": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.0.tgz",
"integrity": "sha512-AHbfoxesfBALg33idaTBVUkLnfXtsgvJREf93p4p0Lwsz4ppfE7g1tpEXVm4vrxUcH4DVhAa9Z1m1zqf9WUC7Q==",
"dev": true
},
"@babel/template": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz",
"integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/parser": "^7.12.13",
"@babel/types": "^7.12.13"
}
},
"@babel/traverse": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz",
"integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.12.13",
"@babel/generator": "^7.14.0",
"@babel/helper-function-name": "^7.12.13",
"@babel/helper-split-export-declaration": "^7.12.13",
"@babel/parser": "^7.14.0",
"@babel/types": "^7.14.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
"to-fast-properties": "^2.0.0"
}
},
"browserslist": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
}
},
"caniuse-lite": {
"version": "1.0.30001219",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz",
"integrity": "sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.723",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz",
"integrity": "sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==",
"dev": true
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node-releases": {
"version": "1.1.71",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz",
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@babel/helper-explode-assignable-expression": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz",
@@ -2029,6 +2250,59 @@
}
}
},
"@babel/plugin-transform-runtime": {
"version": "7.13.15",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.15.tgz",
"integrity": "sha512-d+ezl76gx6Jal08XngJUkXM4lFXK/5Ikl9Mh4HKDxSfGJXmZ9xG64XT2oivBzfxb/eQ62VfvoMkaCZUKJMVrBA==",
"dev": true,
"requires": {
"@babel/helper-module-imports": "^7.13.12",
"@babel/helper-plugin-utils": "^7.13.0",
"babel-plugin-polyfill-corejs2": "^0.2.0",
"babel-plugin-polyfill-corejs3": "^0.2.0",
"babel-plugin-polyfill-regenerator": "^0.2.0",
"semver": "^6.3.0"
},
"dependencies": {
"@babel/helper-module-imports": {
"version": "7.13.12",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz",
"integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==",
"dev": true,
"requires": {
"@babel/types": "^7.13.12"
}
},
"@babel/helper-plugin-utils": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz",
"integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==",
"dev": true
},
"@babel/helper-validator-identifier": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz",
"integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==",
"dev": true
},
"@babel/types": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.0.tgz",
"integrity": "sha512-O2LVLdcnWplaGxiPBz12d0HcdN8QdxdsWYhz5LSeuukV/5mn2xUUc3gBeU4QBYPJ18g/UToe8F532XJ608prmg==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.14.0",
"to-fast-properties": "^2.0.0"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@babel/plugin-transform-shorthand-properties": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz",
@@ -2272,10 +2546,9 @@
}
},
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"dev": true,
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
@@ -2514,9 +2787,9 @@
"integrity": "sha512-rIIisYBvVtxDbY9Lbm+HOLbZyOaaEmtGc9wDN3tJLDUu3sLJOXNN7Pz29ThS+gf2lpMxXnfvk587hxGrnhCghQ=="
},
"@next-auth/typeorm-legacy-adapter": {
"version": "0.0.2-canary.116",
"resolved": "https://registry.npmjs.org/@next-auth/typeorm-legacy-adapter/-/typeorm-legacy-adapter-0.0.2-canary.116.tgz",
"integrity": "sha512-06knawdYdHkiMVw5GfR6Ku5ryITdaOLWIGCbRwAtgCZE5Pf6am+nhnqQVeGq18odu84SmzA+hTtkGSAYbR6MIA==",
"version": "0.0.2-canary.117",
"resolved": "https://registry.npmjs.org/@next-auth/typeorm-legacy-adapter/-/typeorm-legacy-adapter-0.0.2-canary.117.tgz",
"integrity": "sha512-sYJZPWMsM1ZTJcl749UojYDF4q8+ZiYcrR7rM4SACc0qiA9VBdOYUUMUMpQoazKOiwo1rWDKu4wHPt6CdV0ctQ==",
"requires": {
"crypto-js": "^4.0.0",
"require_optional": "^1.0.1",
@@ -4080,6 +4353,105 @@
"object.assign": "^4.1.0"
}
},
"babel-plugin-polyfill-corejs2": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.0.tgz",
"integrity": "sha512-9bNwiR0dS881c5SHnzCmmGlMkJLl0OUZvxrxHo9w/iNoRuqaPjqlvBf4HrovXtQs/au5yKkpcdgfT1cC5PAZwg==",
"dev": true,
"requires": {
"@babel/compat-data": "^7.13.11",
"@babel/helper-define-polyfill-provider": "^0.2.0",
"semver": "^6.1.1"
},
"dependencies": {
"@babel/compat-data": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz",
"integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==",
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"babel-plugin-polyfill-corejs3": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.0.tgz",
"integrity": "sha512-zZyi7p3BCUyzNxLx8KV61zTINkkV65zVkDAFNZmrTCRVhjo1jAS+YLvDJ9Jgd/w2tsAviCwFHReYfxO3Iql8Yg==",
"dev": true,
"requires": {
"@babel/helper-define-polyfill-provider": "^0.2.0",
"core-js-compat": "^3.9.1"
},
"dependencies": {
"browserslist": {
"version": "4.16.6",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz",
"integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001219",
"colorette": "^1.2.2",
"electron-to-chromium": "^1.3.723",
"escalade": "^3.1.1",
"node-releases": "^1.1.71"
}
},
"caniuse-lite": {
"version": "1.0.30001219",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001219.tgz",
"integrity": "sha512-c0yixVG4v9KBc/tQ2rlbB3A/bgBFRvl8h8M4IeUbqCca4gsiCfvtaheUssbnux/Mb66Vjz7x8yYjDgYcNQOhyQ==",
"dev": true
},
"colorette": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
"integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==",
"dev": true
},
"core-js-compat": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.11.1.tgz",
"integrity": "sha512-aZ0e4tmlG/aOBHj92/TuOuZwp6jFvn1WNabU5VOVixzhu5t5Ao+JZkQOPlgNXu6ynwLrwJxklT4Gw1G1VGEh+g==",
"dev": true,
"requires": {
"browserslist": "^4.16.5",
"semver": "7.0.0"
}
},
"electron-to-chromium": {
"version": "1.3.723",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.723.tgz",
"integrity": "sha512-L+WXyXI7c7+G1V8ANzRsPI5giiimLAUDC6Zs1ojHHPhYXb3k/iTABFmWjivEtsWrRQymjnO66/rO2ZTABGdmWg==",
"dev": true
},
"node-releases": {
"version": "1.1.71",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz",
"integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==",
"dev": true
},
"semver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
"integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
"dev": true
}
}
},
"babel-plugin-polyfill-regenerator": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.0.tgz",
"integrity": "sha512-J7vKbCuD2Xi/eEHxquHN14bXAW9CXtecwuLrOIDJtcZzTaPzV1VdEfoUf9AzcRBMolKUQKM9/GVojeh0hFiqMg==",
"dev": true,
"requires": {
"@babel/helper-define-polyfill-provider": "^0.2.0"
}
},
"babel-plugin-syntax-jsx": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
@@ -8903,6 +9275,12 @@
"integrity": "sha1-+CbJtOKoUR2E46yinbBeGk87cqk=",
"dev": true
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
"dev": true
},
"lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
@@ -15935,8 +16313,7 @@
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
"dev": true
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"regenerator-transform": {
"version": "0.14.5",

View File

@@ -30,12 +30,12 @@
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev:setup": "npm run build:css && cd app && npm i",
"dev": "cd app && npm run dev",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test": "echo \"Write some tests...\"; npm run test:types",
"test:types": "dtslint types",
@@ -61,6 +61,7 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "canary",
"@next-auth/typeorm-legacy-adapter": "canary",
"crypto-js": "^4.0.0",
@@ -91,6 +92,7 @@
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@semantic-release/commit-analyzer": "^8.0.1",

View File

@@ -0,0 +1,36 @@
import { UnknownError } from "../lib/errors"
/**
* Handles adapter induced errors.
* @param {import("types/adapters").AdapterInstance} adapter
* @param {import("types").LoggerInstance} logger
* @return {import("types/adapters").AdapterInstance}
*/
export default function adapterErrorHandler(adapter, logger) {
return Object.keys(adapter).reduce((acc, method) => {
const name = capitalize(method)
const code = upperSnake(name, adapter.displayName)
const adapterMethod = adapter[method]
acc[method] = async (...args) => {
try {
logger.debug(code, ...args)
return await adapterMethod(...args)
} catch (error) {
logger.error(`${code}_ERROR`, error)
const e = new UnknownError(error)
e.name = `${name}Error`
throw e
}
}
return acc
}, {})
}
function capitalize(s) {
return `${s[0].toUpperCase()}${s.slice(1)}`
}
function upperSnake(s, prefix = "ADAPTER") {
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
}

View File

@@ -355,6 +355,22 @@ function BroadcastChannel (name = 'nextauth.message') {
}
}
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases. These should be removed in a newer release, as it only
// creates problems for bundlers and adds confusion to users. TypeScript declarations
// will provide sufficient help when importing
export {
setOptions as options,
getSession as session,
getProviders as providers,
getCsrfToken as csrfToken,
signIn as signin,
signOut as signout
}
export default {
getSession,
getCsrfToken,

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

@@ -0,0 +1,20 @@
export default function FortyTwo(options) {
return {
id: '42-school',
name: '42 School',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
authorizationUrl:
'https://api.intra.42.fr/oauth/authorize?response_type=code',
profileUrl: 'https://api.intra.42.fr/v2/me',
profile: (profile) => ({
id: profile.id,
email: profile.email,
image: profile.image_url,
name: profile.usual_full_name,
}),
...options,
}
}

54
src/providers/dropbox.js Normal file
View File

@@ -0,0 +1,54 @@
/**
* @param {import("../server").Provider} options
* @example
*
* ```js
* // pages/api/auth/[...nextauth].js
* import Providers from `next-auth/providers`
* ...
* providers: [
* Providers.Dropbox({
* clientId: process.env.DROPBOX_CLIENT_ID,
* clientSecret: process.env.DROPBOX_CLIENT_SECRET
* })
* ]
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* ...
* <button onClick={() => signIn("dropbox")}>
* Sign in
* </button>
* ...
* ```
* *Resources:*
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/dropbox)
* - [Dropbox Documentation](https://developers.dropbox.com/oauth-guide)
* - [Configuration](https://www.dropbox.com/developers/apps)
*/
export default function Dropbox(options) {
return {
id: 'dropbox',
name: 'Dropbox',
type: 'oauth',
version: '2.0',
scope: 'account_info.read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
authorizationUrl:
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
profile: (profile) => {
return {
id: profile.account_id,
name: profile.name.display_name,
email: profile.email,
image: profile.profile_photo_url,
email_verified: profile.email_verified
}
},
protection: ["state", "pkce"],
...options
}
}

View File

@@ -158,9 +158,8 @@ async function NextAuthHandler (req, res, userOptions) {
return render.signin()
case 'signout':
if (pages.signOut) {
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
}
if (pages.signOut) return res.redirect(pages.signOut)
return render.signout()
case 'callback':
if (provider) {

View File

@@ -1,5 +1,6 @@
import { AccountNotLinkedError } from '../../lib/errors'
import dispatchEvent from '../lib/dispatch-event'
import { AccountNotLinkedError } from "../../lib/errors"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* This function handles the complex flow of signing users in, and either creating,
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
* All verification (e.g. OAuth flows or email address verificaiton flows) are
* done prior to this handler being called to avoid additonal complexity in this
* handler.
* @param {import("types").Session} sessionToken
* @param {import("types").Profile} profile
* @param {import("types").Account} account
* @param {import("types/internals").AppOptions} options
*/
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
export default async function callbackHandler(
sessionToken,
profile,
providerAccount,
options
) {
// Input validation
if (!profile) throw new Error('Missing profile')
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
if (!profile) throw new Error("Missing profile")
if (!providerAccount?.id || !providerAccount.type)
throw new Error("Missing or invalid provider account")
if (!["email", "oauth"].includes(providerAccount.type))
throw new Error("Provider not supported")
const {
adapter,
jwt,
events,
session: {
jwt: useJwtSession
}
session: { jwt: useJwtSession },
} = options
// If no adapter is configured then we don't have a database and cannot
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
user: profile,
account: providerAccount,
session: {}
session: {},
}
}
@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
deleteSession,
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
let session = null
let user = null
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
}
}
if (providerAccount.type === 'email') {
if (providerAccount.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
session,
user,
isNewUser
isNewUser,
}
} else if (providerAccount.type === 'oauth') {
} else if (providerAccount.type === "oauth") {
// If signing in with oauth account, check to see if the account exists already
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
const userByProviderAccountId = await getUserByProviderAccountId(
providerAccount.provider,
providerAccount.id
)
if (userByProviderAccountId) {
if (isSignedIn) {
// If the user is already signed in with this account, we don't need to do anything
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
return {
session,
user,
isNewUser
isNewUser,
}
}
// If the user is currently signed in, but the new account they are signing in
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
}
// If there is no active session, but the account being signed in with is already
// associated with a valid user then create session to sign the user in.
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
session = useJwtSession
? {}
: await createSession(userByProviderAccountId)
return {
session,
user: userByProviderAccountId,
isNewUser
isNewUser,
}
} else {
if (isSignedIn) {
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
// As they are already signed in, we don't need to do anything after linking them
return {
session,
user,
isNewUser
isNewUser,
}
}
@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
//
// OAuth providers should require email address verification to prevent this, but in
// practice that is not always the case; this helps protect against that.
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
if (userByEmail) {
// We end up here when we don't have an account with the same [provider].id *BUT*
// we do already have an account with the same email address as the one in the
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
await dispatchEvent(events.linkAccount, {
user,
providerAccount: providerAccount,
})
session = useJwtSession ? {} : await createSession(user)
isNewUser = true
return {
session,
user,
isNewUser
isNewUser,
}
}
}

View File

@@ -215,6 +215,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
*/
async function getOAuth2 (provider, accessToken, results) {
let url = provider.profileUrl
let httpMethod = 'GET'
const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) {
@@ -238,8 +239,15 @@ async function getOAuth2 (provider, accessToken, results) {
url = prepareProfileUrl({ provider, url, results })
}
/** Dropbox requires POST instead of GET
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
*/
if (provider.id === 'dropbox') {
httpMethod = 'POST'
}
return new Promise((resolve, reject) => {
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
if (error) {
return reject(error)
}

View File

@@ -1,22 +1,44 @@
import { randomBytes } from 'crypto'
import { randomBytes } from "crypto"
import adapterErrorHandler from "../../../adapters/error-handler"
export default async function email (email, provider, options) {
/**
*
* @param {string} email
* @param {import("types/providers").EmailConfig} provider
* @param {import("types/internals").AppOptions} options
* @returns
*/
export default async function email(email, provider, options) {
try {
const { baseUrl, basePath, adapter } = options
const { baseUrl, basePath, adapter, logger } = options
const { createVerificationRequest } = await adapter.getAdapter(options)
const { createVerificationRequest } = adapterErrorHandler(
await adapter.getAdapter(options),
logger
)
// Prefer provider specific secret, but use default secret if none specified
const secret = provider.secret || options.secret
// Generate token
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
const token =
(await provider.generateVerificationToken?.()) ??
randomBytes(32).toString("hex")
// Send email with link containing token (the unhashed version)
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
provider.id
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
// @TODO Create invite (send secret so can be hashed)
await createVerificationRequest(email, url, token, secret, provider, options)
await createVerificationRequest(
email,
url,
token,
secret,
provider,
options
)
// Return promise
return Promise.resolve()

View File

@@ -1,15 +1,15 @@
import oAuthCallback from '../lib/oauth/callback'
import callbackHandler from '../lib/callback-handler'
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* Handle callbacks from login services
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function callback (req, res) {
export default async function callback(req, res) {
const {
provider,
adapter,
@@ -22,21 +22,23 @@ export default async function callback (req, res) {
jwt,
events,
callbacks,
session: {
jwt: useJwtSession,
maxAge: sessionMaxAge
}
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
logger,
} = req.options
// Get session ID (if set)
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
if (provider.type === 'oauth') {
if (provider.type === "oauth") {
try {
const { profile, account, OAuthProfile } = await oAuthCallback(req)
try {
// Make it easier to debug when adding a new provider
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
logger.debug("OAUTH_CALLBACK_RESPONSE", {
profile,
account,
OAuthProfile,
})
// If we don't have a profile object then either something went wrong
// or the user cancelled signing in. We don't know which, so we just
@@ -56,52 +58,85 @@ export default async function callback (req, res) {
// (that just means it's a new user signing in for the first time).
let userOrProfile = profile
if (adapter) {
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
const { getUserByProviderAccountId } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
const userFromProviderAccountId = await getUserByProviderAccountId(
account.provider,
account.id
)
if (userFromProviderAccountId) {
userOrProfile = userFromProviderAccountId
}
}
try {
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
account,
OAuthProfile
)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
const { user, session, isNewUser } = await callbackHandler(
sessionToken,
profile,
account,
req.options
)
if (useJwtSession) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
OAuthProfile,
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
expires: session.expires || null,
...cookies.sessionToken.options,
})
}
await dispatchEvent(events.signIn, { user, account, isNewUser })
@@ -110,94 +145,145 @@ export default async function callback (req, res) {
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
return res.redirect(
`${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
// Callback URL is already verified at this point, so safe to use if specified
return res.redirect(callbackUrl || baseUrl)
} catch (error) {
if (error.name === 'AccountNotLinkedError') {
if (error.name === "AccountNotLinkedError") {
// If the email on the account is already linked, but not with this OAuth account
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
} else if (error.name === 'CreateUserError') {
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
return res.redirect(
`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`
)
} else if (error.name === "CreateUserError") {
return res.redirect(
`${baseUrl}${basePath}/error?error=OAuthCreateAccount`
)
}
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} catch (error) {
if (error.name === 'OAuthCallbackError') {
logger.error('CALLBACK_OAUTH_ERROR', error)
if (error.name === "OAuthCallbackError") {
logger.error("CALLBACK_OAUTH_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
}
logger.error('OAUTH_CALLBACK_ERROR', error)
logger.error("OAUTH_CALLBACK_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} else if (provider.type === 'email') {
} else if (provider.type === "email") {
try {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
const {
getVerificationRequest,
deleteVerificationRequest,
getUserByEmail,
} = adapterErrorHandler(await adapter.getAdapter(req.options), logger)
const verificationToken = req.query.token
const email = req.query.email
// Verify email and verification token exist in database
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
const invite = await getVerificationRequest(
email,
verificationToken,
secret,
provider
)
if (!invite) {
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
}
// If verification token is valid, delete verification request token from
// the database so it cannot be used again
await deleteVerificationRequest(email, verificationToken, secret, provider)
await deleteVerificationRequest(
email,
verificationToken,
secret,
provider
)
// If is an existing user return a user object (otherwise use placeholder)
const profile = await getUserByEmail(email) || { email }
const account = { id: provider.id, type: 'email', providerAccountId: email }
const profile = (await getUserByEmail(email)) || { email }
const account = {
id: provider.id,
type: "email",
providerAccountId: email,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
const signInCallbackResponse = await callbacks.signIn(
profile,
account,
{ email }
)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
// Sign user in
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
const { user, session, isNewUser } = await callbackHandler(
sessionToken,
profile,
account,
req.options
)
if (useJwtSession) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
profile,
isNewUser
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
} else {
// Save Session Token in cookie
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
expires: session.expires || null,
...cookies.sessionToken.options,
})
}
await dispatchEvent(events.signIn, { user, account, isNewUser })
@@ -206,55 +292,93 @@ export default async function callback (req, res) {
// e.g. option to send users to a new account landing page on initial login
// Note that the callback URL is preserved, so the journey can still be resumed
if (isNewUser && pages.newUser) {
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
return res.redirect(
`${pages.newUser}${
pages.newUser.includes("?") ? "&" : "?"
}callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
// Callback URL is already verified at this point, so safe to use if specified
return res.redirect(callbackUrl || baseUrl)
} catch (error) {
if (error.name === 'CreateUserError') {
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
if (error.name === "CreateUserError") {
return res.redirect(
`${baseUrl}${basePath}/error?error=EmailCreateAccount`
)
}
logger.error('CALLBACK_EMAIL_ERROR', error)
logger.error("CALLBACK_EMAIL_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
}
} else if (provider.type === 'credentials' && req.method === 'POST') {
} else if (provider.type === "credentials" && req.method === "POST") {
if (!useJwtSession) {
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
logger.error(
"CALLBACK_CREDENTIALS_JWT_ERROR",
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
)
return res
.status(500)
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
if (!provider.authorize) {
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
logger.error(
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
"Must define an authorize() handler to use credentials authentication provider"
)
return res
.status(500)
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const credentials = req.body
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
return res
.status(401)
.redirect(
`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(
provider.id
)}`
)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
const user = userObjectReturnedFromAuthorizeHandler
const account = { id: provider.id, type: 'credentials' }
const account = { id: provider.id, type: "credentials" }
try {
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
const signInCallbackResponse = await callbacks.signIn(
user,
account,
credentials
)
if (signInCallbackResponse === false) {
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
}
@@ -263,22 +387,33 @@ export default async function callback (req, res) {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString()
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
user,
account,
userObjectReturnedFromAuthorizeHandler,
false
)
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie expiry date
const cookieExpires = new Date()
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: cookieExpires.toISOString(),
...cookies.sessionToken.options,
})
await dispatchEvent(events.signIn, { user, account })
return res.redirect(callbackUrl || baseUrl)
}
return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`)
return res
.status(500)
.end(`Error: Callback for provider type ${provider.type} not supported`)
}

View File

@@ -1,13 +1,15 @@
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/**
* Return a session object (without any private fields)
* for Single Page App clients
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function session (req, res) {
const { cookies, adapter, jwt, events, callbacks } = req.options
export default async function session(req, res) {
const { cookies, adapter, jwt, events, callbacks, logger } = req.options
const useJwtSession = req.options.session.jwt
const sessionMaxAge = req.options.session.maxAge
const sessionToken = req.cookies[cookies.sessionToken.name]
@@ -24,7 +26,9 @@ export default async function session (req, res) {
// Generate new session expiry date
const sessionExpiresDate = new Date()
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
sessionExpiresDate.setTime(
sessionExpiresDate.getTime() + sessionMaxAge * 1000
)
const sessionExpires = sessionExpiresDate.toISOString()
// By default, only exposes a limited subset of information to the client
@@ -33,14 +37,17 @@ export default async function session (req, res) {
user: {
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null
image: decodedJwt.picture || null,
},
expires: sessionExpires
expires: sessionExpires,
}
// Pass Session and JSON Web Token through to the session callback
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
jwtPayload
)
// Return session payload as response
response = sessionPayload
@@ -49,17 +56,29 @@ export default async function session (req, res) {
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
expires: sessionExpires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
await dispatchEvent(events.session, {
session: sessionPayload,
jwt: jwtPayload,
})
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error('JWT_SESSION_ERROR', error)
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
logger.error("JWT_SESSION_ERROR", error)
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0,
})
}
} else {
try {
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
const { getUser, getSession, updateSession } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
const session = await getSession(sessionToken)
if (session) {
// Trigger update to session object to update session expiry
@@ -73,29 +92,38 @@ export default async function session (req, res) {
user: {
name: user.name,
email: user.email,
image: user.image
image: user.image,
},
accessToken: session.accessToken,
expires: session.expires
expires: session.expires,
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
user
)
// Return session payload as response
response = sessionPayload
// Set cookie again to update expiry
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
cookie.set(res, cookies.sessionToken.name, sessionToken, {
expires: session.expires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, { session: sessionPayload })
} else if (sessionToken) {
// If sessionToken was found set but it's not valid for a session then
// remove the sessionToken cookie from browser.
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0,
})
}
} catch (error) {
logger.error('SESSION_ERROR', error)
logger.error("SESSION_ERROR", error)
}
}

View File

@@ -1,35 +1,43 @@
import getAuthorizationUrl from '../lib/signin/oauth'
import emailSignin from '../lib/signin/email'
import logger from '../../lib/logger'
import getAuthorizationUrl from "../lib/signin/oauth"
import emailSignin from "../lib/signin/email"
import adapterErrorHandler from "../../adapters/error-handler"
/** Handle requests to /api/auth/signin */
export default async function signin (req, res) {
/**
* Handle requests to /api/auth/signin
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function signin(req, res) {
const {
provider,
baseUrl,
basePath,
adapter,
callbacks
callbacks,
logger,
} = req.options
if (!provider.type) {
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
}
if (provider.type === 'oauth' && req.method === 'POST') {
if (provider.type === "oauth" && req.method === "POST") {
try {
const authorizationUrl = await getAuthorizationUrl(req)
return res.redirect(authorizationUrl)
} catch (error) {
logger.error('SIGNIN_OAUTH_ERROR', error)
logger.error("SIGNIN_OAUTH_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
}
} else if (provider.type === 'email' && req.method === 'POST') {
} else if (provider.type === "email" && req.method === "POST") {
if (!adapter) {
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
}
const { getUserByEmail } = await adapter.getAdapter(req.options)
const { getUserByEmail } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
// Note: Technically the part of the email address local mailbox element
// (everything before the @ symbol) should be treated as 'case sensitive'
@@ -39,36 +47,43 @@ export default async function signin (req, res) {
const email = req.body.email?.toLowerCase() ?? null
// If is an existing user return a user object (otherwise use placeholder)
const profile = await getUserByEmail(email) || { email }
const account = { id: provider.id, type: 'email', providerAccountId: email }
const profile = (await getUserByEmail(email)) || { email }
const account = { id: provider.id, type: "email", providerAccountId: email }
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
const signInCallbackResponse = await callbacks.signIn(profile, account, {
email,
verificationRequest: true,
})
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
return res.redirect(error)
}
try {
await emailSignin(email, provider, req.options)
} catch (error) {
logger.error('SIGNIN_EMAIL_ERROR', error)
logger.error("SIGNIN_EMAIL_ERROR", error)
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
}
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`)
return res.redirect(
`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
provider.id
)}&type=${encodeURIComponent(provider.type)}`
)
}
return res.redirect(`${baseUrl}${basePath}/signin`)
}

View File

@@ -1,10 +1,14 @@
import * as cookie from '../lib/cookie'
import logger from '../../lib/logger'
import dispatchEvent from '../lib/dispatch-event'
import * as cookie from "../lib/cookie"
import dispatchEvent from "../lib/dispatch-event"
import adapterErrorHandler from "../../adapters/error-handler"
/** Handle requests to /api/auth/signout */
export default async function signout (req, res) {
const { adapter, cookies, events, jwt, callbackUrl } = req.options
/**
* Handle requests to /api/auth/signout
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function signout(req, res) {
const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options
const useJwtSession = req.options.session.jwt
const sessionToken = req.cookies[cookies.sessionToken.name]
@@ -18,7 +22,10 @@ export default async function signout (req, res) {
}
} else {
// Get session from database
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
const { getSession, deleteSession } = adapterErrorHandler(
await adapter.getAdapter(req.options),
logger
)
try {
// Dispatch signout event
@@ -33,14 +40,14 @@ export default async function signout (req, res) {
await deleteSession(sessionToken)
} catch (error) {
// If error, log it but continue
logger.error('SIGNOUT_ERROR', error)
logger.error("SIGNOUT_ERROR", error)
}
}
// Remove Session Token
cookie.set(res, cookies.sessionToken.name, '', {
cookie.set(res, cookies.sessionToken.name, "", {
...cookies.sessionToken.options,
maxAge: 0
maxAge: 0,
})
return res.redirect(callbackUrl)

22
types/adapters.d.ts vendored
View File

@@ -22,6 +22,8 @@ export default Adapters
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export interface AdapterInstance<U = User, P = Profile, S = Session> {
/** Used as a prefix for adapter related log messages. (Defaults to `ADAPTER_`) */
displayName?: string
createUser(profile: P): Promise<U>
getUser(id: string): Promise<U | null>
getUserByEmail(email: string): Promise<U | null>
@@ -49,9 +51,15 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
): Promise<void>
createSession(user: U): Promise<S>
getSession(sessionToken: string): Promise<S | null>
updateSession(session: S, force?: boolean): Promise<S>
updateSession(session: S, force?: boolean): Promise<S | null>
deleteSession(sessionToken: string): Promise<void>
createVerificationRequest?: SendVerificationRequest
createVerificationRequest?(
identifier: string,
url: string,
token: string,
secret: string,
provider: EmailConfig & { maxAge: number; from: string }
): Promise<void>
getVerificationRequest?(
identifier: string,
verificationToken: string,
@@ -112,13 +120,13 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export type Adapter<
C = Record<string, unknown>,
C = unknown,
O = Record<string, unknown>,
U = User,
P = Profile,
S = Session
U = unknown,
P = unknown,
S = unknown
> = (
config: C,
client: C,
options?: O
) => {
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>

12
types/index.d.ts vendored
View File

@@ -127,7 +127,7 @@ export interface NextAuthOptions {
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
*/
adapter?: Adapter
adapter?: ReturnType<Adapter>
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
@@ -180,7 +180,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: "auto" | "dark" | "light"
theme?: Theme
/**
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
@@ -215,6 +215,14 @@ export interface NextAuthOptions {
cookies?: CookiesOptions
}
/**
* Change the theme of the built-in pages.
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
* [Pages](https://next-auth.js.org/configuration/pages)
*/
export type Theme = "auto" | "dark" | "light"
/**
* Override any of the methods, and the rest will use the default logger.
*

View File

@@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "./utils"
import { NextAuthOptions } from ".."
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
import { AppProvider } from "../providers"
/** Options that are the same both in internal and user provided options. */
@@ -9,12 +9,7 @@ export type NextAuthSharedOptions =
| "events"
| "callbacks"
| "cookies"
| "secret"
| "adapter"
| "theme"
| "debug"
| "logger"
| "session"
export interface AppOptions
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
@@ -42,6 +37,11 @@ export interface AppOptions
provider?: AppProvider
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
theme: Theme
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
}
export interface NextAuthRequest extends NextApiRequest {

16
types/providers.d.ts vendored
View File

@@ -29,7 +29,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
scope: string
params: { grant_type: string }
accessTokenUrl: string
requestTokenUrl: string
requestTokenUrl?: string
authorizationUrl: string
profileUrl: string
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
@@ -64,9 +64,11 @@ export type OAuthProviderType =
| "Bungie"
| "Cognito"
| "Discord"
| "Dropbox"
| "EVEOnline"
| "Facebook"
| "FACEIT"
| "FortyTwo"
| "Foursquare"
| "FusionAuth"
| "GitHub"
@@ -132,13 +134,13 @@ export interface EmailConfigServerOptions {
}
}
export type SendVerificationRequest = (
identifier: string,
url: string,
baseUrl: string,
token: string,
export type SendVerificationRequest = (params: {
identifier: string
url: string
baseUrl: string
token: string
provider: EmailConfig
) => Awaitable<void>
}) => Awaitable<void>
export interface EmailConfig extends CommonProviderOptions {
type: "email"

View File

@@ -65,7 +65,7 @@ const exampleVerificationRequest = {
expires: new Date(),
}
const adapter: Adapter = () => {
const MyAdapter: Adapter<Record<string, unknown>> = () => {
return {
async getAdapter(appOptions: AppOptions) {
return {
@@ -131,6 +131,8 @@ const adapter: Adapter = () => {
}
}
const client = {} // Create a fake db client
const allConfig: NextAuthTypes.NextAuthOptions = {
providers: [
Providers.Twitter({
@@ -190,7 +192,7 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
return undefined
},
},
adapter,
adapter: MyAdapter(client),
useSecureCookies: true,
cookies: {
sessionToken: {

View File

@@ -68,7 +68,7 @@ See the [providers documentation](/configuration/providers) for a list of suppor
A random string used to hash tokens, sign cookies and generate crytographic keys.
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
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.
@@ -404,7 +404,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 if used in production. It is intended to support development and testing. Using this option is not recommended.
Setting this option to *false* in production is a security risk and may allow sessions to be hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
:::
---

View File

@@ -3,59 +3,123 @@ id: providers
title: Providers
---
Authentication Providers in NextAuth.js are services that can be used to sign in (OAuth, Email, etc).
Authentication Providers in **NextAuth.js** are services that can be used to sign in a user.
## Sign in with OAuth
There's four ways a user can be signed 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.
- [Using a built-in OAuth Provider](#oauth-providers) (e.g Github, Twitter, Google, etc...)
- [Using a custom OAuth Provider](#-using-a-custom-provider)
- [Using Email](#email-provider)
- [Using Credentials](#credentials-provider)
### Built-in OAuth providers
:::note
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 most popular sign-in services.
:::
<ul>
## OAuth Providers
### Available providers
<div className="provider-name-list">
{Object.entries(require("../../providers.json"))
.filter(([key]) => !["email", "credentials"].includes(key))
.sort(([, a], [, b]) => a.localeCompare(b))
.map(([key, name]) =>
<li key={key}><a href={`/providers/${key}`}>{name}</a></li>
.map(([key, name]) => (
<span key={key}>
<a href={`/providers/${key}`}>{name}</a>
<span className="provider-name-list__comma">,</span>
</span>
)
)}
</ul>
</div>
### Using a built-in OAuth provider
### How to
1. Register your application at the developer portal of your provider. There are links above to the developer docs for most supported providers with details on how to register your application.
2. The redirect URI should follow this format:
```
[origin]/api/auth/callback/[provider]
```
For example, Twitter on `localhost` this would be:
```
http://localhost:3000/api/auth/callback/twitter
```
```
[origin]/api/auth/callback/[provider]
```
For example, Twitter on `localhost` this would be:
```
http://localhost:3000/api/auth/callback/twitter
```
3. Create a `.env` file at the root of your project and add the client ID and client secret. For Twitter this would be:
```
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
```
```
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
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.
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [
Providers.Twitter({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
})
],
...
```
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
providers: [
Providers.Twitter({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
})
],
...
```
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
<Image src="/img/signin.png" alt="Signin Screenshot" />
### Options
| Name | Description | Type | Required |
| :-----------------: | :--------------------------------------------------------------: | :---------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
| requestTokenUrl | Endpoint to retrieve a request token | `string` | Yes |
| profileUrl | Endpoint to retrieve the user's profile | `string` | Yes |
| clientId | Client ID of the OAuth provider | `string` | Yes |
| clientSecret | Client Secret of the OAuth provider | `string` | Yes |
| profile | A callback returning an object with the user's info | `(profile, tokens) => Object` | Yes |
| protection | Additional security for OAuth login flows (defaults to `state`) | `"pkce"`,`"state"`,`"none"` | No |
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
| headers | Any headers that should be sent to the OAuth provider | `Object` | No |
| authorizationParams | Additional params to be sent to the authorization endpoint | `Object` | No |
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
| region | Only when using BattleNet | `string` | No |
| domain | Only when using certain Providers | `string` | No |
| tenantId | Only when using Azure, Active Directory, B2C, FusionAuth | `string` | No |
:::tip
Even if you are using a built-in provider, you can override any of these options to tweak the default configuration.
```js title=[...nextauth].js
import Providers from "next-auth/providers"
Providers.Auth0({
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
domain: process.env.DOMAIN,
scope: "openid your_custom_scope", // We do provide a default, but this will override it if defined
profile(profile) {
return {} // Return the profile in a shape that is different from the built-in one.
},
})
```
:::
### Using a custom provider
You can use an OAuth provider that isn't built-in by using a custom object.
@@ -76,7 +140,7 @@ As an example of what this looks like, this is the provider object returned for
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
async profile(profile, tokens) {
// You can use the tokens, in case you want to fetch more profile information
// For example several OAuth provider does not return e-mail by default.
// For example several OAuth providers do not return email by default.
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
return {
id: profile.id,
@@ -89,7 +153,8 @@ As an example of what this looks like, this is the provider object returned for
clientSecret: ""
}
```
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:
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 when initializing the library:
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
@@ -111,33 +176,24 @@ providers: [
...
```
### Adding a new provider
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
You only need to add two changes:
### OAuth provider options
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)<br />
• make sure you use a named default export, like this: `export default function YourProvider`
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
3. Add it to our [provider types](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts) (for TS projects)<br />
• you just need to add your new provider name to [this list](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L56-L97)<br />
• in case you new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
| Name | Description | Type | Required |
| :-----------------: | :--------------------------------------------------------------: | :-----------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case it should be `oauth` | `oauth`, `email`, `credentials` | Yes |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
| clientId | Client ID of the OAuth provider | `string` | Yes |
| clientSecret | Client Secret of the OAuth provider | `string` | No |
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | No |
| params | Additional authorization URL parameters | `object` | No |
| requestTokenUrl | Endpoint to retrieve a request token | `string` | No |
| authorizationParams | Additional params to be sent to the authorization endpoint | `object` | No |
| profileUrl | Endpoint to retrieve the user's profile | `string` | No |
| profile | An callback returning an object with the user's info | `object` | No |
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
| protection | Additional security for OAuth login flows (defaults to `state`) |`[pkce]`,`[state]`,`[pkce,state]`| No |
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
That's it! 🎉 Others will be able to discover this provider much more easily now!
## Sign in with Email
## Email Provider
### How to
The Email provider uses email to send "magic links" that can be used sign in, you will likely have seen them before if you have used software like Slack.
@@ -164,8 +220,21 @@ See the [Email provider documentation](/providers/email) for more information on
The email provider requires a database, it cannot be used without one.
:::
### Options
## Sign in with Credentials
| Name | Description | Type | Required |
| :---------------------: | :---------------------------------------------------------------------------------: | :------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `email` | `"email"` | Yes |
| server | Path or object pointing to the email server | `string` or `Object` | Yes |
| sendVerificationRequest | Callback to execute when a verification request is sent | `(params) => Promise<undefined>` | Yes |
| from | The email address from which emails are sent, default: "<no-reply@example.com>" | `string` | No |
| maxAge | How long until the e-mail can be used to log the user in seconds. Defaults to 1 day | `number` | No |
## Credentials Provider
### How to
The Credentials provider allows you to handle signing in with arbitrary credentials, such as a username and password, two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
@@ -211,26 +280,12 @@ See the [Credentials provider documentation](/providers/credentials) for more in
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>
)
### Options
## Adding a new built-in provider
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
That's it! 🎉 Others will be able to discover this provider much more easily now!
You can look at the existing built-in providers for inspiration.
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials) => Promise<User>` | Yes |

View File

@@ -95,7 +95,7 @@ If you are unable to use an HS512 key (for example to interoperate with other se
````
jwt: {
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"}
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
verificationOptions: {
algorithms: ["HS256"]
}

26
www/docs/providers/42.md Normal file
View File

@@ -0,0 +1,26 @@
---
id: 42-school
title: 42 School
---
## Documentation
https://api.intra.42.fr/apidoc/guides/web_application_flow
## Configuration
https://profile.intra.42.fr/oauth/applications/new
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.FortyTwo({
clientId: process.env.FORTY_TWO_CLIENT_ID,
clientSecret: process.env.FORTY_TWO_CLIENT_SECRET
})
]
...
```

View File

@@ -0,0 +1,26 @@
---
id: dropbox
title: Dropbox
---
## Documentation
https://developers.dropbox.com/oauth-guide
## Configuration
https://www.dropbox.com/developers/apps
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Dropbox({
clientId: process.env.DROPBOX_CLIENT_ID,
clientSecret: process.env.DROPBOX_CLIENT_SECRET
})
]
...
```

View File

@@ -7,7 +7,6 @@ title: IdentityServer4
https://identityserver4.readthedocs.io/en/latest/
## Example
```js
@@ -15,8 +14,8 @@ import Providers from `next-auth/providers`
...
providers: [
Providers.IdentityServer4({
id: "identity-server4",
name: "IdentityServer4",
id: "identity-server4",
name: "IdentityServer4",
scope: "openid profile email api offline_access", // Allowed Scopes
domain: process.env.IdentityServer4_Domain,
clientId: process.env.IdentityServer4_CLIENT_ID,
@@ -33,18 +32,20 @@ The configuration below is for the demo server at https://demo.identityserver.io
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`
...
providers: [
Providers.IdentityServer4({
id: "demo-identity-server",
name: "Demo IdentityServer4",
scope: "openid profile email api offline_access",
id: "demo-identity-server",
name: "Demo IdentityServer4",
scope: "openid profile email api offline_access",
domain: "demo.identityserver.io",
clientId: "server.code",
clientSecret: "secret"
clientId: "interactive.confidential",
clientSecret: "secret",
protection: "pkce"
})
}
...
```

6
www/package-lock.json generated
View File

@@ -17485,9 +17485,9 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"requires": {
"figgy-pudding": "^3.5.1"
}

View File

@@ -46,6 +46,7 @@ html[data-theme="dark"]:root {
@import "buttons.css";
@import "table-of-contents.css";
@import "sidebar.css";
@import "providers.css";
@media screen and (max-width: 360px) {
html {

View File

@@ -0,0 +1,9 @@
.provider-name-list {
display: flex;
flex-wrap: wrap;
}
.provider-name-list__comma {
display: inline-flex;
margin-right: 5px;
}