Compare commits

..

24 Commits

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

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

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

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

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

* chore(ci): fix typo on types workflow

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

* chore(types): build types script

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

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

* refactor(lint): fix build-types script

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

#1677 seemed to miss this

* fix(built): typo in package.json

* fix(build): fix release

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

* chore(ts): remove unused imports

* refactor(ts): clean up CallbackOptions

* docs(ts): explain Module Augmentation

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

* test(ts): make jwt params optional

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

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

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

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

* fix: add remaining missing dependencies/config

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

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

Regarding ESLint / Prettier use and link to their VSCode extensions

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

* refactor(ts): deduplicate internal types

* refactor(ts): ease up providers typings

* test(ts): fix failing TS tests

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

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

* feat(ts): move/update client types

* refactor(TS): rename some types

* test(ts): fix client tests

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

* chore: fix lint error

* refactor(ts): separate internal types

* chore: simplify build-types script

* chore: update type import paths in src

* chore(build): create root files at build

* chore: remove unnecessary .npmignore

* chore: run prettier on types

* fix(ts): clean up jwt types

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

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

* docs(ts): add JSDoc to NextAuthOptions props

* chore(ts): remove unused import

* docs(ts): change JSDOC docs notation

* refactor(build): extract module entries into enum

* chore(ts): move ClientSafeProvider

* chore(ts): simplify GetTokenParams generic

* style(lint): fix linting errors

* chore: re-add generic extension to GetTokenParams

* fix(ts): extract EmailConfigServerOptions to interface

* fix(ts): use relative imports

* Merge branch 'main' into beta

* Merge main into beta

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

* test(ts): update JWT getToken test

* fix(build): fix tsconfig.json formatting

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

* fix(ts): add missing callbacks JSDoc

* docs: mention TS in FAQ, fix typos

* docs: fix some typos in the docs

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

* chore: remove console.log

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

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

* refactor: set csrfToken on req.options implicitly

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

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

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

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

* feat: update docs with new protection value

* fix: lint files

* refactor: change protection from string to array

* chore: reverting unespected change

* chore: lint files

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 00:24:39 +02:00
78 changed files with 1090 additions and 772 deletions

6
.github/labeler.yml vendored
View File

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

View File

@@ -2,14 +2,14 @@ name: Release
on:
push:
branches:
- 'main'
- 'beta'
- 'next'
- '3.x'
- "main"
- "beta"
- "next"
- "3.x"
pull_request:
jobs:
release:
name: 'Release'
name: "Release"
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -20,7 +20,6 @@ jobs:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run build
- run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

1
.gitignore vendored
View File

@@ -27,6 +27,7 @@ node_modules
.cache-loader
.next
www/providers.json
src/providers/index.js
# VS
/.vs/slnx.sqlite-journal

View File

@@ -42,7 +42,7 @@ npm i
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
1. Start the dev application/server and CSS watching:
1. Start the dev application/server:
```sh
npm run dev
```
@@ -59,9 +59,19 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
#### Providers
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
That's it! 🎉 Others will be able to discover this provider much more easily now!
You can look at the existing built-in providers for inspiration.
#### Databases
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.

View File

@@ -9,6 +9,8 @@ const MODULE_ENTRIES = {
JWT: "jwt",
}
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
@@ -24,6 +26,8 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
})
})
// Building types
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
@@ -43,3 +47,40 @@ TYPES_TARGETS.forEach((target) => {
}
)
})
// Building providers
const providersDir = path.join(process.cwd(), "/src/providers")
const files = fs.readdirSync(providersDir, "utf8")
let importLines = ""
let exportLines = `export default {\n`
files.forEach((file) => {
const provider = fs.readFileSync(path.join(providersDir, file), "utf8")
try {
// NOTE: If this fails, the default export probably wasn't a named function.
// Always use a named function as default export.
// Eg.: export default function YourProvider ...
const { functionName } = provider.match(
/export default function (?<functionName>.+)\s?\(/
).groups
importLines += `import ${functionName} from "./${file}"\n`
exportLines += ` ${functionName},\n`
} catch (error) {
console.error(
[
`\nThe provider file '${file}' should have a single named default export`,
"Example: 'export default function YourProvider'\n\n",
].join("\n")
)
process.exit(1)
}
})
exportLines += `}\n`
fs.writeFile(
path.join(process.cwd(), "src/providers/index.js"),
[importLines, exportLines].join("\n")
)

58
package-lock.json generated
View File

@@ -4332,6 +4332,12 @@
"yallist": "^3.0.2"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -4692,11 +4698,6 @@
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -6073,6 +6074,12 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true
},
"y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"dev": true
},
"yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
@@ -8430,9 +8437,9 @@
}
},
"jose": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-1.28.0.tgz",
"integrity": "sha512-JmfDRzt/HSj8ipd9TsDtEHoLUnLYavG+7e8F6s1mx2jfVSfXOTaFQsJUydbjJpTnTDHP1+yKL9Ke7ktS/a0Eiw==",
"version": "1.28.1",
"resolved": "https://registry.npmjs.org/jose/-/jose-1.28.1.tgz",
"integrity": "sha512-6JK28rFu5ENp/yxMwM+iN7YeaInnY9B9Bggjkz5fuwLiJhbVrl2O4SJr65bdNBPl9y27fdC3Mymh+FVCvozLIg==",
"requires": {
"@panva/asn1.js": "^1.0.0"
}
@@ -13465,8 +13472,9 @@
"dev": true
},
"y18n": {
"version": "4.0.0",
"bundled": true,
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yallist": {
@@ -14510,6 +14518,12 @@
"is-number": "^7.0.0"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
@@ -17829,11 +17843,6 @@
"strip-ansi": "^6.0.0"
}
},
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -18665,10 +18674,9 @@
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
},
"yallist": {
"version": "4.0.0",
@@ -18835,6 +18843,12 @@
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
@@ -18964,6 +18978,12 @@
"strip-ansi": "^5.0.0"
}
},
"y18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz",
"integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==",
"dev": true
},
"yargs": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz",

View File

@@ -8,9 +8,10 @@
"main": "index.js",
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "babel --config-file ./config/babel.config.json src --out-dir dist && node ./config/build.js",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev": "next | npm run watch:css",
"dev:with-css": "next | npm run watch:css",
"dev": "next",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",

View File

@@ -6,6 +6,27 @@ import Providers from 'next-auth/providers'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
// csrfToken: {
// name: 'next-auth.csrf-token',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// },
// pkceCodeVerifier: {
// name: 'next-auth.pkce.code_verifier',
// options: {
// httpOnly: true,
// sameSite: 'none',
// path: '/',
// secure: true
// }
// }
// },
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
@@ -19,6 +40,11 @@ export default NextAuth({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// protection: ["pkce", "state"],
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
}),
Providers.Twitter({

View File

@@ -106,7 +106,8 @@ async function getToken (params) {
// 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
raw = false,
decode: _decode = decode
} = params
if (!req) throw new Error('Must pass `req` to JWT getToken()')
@@ -126,7 +127,7 @@ async function getToken (params) {
}
try {
return decode({ token, ...params })
return _decode({ token, ...params })
} catch {
return null
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,48 +1,54 @@
import nodemailer from 'nodemailer'
import logger from '../lib/logger'
import nodemailer from "nodemailer"
import logger from "../lib/logger"
export default (options) => {
export default function Email(options) {
return {
id: 'email',
type: 'email',
name: 'Email',
id: "email",
type: "email",
name: "Email",
// Server can be an SMTP connection string or a nodemailer config object
server: {
host: 'localhost',
host: "localhost",
port: 25,
auth: {
user: '',
pass: ''
}
user: "",
pass: "",
},
},
from: 'NextAuth <no-reply@example.com>',
maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
from: "NextAuth <no-reply@example.com>",
maxAge: 24 * 60 * 60,
sendVerificationRequest,
...options
...options,
}
}
const sendVerificationRequest = ({ identifier: email, url, baseUrl, provider }) => {
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer
.createTransport(server)
.sendMail({
nodemailer.createTransport(server).sendMail(
{
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
html: html({ url, site, email })
}, (error) => {
html: html({ url, site, email }),
},
(error) => {
if (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
logger.error("SEND_VERIFICATION_EMAIL_ERROR", email, error)
return reject(new Error("SEND_VERIFICATION_EMAIL_ERROR", error))
}
return resolve()
})
}
)
})
}
@@ -52,16 +58,16 @@ const html = ({ url, site, email }) => {
// 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;.')}`
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'
const backgroundColor = "#f9f9f9"
const textColor = "#444444"
const mainBackgroundColor = "#ffffff"
const buttonBackgroundColor = "#346df1"
const buttonBorderColor = "#346df1"
const buttonTextColor = "#ffffff"
return `
<body style="background: ${backgroundColor};">

View File

@@ -1,21 +1,22 @@
export default (options) => {
export default function EVEOnline(options) {
return {
id: 'eveonline',
name: 'EVE Online',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://login.eveonline.com/oauth/token',
authorizationUrl: 'https://login.eveonline.com/oauth/authorize?response_type=code',
profileUrl: 'https://login.eveonline.com/oauth/verify',
profile: (profile) => {
id: "eveonline",
name: "EVE Online",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://login.eveonline.com/oauth/token",
authorizationUrl:
"https://login.eveonline.com/oauth/authorize?response_type=code",
profileUrl: "https://login.eveonline.com/oauth/verify",
profile(profile) {
return {
id: profile.CharacterID,
name: profile.CharacterName,
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
email: null
email: null,
}
},
...options
...options,
}
}

View File

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

View File

@@ -1,25 +1,28 @@
export default (options) => {
export default function FACEIT(options) {
return {
id: 'faceit',
name: 'FACEIT',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
id: "faceit",
name: "FACEIT",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
headers: {
Authorization: `Basic ${Buffer.from(`${options.clientId}:${options.clientSecret}`).toString('base64')}`
Authorization: `Basic ${Buffer.from(
`${options.clientId}:${options.clientSecret}`
).toString("base64")}`,
},
accessTokenUrl: 'https://api.faceit.com/auth/v1/oauth/token',
authorizationUrl: 'https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code',
profileUrl: 'https://api.faceit.com/auth/v1/resources/userinfo',
profile (profile) {
accessTokenUrl: "https://api.faceit.com/auth/v1/oauth/token",
authorizationUrl:
"https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code",
profileUrl: "https://api.faceit.com/auth/v1/resources/userinfo",
profile(profile) {
const { guid: id, nickname: name, email, picture: image } = profile
return {
id,
name,
email,
image
image,
}
},
...options
...options,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,21 +1,22 @@
export default (options) => {
export default function Kakao(options) {
return {
id: 'kakao',
name: 'Kakao',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://kauth.kakao.com/oauth/token',
authorizationUrl: 'https://kauth.kakao.com/oauth/authorize?response_type=code',
profileUrl: 'https://kapi.kakao.com/v2/user/me',
profile: (profile) => {
id: "kakao",
name: "Kakao",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://kauth.kakao.com/oauth/token",
authorizationUrl:
"https://kauth.kakao.com/oauth/authorize?response_type=code",
profileUrl: "https://kapi.kakao.com/v2/user/me",
profile(profile) {
return {
id: profile.id,
name: profile.kakao_account?.profile.nickname,
email: profile.kakao_account?.email,
image: profile.kakao_account?.profile.profile_image_url
image: profile.kakao_account?.profile.profile_image_url,
}
},
...options
...options,
}
}

View File

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

View File

@@ -1,26 +1,28 @@
export default (options) => {
export default function LinkedIn(options) {
return {
id: 'linkedin',
name: 'LinkedIn',
type: 'oauth',
version: '2.0',
scope: 'r_liteprofile',
id: "linkedin",
name: "LinkedIn",
type: "oauth",
version: "2.0",
scope: "r_liteprofile",
params: {
grant_type: 'authorization_code',
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret
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) => {
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,
name: profile.localizedFirstName + " " + profile.localizedLastName,
email: null,
image: null
image: null,
}
},
...options
...options,
}
}

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,22 @@
export default (options) => {
export default function Okta(options) {
return {
id: 'okta',
name: 'Okta',
type: 'oauth',
version: '2.0',
scope: 'openid profile email',
id: "okta",
name: "Okta",
type: "oauth",
version: "2.0",
scope: "openid profile email",
params: {
grant_type: 'authorization_code',
grant_type: "authorization_code",
client_id: options.clientId,
client_secret: options.clientSecret
client_secret: options.clientSecret,
},
// These will be different depending on the Org.
accessTokenUrl: `https://${options.domain}/v1/token`,
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
profileUrl: `https://${options.domain}/v1/userinfo/`,
profile: (profile) => {
profile(profile) {
return { ...profile, id: profile.sub }
},
...options
...options,
}
}

View File

@@ -1,20 +1,20 @@
export default (options) => {
export default function Osso(options) {
return {
id: 'osso',
name: 'SAML SSO',
type: 'oauth',
version: '2.0',
params: { grant_type: 'authorization_code' },
id: "osso",
name: "SAML SSO",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: `https://${options.domain}/oauth/token`,
authorizationUrl: `https://${options.domain}/oauth/authorize?response_type=code`,
profileUrl: `https://${options.domain}/oauth/me`,
profile: (profile) => {
profile(profile) {
return {
id: profile.id,
name: profile.name || profile.email,
email: profile.email
email: profile.email,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Reddit(options) {
return {
id: 'reddit',
name: 'Reddit',
type: 'oauth',
version: '2.0',
scope: 'identity',
params: { grant_type: 'authorization_code' },
accessTokenUrl: ' https://www.reddit.com/api/v1/access_token',
id: "reddit",
name: "Reddit",
type: "oauth",
version: "2.0",
scope: "identity",
params: { grant_type: "authorization_code" },
accessTokenUrl: " https://www.reddit.com/api/v1/access_token",
authorizationUrl:
'https://www.reddit.com/api/v1/authorize?response_type=code',
profileUrl: 'https://oauth.reddit.com/api/v1/me',
profile: (profile) => {
"https://www.reddit.com/api/v1/authorize?response_type=code",
profileUrl: "https://oauth.reddit.com/api/v1/me",
profile(profile) {
return {
id: profile.id,
name: profile.name,
image: null,
email: null
email: null,
}
},
...options
...options,
}
}

View File

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

View File

@@ -1,24 +1,26 @@
export default (options) => {
export default function Slack(options) {
return {
id: 'slack',
name: 'Slack',
type: 'oauth',
version: '2.0',
id: "slack",
name: "Slack",
type: "oauth",
version: "2.0",
scope: [],
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
authorizationParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
profileUrl: 'https://slack.com/api/users.identity',
profile: (profile) => {
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://slack.com/api/oauth.v2.access",
authorizationUrl: "https://slack.com/oauth/v2/authorize",
authorizationParams: {
user_scope: "identity.basic,identity.email,identity.avatar",
},
profileUrl: "https://slack.com/api/users.identity",
profile(profile) {
const { user } = profile
return {
id: user.id,
name: user.name,
image: user.image_512,
email: user.email
email: user.email,
}
},
...options
...options,
}
}

View File

@@ -1,23 +1,23 @@
export default (options) => {
export default function Spotify(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',
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) => {
"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
image: profile.images?.[0]?.url,
}
},
...options
...options,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,23 @@
export default (options) => {
export default function Zoho(options) {
return {
id: 'zoho',
name: 'Zoho',
type: 'oauth',
version: '2.0',
scope: 'AaaServer.profile.Read',
params: { grant_type: 'authorization_code' },
accessTokenUrl: 'https://accounts.zoho.com/oauth/v2/token',
authorizationUrl: 'https://accounts.zoho.com/oauth/v2/auth?response_type=code',
profileUrl: 'https://accounts.zoho.com/oauth/user/info',
profile: (profile) => {
id: "zoho",
name: "Zoho",
type: "oauth",
version: "2.0",
scope: "AaaServer.profile.Read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://accounts.zoho.com/oauth/v2/token",
authorizationUrl:
"https://accounts.zoho.com/oauth/v2/auth?response_type=code",
profileUrl: "https://accounts.zoho.com/oauth/user/info",
profile(profile) {
return {
id: profile.ZUID,
name: `${profile.First_Name} ${profile.Last_Name}`,
email: profile.Email,
image: null
image: null,
}
},
...options
...options,
}
}

View File

@@ -6,12 +6,12 @@ import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-req'
import * as routes from './routes'
import renderPage from './pages'
import csrfTokenHandler from './lib/csrf-token-handler'
import createSecret from './lib/create-secret'
import callbackUrlHandler from './lib/callback-url-handler'
import extendRes from './lib/extend-res'
import csrfTokenHandler from './lib/csrf-token-handler'
import * as pkce from './lib/oauth/pkce-handler'
import * as state from './lib/oauth/state-handler'
@@ -67,16 +67,18 @@ async function NextAuthHandler (req, res, userOptions) {
const secret = createSecret({ userOptions, basePath, baseUrl })
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const provider = providers.find(({ id }) => id === providerId)
if (provider &&
provider.type === 'oauth' && provider.version?.startsWith('2') &&
(!provider.protection && provider.state !== false)
) {
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
// Protection only works on OAuth 2.x providers
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
// When provider.state is undefined, we still want this to pass
if (!provider.protection && provider.state !== false) {
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
provider.protection = ['state']
} else if (typeof provider.protection === 'string') {
provider.protection = [provider.protection]
}
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
@@ -103,7 +105,6 @@ async function NextAuthHandler (req, res, userOptions) {
provider,
cookies,
secret,
csrfToken,
providers,
// Session options
session: {
@@ -134,6 +135,7 @@ async function NextAuthHandler (req, res, userOptions) {
logger
}
csrfTokenHandler(req, res)
await callbackUrlHandler(req, res)
const render = renderPage(req, res)
@@ -146,7 +148,7 @@ async function NextAuthHandler (req, res, userOptions) {
case 'session':
return routes.session(req, res)
case 'csrf':
return res.json({ csrfToken })
return res.json({ csrfToken: req.options.csrfToken })
case 'signin':
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
@@ -199,7 +201,7 @@ async function NextAuthHandler (req, res, userOptions) {
switch (action) {
case 'signin':
// Verified CSRF Token required for all sign in routes
if (csrfTokenVerified && provider) {
if (req.options.csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
if (await state.handleSignin(req, res)) return
return routes.signin(req, res)
@@ -208,14 +210,14 @@ async function NextAuthHandler (req, res, userOptions) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
// Verified CSRF Token required for signout
if (csrfTokenVerified) {
if (req.options.csrfTokenVerified) {
return routes.signout(req, res)
}
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
if (provider) {
// Verified CSRF Token required for credentials providers only
if (provider.type === 'credentials' && !csrfTokenVerified) {
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}

View File

@@ -14,29 +14,30 @@ import * as cookie from './cookie'
* For more details, see the following OWASP links:
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
* @param {import("..").NextAuthRequest} req
* @param {import("..").NextAuthResponse} res
*/
export default function csrfTokenHandler (req, res, cookies, secret) {
const { csrfToken: csrfTokenFromRequest } = req.body
let csrfTokenFromCookie
let csrfTokenVerified = false
if (req.cookies[cookies.csrfToken.name]) {
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
export default function csrfTokenHandler (req, res) {
const { cookies, secret } = req.options
if (cookies.csrfToken.name in req.cookies) {
const [csrfToken, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
const expectedCsrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
if (csrfTokenHash === expectedCsrfTokenHash) {
// If hash matches then we trust the CSRF token value
csrfTokenFromCookie = csrfTokenValue
// If this is a POST request and the CSRF Token in the Post request matches
// the cookie we have already verified is one we have set, then token is verified!
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
// If this is a POST request and the CSRF Token in the POST request matches
// the cookie we have already verified is the one we have set, then the token is verified!
const csrfTokenVerified = req.method === 'POST' && csrfToken === req.body.csrfToken
req.options.csrfToken = csrfToken
req.options.csrfTokenVerified = csrfTokenVerified
return
}
}
if (!csrfTokenFromCookie) {
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
// (e.g. because it's been modifed or because the secret has changed) create a new token.
csrfTokenFromCookie = randomBytes(32).toString('hex')
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
}
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
// If no csrfToken from cookie - because it's not been set yet,
// or because the hash doesn't match (e.g. because it's been modifed or because the secret has changed)
// create a new token.
const csrfToken = randomBytes(32).toString('hex')
const csrfTokenHash = createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')
const csrfTokenCookie = `${csrfToken}|${csrfTokenHash}`
cookie.set(res, cookies.csrfToken.name, csrfTokenCookie, cookies.csrfToken.options)
req.options.csrfToken = csrfToken
}

View File

@@ -1,19 +1,19 @@
import { decode as jwtDecode } from 'jsonwebtoken'
import oAuthClient from './client'
import logger from '../../../lib/logger'
import { OAuthCallbackError } from '../../../lib/errors'
import { decode as jwtDecode } from "jsonwebtoken"
import oAuthClient from "./client"
import logger from "../../../lib/logger"
import { OAuthCallbackError } from "../../../lib/errors"
/** @param {import("types/internals").NextAuthRequest} req */
export default async function oAuthCallback (req) {
export default async function oAuthCallback(req) {
const { provider, pkce } = req.options
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
if (provider.version?.startsWith("2.")) {
// The "user" object is specific to the Apple provider and is provided on first sign in
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
let { code, user } = req.query // eslint-disable-line camelcase
if (req.method === 'POST') {
if (req.method === "POST") {
try {
const body = JSON.parse(JSON.stringify(req.body))
if (body.error) {
@@ -23,25 +23,35 @@ export default async function oAuthCallback (req) {
code = body.code
user = body.user != null ? JSON.parse(body.user) : null
} catch (error) {
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
logger.error(
"OAUTH_CALLBACK_HANDLER_ERROR",
error,
req.body,
provider.id,
code
)
throw error
}
}
// REVIEW: Is this used by any of the providers?
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
if (Object.prototype.hasOwnProperty.call(provider, "useAuthTokenHeader")) {
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
} else {
client.useAuthorizationHeaderforGET(true)
}
try {
const tokens = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
const tokens = await client.getOAuthAccessToken(
code,
provider,
pkce.code_verifier
)
let profileData
if (provider.idToken) {
if (!tokens?.id_token) {
throw new OAuthCallbackError('Missing JWT ID Token')
throw new OAuthCallbackError("Missing JWT ID Token")
}
// Support services that use OpenID ID Tokens to encode profile data
@@ -52,26 +62,28 @@ export default async function oAuthCallback (req) {
return getProfile({ profileData, provider, tokens, user })
} catch (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, provider.id, code)
throw error
}
}
try {
// Handle OAuth v1.x
const {
oauth_token: oauthToken, oauth_verifier: oauthVerifier
} = req.query
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
// eslint-disable-next-line camelcase
const { oauth_token, oauth_verifier } = req.query
// eslint-disable-next-line camelcase
const { token_secret } = await client.getOAuthRequestToken(provider.params)
const tokens = await client.getOAuthAccessToken(oauth_token, token_secret, oauth_verifier)
const profileData = await client.get(
provider.profileUrl,
tokens.accessToken,
tokens.refreshToken
tokens.oauth_token,
tokens.oauth_token_secret
)
return getProfile({ profileData, tokens, provider })
} catch (error) {
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error)
throw error
}
}
@@ -81,15 +93,27 @@ export default async function oAuthCallback (req) {
* Returns profile, raw profile and auth provider details
* @param {{
* profileData: object | string
* tokens: import("types").TokenSet
* provider: import("types/providers").OAuthConfig
* tokens: {
* accessToken: string
* idToken?: string
* refreshToken?: string
* access_token: string
* expires_in?: string | Date | null
* refresh_token?: string
* id_token?: string
* token?: string
* token_secret?: string
* tokenSecret?: string
* params?: any
* }
* provider: import("../..").Provider
* user?: object
* }} profileParams
*/
async function getProfile ({ profileData, tokens, provider, user }) {
async function getProfile({ profileData, tokens, provider, user }) {
try {
// Convert profileData into an object if it's a string
if (typeof profileData === 'string' || profileData instanceof String) {
if (typeof profileData === "string" || profileData instanceof String) {
profileData = JSON.parse(profileData)
}
@@ -98,22 +122,22 @@ async function getProfile ({ profileData, tokens, provider, user }) {
profileData.user = user
}
logger.debug('PROFILE_DATA', profileData)
logger.debug("PROFILE_DATA", profileData)
const profile = await provider.profile(profileData, tokens)
// Return profile, raw profile and auth provider details
return {
profile: {
...profile,
email: profile.email?.toLowerCase() ?? null
email: profile.email?.toLowerCase() ?? null,
},
account: {
provider: provider.id,
type: provider.type,
id: profile.id,
...tokens
...tokens,
},
OAuthProfile: profileData
OAuthProfile: profileData,
}
} catch (exception) {
// If we didn't get a response either there was a problem with the provider
@@ -123,11 +147,11 @@ async function getProfile ({ profileData, tokens, provider, user }) {
// 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)
logger.error("OAUTH_PARSE_PROFILE_ERROR", exception, profileData)
return {
profile: null,
account: null,
OAuthProfile: profileData
OAuthProfile: profileData,
}
}
}

View File

@@ -54,23 +54,36 @@ export default function oAuthClient (provider) {
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
// eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ accessToken, refreshToken, results })
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params
})
})
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (...args) => {
oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => {
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
// eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve(oauthToken)
resolve({ oauth_token, oauth_token_secret, params })
})
})
}
@@ -136,7 +149,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
headers.Authorization = `Bearer ${code}`
}
if (provider.protection === 'pkce') {
if (provider.protection.includes('pkce')) {
params.code_verifier = codeVerifier
}

View File

@@ -16,7 +16,8 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
export async function handleCallback (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
// Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) {
return
}
@@ -50,7 +51,7 @@ export async function handleCallback (req, res) {
export async function handleSignin (req, res) {
const { cookies, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
if (!provider.protection?.includes('pkce')) { // Provider does not support PKCE, nothing to do.
return
}
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie

View File

@@ -12,11 +12,12 @@ import { OAuthCallbackError } from '../../../lib/errors'
export async function handleCallback (req, res) {
const { csrfToken, provider, baseUrl, basePath } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
// Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) {
return
}
const { state } = req.query
const state = req.query.state || req.body.state
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
logger.debug(
@@ -41,7 +42,7 @@ export async function handleCallback (req, res) {
export async function handleSignin (req, res) {
const { provider, baseUrl, basePath, csrfToken } = req.options
try {
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
if (!provider.protection?.includes('state')) { // Provider does not support state, nothing to do.
return
}

View File

@@ -5,13 +5,17 @@ import logger from '../../../lib/logger'
export default async function getAuthorizationUrl (req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query
}
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
delete req.query?.nextauth
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
...provider.authorizationParams,
...req.query,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
})
@@ -34,8 +38,12 @@ export default async function getAuthorizationUrl (req) {
}
try {
const oAuthToken = await client.getOAuthRequestToken()
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
const tokens = await client.getOAuthRequestToken(params)
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
return url
} catch (error) {

View File

@@ -52,7 +52,7 @@ export default function error ({ baseUrl, basePath, error = 'default', res }) {
}
}
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] ?? errors.default
res.status(statusCode)

View File

@@ -262,7 +262,8 @@ export default async function callback (req, res) {
const defaultJwtPayload = {
name: user.name,
email: user.email,
picture: user.image
picture: user.image,
sub: user.id?.toString()
}
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)

4
types/adapters.d.ts vendored
View File

@@ -1,7 +1,7 @@
import { AppOptions } from "internals"
import { AppOptions } from "./internals"
import { ConnectionOptions, EntitySchema } from "typeorm"
import { User } from "."
import { AppProvider } from "internals/providers"
import { AppProvider } from "./internals/providers"
export interface Profile {
id: string

109
types/index.d.ts vendored
View File

@@ -11,7 +11,7 @@ import {
NextApiRequest,
NextApiResponse,
NextApiHandler,
} from "internals/utils"
} from "./internals/utils"
/**
* Configure your NextAuth instance
@@ -26,7 +26,7 @@ export interface NextAuthOptions {
* * **Default value**: `[]`
* * **Required**: *Yes*
*
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentaion](https://next-auth.js.org/configuration/providers)
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: AppProviders
/**
@@ -38,9 +38,9 @@ export interface NextAuthOptions {
*/
database?: string | Record<string, any> | ConnectionOptions
/**
* A random string used to hash tokens, sign cookies and generate crytographic keys.
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
* The default behaviour is volatile, and **it is strongly recommended** you explicitly specify a value
* The default behavior is volatile, and **it is strongly recommended** you explicitly specify a value
* to avoid invalidating end user sessions when configuration changes are deployed.
* * **Default value**: `string` (SHA hash of the "options" object)
* * **Required**: No - **but strongly recommended**!
@@ -51,7 +51,7 @@ export interface NextAuthOptions {
/**
* Configure your session like if you want to use JWT or a database,
* how long until an idle session expires, or to throttle write operations in case you are using a database.
* * **Default value**: See the documentaion page
* * **Default value**: See the documentation page
* * **Required**: No
*
* [Documentation](https://next-auth.js.org/configuration/options#session)
@@ -86,17 +86,17 @@ export interface NextAuthOptions {
* }
* ```
*
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentaion](https://next-auth.js.org/configuration/pages)
* [Documentation](https://next-auth.js.org/configuration/options#pages) | [Pages documentation](https://next-auth.js.org/configuration/pages)
*/
pages?: PagesOptions
/**
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
* 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**.
* * **Default value**: See the Callbacks documentaion
* * **Default value**: See the Callbacks documentation
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentaion](https://next-auth.js.org/configuration/callbacks)
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: CallbacksOptions
/**
@@ -109,7 +109,7 @@ export interface NextAuthOptions {
* * **Default value**: `{}`
* * **Required**: *No*
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentaion](https://next-auth.js.org/configuration/events)
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: EventsOptions
/**
@@ -135,7 +135,7 @@ export interface NextAuthOptions {
*
* - ⚠ If you added a custom `logger`, this setting is ignored.
*
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentaion](https://next-auth.js.org/configuration/options#logger)
* [Documentation](https://next-auth.js.org/configuration/options#debug) | [Logger documentation](https://next-auth.js.org/configuration/options#logger)
*/
debug?: boolean
/**
@@ -166,7 +166,8 @@ export interface NextAuthOptions {
*
* - ⚠ When set, the `debug` option is ignored
*
* [Documentation](https://next-auth.js.org/configuration/options#logger) | [Debug documentaion](https://next-auth.js.org/configuration/options#debug)
* [Documentation](https://next-auth.js.org/configuration/options#logger) |
* [Debug documentation](https://next-auth.js.org/configuration/options#debug)
*/
logger?: LoggerInstance
/**
@@ -182,7 +183,7 @@ export interface NextAuthOptions {
theme?: "auto" | "dark" | "light"
/**
* 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.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
* You can manually set this option to `false` to disable this security feature and allow cookies
* to be accessible from non-secured URLs (this is not recommended).
* * **Default value**: `true` for HTTPS and `false` for HTTP sites
@@ -199,7 +200,7 @@ export interface NextAuthOptions {
* You can override the default cookie names and options for any of the cookies used by NextAuth.js.
* You can specify one or more cookies with custom properties,
* but if you specify custom options for a cookie you must provide all the options for that cookie.
* If you use this feature, you will likely want to create conditional behaviour
* If you use this feature, you will likely want to create conditional behavior
* to support setting different cookies policies in development and production builds,
* as you will be opting out of the built-in dynamic policy.
* * **Default value**: `{}`
@@ -214,12 +215,22 @@ export interface NextAuthOptions {
cookies?: CookiesOptions
}
/**
* Override any of the methods, and the rest will use the default logger.
*
* [Documentation](https://next-auth.js.org/configuration/options#logger)
*/
export interface LoggerInstance {
warn(code: string, ...message: unknown[]): void
error(code: string, ...message: unknown[]): void
debug(code: string, ...message: unknown[]): void
}
/**
* Different tokens returned by OAuth Providers.
* Some of them are available with different casing,
* but they refer to the same value.
*/
export interface TokenSet {
accessToken: string
idToken?: string
@@ -230,12 +241,17 @@ export interface TokenSet {
id_token?: string
}
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface Account extends TokenSet, Record<string, unknown> {
id: string
provider: string
type: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown> {
sub?: string
name?: string
@@ -243,13 +259,54 @@ export interface Profile extends Record<string, unknown> {
image?: string
}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
* Throwing an error or returning a string will stop the flow, and redirect the user.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
/**
* This 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 this callback to customise that behaviour.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect?(url: string, baseUrl: string): Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* - ⚠ By default, only a subset of the token is returned for increased security.
* If you want to make something available you added to the token through the `jwt` callback,
* you have to explicitely forward it here to make it available to the client.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#session-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
* Its content is forwarded to the `session` callback,
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* - ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt?(
token: JWT,
user?: User,
@@ -259,6 +316,7 @@ export interface CallbacksOptions<
): Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookieOption {
name: string
options: {
@@ -271,6 +329,7 @@ export interface CookieOption {
}
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken?: CookieOption
callbackUrl?: CookieOption
@@ -278,6 +337,7 @@ export interface CookiesOptions {
pkceCodeVerifier?: CookieOption
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventType =
| "signIn"
| "signOut"
@@ -287,10 +347,13 @@ export type EventType =
| "session"
| "error"
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventCallback = (message: any) => Promise<void>
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventsOptions = Partial<Record<EventType, EventCallback>>
/** [Documentation](https://next-auth.js.org/configuration/pages) */
export interface PagesOptions {
signIn?: string
signOut?: string
@@ -301,18 +364,38 @@ export interface PagesOptions {
newUser?: string
}
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `Provider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown> {
user?: User
accessToken?: string
expires: string
}
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
export interface SessionOptions {
jwt?: boolean
maxAge?: number
updateAge?: number
}
/**
* The shape of the returned object in the OAuth providers' `profile` callback,
* available in the `jwt` and `session` callbacks,
* or the second parameter of the `session` callback, when using a database.
*
* [`signIn` callback](https://next-auth.js.org/configuration/callbacks#sign-in-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User {
name?: string | null
email?: string | null

View File

@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "internals/utils"
import { NextApiRequest, NextApiResponse } from "./utils"
import { NextAuthOptions } from ".."
import { AppProvider } from "internals/providers"
import { AppProvider } from "./providers"
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions =
@@ -40,6 +40,7 @@ export interface AppOptions
| "verify-request"
| "error"
csrfToken?: string
csrfTokenVerified?: boolean
}
export interface NextAuthRequest extends NextApiRequest {

View File

@@ -1,4 +1,4 @@
import { CommonProviderOptions } from "next-auth/providers"
import { CommonProviderOptions } from "../providers"
export interface AppProvider extends CommonProviderOptions {
signinUrl: string

10
types/jwt.d.ts vendored
View File

@@ -1,6 +1,11 @@
import { JWT as JoseJWT, JWE } from "jose"
import { NextApiRequest } from "internals/utils"
import { NextApiRequest } from "./internals/utils"
/**
* Returned by the `jwt` callback and `getToken`, when using JWT sessions
*
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) | [`getToken`](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken)
*/
export interface JWT extends Record<string, unknown> {
name?: string | null
email?: string | null
@@ -44,9 +49,10 @@ export type GetTokenParams<R extends boolean = false> = {
secret?: string
} & Omit<JWTDecodeParams, "secret">
/** [Documentation](https://next-auth.js.org/tutorials/securing-pages-and-api-routes#using-gettoken) */
export function getToken<R extends boolean = false>(
params?: GetTokenParams<R>
): Promise<R extends true ? string : JWT>
): Promise<R extends true ? string : JWT | null>
export interface JWTOptions {
secret?: string

View File

@@ -1,5 +1,5 @@
import { Profile, TokenSet, User } from "."
import { Awaitable } from "internals/utils"
import { Awaitable } from "./internals/utils"
export type ProviderType = "oauth" | "email" | "credentials"
@@ -55,7 +55,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
export type OAuthProviderType =
| "Apple"
| "Attlassian"
| "Atlassian"
| "Auth0"
| "AzureADB2C"
| "Basecamp"
@@ -144,6 +144,7 @@ export interface EmailConfig extends CommonProviderOptions {
provider: EmailConfig
}): Awaitable<void>
}
export type EmailProvider = (options: Partial<EmailConfig>) => EmailConfig
// TODO: Rename to Token provider

View File

@@ -19,7 +19,7 @@ JWTType.getToken({
raw: true,
})
// $ExpectType Promise<JWT>
// $ExpectType Promise<JWT | null>
JWTType.getToken({
req: nextReq,
secret: "secret",

View File

@@ -16,8 +16,7 @@
"next-auth/providers": ["./providers"],
"next-auth/adapters": ["./adapters"],
"next-auth/client": ["./client"],
"next-auth/jwt": ["./jwt"],
"internals": ["./internals"]
"next-auth/jwt": ["./jwt"]
}
}
}

View File

@@ -79,7 +79,7 @@ When using NextAuth.js without a database, the user object it will always be a p
:::
:::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 only want to allow users who already have accounts in the database to sign in, you can check for the existence 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.
:::

View File

@@ -121,11 +121,29 @@ By default JSON Web Tokens are signed (JWS) but not encrypted (JWE), as JWT encr
jwt: {
// A secret to use for key generation - you should set this explicitly
// Defaults to NextAuth.js secret if not explicitly specified.
// This is used to generate the actual signingKey and produces a warning
// message if not defined explicitly.
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
// This gives you direct knowledge of the key used to sign the token so you can use it
// to authenticate indirectly (eg. to a database driver)
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
// If you chose something other than the default algorithm for the signingKey (HS512)
// you also need to configure the algorithm
// verificationOptions: {
// algorithms: ['HS256']
// },
// Set to true to use encryption. Defaults to false (signing only).
// encryption: true,
// encryptionKey: "",
// decryptionKey = encryptionKey,
// decryptionOptions = {
// algorithms: ['A256GCM']
// },
// You can define your own encode/decode functions for signing and encryption
// if you want to override the default behaviour.
// async encode({ secret, token, maxAge }) {},

View File

@@ -111,14 +111,7 @@ providers: [
...
```
:::tip
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 three changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)
2. Re-export your config: at [`src/providers/index.js`](https://github.com/nextauthjs/next-auth/blob/main/src/providers/index.js)
3. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
You can look at the existing built-in providers for inspiration.
:::
### OAuth provider options
@@ -141,7 +134,7 @@ You can look at the existing built-in providers for inspiration.
| 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`, `none` | 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 |
## Sign in with Email
@@ -230,3 +223,14 @@ export const Image = ({ children, src, alt = '' }) => (
<img alt={alt} src={src} />
</div>
)
## 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.

View File

@@ -76,6 +76,7 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
The `code_verifier` is saved in a cookie called (by default) `__Secure-next-auth.pkce.code_verifier` which expires after 15 minutes.
Check if `cookies.pkceCodeVerifier` is configured correctly. The default `code_challenge_method` is `"S256"`. This is currently not configurable to `"plain"`, as it is not recommended, and in most cases it is only supported for backward compatibility.
---
### Session Handling

View File

@@ -54,10 +54,15 @@ NextAuth.js does not currently support automatically signing into sites on diffe
### Can I use NextAuth.js with React Native?
NextAuth.js is designed as a secure, confidental client and implements a server side authentication flow.
NextAuth.js is designed as a secure, confidential client and implements a server side authentication flow.
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
### Is NextAuth.js supporting TypeScript?
Yes! Check out the [TypeScript docs](/getting-started/typescript)
---
## Databases
@@ -70,7 +75,7 @@ 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.
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.
@@ -126,7 +131,7 @@ When an email address is associated with an OAuth account it does not necessaril
With automatic account linking on sign in, this can be exploited by bad actors to hijack accounts by creating an OAuth account associated with the email address of another user.
For this reason it is not secure to automatically link accounts between abitrary providers on sign in, which is why this feature is generally not provided by authentication service and is not provided by NextAuth.js.
For this reason it is not secure to automatically link accounts between arbitrary providers on sign in, which is why this feature is generally not provided by authentication service and is not provided by NextAuth.js.
Automatic account linking is seen on some sites, sometimes insecurely. It can be technically possible to do automatic account linking securely if you trust all the providers involved to ensure they have securely verified the email address associated with the account, but requires placing trust (and transferring the risk) to those providers to handle the process securely.
@@ -169,7 +174,7 @@ 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.
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
### What are the advantages of JSON Web Tokens?
@@ -201,13 +206,13 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
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 you do not explicitly 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).
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space available 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.
@@ -217,7 +222,7 @@ You can specify other valid algorithms - [as specified in RFC 7518](https://tool
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.
Using explicit public/private keys for signing is strongly recommended.
### What signing and encryption standards does NextAuth.js support?

View File

@@ -3,18 +3,19 @@ id: typescript
title: TypeScript
---
NextAuth.js comes with its own types, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions of what certain objects are, and sometimes also links to documentation, and examples.
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about how certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
:::note
:::warning
The types at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) under the name of `@types/next-auth` are now deprecated, and not maintained anymore.
:::
***
## Module Augmentaion
## Module Augmentation
`next-auth` comes with certain types/interfaces, that are shared across submodules. Good examples are `Session` and `JWT`. Ideally, you should only need to create these types at a single place, and TS should pick them up in every location where they are referenced. Luckily, this is exactly what Module Agumentation can do for us. Define your shared interfaces in a single location, and get type-safety across your application, when you use `next-auth` (or one of its submodules).
`next-auth` comes with certain types/interfaces, that are shared across submodules. Good examples are `Session` and `JWT`. Ideally, you should only need to create these types at a single place, and TS should pick them up in every location where they are referenced. Luckily, this is exactly what Module Augmentation can do for us. Define your shared interfaces in a single location, and get type-safety across your application, when you use `next-auth` (or one of its submodules).
1. Let's look at `Session`:
### Main module
Let's look at `Session`:
```ts title="pages/api/[...nextauth].ts"
import NextAuth from "next-auth"
@@ -47,6 +48,9 @@ To extend/augment this type, create a `types/next-auth.d.ts` file in your projec
import NextAuth from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
*/
interface Session {
user: {
/** The user's postal address. */
@@ -55,23 +59,47 @@ declare module "next-auth" {
}
}
```
#### Popular interfaces to augment
Although you can augment almost anything, here are some of the more common interfaces that you might want to override in the `next-auth` module:
```ts
/**
* The shape of the user object returned in the OAuth providers' `profile` callback,
* or the second parameter of the `session` callback, when using a database.
*/
interface User {}
/**
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
interface Account {}
/** The OAuth profile returned from your provider */
interface Profile {}
```
Make sure that the `types` folder is added to [`typeRoots`](https://www.typescriptlang.org/tsconfig/#typeRoots) in your project's `tsconfig.json` file.
2. Check out `JWT` also:
### Submodules
The `JWT` interface can be found in the `next-auth/jwt` submodule:
```ts title="types/next-auth.d.ts"
declare module "next-auth/jwt" {
interface JWT {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
/** OpenID ID Token */
idToken?: string
}
}
```
Note that this time we declared `JWT` inside `next-auth/jwt`, as this is its default location.
### Useful links
1. [TypeScript documentation: Module Augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)
2. [Digital Ocean: Module Augmentation in TypeScript](https://www.digitalocean.com/community/tutorials/typescript-module-augmentation)
## Contributing
Contributions of any kind are always welcome, especially for TypeScript. Please keep in mind that we are a small team working on this project in our free time. We will try our best to give support, but if you think you have a solution for a problem, please open a PR!
:::note
When contributing to TypeScript, if the actual JavaScript user API does not change in a breaking manner, we reserve the right to push any TypeScript change in a minor release. This is to ensure that we can keep us on a faster release cycle.
:::

View File

@@ -11,6 +11,10 @@ https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-co
https://www.linkedin.com/developers/apps/
From the Auth tab get the client ID and client secret. On the same tab, add redirect URLs such as http://localhost:3000/api/auth/callback/linkedin so LinkedIn can correctly redirect back to your application. Finally, head over to the Products tab and enable the "Sign In with LinkedIn" product. The LinkedIn team will review and approve your request before you can test it out.
![image](https://user-images.githubusercontent.com/330396/114429603-68195600-9b72-11eb-8311-62e58383c42b.png)
## Example
```js

View File

@@ -185,7 +185,7 @@ Once you have saved your schema, use the Prisma CLI to generate the Prisma Clien
npx prisma generate
```
To configure you database to use the new schema (i.e. create tables and columns) use the `primsa migrate` command:
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
```
npx prisma migrate dev --preview-feature

View File

@@ -11,7 +11,7 @@ _New submissions and edits are welcome!_
### [NextJS Authentication Crash Course with NextAuth.js](https://youtu.be/o_wZIVmWteQ)
This tutorial dives in to the ins and outs of NextAuth including email, Github, Twitter and integrating with Auth0 in under hour.
This tutorial dives in to the ins and outs of NextAuth including email, GitHub, Twitter and integrating with Auth0 in under hour.
### [Create your own NextAuth.js Login Pages](https://youtu.be/kB6YNYZ63fw)
@@ -43,6 +43,10 @@ This approach can be used to authenticate existing user accounts against any bac
How to write tests using Cypress.
### [Usage with class components](tutorials/usage-with-class-components)
How to use `useSession()` hook with class components.
## Other tutorials and explainers
_These are tutorials and explainers that have been submitted or that we have found on the web and are hosted elsewhere They include articles, videos and example projects. Submissions for inclusion are welcome!_
@@ -77,7 +81,7 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
### [Adding Authentication to an existing Next.js Application in no time!](https://dev.to/ndom91/adding-authentication-to-an-existing-serverless-next-js-app-in-no-time-with-nextauth-js-192h)
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and api routes behind that authentication, etc.
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and API routes behind that authentication, etc.
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)

View File

@@ -0,0 +1,61 @@
---
id: usage-with-class-components
title: Usage with class components
---
If you want to use the `useSession()` hook in your class components you can do so with the help of a higher order component or with a render prop.
## Higher Order Component
```js
import { useSession } from "next-auth/client"
const withSession = Component => props => {
const [session, loading] = useSession()
// if the component has a render property, we are good
if (Component.prototype.render) {
return <Component session={session} loading={loading} {...props} />
}
// if the passed component is a function component, there is no need for this wrapper
throw new Error([
"You passed a function component, `withSession` is not needed.",
"You can `useSession` directly in your component."
].join("\n"))
};
// Usage
class ClassComponent extends React.Component {
render() {
const {session, loading} = this.props
return null
}
}
const ClassComponentWithSession = withSession(ClassComponent)
```
## Render Prop
```js
import { useSession } from "next-auth/client"
const UseSession = ({ children }) => {
const [session, loading] = useSession()
return children({ session, loading })
};
// Usage
class ClassComponent extends React.Component {
render() {
return (
<UseSession>
{({ session, loading }) => (
<pre>{JSON.stringify(session, null, 2)}</pre>
)}
</UseSession>
)
}
}
```