Compare commits

...

44 Commits

Author SHA1 Message Date
Balázs Orbán
35583a513d fix: ts type, and transpilation (#2037)
* fix(ts): mark getUserByEmail param as nullable

* fix(build): transpile with optional-catch-binding
2021-05-20 20:40:45 +02:00
Nico Domino
665d91019f style: small tweaks to navbar (#2024) 2021-05-20 16:22:31 +02:00
Daniel Sabbagh
f2b816b7b9 docs: fix minor typo (#2022)
* fix minor typo

* fix typo again

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-19 20:59:00 +02:00
Nico Domino
2e770fb0bf docs: update github PR template comments (#2025) 2021-05-19 01:11:42 +02:00
Nico Domino
e83e7231fb docs(search): add new algolia docsearch (#2023)
* docs(search): add new algolia docsearch

* style(search): fix algolia docsearch mobile style
2021-05-18 21:49:43 +02:00
Marco Valsecchi
4593ec8b01 docs(provider): Fix Using a custom OAuth Provider index link (#2019) 2021-05-18 14:30:17 +02:00
Nico Domino
12517f629b docs(style): add github star counter to navbar (#2015)
* docs(style): add github star counter to navbar

* chore: cleanup kFormatter logic
2021-05-18 00:23:44 +02:00
Balázs Orbán
77012bc00c fix(deps): pin down legacy adapter versions (#2009)
* fix(deps): pin down legacy adapter versions

* chore: trigger github actions
2021-05-16 20:52:04 +02:00
Chalk
60fdf26a56 fix(provider): support multiple image formats for Twitter profile (#1995)
see supported formats: https://developer.twitter.com/en/docs/twitter-api/v1/accounts-and-users/manage-account-settings/api-reference/post-account-update_profile_image

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-15 23:28:34 +02:00
Igor Danchenko
0fae0c7a8e feat(provider): forward request to authorize (#1979)
* feat/add-request-to-credentials-authorize

* Update src/server/routes/callback.js

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

* Update types/providers.d.ts

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

* Update www/docs/providers/credentials.md

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

* Update www/docs

* Update test app

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-15 03:00:39 +02:00
Nico Domino
eba79f4445 feat: upgrade docusaurus + style a bit (#1993) 2021-05-13 23:24:02 +02:00
Balázs Orbán
e3bb9881ea chore(dev): fix dev app imports (#1991) 2021-05-13 12:36:28 +02:00
Balázs Orbán
827049cb35 docs(www): Docusaurus webpack 5 (#1989)
This reverts commit bc9805d1ba.
2021-05-13 01:28:36 +02:00
Nico Domino
ad8100d402 docs: max cookie size information (#1949)
* fix: max cookie information

* fix: typo

* fix: wording regarding cookie size

* Update www/docs/faq.md

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:22:08 +02:00
dependabot[bot]
7b5defff16 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#1976)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-12 10:20:24 +02:00
Balázs Orbán
bc9805d1ba docs: revert "Docusaurus webpack 5" (#1982)
This reverts commit c823016b36.
2021-05-12 10:19:30 +02:00
Sébastien Lorber
c823016b36 docs(www): update Docusaurus to webpack 5 (#1826)
* upgrade

* upgrade

* fix lunr plugin bug

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-12 10:01:33 +02:00
dependabot[bot]
ca0f4c6fba chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /www (#1977)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-11 21:04:27 +02:00
Balázs Orbán
c0d2f2d852 fix(adapter): upgrade legacy adapters (#1952)
* refactor(adapter): upgrade typeorm-legacy-adapter

* fix(ts): correct exported typeorm types

* fix(adapter): correct adapter exports

* chore(deps): upgrade typeorm-legacy-adapter

* chore(deps): upgrade dependencies

* chore: match comment for legacy adapters

* fix(ts): correctly export Prisma legacy types

* chore(deps): upgrade prisma legacy adapter

* chore(deps): remove unused dependencies

* test(ts): only run TS tests on latest TS version

* chore(deps): remove unused dev dependencies

* chore(deps): upgrade prisma adapter
2021-05-11 00:15:01 +02:00
Balázs Orbán
71f63117a9 fix(oauth): correctly set internal protection value (#1962) 2021-05-09 23:00:06 +02:00
i-palindrome-i
d04ce29314 feat(provider): add WorkOS provider (#1939)
* feat(provider): add WorkOS provider

* Update www/docs/providers/workos.md

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

* Update workos.md

Co-authored-by: Adam Kaczmarek <adamkaz+workos@gmail.com>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-05-09 21:45:37 +02:00
Lluis Agusti
d2882f1958 fix(deps): unpin react-dom version (#1956) 2021-05-09 21:43:51 +02:00
Lluis Agusti
66db563ca5 docs(provider): link to providers' source code (#1955) 2021-05-09 21:41:28 +02:00
Marcus Reinhardt
9619077363 docs(typeorm): update link to source (#1957) 2021-05-08 23:37:55 +02:00
dependabot[bot]
013ccb4cb0 chore(deps): bump lodash from 4.17.19 to 4.17.21 (#1954)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-08 00:40:35 +02:00
dependabot[bot]
6eb41259d1 chore(deps): bump underscore from 1.10.2 to 1.13.1 (#1951)
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.10.2 to 1.13.1.
- [Release notes](https://github.com/jashkenas/underscore/releases)
- [Commits](https://github.com/jashkenas/underscore/compare/1.10.2...1.13.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-07 17:58:22 +02:00
Nico Domino
141f8d07e2 docs(provider): clarify where user is created with email provider (#1950) 2021-05-07 00:54:52 +02:00
Zack Sheppard
ffd0601ab0 fix(ts): improve events handlers' types (#1853)
* Constrain the adapters type generics more accurately

* Add types for the incoming messages to events callbacks

* Code review comments from @lluia

* Rebase from trunk and fix merge conflicts

* Update documentation

* Rip out generics

* fix(build): export aliases from client (#1909)

* docs(provider): update providers documentation (#1900)

* docs(providers): update providers documentation

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

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

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

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

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

* docs(providers): make names list inline

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

* fix(ts): unset generics defaults for overriding (#1891)

Co-authored-by: Lluis Agusti <hi@llu.lu>

* fix(ts): tweak Adapter related types (#1914)

Contains the following squashed commits:

* fix(ts): make first adapter parameter non-optional
* fix(ts): make defaulted values non-optional internally
* test(ts): fix linting

* fix(page): don't pass params to custom signout page (#1912)

* For the custom signout page addressed two issues with the query params being added to the signout url. A conditional check on the error value is now made before adding it as a query param. Also added a conditional check on the callbackUrl and if present that then gets appended as a query param to the signout api call.

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

Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>

* docs(www): fix typo (#1922)

* docs(provider): Update IdentityServer 4 demo configuration (#1932)

* Responding to code review comments

* Fix tests

* Fix lint error

Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Kristóf Poduszló <kripod@protonmail.com>
Co-authored-by: Anubisoft <1471887+anubisoft@users.noreply.github.com>
Co-authored-by: anubisoft <anubisoftprez@gmail.com>
Co-authored-by: Ernie Miranda <emiranda04@users.noreply.github.com>
Co-authored-by: Mathis Møller <thisen-dk@hotmail.com>
2021-05-06 11:44:30 +02:00
Lluis Agusti
7864d4705d docs(adapter): mention new types (#1916)
Containts the following squashed commits:

* docs(adapters): mention new types
* docs(adapters): rename interface on example
* docs(adapters): move section above
* docs(adapters): fix casing
* docs(adapters): fix example import
* fix(www): Typescript -> TypeScript
2021-05-06 11:10:37 +02:00
i-palindrome-i
98dc82e5d6 docs: fix command in CONTRIBUTING.md (#1940)
Co-authored-by: Adam Kaczmarek <adamkaz+workos@gmail.com>
2021-05-06 10:19:50 +02:00
Balázs Orbán
86baefdd9d feat(adapter): take away error handling from adapters (#1871) 2021-05-05 19:45:11 +02:00
Manish Chiniwalar
332e237c3e feat(provider): add Dropbox (#1756)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Adam Bergman <adam@fransvilhelm.com>
2021-05-05 19:42:55 +02:00
Mathis Møller
2fce08c0b5 docs(provider): Update IdentityServer 4 demo configuration (#1932) 2021-05-05 15:17:22 +02:00
Ernie Miranda
adf3fb669f docs(www): fix typo (#1922) 2021-05-04 19:34:06 +02:00
Anubisoft
5323be3594 fix(page): don't pass params to custom signout page (#1912)
* For the custom signout page addressed two issues with the query params being added to the signout url. A conditional check on the error value is now made before adding it as a query param. Also added a conditional check on the callbackUrl and if present that then gets appended as a query param to the signout api call.

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

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

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

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

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

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

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

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

* docs(providers): make names list inline

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

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

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

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

* test(ts): fix adapter tests
2021-04-29 19:43:34 +02:00
101 changed files with 21360 additions and 12414 deletions

View File

@@ -18,21 +18,23 @@ merge of your pull request!
## Reasoning 💡
What changes are being made? What feature/bug is being fixed here?
<!-- What changes are being made? What feature/bug is being fixed here? -->
## Checklist 🧢
Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
<!-- Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
To check an item, place an `x` in the box like so: `- [x] Documentation`.
To check an item, place an `x` in the box like so: `- [x] Documentation`. -->
- [ ] Documentation
- [ ] Tests
- [ ] Ready to be merged
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
## Affected issues 🎟
<!--
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
If you write `"Fixes"` or `"Closes"` before the issue link like so:
@@ -42,3 +44,5 @@ Fixes #359
```
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
-->

View File

@@ -32,7 +32,7 @@ cd next-auth
2. Install packages:
```sh
npm i && npm dev:setup
npm i && npm run dev:setup
```
3. Populate `.env.local`:

View File

@@ -1,5 +1,9 @@
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import NextAuth from "next-auth"
import EmailProvider from "next-auth/providers/email"
import GitHubProvider from "next-auth/providers/github"
import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
@@ -28,15 +32,15 @@ export default NextAuth({
// }
// },
providers: [
Providers.Email({
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
from: process.env.EMAIL_FROM,
}),
Providers.GitHub({
GitHubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Auth0({
Auth0Provider({
clientId: process.env.AUTH0_ID,
clientSecret: process.env.AUTH0_SECRET,
domain: process.env.AUTH0_DOMAIN,
@@ -45,36 +49,36 @@ export default NextAuth({
// authorizationParams: {
// response_mode: 'form_post'
// }
protection: 'pkce'
protection: "pkce",
}),
Providers.Twitter({
TwitterProvider({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET
clientSecret: process.env.TWITTER_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
CredentialsProvider({
name: "Credentials",
credentials: {
password: { label: 'Password', type: 'password' }
password: { label: "Password", type: "password" },
},
async authorize (credentials) {
if (credentials.password === 'password') {
async authorize(credentials, req) {
if (credentials.password === "password") {
return {
id: 1,
name: 'Fill Murray',
email: 'bill@fillmurray.com',
image: 'https://www.fillmurray.com/64/64'
name: "Fill Murray",
email: "bill@fillmurray.com",
image: "https://www.fillmurray.com/64/64",
}
}
return null
}
})
},
}),
],
jwt: {
encryption: true,
secret: process.env.SECRET
secret: process.env.SECRET,
},
debug: false,
theme: 'auto'
theme: "auto",
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL

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

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

View File

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

5043
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,15 +30,15 @@
},
"scripts": {
"build": "npm run build:js && npm run build:css",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev:setup": "npm run build:css && cd app && npm i",
"dev": "cd app && npm run dev",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
"test": "echo \"Write some tests...\"; npm run test:types",
"test:types": "dtslint types",
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
@@ -61,9 +61,9 @@
],
"license": "ISC",
"dependencies": {
"@next-auth/prisma-legacy-adapter": "canary",
"@next-auth/typeorm-legacy-adapter": "canary",
"crypto-js": "^4.0.0",
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
@@ -72,13 +72,11 @@
"pkce-challenge": "^2.1.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.14",
"querystring": "^0.2.0",
"require_optional": "^1.0.1",
"typeorm": "^0.2.30"
"querystring": "^0.2.0"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "16.13.1 || ^17"
"react-dom": "^16.13.1 || ^17"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
@@ -90,9 +88,9 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.14.2",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
@@ -113,19 +111,10 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"mocha": "^8.1.3",
"mongodb": "^3.5.9",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"next": "^10.0.5",
"pg": "^8.2.1",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"prisma": "^2.16.1",
"puppeteer": "^5.2.1",
"puppeteer-extra": "^3.1.15",
"puppeteer-extra-plugin-stealth": "^2.6.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3"

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
/*
* Source code is now at:
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
*/
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
export default PrismaLegacyAdapter
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"

View File

@@ -1,8 +1,9 @@
/*
* Source code is now at:
* Source code can be found at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
*/
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
export default TypeORMLegacyAdapter
export {
TypeORMLegacyAdapter as Adapter,
Models,
} from "@next-auth/typeorm-legacy-adapter"

View File

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

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

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

View File

@@ -15,7 +15,7 @@ export default function Twitter(options) {
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|png|gif)$/, ".$1"),
}
},
...options,

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

@@ -0,0 +1,24 @@
export default function WorkOS(options) {
return {
id: 'workos',
name: 'WorkOS',
type: 'oauth',
version: '2.0',
scope: '',
params: {
grant_type: 'authorization_code',
client_id: options.clientId,
client_secret: options.clientSecret
},
accessTokenUrl: 'https://api.workos.com/sso/token/',
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
profileUrl: 'https://api.workos.com/sso/profile/',
profile: (profile) => {
return {
...profile,
name: `${profile.first_name} ${profile.last_name}`
}
},
...options
}
}

View File

@@ -1,24 +1,24 @@
import adapters from '../adapters'
import jwt from '../lib/jwt'
import parseUrl from '../lib/parse-url'
import logger, { setLogger } from '../lib/logger'
import * as cookie from './lib/cookie'
import * as defaultEvents from './lib/default-events'
import * as defaultCallbacks from './lib/default-callbacks'
import parseProviders from './lib/providers'
import * as routes from './routes'
import renderPage from './pages'
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'
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
import * as cookie from "./lib/cookie"
import * as defaultEvents from "./lib/default-events"
import * as defaultCallbacks from "./lib/default-callbacks"
import parseProviders from "./lib/providers"
import * as routes from "./routes"
import renderPage from "./pages"
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"
// To work properly in production with OAuth providers the NEXTAUTH_URL
// environment variable must be set.
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
/**
@@ -26,7 +26,7 @@ if (!process.env.NEXTAUTH_URL) {
* @param {import("next").NextApiResponse} res
* @param {import("types").NextAuthOptions} userOptions
*/
async function NextAuthHandler (req, res, userOptions) {
async function NextAuthHandler(req, res, userOptions) {
if (userOptions.logger) {
setLogger(userOptions.logger)
}
@@ -39,13 +39,15 @@ async function NextAuthHandler (req, res, userOptions) {
// to avoid early termination of calls to the serverless function
// (and then return that promise when we are done) - eslint
// complains but I'm not sure there is another way to do this.
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
extendRes(req, res, resolve)
if (!req.query.nextauth) {
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
const error =
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
return res.status(500).end(`Error: ${error}`)
}
@@ -53,31 +55,48 @@ async function NextAuthHandler (req, res, userOptions) {
nextauth,
action = nextauth[0],
providerId = nextauth[1],
error = nextauth[1]
error = nextauth[1],
} = req.query
// @todo refactor all existing references to baseUrl and basePath
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
const { basePath, baseUrl } = parseUrl(
process.env.NEXTAUTH_URL || process.env.VERCEL_URL
)
const cookies = {
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
...cookie.defaultCookies(
userOptions.useSecureCookies || baseUrl.startsWith("https://")
),
// Allow user cookie options to override any cookie settings above
...userOptions.cookies
...userOptions.cookies,
}
const secret = createSecret({ userOptions, basePath, baseUrl })
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
const providers = parseProviders({
providers: userOptions.providers,
baseUrl,
basePath,
})
const provider = providers.find(({ id }) => id === providerId)
// 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]
// TODO:
// - rename to `checks` in 4.x, so it is similar to `openid-client`
// - stop supporting `protection` as string
// - remove `state` property
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
// Priority: (protection array > protection string) > state > default
if (provider.protection) {
provider.protection = Array.isArray(provider.protection)
? provider.protection
: [provider.protection]
} else if (provider.state !== undefined) {
provider.protection = [provider.state ? "state" : "none"]
} else {
// Default to state, as we did in 3.1
// REVIEW: should we use "pkce" or "none" as default?
provider.protection = ["state"]
}
}
@@ -86,14 +105,16 @@ async function NextAuthHandler (req, res, userOptions) {
// Parse database / adapter
// If adapter is provided, use it (advanced usage, overrides database)
// If database URI or config object is provided, use it (simple usage)
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
const adapter =
userOptions.adapter ??
(userOptions.database && adapters.Default(userOptions.database))
// User provided options are overriden by other options,
// except for the options with special handling above
req.options = {
debug: false,
pages: {},
theme: 'auto',
theme: "auto",
// Custom options override defaults
...userOptions,
// These computed settings can have values in userOptions but we override them
@@ -111,7 +132,7 @@ async function NextAuthHandler (req, res, userOptions) {
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
maxAge,
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
...userOptions.session
...userOptions.session,
},
// JWT options
jwt: {
@@ -119,20 +140,20 @@ async function NextAuthHandler (req, res, userOptions) {
maxAge, // same as session maxAge,
encode: jwt.encode,
decode: jwt.decode,
...userOptions.jwt
...userOptions.jwt,
},
// Event messages
events: {
...defaultEvents,
...userOptions.events
...userOptions.events,
},
// Callback functions
callbacks: {
...defaultCallbacks,
...userOptions.callbacks
...userOptions.callbacks,
},
pkce: {},
logger
logger,
}
csrfTokenHandler(req, res)
@@ -141,65 +162,74 @@ async function NextAuthHandler (req, res, userOptions) {
const render = renderPage(req, res)
const { pages } = req.options
if (req.method === 'GET') {
if (req.method === "GET") {
switch (action) {
case 'providers':
case "providers":
return routes.providers(req, res)
case 'session':
case "session":
return routes.session(req, res)
case 'csrf':
case "csrf":
return res.json({ csrfToken: req.options.csrfToken })
case 'signin':
case "signin":
if (pages.signIn) {
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
if (error) { signinUrl = `${signinUrl}&error=${error}` }
let signinUrl = `${pages.signIn}${
pages.signIn.includes("?") ? "&" : "?"
}callbackUrl=${req.options.callbackUrl}`
if (error) {
signinUrl = `${signinUrl}&error=${error}`
}
return res.redirect(signinUrl)
}
return render.signin()
case 'signout':
if (pages.signOut) {
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
}
case "signout":
if (pages.signOut) return res.redirect(pages.signOut)
return render.signout()
case 'callback':
case "callback":
if (provider) {
if (await pkce.handleCallback(req, res)) return
if (await state.handleCallback(req, res)) return
return routes.callback(req, res)
}
break
case 'verify-request':
case "verify-request":
if (pages.verifyRequest) {
return res.redirect(pages.verifyRequest)
}
return render.verifyRequest()
case 'error':
case "error":
if (pages.error) {
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
return res.redirect(
`${pages.error}${
pages.error.includes("?") ? "&" : "?"
}error=${error}`
)
}
// These error messages are displayed in line on the sign in page
if ([
'Signin',
'OAuthSignin',
'OAuthCallback',
'OAuthCreateAccount',
'EmailCreateAccount',
'Callback',
'OAuthAccountNotLinked',
'EmailSignin',
'CredentialsSignin'
].includes(error)) {
if (
[
"Signin",
"OAuthSignin",
"OAuthCallback",
"OAuthCreateAccount",
"EmailCreateAccount",
"Callback",
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
}
return render.error({ error })
default:
}
} else if (req.method === 'POST') {
} else if (req.method === "POST") {
switch (action) {
case 'signin':
case "signin":
// Verified CSRF Token required for all sign in routes
if (req.options.csrfTokenVerified && provider) {
if (await pkce.handleSignin(req, res)) return
@@ -208,16 +238,19 @@ async function NextAuthHandler (req, res, userOptions) {
}
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
case 'signout':
case "signout":
// Verified CSRF Token required for signout
if (req.options.csrfTokenVerified) {
return routes.signout(req, res)
}
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
case 'callback':
case "callback":
if (provider) {
// Verified CSRF Token required for credentials providers only
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
if (
provider.type === "credentials" &&
!req.options.csrfTokenVerified
) {
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
}
@@ -226,31 +259,33 @@ async function NextAuthHandler (req, res, userOptions) {
return routes.callback(req, res)
}
break
case '_log':
case "_log":
if (userOptions.logger) {
try {
const {
code = 'CLIENT_ERROR',
level = 'error',
message = '[]'
code = "CLIENT_ERROR",
level = "error",
message = "[]",
} = req.body
logger[level](code, ...JSON.parse(message))
} catch (error) {
// If logging itself failed...
logger.error('LOGGER_ERROR', error)
logger.error("LOGGER_ERROR", error)
}
}
return res.end()
default:
}
}
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
return res
.status(400)
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
})
}
/** Tha main entry point to next-auth */
export default function NextAuth (...args) {
export default function NextAuth(...args) {
if (args.length === 1) {
return (req, res) => NextAuthHandler(req, res, args[0])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

47
types/adapters.d.ts vendored
View File

@@ -1,13 +1,36 @@
import { AppOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig, SendVerificationRequest } from "./providers"
import { ConnectionOptions } from "typeorm"
import { EmailConfig } from "./providers"
/** Legacy */
export {
TypeORMAccountModel,
TypeORMSessionModel,
TypeORMUserModel,
TypeORMVerificationRequestModel,
} from "@next-auth/typeorm-legacy-adapter"
import {
TypeORMAdapter,
TypeORMAdapterModels,
} from "@next-auth/typeorm-legacy-adapter"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
export const TypeORM: {
Models: TypeORMAdapterModels
Adapter: TypeORMAdapter
}
export const Prisma: {
Adapter: PrismaLegacyAdapter
}
declare const Adapters: {
Default: Adapter<ConnectionOptions>
TypeORM: { Adapter: Adapter<ConnectionOptions> }
Prisma: { Adapter: Adapter }
Default: TypeORMAdapter
TypeORM: typeof TypeORM
Prisma: typeof Prisma
}
export default Adapters
@@ -22,9 +45,11 @@ export default Adapters
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export interface AdapterInstance<U = User, P = Profile, S = Session> {
/** Used as a prefix for adapter related log messages. (Defaults to `ADAPTER_`) */
displayName?: string
createUser(profile: P): Promise<U>
getUser(id: string): Promise<U | null>
getUserByEmail(email: string): Promise<U | null>
getUserByEmail(email: string | null): Promise<U | null>
getUserByProviderAccountId(
providerId: string,
providerAccountId: string
@@ -118,13 +143,13 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export type Adapter<
C = Record<string, unknown>,
C = unknown,
O = Record<string, unknown>,
U = User,
P = Profile,
S = Session
U = unknown,
P = unknown,
S = unknown
> = (
config: C,
client: C,
options?: O
) => {
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>

81
types/index.d.ts vendored
View File

@@ -1,4 +1,4 @@
// Minimum TypeScript Version: 3.5
// Minimum TypeScript Version: 3.6
/// <reference types="node" />
@@ -111,7 +111,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
*/
events?: EventsOptions
events?: Partial<JWTEventCallbacks | SessionEventCallbacks>
/**
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
@@ -127,7 +127,7 @@ export interface NextAuthOptions {
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
* [Community adapters](https://github.com/nextauthjs/adapters)
*/
adapter?: Adapter
adapter?: ReturnType<Adapter>
/**
* Set debug to true to enable debug messages for authentication and database operations.
* * **Default value**: `false`
@@ -180,7 +180,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
*/
theme?: "auto" | "dark" | "light"
theme?: Theme
/**
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
@@ -215,6 +215,14 @@ export interface NextAuthOptions {
cookies?: CookiesOptions
}
/**
* Change the theme of the built-in pages.
*
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
* [Pages](https://next-auth.js.org/configuration/pages)
*/
export type Theme = "auto" | "dark" | "light"
/**
* Override any of the methods, and the rest will use the default logger.
*
@@ -342,20 +350,61 @@ export interface CookiesOptions {
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventType =
| "signIn"
| "signOut"
| "createUser"
| "updateUser"
| "linkAccount"
| "session"
| "error"
export type EventCallback<MessageType = unknown> = (
message: MessageType
) => Promise<void>
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventCallback = (message: any) => Promise<void>
/**
* If using a `credentials` type auth, the user is the raw response from your
* credential provider.
* For other providers, you'll get the User object from your adapter, the account,
* and an indicator if the user was new to your Adapter.
*/
export interface SignInEventMessage {
user: User
account: Account
isNewUser?: boolean
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
export type EventsOptions = Partial<Record<EventType, EventCallback>>
export interface LinkAccountEventMessage {
user: User
providerAccount: Record<string, unknown>
}
/**
* The various event callbacks you can register for from next-auth
*/
export interface CommonEventCallbacks {
signIn: EventCallback<SignInEventMessage>
createUser: EventCallback<User>
updateUser: EventCallback<User>
linkAccount: EventCallback<LinkAccountEventMessage>
error: EventCallback
}
/**
* The event callbacks will take this form if you are using JWTs:
* signOut will receive the JWT and session will receive the session and JWT.
*/
export interface JWTEventCallbacks extends CommonEventCallbacks {
signOut: EventCallback<JWT>
session: EventCallback<{
session: Session
jwt: JWT
}>
}
/**
* The event callbacks will take this form if you are using Sessions
* and not using JWTs:
* signOut will receive the underlying DB adapter's session object, and session
* will receive the NextAuth client session with extra data.
*/
export interface SessionEventCallbacks extends CommonEventCallbacks {
signOut: EventCallback<Session | null>
session: EventCallback<{ session: Session }>
}
export type EventCallbacks = JWTEventCallbacks | SessionEventCallbacks
export type EventType = keyof EventCallbacks
/** [Documentation](https://next-auth.js.org/configuration/pages) */
export interface PagesOptions {

View File

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

View File

@@ -1,5 +1,5 @@
import { Profile, TokenSet, User } from "."
import { Awaitable } from "./internals/utils"
import { Awaitable, NextApiRequest } from "./internals/utils"
export type ProviderType = "oauth" | "email" | "credentials"
@@ -29,7 +29,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
scope: string
params: { grant_type: string }
accessTokenUrl: string
requestTokenUrl: string
requestTokenUrl?: string
authorizationUrl: string
profileUrl: string
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
@@ -64,6 +64,7 @@ export type OAuthProviderType =
| "Bungie"
| "Cognito"
| "Discord"
| "Dropbox"
| "EVEOnline"
| "Facebook"
| "FACEIT"
@@ -93,6 +94,7 @@ export type OAuthProviderType =
| "Twitter"
| "VK"
| "WordPress"
| "WorkOS"
| "Yandex"
| "Zoho"
@@ -113,7 +115,7 @@ interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(credentials: Record<keyof C, string>): Awaitable<User | null>
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
}
export type CredentialsProvider = (

View File

@@ -1,12 +1,7 @@
import Providers, {
AppProvider,
EmailConfig,
OAuthConfig,
} from "next-auth/providers"
import { Adapter, AdapterInstance } from "next-auth/adapters"
import Providers, { OAuthConfig } from "next-auth/providers"
import { Adapter } from "next-auth/adapters"
import NextAuth, * as NextAuthTypes from "next-auth"
import { IncomingMessage, ServerResponse } from "http"
import * as JWTType from "next-auth/jwt"
import { Socket } from "net"
import { NextApiRequest, NextApiResponse } from "internals/utils"
import { AppOptions } from "internals"
@@ -65,7 +60,7 @@ const exampleVerificationRequest = {
expires: new Date(),
}
const adapter: Adapter = () => {
const MyAdapter: Adapter<Record<string, unknown>> = () => {
return {
async getAdapter(appOptions: AppOptions) {
return {
@@ -131,6 +126,8 @@ const adapter: Adapter = () => {
}
}
const client = {} // Create a fake db client
const allConfig: NextAuthTypes.NextAuthOptions = {
providers: [
Providers.Twitter({
@@ -171,26 +168,29 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
},
},
events: {
async signIn(message) {
async signIn(message: NextAuthTypes.SignInEventMessage) {
return undefined
},
async signOut(message) {
async signOut(message: NextAuthTypes.Session | null) {
return undefined
},
async createUser(message) {
async createUser(message: NextAuthTypes.User) {
return undefined
},
async linkAccount(message) {
async updateUser(message: NextAuthTypes.User) {
return undefined
},
async session(message) {
async linkAccount(message: NextAuthTypes.LinkAccountEventMessage) {
return undefined
},
async error(message) {
async session(message: NextAuthTypes.Session) {
return undefined
},
async error(message: any) {
return undefined
},
},
adapter,
adapter: MyAdapter(client),
useSecureCookies: true,
cookies: {
sessionToken: {

View File

@@ -156,9 +156,9 @@ Check out the content of all the params in addition `token`, to see what info yo
:::
:::warning
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain is commonly imposed by browsers.
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
:::
## Session callback

View File

@@ -7,17 +7,60 @@ Events are asynchronous functions that do not return a response, they are useful
You can specify a handler for any of these events below, for debugging or for an audit log.
```js title="pages/api/auth/[...nextauth].js"
...
events: {
async signIn(message) { /* on successful sign in */ },
async signOut(message) { /* on signout */ },
async createUser(message) { /* user created */ },
async linkAccount(message) { /* account linked to a user */ },
async session(message) { /* session is active */ },
async error(message) { /* error in authentication flow */ }
}
...
```
:::note
Execution of your auth API will be blocked by an `await` on your event handler. If your event handler starts any burdensome work it should not block its own promise on that work.
:::
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc) but typically contains a user object and/or contents of the JSON Web Token and other information relevant to the event.
## Events
### signIn
Sent on successful sign in.
The message will be an object and contain:
- `user` (from your adapter or from the provider if a `credentials` type provider)
- `account` (from your adapter or the provider)
- `isNewUser` (whether your adapter had a user for this account already)
### signOut
Sent when the user signs out.
The message object is the JWT, if using them, or the adapter session object for the session that is being ended.
### createUser
Sent when the adapter is told to create a new user.
The message object will be the user.
### updateUser
Sent when the adapter is told to update an existing user. Currently this is only sent when the user verifies their email address.
The message object will be the user.
### linkAccount
Sent when an account in a given provider is linked to a user in our userbase. For example, when a user signs up with Twitter or when an existing user links their Google account.
The message will be an object and contain:
- `user`: The user object from your adapter
- `providerAccount`: The object returned from the provider.
### session
Sent at the end of a request for the current session.
The message will be an object and contain:
- `session`: The session object from your adapter
- `jwt`: If using JWT, the token for this session.
### error
Sent when an error occurs
The message could be any object relevant to describing the error.

View File

@@ -5,7 +5,7 @@ title: Options
## Environment Variables
### NEXTAUTH_URL
### NEXTAUTH_URL
When deploying to production, set the `NEXTAUTH_URL` environment variable to the canonical URL of your site.
@@ -37,10 +37,10 @@ Options are passed to NextAuth.js when initializing it in an API route.
### providers
* **Default value**: `[]`
* **Required**: *Yes*
- **Default value**: `[]`
- **Required**: _Yes_
#### Description
#### Description
An array of authentication providers for signing in (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order. This can be one of the built-in providers or an object with a custom provider.
@@ -50,10 +50,10 @@ See the [providers documentation](/configuration/providers) for a list of suppor
### database
* **Default value**: `null`
* **Required**: *No (unless using email provider)*
- **Default value**: `null`
- **Required**: _No (unless using email provider)_
#### Description
#### Description
[A database connection string or configuration object.](/configuration/databases)
@@ -61,14 +61,14 @@ See the [providers documentation](/configuration/providers) for a list of suppor
### secret
* **Default value**: `string` (*SHA hash of the "options" object*)
* **Required**: *No - but strongly recommended!*
- **Default value**: `string` (_SHA hash of the "options" object_)
- **Required**: _No - but strongly recommended!_
#### Description
A random string used to hash tokens, sign cookies and generate crytographic keys.
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
The default behaviour is volatile, and it is strongly recommended you explicitly specify a value to avoid invalidating end user sessions when configuration changes are deployed.
@@ -76,8 +76,8 @@ The default behaviour is volatile, and it is strongly recommended you explicitly
### session
* **Default value**: `object`
* **Required**: *No*
- **Default value**: `object`
- **Required**: _No_
#### Description
@@ -90,14 +90,14 @@ session: {
// Use JSON Web Tokens for session instead of database sessions.
// This option can be used with or without a database for users/accounts.
// Note: `jwt` is automatically set to `true` if no database is specified.
jwt: false,
jwt: false,
// Seconds - How long until an idle session expires and is no longer valid.
maxAge: 30 * 24 * 60 * 60, // 30 days
// Seconds - Throttle how frequently to write to database to extend a session.
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
}
```
@@ -106,8 +106,8 @@ session: {
### jwt
* **Default value**: `object`
* **Required**: *No*
- **Default value**: `object`
- **Required**: _No_
#### Description
@@ -124,18 +124,15 @@ jwt: {
// 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: "",
@@ -143,7 +140,6 @@ jwt: {
// 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 }) {},
@@ -168,13 +164,13 @@ An example JSON Web Token contains a payload like this:
You can use the built-in `getToken()` helper method to verify and decrypt the token, like this:
```js
import jwt from 'next-auth/jwt'
import jwt from "next-auth/jwt"
const secret = process.env.JWT_SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
console.log('JSON Web Token', token)
console.log("JSON Web Token", token)
res.end()
}
```
@@ -185,10 +181,10 @@ _For convenience, this helper function is also able to read and decode tokens pa
The getToken() helper requires the following options:
* `req` - (object) Request object
* `secret` - (string) JWT Secret
- `req` - (object) Request object
- `secret` - (string) JWT Secret
You must also pass *any options configured on the `jwt` option* to the helper.
You must also pass _any options configured on the `jwt` option_ to the helper.
e.g. Including custom session `maxAge` and custom signing and/or encryption keys or options
@@ -196,15 +192,15 @@ e.g. Including custom session `maxAge` and custom signing and/or encryption keys
It also supports the following options:
* `secureCookie` - (boolean) Use secure prefixed cookie name
- `secureCookie` - (boolean) Use secure prefixed cookie name
By default, the helper function will attempt to determine if it should use the secure prefixed cookie (e.g. `true` in production and `false` in development, unless NEXTAUTH_URL contains an HTTPS URL).
* `cookieName` - (string) Session token cookie name
- `cookieName` - (string) Session token cookie name
The `secureCookie` option is ignored if `cookieName` is explicitly specified.
* `raw` - (boolean) Get raw token (not decoded)
- `raw` - (boolean) Get raw token (not decoded)
If set to `true` returns the raw token without decrypting or verifying it.
@@ -216,8 +212,8 @@ The JWT is stored in the Session Token cookie, the same cookie used for tokens w
### pages
* **Default value**: `{}`
* **Required**: *No*
- **Default value**: `{}`
- **Required**: _No_
#### Description
@@ -225,7 +221,7 @@ Specify URLs to be used if you want to create custom sign in, sign out and error
Pages specified will override the corresponding built-in page.
*For example:*
_For example:_
```js
pages: {
@@ -243,8 +239,8 @@ See the documentation for the [pages option](/configuration/pages) for more info
### callbacks
* **Default value**: `object`
* **Required**: *No*
- **Default value**: `object`
- **Required**: _No_
#### Description
@@ -277,8 +273,8 @@ See the [callbacks documentation](/configuration/callbacks) for more information
### events
* **Default value**: `object`
* **Required**: *No*
- **Default value**: `object`
- **Required**: _No_
#### Description
@@ -286,27 +282,26 @@ Events are asynchronous functions that do not return a response, they are useful
You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc), but typically contains a user object and/or contents of the JSON Web Token and other information relevant to the event.
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc). See the [events documentation](/configuration/events) for more information on the form of each message object and how to use the events functions.
```js
events: {
async signIn(message) { /* on successful sign in */ },
async signOut(message) { /* on signout */ },
async createUser(message) { /* user created */ },
async linkAccount(message) { /* account linked to a user */ },
async updateUser(message) { /* user updated - e.g. their email was verified */ },
async linkAccount(message) { /* account (e.g. Twitter) linked to a user */ },
async session(message) { /* session is active */ },
async error(message) { /* error in authentication flow */ }
}
```
See the [events documentation](/configuration/events) for more information on how to use the events functions.
---
### adapter
* **Default value**: *Adapter.Default()*
* **Required**: *No*
- **Default value**: _Adapter.Default()_
- **Required**: _No_
#### Description
@@ -324,8 +319,8 @@ If the `adapter` option is specified it overrides the `database` option, only sp
### debug
* **Default value**: `false`
* **Required**: *No*
- **Default value**: `false`
- **Required**: _No_
#### Description
@@ -335,14 +330,15 @@ Set debug to `true` to enable debug messages for authentication and database ope
### logger
* **Default value**: `console`
* **Required**: *No*
- **Default value**: `console`
- **Required**: _No_
#### Description
Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service.
Example:
```js title="/pages/api/auth/[...nextauth].js"
import log from "logging-service"
@@ -371,8 +367,8 @@ If the `debug` level is defined by the user, it will be called regardless of the
### theme
* **Default value**: `"auto"`
* **Required**: *No*
- **Default value**: `"auto"`
- **Required**: _No_
#### Description
@@ -388,8 +384,8 @@ Advanced options are passed the same way as basic options, but may have complex
### useSecureCookies
* **Default value**: `true` for HTTPS sites / `false` for HTTP sites
* **Required**: *No*
- **Default value**: `true` for HTTPS sites / `false` for HTTP sites
- **Required**: _No_
#### Description
@@ -404,15 +400,15 @@ Properties on any custom `cookies` that are specified override this option.
:::
:::warning
Setting this option to *false* in production is a security risk and may allow sessions to hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
Setting this option to _false_ in production is a security risk and may allow sessions to be hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
:::
---
### cookies
* **Default value**: `{}`
* **Required**: *No*
- **Default value**: `{}`
- **Required**: _No_
#### Description

View File

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

View File

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

View File

@@ -196,9 +196,9 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.

View File

@@ -3,7 +3,7 @@ id: typescript
title: TypeScript
---
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.
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 what certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
https://github.com/nextauthjs/next-auth-typescript-example
@@ -14,6 +14,43 @@ The types at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyType
---
## Adapters
If you're writing your own custom Adapter, you can take advantage of the types to make sure your implementation conforms to what's expected:
```ts
import type { Adapter } from "next-auth/adapters"
const MyAdapter: Adapter = () => {
return {
async getAdapter() {
return {
// your adapter methods here
}
},
}
}
```
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
```js
/** @type { import("next-auth/adapters").Adapter } */
const MyAdapter = () => {
return {
async getAdapter() {
return {
// your adapter methods here
}
},
}
}
```
:::note
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
:::
## 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 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).

View File

@@ -11,6 +11,14 @@ https://api.intra.42.fr/apidoc/guides/web_application_flow
https://profile.intra.42.fr/oauth/applications/new
## Options
The **42 School Provider** comes with a set of default options:
- [42 School Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/42.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developer.apple.com/sign-in-with-apple/get-started/
https://developer.apple.com/account/resources/identifiers/list/serviceId
## Options
The **Apple Provider** comes with a set of default options:
- [Apple Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/apple.js)
You can override any of the options to suit your own use case.
## Example
There are two ways you can use the Sign in with Apple provider.
@@ -25,7 +33,7 @@ import Providers from `next-auth/providers`
providers: [
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: {
clientSecret: {
teamId: process.env.APPLE_TEAM_ID,
privateKey: process.env.APPLE_PRIVATE_KEY,
keyId: process.env.APPLE_KEY_ID,
@@ -40,18 +48,18 @@ providers: [
You can convert your Apple key to a single line to use it in a environment variable.
**Mac**
```bash
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
```
**Windows**
```powershell
$k8file = "AuthKey_ID.k8"
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
```
:::
### Pre-generated secret
@@ -92,9 +100,9 @@ Apple doesn't allow you to use localhost in domains or subdomains.
The following guides may be helpful:
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
- [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
* [Guide to configuring Sign in with Apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
- [Guide to configuring Sign in with Apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
### Example server
@@ -114,7 +122,6 @@ Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.e
#### Create certificate
Creating a certificate for localhost is easy with openssl . Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
```bash
@@ -127,7 +134,7 @@ openssl req -x509 -out localhost.crt -keyout localhost.key \
:::tip
**Windows**
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if its not already done.
Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.cnf`
@@ -142,32 +149,30 @@ Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.
Create directory `certificates` and place `localhost.key` and `localhost.crt`
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Apple integration locally:
```js
const { createServer } = require('https')
const { parse } = require('url')
const next = require('next')
const fs = require('fs')
const { createServer } = require("https")
const { parse } = require("url")
const next = require("next")
const fs = require("fs")
const dev = process.env.NODE_ENV !== 'production'
const dev = process.env.NODE_ENV !== "production"
const app = next({ dev })
const handle = app.getRequestHandler()
const httpsOptions = {
key: fs.readFileSync('./certificates/localhost.key'),
cert: fs.readFileSync('./certificates/localhost.crt')
key: fs.readFileSync("./certificates/localhost.key"),
cert: fs.readFileSync("./certificates/localhost.crt"),
}
app.prepare().then(() => {
createServer(httpsOptions, (req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
}).listen(3000, err => {
}).listen(3000, (err) => {
if (err) throw err
console.log('> Ready on https://localhost:3000')
console.log("> Ready on https://localhost:3000")
})
})
```
@@ -177,25 +182,28 @@ app.prepare().then(() => {
If you want to pre-generate your secret, this is an example of the code you will need:
```js
const jwt = require('jsonwebtoken')
const fs = require('fs')
const jwt = require("jsonwebtoken")
const fs = require("fs")
const appleId = 'myapp.example.com'
const keyId = ''
const teamId = ''
const privateKey = fs.readFileSync('path/to/key')
const appleId = "myapp.example.com"
const keyId = ""
const teamId = ""
const privateKey = fs.readFileSync("path/to/key")
const secret = jwt.sign(
{
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + ( 86400 * 180 ), // 6 months
aud: 'https://appleid.apple.com',
sub: appleId
}, privateKey, {
algorithm: 'ES256',
keyid: keyId
})
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
aud: "https://appleid.apple.com",
sub: appleId,
},
privateKey,
{
algorithm: "ES256",
keyid: keyId,
}
)
console.log(secret)
```

View File

@@ -7,6 +7,14 @@ title: Atlassian
https://developer.atlassian.com/cloud/jira/platform/oauth-2-authorization-code-grants-3lo-for-apps/#implementing-oauth-2-0--3lo-
## Options
The **Atlassian Provider** comes with a set of default options:
- [Atlassian Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/atlassian.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -15,6 +15,14 @@ https://manage.auth0.com/dashboard
Configure your application in Auth0 as a 'Regular Web Application' (not a 'Single Page App').
:::
## Options
The **Auth0 Provider** comes with a set of default options:
- [Auth0 Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/auth0.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -32,4 +40,4 @@ providers: [
:::note
`domain` should be the fully qualified domain  e.g. `dev-s6clz2lv.eu.auth0.com`
:::
:::

View File

@@ -11,7 +11,16 @@ https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-c
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
## Options
The **Azure Active Directory Provider** comes with a set of default options:
- [Azure Active Directory Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/azure-ad-b2c.js)
You can override any of the options to suit your own use case.
## Example
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
- Make sure to remember / copy
- Application (client) ID
@@ -22,13 +31,13 @@ https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tena
In `.env.local` create the follwing entries:
```
AZURE_CLIENT_ID=<copy Application (client) ID here>
AZURE_CLIENT_ID=<copy Application (client) ID here>
AZURE_CLIENT_SECRET=<copy generated secret value here>
AZURE_TENANT_ID=<copy the tenant id here>
```
In `pages/api/auth/[...nextauth].js` find or add the AZURE entries:
```js
import Providers from 'next-auth/providers';
...

View File

@@ -11,9 +11,18 @@ https://github.com/basecamp/api/blob/master/sections/authentication.md
https://launchpad.37signals.com/integrations
## Options
The **Basecamp Provider** comes with a set of default options:
- [Basecamp Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/basecamp.js)
You can override any of the options to suit your own use case.
## Examples
### Basic profile information
```js
import Providers from `next-auth/providers`
...
@@ -27,7 +36,7 @@ providers: [
```
:::note
Using the example above, it is only possible to retrieve profile information such as account id, email and name. If you wish to retrieve user data in relation to a specific team, you must provide a different profileUrl and a custom function to handle profile information as shown in the example below.
Using the example above, it is only possible to retrieve profile information such as account id, email and name. If you wish to retrieve user data in relation to a specific team, you must provide a different profileUrl and a custom function to handle profile information as shown in the example below.
:::
### Profile information in relation to specific team
@@ -57,4 +66,4 @@ providers: [
:::tip
The BASECAMP_TEAM_ID is found in the url path of your team's homepage. For example, if the url is `https://3.basecamp.com/1234567/projects`, then in this case the BASECAMP_TEAM_ID is 1234567
:::
:::

View File

@@ -11,6 +11,14 @@ https://develop.battle.net/documentation/guides/using-oauth
https://develop.battle.net/access/clients
## Options
The **Battle.net Provider** comes with a set of default options:
- [Battle.net Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/battlenet.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developer.box.com/reference/
https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/
## Options
The **Box Provider** comes with a set of default options:
- [Box Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/box.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://github.com/Bungie-net/api/wiki/OAuth-Documentation
https://www.bungie.net/en/Application
## Options
The **Bungie Provider** comes with a set of default options:
- [Bungie Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/bungie.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -28,8 +36,6 @@ providers: [
...
```
## Instructions
### Configuration
:::tip
@@ -42,20 +48,20 @@ Bungie doesn't allow you to use localhost as the website URL, instead you need t
Navigate to https://www.bungie.net/en/Application and fill in the required details:
* Application name
* Application Status
* Website
* OAuth Client Type
- Application name
- Application Status
- Website
- OAuth Client Type
- Confidential
* Redirect URL
- Redirect URL
- https://localhost:3000/api/auth/callback/bungie
* Scope
- Scope
- `Access items like your Bungie.net notifications, memberships, and recent Bungie.Net forum activity.`
* Origin Header
- Origin Header
The following guide may be helpful:
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
- [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
### Example server
@@ -75,7 +81,6 @@ Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.e
#### Create certificate
Creating a certificate for localhost is easy with openssl. Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
```bash
@@ -103,32 +108,30 @@ Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.
Create directory `certificates` and place `localhost.key` and `localhost.crt`
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Bungie integration locally:
```js
const { createServer } = require('https')
const { parse } = require('url')
const next = require('next')
const fs = require('fs')
const { createServer } = require("https")
const { parse } = require("url")
const next = require("next")
const fs = require("fs")
const dev = process.env.NODE_ENV !== 'production'
const dev = process.env.NODE_ENV !== "production"
const app = next({ dev })
const handle = app.getRequestHandler()
const httpsOptions = {
key: fs.readFileSync('./certificates/localhost.key'),
cert: fs.readFileSync('./certificates/localhost.crt')
key: fs.readFileSync("./certificates/localhost.key"),
cert: fs.readFileSync("./certificates/localhost.crt"),
}
app.prepare().then(() => {
createServer(httpsOptions, (req, res) => {
const parsedUrl = parse(req.url, true)
handle(req, res, parsedUrl)
}).listen(3000, err => {
}).listen(3000, (err) => {
if (err) throw err
console.log('> Ready on https://localhost:3000')
console.log("> Ready on https://localhost:3000")
})
})
```

View File

@@ -13,6 +13,14 @@ https://console.aws.amazon.com/cognito/users/
You need to select your AWS region to go the the Cognito dashboard.
## Options
The **Amazon Cognito Provider** comes with a set of default options:
- [Amazon Cognito Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/cognito.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -15,6 +15,14 @@ It comes with the constraint that users authenticated in this manner are not per
The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them and the additional complexity associated with supporting usernames and passwords.
:::
## Options
The **Credentials Provider** comes with a set of default options:
- [Credentials Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/credentials.js)
You can override any of the options to suit your own use case.
## Example
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials submitted via HTTP POST as input and returns either:
@@ -31,6 +39,8 @@ The Credentials provider is specified like other providers, except that you need
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
The Credentials provider's `authorize()` method also provides the request object as the second parameter (see example below).
```js title="pages/api/auth/[...nextauth].js"
import Providers from `next-auth/providers`
...
@@ -45,7 +55,7 @@ providers: [
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
},
async authorize(credentials) {
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
@@ -82,7 +92,7 @@ As with all providers, the order you specify them is the order they are displaye
Providers.Credentials({
id: 'domain-login',
name: "Domain Account",
async authorize(credentials) {
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},
@@ -95,7 +105,7 @@ As with all providers, the order you specify them is the order they are displaye
Providers.Credentials({
id: 'intranet-credentials',
name: "Two Factor Auth",
async authorize(credentials) {
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},

View File

@@ -11,6 +11,14 @@ https://discord.com/developers/docs/topics/oauth2
https://discord.com/developers/applications
## Options
The **Discord Provider** comes with a set of default options:
- [Discord Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/discord.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -0,0 +1,34 @@
---
id: dropbox
title: Dropbox
---
## Documentation
https://developers.dropbox.com/oauth-guide
## Configuration
https://www.dropbox.com/developers/apps
## Options
The **Dropbox Provider** comes with a set of default options:
- [Dropbox Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/dropbox.js)
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.Dropbox({
clientId: process.env.DROPBOX_CLIENT_ID,
clientSecret: process.env.DROPBOX_CLIENT_SECRET
})
]
...
```

View File

@@ -15,70 +15,80 @@ The Email provider can be used in conjunction with (or instead of) one or more O
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
If someone provides the email address of an *existing account* when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
If someone provides the email address of an _existing account_ when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
:::tip
The Email Provider can be used with both JSON Web Tokens and database sessions, but you **must** configure a database to use it. It is not possible to enable email sign in without using a database.
:::
## Options
The **Email Provider** comes with a set of default options:
- [Email Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/email.js)
You can override any of the options to suit your own use case.
## Configuration
1. You will need an SMTP account; ideally for one of the [services known to work with nodemailer](http://nodemailer.com/smtp/well-known/).
2. There are two ways to configure the SMTP server connection.
You can either use a connection string or a nodemailer configuration object.
You can either use a connection string or a nodemailer configuration object.
2.1 **Using a connection string**
2.1 **Using a connection string**
Create an .env file to the root of your project and add the connection string and email address.
```js title=".env" {1}
Create an .env file to the root of your project and add the connection string and email address.
```js title=".env" {1}
EMAIL_SERVER=smtp://username:password@smtp.example.com:587
EMAIL_FROM=noreply@example.com
```
```
Now you can add the email provider like this:
Now you can add the email provider like this:
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
}),
],
```
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM
}),
],
```
2.2 **Using a configuration object**
2.2 **Using a configuration object**
In your `.env` file in the root of your project simply add the configuration object options individually:
In your `.env` file in the root of your project simply add the configuration object options individually:
```js title=".env"
EMAIL_SERVER_USER=username
EMAIL_SERVER_PASSWORD=password
EMAIL_SERVER_HOST=smtp.example.com
```js title=".env"
EMAIL_SERVER_USER=username
EMAIL_SERVER_PASSWORD=password
EMAIL_SERVER_HOST=smtp.example.com
EMAIL_SERVER_PORT=587
EMAIL_FROM=noreply@example.com
```
Now you can add the provider settings to the NextAuth options object in the Email Provider.
```
Now you can add the provider settings to the NextAuth options object in the Email Provider.
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD
}
},
from: process.env.EMAIL_FROM
}),
],
```
```js title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: {
host: process.env.EMAIL_SERVER_HOST,
port: process.env.EMAIL_SERVER_PORT,
auth: {
user: process.env.EMAIL_SERVER_USER,
pass: process.env.EMAIL_SERVER_PASSWORD
}
},
from: process.env.EMAIL_FROM
}),
],
```
3. You can now sign in with an email address at `/api/auth/signin`.
An account will not be created for the user until the first time they verify their email address. If an email address already associated with an account, the user will be signed in to that account when they use the link in the email.
A user account (i.e. an entry in the Users table) will not be created for the user until the first time they verify their email address. If an email address is already associated with an account, the user will be signed in to that account when they use the link in the email.
## Customising emails
@@ -89,39 +99,54 @@ e.g.
```js {3} title="pages/api/auth/[...nextauth].js"
providers: [
Providers.Email({
server: process.env.EMAIL_SERVER,
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
sendVerificationRequest: ({ identifier: email, url, token, baseUrl, provider }) => { /* your function */ }
})
sendVerificationRequest: ({
identifier: email,
url,
token,
baseUrl,
provider,
}) => {
/* your function */
},
}),
]
```
The following code shows the complete source for the built-in `sendVerificationRequest()` method:
```js
import nodemailer from 'nodemailer'
import nodemailer from "nodemailer"
const sendVerificationRequest = ({ identifier: email, url, token, baseUrl, provider }) => {
const sendVerificationRequest = ({
identifier: email,
url,
token,
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()
})
}
)
})
}
@@ -131,16 +156,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"
// Uses tables for layout and inline CSS due to email client limitations
return `
@@ -185,7 +210,6 @@ const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
:::
## Customising the Verification Token
By default, we are generating a random verification token. You can define a `generateVerificationToken` method in your provider options if you want to override it:
@@ -197,4 +221,5 @@ providers: [
return "ABC123"
}
})
],
],
```

View File

@@ -11,6 +11,14 @@ https://developers.eveonline.com/blog/article/sso-to-authenticated-calls
https://developers.eveonline.com/
## Options
The **EVE Online Provider** comes with a set of default options:
- [EVE Online Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/eveonline.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
https://developers.facebook.com/apps/
## Options
The **Facebook Provider** comes with a set of default options:
- [Facebook Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/facebook.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -31,4 +39,4 @@ Production applications cannot use localhost URLs to sign in with Facebook. You
:::tip
Email address may not be returned for accounts created on mobile.
:::
:::

View File

@@ -15,6 +15,14 @@ Grant type: `Authorization Code`
Scopes to have basic infos (email, nickname, guid and avatar) : `openid`, `email`, `profile`
## Options
The **FACEIT Provider** comes with a set of default options:
- [FACEIT Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/faceit.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -14,6 +14,14 @@ https://developer.foursquare.com/
:::warning
Foursquare requires an additional `apiVersion` parameter in [`YYYYMMDD` format](https://developer.foursquare.com/docs/places-api/versioning/), which essentially states "I'm prepared for API changes up to this date".
## Options
The **Foursquare Provider** comes with a set of default options:
- [Foursquare Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/foursquare.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -7,6 +7,14 @@ title: FusionAuth
https://fusionauth.io/docs/v1/tech/oauth/
## Options
The **FusionAuth Provider** comes with a set of default options:
- [FusionAuth Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/fusionauth.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -14,8 +22,8 @@ import Providers from `next-auth/providers`
...
providers: [
Providers.FusionAuth({
id: "fusionauth",
name: "FusionAuth",
id: "fusionauth",
name: "FusionAuth",
domain: process.env.FUSIONAUTH_DOMAIN,
clientId: process.env.FUSIONAUTH_CLIENT_ID,
clientSecret: process.env.FUSIONAUTH_SECRET,
@@ -40,7 +48,8 @@ For more information, follow the [FusionAuth 5-minute setup guide](https://fusio
:::
In the OAuth settings for your application, configure the following.
* Redirect URL
- Redirect URL
- https://localhost:3000/api/auth/callback/fusionauth
* Enabled grants
- Make sure *Authorization Code* is enabled.
- Enabled grants
- Make sure _Authorization Code_ is enabled.

View File

@@ -11,6 +11,14 @@ https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
https://github.com/settings/apps
## Options
The **Github Provider** comes with a set of default options:
- [Github Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/github.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -30,5 +38,5 @@ Only allows one callback URL per Client ID / Client Secret.
:::
:::tip
Email address is not returned if privacy settings are enabled.
:::
Email address is not returned if privacy settings are enabled.
:::

View File

@@ -11,6 +11,14 @@ https://docs.gitlab.com/ee/api/oauth2.html
https://gitlab.com/profile/applications
## Options
The **Gitlab Provider** comes with a set of default options:
- [Gitlab Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/gitlab.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -26,5 +34,5 @@ providers: [
```
:::tip
Enable the *"read_user"* option in scope if you want to save the users email address on sign up.
:::
Enable the _"read_user"_ option in scope if you want to save the users email address on sign up.
:::

View File

@@ -11,6 +11,14 @@ https://developers.google.com/identity/protocols/oauth2
https://console.developers.google.com/apis/credentials
## Options
The **Google Provider** comes with a set of default options:
- [Google Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/google.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -48,6 +56,7 @@ const options = {
...
}
```
:::
:::tip
@@ -72,4 +81,5 @@ const options = {
...
}
```
:::

View File

@@ -7,6 +7,13 @@ title: IdentityServer4
https://identityserver4.readthedocs.io/en/latest/
## Options
The **IdentityServer4 Provider** comes with a set of default options:
- [IdentityServer4 Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/identity-server4.js)
You can override any of the options to suit your own use case.
## Example
@@ -15,8 +22,8 @@ import Providers from `next-auth/providers`
...
providers: [
Providers.IdentityServer4({
id: "identity-server4",
name: "IdentityServer4",
id: "identity-server4",
name: "IdentityServer4",
scope: "openid profile email api offline_access", // Allowed Scopes
domain: process.env.IdentityServer4_Domain,
clientId: process.env.IdentityServer4_CLIENT_ID,
@@ -33,18 +40,20 @@ The configuration below is for the demo server at https://demo.identityserver.io
If you want to try it out, you can copy and paste the configuration below.
You can sign in to the demo service with either <b>bob/bob</b> or <b>alice/alice</b>.
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.IdentityServer4({
id: "demo-identity-server",
name: "Demo IdentityServer4",
scope: "openid profile email api offline_access",
id: "demo-identity-server",
name: "Demo IdentityServer4",
scope: "openid profile email api offline_access",
domain: "demo.identityserver.io",
clientId: "server.code",
clientSecret: "secret"
clientId: "interactive.confidential",
clientSecret: "secret",
protection: "pkce"
})
}
...
```

View File

@@ -11,6 +11,14 @@ https://developers.facebook.com/docs/instagram-basic-display-api/getting-started
https://developers.facebook.com/apps/
## Options
The **Instagram Provider** comes with a set of default options:
- [Instagram Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/instagram.js)
You can override any of the options to suit your own use case.
## Example
```jsx
@@ -39,4 +47,4 @@ Email address is not returned by the Instagram API.
:::tip
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrock](https://ngrok.com/docs).
:::
:::

View File

@@ -11,6 +11,14 @@ https://developers.kakao.com/product/kakaoLogin
https://developers.kakao.com/docs/latest/en/kakaologin/common
## Options
The **Kakao Provider** comes with a set of default options:
- [Kakao Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/kakao.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developers.line.biz/en/docs/line-login/integrate-line-login/
https://developers.line.biz/console/
## Options
The **Line Provider** comes with a set of default options:
- [Line Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/line.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -32,4 +40,4 @@ providers: [
Create a provider and a LINE login channel at `https://developers.line.biz/console/`. In the settings of the channel under LINE Login, activate web app and configure the following:
- Callback URL
- http://localhost:3000/api/auth/callback/line
- http://localhost:3000/api/auth/callback/line

View File

@@ -15,6 +15,14 @@ From the Auth tab get the client ID and client secret. On the same tab, add redi
![image](https://user-images.githubusercontent.com/330396/114429603-68195600-9b72-11eb-8311-62e58383c42b.png)
## Options
The **LinkedIn Provider** comes with a set of default options:
- [LinkedIn Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/linked-in.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -27,3 +35,4 @@ providers: [
})
]
...
```

View File

@@ -11,6 +11,14 @@ https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/
https://admin.mailchimp.com/account/oauth2/client/
## Options
The **Mailchimp Provider** comes with a set of default options:
- [Mailchimp Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/mailchimp.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://o2.mail.ru/docs
https://o2.mail.ru/app/
## Options
The **Mail.ru Provider** comes with a set of default options:
- [Mail.ru Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/mailru.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -23,3 +31,4 @@ providers: [
})
]
...
```

View File

@@ -11,6 +11,14 @@ https://github.com/Medium/medium-api-docs
https://medium.com/me/applications
## Options
The **Medium Provider** comes with a set of default options:
- [Medium Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/medium.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://www.netlify.com/blog/2016/10/10/integrating-with-netlify-oauth2/
https://github.com/netlify/netlify-oauth-example
## Options
The **Netlify Provider** comes with a set of default options:
- [Netlify Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/netlify.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -7,6 +7,14 @@ title: Okta
https://developer.okta.com/docs/reference/api/oidc
## Options
The **Okta Provider** comes with a set of default options:
- [Okta Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/okta.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -20,4 +28,4 @@ providers: [
})
]
...
```
```

View File

@@ -17,6 +17,14 @@ You can configure your OAuth Clients on your Osso Admin UI, i.e. https://demo.os
See Osso's complete configuration and testing documentation at https://ossoapp.com/docs/configure/overview
## Options
The **Osso Provider** comes with a set of default options:
- [Osso Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/osso.js)
You can override any of the options to suit your own use case.
## Example
A full example application is available at https://github.com/enterprise-oss/osso-next-auth-example and https://nextjs-demo.ossoapp.com

View File

@@ -11,6 +11,14 @@ https://www.reddit.com/dev/api/
https://www.reddit.com/prefs/apps/
## Options
The **Reddit Provider** comes with a set of default options:
- [Reddit Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/reddit.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -38,27 +46,28 @@ This Provider template only has a one hour access token to it and only has the '
```js
providers: [
{
id: "reddit",
name: "Reddit",
clientId: process.env.REDDIT_CLIENT_ID,
clientSecret: process.env.REDDIT_CLIENT_SECRET,
scope: "identity mysubreddits read", //Check Reddit API Documentation for more. The identity scope is required.
type: "oauth",
version: "2.0",
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&duration=permanent",
profileUrl: "https://oauth.reddit.com/api/v1/me",
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: null,
}
{
id: "reddit",
name: "Reddit",
clientId: process.env.REDDIT_CLIENT_ID,
clientSecret: process.env.REDDIT_CLIENT_SECRET,
scope: "identity mysubreddits read", //Check Reddit API Documentation for more. The identity scope is required.
type: "oauth",
version: "2.0",
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&duration=permanent",
profileUrl: "https://oauth.reddit.com/api/v1/me",
profile: (profile) => {
return {
id: profile.id,
name: profile.name,
email: null,
}
}
]
},
},
]
```
:::

View File

@@ -7,6 +7,14 @@ title: Salesforce
https://help.salesforce.com/articleView?id=remoteaccess_authenticate.htm&type=5
## Options
The **Salesforce Provider** comes with a set of default options:
- [Salesforce Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/salesforce.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -12,6 +12,14 @@ https://api.slack.com/docs/sign-in-with-slack
https://api.slack.com/apps
## Options
The **Slack Provider** comes with a set of default options:
- [Slack Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/slack.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developer.spotify.com/documentation
https://developer.spotify.com/dashboard/applications
## Options
The **Spotify Provider** comes with a set of default options:
- [Spotify Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/spotify.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -7,6 +7,14 @@ title: Strava
http://developers.strava.com/docs/reference/
## Options
The **Strava Provider** comes with a set of default options:
- [Strava Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/strava.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -13,6 +13,14 @@ https://dev.twitch.tv/console/apps
Add the following redirect URL into the console `http://<your-next-app-url>/api/auth/callback/twitch`
## Options
The **Twitch Provider** comes with a set of default options:
- [Twitch Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/twitch.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developer.twitter.com
https://developer.twitter.com/en/apps
## Options
The **Twitter Provider** comes with a set of default options:
- [Twitter Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/twitter.js)
You can override any of the options to suit your own use case.
## Example
```js
@@ -26,7 +34,7 @@ providers: [
```
:::tip
You must enable the *"Request email address from users"* option in your app permissions if you want to obtain the users email address.
You must enable the _"Request email address from users"_ option in your app permissions if you want to obtain the users email address.
:::
![twitter](https://user-images.githubusercontent.com/7902980/83944068-1640ca80-a801-11ea-959c-0e744e2144f7.PNG)
![twitter](https://user-images.githubusercontent.com/7902980/83944068-1640ca80-a801-11ea-959c-0e744e2144f7.PNG)

View File

@@ -11,6 +11,14 @@ https://vk.com/dev/first_guide
https://vk.com/apps?act=manage
## Options
The **VK Provider** comes with a set of default options:
- [VK Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/vk.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://developer.wordpress.com/docs/oauth2/
https://developer.wordpress.com/apps/
## Options
The **Wordpress Provider** comes with a set of default options:
- [Wordpress Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/wordpress.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -0,0 +1,31 @@
---
id: workos
title: WorkOS
---
## Documentation
https://workos.com/docs/sso/guide
## Options
The **WorkOS Provider** comes with a set of default options:
- [WorkOS Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/workos.js)
You can override any of the options to suit your own use case.
## Example
```js
import Providers from `next-auth/providers`
...
providers: [
Providers.WorkOS({
clientId: process.env.WORKOS_ID,
clientSecret: process.env.WORKOS_SECRET,
domain: process.env.WORKOS_DOMAIN
}),
],
...
```

View File

@@ -11,6 +11,14 @@ https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/
https://oauth.yandex.com/client/new
## Options
The **Yandex Provider** comes with a set of default options:
- [Yandex Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/yandex.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -11,6 +11,14 @@ https://www.zoho.com/accounts/protocol/oauth/web-server-applications.html
https://api-console.zoho.com/
## Options
The **Zoho Provider** comes with a set of default options:
- [Zoho Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/zoho.js)
You can override any of the options to suit your own use case.
## Example
```js

View File

@@ -5,21 +5,21 @@ title: Database Adapters
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
You do not need to specify an adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma adapter instead of the default TypeORM adapter, or if you are creating a custom adapter to connect to a database that is not one of the supported databases.
You do not need to specify an Adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma Adapter instead of the default TypeORM Adapter, or if you are creating a custom Adapter to connect to a database that is not one of the supported databases.
### Database Schemas
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
* [MySQL Schema](/schemas/mysql)
* [Postgres Schema](/schemas/postgres)
* [Microsoft SQL Server Schema](/schemas/mssql)
- [MySQL Schema](/schemas/mysql)
- [Postgres Schema](/schemas/postgres)
- [Microsoft SQL Server Schema](/schemas/mssql)
## TypeORM Adapter
NextAuth.js comes with a default adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
The default adapter is the TypeORM adapter, the following configuration options are exactly equivalent.
The default Adapter is the TypeORM Adapter, the following configuration options are exactly equivalent.
```javascript
database: {
@@ -31,21 +31,21 @@ database: {
```javascript
adapter: Adapters.Default({
type: 'sqlite',
database: ':memory:',
synchronize: true
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
```javascript
adapter: Adapters.TypeORM.Adapter({
type: 'sqlite',
database: ':memory:',
synchronize: true
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM adapter. You can use these models in your own code.
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
:::tip
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
@@ -55,22 +55,22 @@ However, it should not be enabled against production databases as it may cause d
## Prisma Adapter
You can also use NextAuth.js with the experimental adapter for [Prisma 2](https://www.prisma.io/docs/).
You can also use NextAuth.js with the experimental Adapter for [Prisma 2](https://www.prisma.io/docs/).
To use this adapter, you need to install Prisma Client and Prisma CLI:
To use this Adapter, you need to install Prisma Client and Prisma CLI:
```
npm install @prisma/client
npm install prisma --save-dev
```
Configure your NextAuth.js to use the Prisma adapter:
Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import Adapters from 'next-auth/adapters'
import { PrismaClient } from '@prisma/client'
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import Adapters from "next-auth/adapters"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
@@ -78,8 +78,8 @@ export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
})
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: Adapters.Prisma.Adapter({ prisma }),
})
@@ -175,6 +175,7 @@ datasource db {
url = env("DATABASE_URL")
}
```
:::
### Generate Client
@@ -232,9 +233,30 @@ if (process.env.NODE_ENV === "production") {
prisma = global.prisma
}
```
:::
:::
## Custom Adapter
See the tutorial for [creating a database adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom adapter. Have a look at the [adapters repository](https://github.com/nextauthjs/adapters) to see community maintained custom adapters or add your own.
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
### Editor integration
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
```js
/** @type { import("next-auth/adapters").Adapter } */
const MyAdapter = () => {
return {
async getAdapter() {
return {
// your adapter methods here
}
},
}
}
```
:::note
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
:::

View File

@@ -22,7 +22,7 @@ export default NextAuth({
username: { label: "DN", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
async authorize(credentials, req) {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: process.env.LDAP_URI,

View File

@@ -48,7 +48,7 @@ export default {
```
:::note
[View source for built-in TypeORM models and schemas](https://github.com/nextauthjs/next-auth/tree/main/src/adapters/typeorm/models)
[View source for built-in TypeORM models and schemas](https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy/src/models)
:::
## Using custom models

View File

@@ -1,52 +1,57 @@
module.exports = {
title: 'NextAuth.js',
tagline: 'Authentication for Next.js',
url: 'https://next-auth.js.org',
baseUrl: '/',
favicon: 'img/favicon.ico',
organizationName: 'nextauthjs',
projectName: 'next-auth',
title: "NextAuth.js",
tagline: "Authentication for Next.js",
url: "https://next-auth.js.org",
baseUrl: "/",
favicon: "img/favicon.ico",
organizationName: "nextauthjs",
projectName: "next-auth",
themeConfig: {
sidebarCollapsible: true,
prism: {
theme: require('prism-react-renderer/themes/vsDark')
theme: require("prism-react-renderer/themes/vsDark"),
},
algolia: {
apiKey: "b81e3ca39a920b7815e880aea49c00ec",
indexName: "next-auth",
searchParameters: {},
},
navbar: {
title: 'NextAuth.js',
title: "NextAuth.js",
logo: {
alt: 'NextAuth Logo',
src: 'img/logo/logo-xs.png'
alt: "NextAuth Logo",
src: "img/logo/logo-xs.png",
},
items: [
{
to: '/getting-started/introduction',
activeBasePath: 'docs',
label: 'Documentation',
position: 'left'
to: "/getting-started/introduction",
activeBasePath: "docs",
label: "Documentation",
position: "left",
},
{
to: '/tutorials',
activeBasePath: 'docs',
label: 'Tutorials',
position: 'left'
to: "/tutorials",
activeBasePath: "docs",
label: "Tutorials",
position: "left",
},
{
to: '/faq',
activeBasePath: 'docs',
label: 'FAQ',
position: 'left'
to: "/faq",
activeBasePath: "docs",
label: "FAQ",
position: "left",
},
{
href: 'https://www.npmjs.com/package/next-auth',
label: 'npm',
position: 'right'
href: "https://www.npmjs.com/package/next-auth",
label: "npm",
position: "right",
},
{
href: 'https://github.com/nextauthjs/next-auth',
label: 'GitHub',
position: 'right'
}
]
href: "https://github.com/nextauthjs/next-auth",
label: "GitHub",
position: "right",
},
],
},
// announcementBar: {
// id: 'release-candiate-announcement',
@@ -57,45 +62,45 @@ module.exports = {
footer: {
links: [
{
title: 'About NextAuth.js',
title: "About NextAuth.js",
items: [
{
label: 'Introduction',
to: '/getting-started/introduction'
label: "Introduction",
to: "/getting-started/introduction",
},
{
label: 'Contributors',
to: '/contributors'
label: "Contributors",
to: "/contributors",
},
{
label: 'Canary documentation',
to: 'https://next-auth-git-canary.nextauthjs.vercel.app/'
}
]
label: "Canary documentation",
to: "https://next-auth-git-canary.nextauthjs.vercel.app/",
},
],
},
{
title: 'Download',
title: "Download",
items: [
{
label: 'GitHub',
to: 'https://github.com/nextauthjs/next-auth'
label: "GitHub",
to: "https://github.com/nextauthjs/next-auth",
},
{
label: 'NPM',
to: 'https://www.npmjs.com/package/next-auth'
}
]
label: "NPM",
to: "https://www.npmjs.com/package/next-auth",
},
],
},
{
title: 'Acknowledgements',
title: "Acknowledgements",
items: [
{
label: 'Docusaurus',
to: 'https://v2.docusaurus.io/'
label: "Docusaurus",
to: "https://v2.docusaurus.io/",
},
{
label: 'Images by unDraw',
to: 'https://undraw.co/'
label: "Images by unDraw",
to: "https://undraw.co/",
},
{
html: `
@@ -106,28 +111,27 @@ module.exports = {
height="32"
src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg"
/>
</a>`
}
]
}
</a>`,
},
],
},
],
copyright: 'NextAuth.js &copy; Iain Collins 2021'
}
copyright: "NextAuth.js &copy; Iain Collins 2021",
},
},
presets: [
[
'@docusaurus/preset-classic',
"@docusaurus/preset-classic",
{
docs: {
routeBasePath: '/',
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/nextauthjs/next-auth/edit/main/www'
routeBasePath: "/",
sidebarPath: require.resolve("./sidebars.js"),
editUrl: "https://github.com/nextauthjs/next-auth/edit/main/www",
},
theme: {
customCss: require.resolve('./src/css/index.css')
}
}
]
customCss: require.resolve("./src/css/index.css"),
},
},
],
],
plugins: ['docusaurus-lunr-search']
}

24109
www/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,27 @@
{
"name": "next-auth-docs",
"version": "0.1.1",
"version": "0.2.0",
"scripts": {
"start": "npm run generate-providers && docusaurus start",
"build": "npm run generate-providers && docusaurus build",
"docusaurus": "docusaurus",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"serve": "docusaurus serve",
"clear": "docusaurus clear",
"lint": "standard",
"lint:fix": "standard --fix",
"generate-providers": "node ./scripts/generate-providers.js"
},
"dependencies": {
"@docusaurus/core": "^2.0.0-alpha.70",
"@docusaurus/preset-classic": "^2.0.0-alpha.70",
"classnames": "^2.2.6",
"docusaurus-lunr-search": "^2.1.10",
"jose": "^2.0.2",
"@docusaurus/core": "2.0.0-beta.0",
"@docusaurus/preset-classic": "2.0.0-beta.0",
"classnames": "^2.3.1",
"lodash.times": "^4.3.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-marquee-slider": "^1.1.2",
"styled-components": "^5.2.1"
"styled-components": "^5.2.3"
},
"browserslist": {
"production": [

View File

@@ -1,31 +1,53 @@
const providers = require('./providers.json')
module.exports = {
sidebar: {
'Getting Started': [
'getting-started/introduction',
'getting-started/example',
'getting-started/client',
'getting-started/rest-api',
'getting-started/typescript'
],
Configuration: [
'configuration/options',
'configuration/providers',
'configuration/databases',
'configuration/pages',
'configuration/callbacks',
'configuration/events'
],
'Models & Schemas': [
'schemas/models',
'schemas/mysql',
'schemas/postgres',
'schemas/mssql',
'schemas/mongodb',
'schemas/adapters'
],
'Authentication Providers': Object.entries(providers)
.sort(([, a], [, b]) => a.localeCompare(b))
.map(([provider]) => `providers/${provider}`)
}
docs: [
{
type: "category",
label: "Getting Started",
collapsed: false,
items: [
"getting-started/introduction",
"getting-started/example",
"getting-started/client",
"getting-started/rest-api",
"getting-started/typescript",
],
},
{
type: "category",
label: "Configuration",
collapsed: true,
items: [
"configuration/options",
"configuration/providers",
"configuration/databases",
"configuration/pages",
"configuration/callbacks",
"configuration/events",
],
},
{
type: "category",
label: "Models & Schemas",
collapsed: true,
items: [
"schemas/models",
"schemas/mysql",
"schemas/postgres",
"schemas/mssql",
"schemas/mongodb",
"schemas/adapters",
],
},
{
type: "category",
label: "Authentication Providers",
collapsed: true,
items: [
{
type: "autogenerated",
dirName: "providers",
},
],
},
],
}

View File

@@ -34,9 +34,9 @@
html[data-theme="dark"]:root {
--ifm-color-link: #289ef9;
--ifm-footer-background-color: #111;
--ifm-footer-background-color: #000;
--ifm-html-background-color: #242526;
--ifm-background-color: #000000;
--ifm-background-color: #090909;
--ifm-hero-background-color: #111111;
--ifm-navbar-background-color: rgba(0, 0, 0, 0.95);
}
@@ -46,6 +46,8 @@ html[data-theme="dark"]:root {
@import "buttons.css";
@import "table-of-contents.css";
@import "sidebar.css";
@import "providers.css";
@import "search.css";
@media screen and (max-width: 360px) {
html {
@@ -182,9 +184,26 @@ html[data-theme="dark"] hr {
border-color: #242526;
}
.github-counter {
position: absolute;
color: #000;
top: -10px;
right: 5px;
font-size: 9px;
background-color: #ccc;
padding: 2px 5px;
border-radius: 10px;
z-index: -1;
}
html[data-theme="dark"] .github-counter {
background-color: #222;
color: #fff;
}
.navbar__item.navbar__link[href*="github"],
.navbar__item.navbar__link[href*="npmjs"] {
padding: 0 1rem 0 0;
padding: 0 1.5rem 0 0;
display: flex;
font-size: 0;
}
@@ -197,6 +216,26 @@ html[data-theme="dark"] hr {
background-repeat: no-repeat;
}
.navbar__items .react-toggle {
margin-right: 5px;
}
.react-toggle--focus .react-toggle-thumb,
.react-toggle:hover .react-toggle-thumb {
box-shadow: none !important;
}
.navbar__search-input:focus {
outline: none;
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
rgba(19, 19, 19, 0.2) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
transition: box-shadow 350ms ease-in-out;
}
html[data-theme="dark"] .navbar__search-input:focus {
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
rgba(29, 29, 29, 0.5) 0px 0px 0px 4px, rgba(0, 0, 0, 0) 0px 0px 0px 0px;
}
html[data-theme="dark"] .navbar__item.navbar__link[href*="github"]:before {
background-image: url("/img/brand-github-inverted.svg");
}

View File

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

28
www/src/css/search.css Normal file
View File

@@ -0,0 +1,28 @@
html[data-theme="light"]:root {
--docsearch-searchbox-shadow: inset 0 0 0 2px #a553b3;
}
html[data-theme="light"] .DocSearch-Modal .DocSearch-Search-Icon {
color: #a553b3;
}
html[data-theme="dark"] .DocSearch-Modal .DocSearch-Search-Icon {
color: #7c2f89;
}
html[data-theme="dark"]:root {
--docsearch-searchbox-background: #040404;
--docsearch-key-gradient: #000;
--docsearch-key-shadow: #ccc;
--docsearch-searchbox-shadow: inset 0 0 0 2px #7c2f89;
}
html[data-theme="dark"] .DocSearch-Button-Key {
--docsearch-muted-color: #333;
}
@media screen and (max-width: 740px) {
.DocSearch-Container {
margin-top: 60px;
}
}

View File

@@ -1,47 +1,50 @@
import React from 'react'
import classnames from 'classnames'
import Layout from '@theme/Layout'
import Link from '@docusaurus/Link'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import useBaseUrl from '@docusaurus/useBaseUrl'
import CodeBlock from '@theme/CodeBlock'
import ProviderMarquee from '../components/ProviderMarquee'
import Seo from './seo'
import styles from './index.module.css'
import React, { useEffect } from "react"
import classnames from "classnames"
import Layout from "@theme/Layout"
import Link from "@docusaurus/Link"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
import useBaseUrl from "@docusaurus/useBaseUrl"
import CodeBlock from "@theme/CodeBlock"
import ProviderMarquee from "../components/ProviderMarquee"
import Seo from "./seo"
import styles from "./index.module.css"
const features = [
{
title: 'Easy',
imageUrl: 'img/undraw_social.svg',
title: "Easy",
imageUrl: "img/undraw_social.svg",
description: (
<ul>
<li>Built in support for popular services<br />
<li>
Built in support for popular services
<br />
<em>(Google, Facebook, Auth0, Apple)</em>
</li>
<li>Built in email / passwordless / magic link</li>
<li>Use with any username / password store</li>
<li>Use with OAuth 1.0 &amp; 2.0 services</li>
</ul>
)
),
},
{
title: 'Flexible',
imageUrl: 'img/undraw_authentication.svg',
title: "Flexible",
imageUrl: "img/undraw_authentication.svg",
description: (
<ul>
<li>Built for Serverless, runs anywhere</li>
<li>
Bring Your Own Database - or none!<br />
Bring Your Own Database - or none!
<br />
<em>(MySQL, Postgres, MSSQL, MongoDB)</em>
</li>
<li>Choose database sessions or JWT</li>
<li>Secure web pages and API routes</li>
</ul>
)
),
},
{
title: 'Secure',
imageUrl: 'img/undraw_secure.svg',
title: "Secure",
imageUrl: "img/undraw_secure.svg",
description: (
<ul>
<li>Signed, prefixed, server-only cookies</li>
@@ -50,84 +53,105 @@ const features = [
<li>Tab syncing, auto-revalidation, keepalives</li>
<li>Doesn't rely on client side JavaScript</li>
</ul>
)
}
),
},
]
function Feature ({ imageUrl, title, description }) {
const kFormatter = (num) => {
return Math.sign(num) * (Math.abs(num) / 1000).toFixed(1) + "k"
}
function Feature({ imageUrl, title, description }) {
const imgUrl = useBaseUrl(imageUrl)
return (
<div className={classnames('col col--4', styles.feature)}>
<div className={classnames("col col--4", styles.feature)}>
{imgUrl && (
<div className='text--center'>
<div className='feature-image-wrapper'>
<div className="text--center">
<div className="feature-image-wrapper">
<img className={styles.featureImage} src={imgUrl} alt={title} />
</div>
</div>
)}
<h3 className='text--center'>{title}</h3>
<h3 className="text--center">{title}</h3>
<p>{description}</p>
</div>
)
}
function Home () {
function Home() {
const context = useDocusaurusContext()
const { siteConfig = {} } = context
useEffect(() => {
fetch("https://api.github.com/repos/nextauthjs/next-auth")
.then((res) => res.json())
.then((data) => {
const navLinks = document.getElementsByClassName(
"navbar__item navbar__link"
)
const githubStat = document.createElement("span")
githubStat.innerHTML = kFormatter(data.stargazers_count)
githubStat.className = "github-counter"
navLinks[4].appendChild(githubStat)
})
}, [])
return (
<Layout description={siteConfig.tagline}>
<Seo />
<div className='home-wrapper'>
<header className={classnames('hero', styles.heroBanner)}>
<div className='container'>
<div className='hero-inner'>
<div className="home-wrapper">
<header className={classnames("hero", styles.heroBanner)}>
<div className="container">
<div className="hero-inner">
<img
src='/img/logo/logo-sm.png'
alt='Shield with key icon'
src="/img/logo/logo-sm.png"
alt="Shield with key icon"
className={styles.heroLogo}
/>
<div className={styles.heroText}>
<h1 className='hero__title'>{siteConfig.title}</h1>
<p className='hero__subtitle'>{siteConfig.tagline}</p>
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
</div>
<div className={styles.buttons}>
<a
className={classnames(
'button button--outline button--secondary button--lg rounded-pill',
"button button--outline button--secondary button--lg rounded-pill",
styles.button
)}
href='https://next-auth-example.now.sh'
>Live Demo
href="https://next-auth-example.now.sh"
>
Live Demo
</a>
<Link
className={classnames(
'button button--primary button--lg rounded-pill',
"button button--primary button--lg rounded-pill",
styles.button
)}
to={useBaseUrl('/getting-started/example')}
>Get Started
to={useBaseUrl("/getting-started/example")}
>
Get Started
</Link>
</div>
</div>
<div className='hero-marquee'>
<div className="hero-marquee">
<ProviderMarquee />
</div>
</div>
<div className='hero-wave'>
<div className='hero-wave-inner' />
<div className="hero-wave">
<div className="hero-wave-inner" />
</div>
</header>
<main className='home-main'>
<main className="home-main">
<section className={`section-features ${styles.features}`}>
<div className='container'>
<div className='row'>
<div className='col'>
<div className="container">
<div className="row">
<div className="col">
<h2 className={styles.featuresTitle}>
<span>Open Source.</span> <span>Full Stack.</span> <span>Own Your Data.</span>
<span>Open Source.</span> <span>Full Stack.</span>{" "}
<span>Own Your Data.</span>
</h2>
</div>
</div>
<div className='row'>
<div className="row">
{features.map((props, idx) => (
<Feature key={idx} {...props} />
))}
@@ -135,53 +159,63 @@ function Home () {
</div>
</section>
<section>
<div className='container'>
<div className='row'>
<div className='col'>
<p className='text--center'>
<div className="container">
<div className="row">
<div className="col">
<p className="text--center">
<a
href='https://www.npmjs.com/package/next-auth'
className='button button--primary button--outline rounded-pill button--lg'
>npm install next-auth
href="https://www.npmjs.com/package/next-auth"
className="button button--primary button--outline rounded-pill button--lg"
>
npm install next-auth
</a>
</p>
</div>
</div>
<div className='row'>
<div className='col'>
<h2 className='text--center' style={{ fontSize: '2.5rem' }}>
<div className="row">
<div className="col">
<h2 className="text--center" style={{ fontSize: "2.5rem" }}>
Add authentication in minutes!
</h2>
</div>
</div>
<div className='row'>
<div className='col col--6'>
<div className='code'>
<h4 className='code-heading'>Server <span>/pages/api/auth/[...nextauth].js</span></h4>
<CodeBlock className='javascript'>{serverlessFunctionCode}</CodeBlock>
<div className="row">
<div className="col col--6">
<div className="code">
<h4 className="code-heading">
Server <span>/pages/api/auth/[...nextauth].js</span>
</h4>
<CodeBlock className="javascript">
{serverlessFunctionCode}
</CodeBlock>
</div>
</div>
<div className='col col--6'>
<div className='code'>
<h4 className='code-heading'>Client <span>/pages/index.js</span></h4>
<CodeBlock className='javascript'>{reactComponentCode}</CodeBlock>
<div className="col col--6">
<div className="code">
<h4 className="code-heading">
Client <span>/pages/index.js</span>
</h4>
<CodeBlock className="javascript">
{reactComponentCode}
</CodeBlock>
</div>
</div>
</div>
<div className='row'>
<div className='col'>
<p className='text--center' style={{ marginTop: '2rem' }}>
<div className="row">
<div className="col">
<p className="text--center" style={{ marginTop: "2rem" }}>
<Link
to='/getting-started/example'
className='button button--primary button--lg rounded-pill'
>Example Code
to="/getting-started/example"
className="button button--primary button--lg rounded-pill"
>
Example Code
</Link>
</p>
</div>
</div>
</div>
</section>
<div className='home-subtitle'>
<div className="home-subtitle">
<p>NextAuth.js is an open source community project.</p>
</div>
</main>

View File

@@ -17,13 +17,13 @@
}
.heroLogo {
margin-bottom: .5rem;
margin-bottom: 0.5rem;
width: 8rem;
}
@media screen and (min-width: 689px) {
.heroLogo {
margin-bottom: -.5rem;
margin-bottom: -0.5rem;
}
}
@@ -87,8 +87,8 @@
}
.features ul li {
margin-top: .5rem;
margin-bottom: .5rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-size: 1rem;
white-space: nowrap;
text-align: center;
@@ -102,4 +102,4 @@
.featureImage {
height: 220px;
width: 220px;
}
}

View File

@@ -1,28 +1,24 @@
import React from 'react'
import Head from '@docusaurus/Head'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
import React from "react"
import Head from "@docusaurus/Head"
import useDocusaurusContext from "@docusaurus/useDocusaurusContext"
const Seo = () => {
const context = useDocusaurusContext()
const { siteConfig = {} } = context
const {
title,
tagline,
url
} = siteConfig
const { title, tagline, url } = siteConfig
return (
<Head>
<meta charSet='utf-8' />
<link rel='canonical' href={url} />
<meta property='og:title' content={title} />
<meta property='og:description' content={tagline} />
<meta property='og:image' content={`${url}/img/social-media-card.png`} />
<meta property='og:url' content={url} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={tagline} />
<meta name='twitter:image' content={`${url}/img/social-media-card.png`} />
<meta charSet="utf-8" />
<link rel="canonical" href={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={tagline} />
<meta property="og:image" content={`${url}/img/social-media-card.png`} />
<meta property="og:url" content={url} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={tagline} />
<meta name="twitter:image" content={`${url}/img/social-media-card.png`} />
</Head>
)
}

File diff suppressed because one or more lines are too long

View File

@@ -1,108 +0,0 @@
/* eslint-disable */
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, { useRef, useCallback } from "react";
import classnames from "classnames";
import { useHistory } from "@docusaurus/router";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
let loaded = false;
const Search = (props) => {
const initialized = useRef(false);
const searchBarRef = useRef(null);
const history = useHistory();
const { siteConfig = {} } = useDocusaurusContext();
const { baseUrl } = siteConfig;
const initAlgolia = () => {
if (!initialized.current) {
new window.DocSearch({
searchData: window.searchData,
inputSelector: "#search_input_react",
// Override algolia's default selection event, allowing us to do client-side
// navigation and avoiding a full page refresh.
handleSelected: (_input, _event, suggestion) => {
const url = baseUrl + suggestion.url;
// Use an anchor tag to parse the absolute url into a relative url
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
const a = document.createElement("a");
a.href = url;
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
history.push(url);
},
});
initialized.current = true;
}
};
const getSearchData = () =>
process.env.NODE_ENV === "production"
? fetch(`${baseUrl}search-doc.json`).then((content) => content.json())
: Promise.resolve([]);
const loadAlgolia = () => {
if (!loaded) {
Promise.all([
getSearchData(),
import("./lib/DocSearch"),
import("./algolia.css"),
]).then(([searchData, { default: DocSearch }]) => {
loaded = true;
window.searchData = searchData;
window.DocSearch = DocSearch;
initAlgolia();
});
} else {
initAlgolia();
}
};
const toggleSearchIconClick = useCallback(
(e) => {
if (!searchBarRef.current.contains(e.target)) {
searchBarRef.current.focus();
}
props.handleSearchBarToggle(!props.isSearchBarExpanded);
},
[props.isSearchBarExpanded]
);
return (
<div className="navbar__search" key="search-box">
<span
aria-label="expand searchbar"
role="button"
className={classnames("search-icon", {
"search-icon-hidden": props.isSearchBarExpanded,
})}
onClick={toggleSearchIconClick}
onKeyDown={toggleSearchIconClick}
tabIndex={0}
/>
<input
id="search_input_react"
type="search"
placeholder="Search"
aria-label="Search"
className={classnames(
"navbar__search-input",
{ "search-bar-expanded": props.isSearchBarExpanded },
{ "search-bar": !props.isSearchBarExpanded }
)}
onClick={loadAlgolia}
onMouseOver={loadAlgolia}
onFocus={toggleSearchIconClick}
onBlur={toggleSearchIconClick}
ref={searchBarRef}
/>
</div>
);
};
export default Search;

View File

@@ -1,340 +0,0 @@
import Hogan from 'hogan.js'
import LunrSearchAdapter from './lunar-search'
import autocomplete from 'autocomplete.js'
import templates from './templates'
import utils from './utils'
import $ from './zepto'
/**
* Adds an autocomplete dropdown to an input field
* @function DocSearch
* @param {Object} options.searchData Read-only API key
* @param {string} options.inputSelector CSS selector that targets the input
* value.
* @param {Object} [options.autocompleteOptions] Options to pass to the underlying autocomplete instance
* @return {Object}
*/
const usage = `Usage:
documentationSearch({
searchData,
inputSelector,
[ appId ],
[ autocompleteOptions.{hint,debug} ]
})`
class DocSearch {
constructor ({
searchData,
inputSelector,
debug = false,
queryDataCallback = null,
autocompleteOptions = {
debug: false,
hint: false,
autoselect: true
},
transformData = false,
queryHook = false,
handleSelected = false,
enhancedSearchInput = false,
layout = 'collumns'
}) {
DocSearch.checkArguments({
searchData,
inputSelector
})
this.searchData = searchData
this.input = DocSearch.getInputFromSelector(inputSelector)
this.queryDataCallback = queryDataCallback || null
const autocompleteOptionsDebug =
autocompleteOptions && autocompleteOptions.debug
? autocompleteOptions.debug
: false
// eslint-disable-next-line no-param-reassign
autocompleteOptions.debug = debug || autocompleteOptionsDebug
this.autocompleteOptions = autocompleteOptions
this.autocompleteOptions.cssClasses =
this.autocompleteOptions.cssClasses || {}
this.autocompleteOptions.cssClasses.prefix =
this.autocompleteOptions.cssClasses.prefix || 'ds'
const inputAriaLabel =
this.input &&
typeof this.input.attr === 'function' &&
this.input.attr('aria-label')
this.autocompleteOptions.ariaLabel =
this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input'
this.isSimpleLayout = layout === 'simple'
this.client = new LunrSearchAdapter(this.searchData)
if (enhancedSearchInput) {
this.input = DocSearch.injectSearchBox(this.input)
}
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
{
source: this.getAutocompleteSource(transformData, queryHook),
templates: {
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
footer: templates.footer,
empty: DocSearch.getEmptyTemplate()
}
}
])
const customHandleSelected = handleSelected
this.handleSelected = customHandleSelected || this.handleSelected
// We prevent default link clicking if a custom handleSelected is defined
if (customHandleSelected) {
$('.algolia-autocomplete').on('click', '.ds-suggestions a', event => {
event.preventDefault()
})
}
this.autocomplete.on(
'autocomplete:selected',
this.handleSelected.bind(null, this.autocomplete.autocomplete)
)
this.autocomplete.on(
'autocomplete:shown',
this.handleShown.bind(null, this.input)
)
if (enhancedSearchInput) {
DocSearch.bindSearchBoxEvent()
}
}
/**
* Checks that the passed arguments are valid. Will throw errors otherwise
* @function checkArguments
* @param {object} args Arguments as an option object
* @returns {void}
*/
static checkArguments (args) {
if (!args.searchData) {
throw new Error(usage)
}
if (typeof args.inputSelector !== 'string') {
throw new Error(
`Error: inputSelector:${args.inputSelector} must be a string. Each selector must match only one element and separated by ','`
)
}
if (!DocSearch.getInputFromSelector(args.inputSelector)) {
throw new Error(
`Error: No input element in the page matches ${args.inputSelector}`
)
}
}
static injectSearchBox (input) {
input.before(templates.searchBox)
const newInput = input
.prev()
.prev()
.find('input')
input.remove()
return newInput
}
static bindSearchBoxEvent () {
$('.searchbox [type="reset"]').on('click', function () {
$('input#docsearch').focus()
$(this).addClass('hide')
autocomplete.autocomplete.setVal('')
})
$('input#docsearch').on('keyup', () => {
const searchbox = document.querySelector('input#docsearch')
const reset = document.querySelector('.searchbox [type="reset"]')
reset.className = 'searchbox__reset'
if (searchbox.value.length === 0) {
reset.className += ' hide'
}
})
}
/**
* Returns the matching input from a CSS selector, null if none matches
* @function getInputFromSelector
* @param {string} selector CSS selector that matches the search
* input of the page
* @returns {void}
*/
static getInputFromSelector (selector) {
const input = $(selector).filter('input')
return input.length ? $(input[0]) : null
}
/**
* Returns the `source` method to be passed to autocomplete.js. It will query
* the Algolia index and call the callbacks with the formatted hits.
* @function getAutocompleteSource
* @param {function} transformData An optional function to transform the hits
* @param {function} queryHook An optional function to transform the query
* @returns {function} Method to be passed as the `source` option of
* autocomplete
*/
getAutocompleteSource (transformData, queryHook) {
return (query, callback) => {
if (queryHook) {
// eslint-disable-next-line no-param-reassign
query = queryHook(query) || query
}
this.client.search(query).then(hits => {
if (
this.queryDataCallback &&
typeof this.queryDataCallback === 'function'
) {
this.queryDataCallback(hits)
}
if (transformData) {
hits = transformData(hits) || hits
}
callback(DocSearch.formatHits(hits))
})
}
}
// Given a list of hits returned by the API, will reformat them to be used in
// a Hogan template
static formatHits (receivedHits) {
const clonedHits = utils.deepClone(receivedHits)
const hits = clonedHits.map(hit => {
if (hit._highlightResult) {
// eslint-disable-next-line no-param-reassign
hit._highlightResult = utils.mergeKeyWithParent(
hit._highlightResult,
'hierarchy'
)
}
return utils.mergeKeyWithParent(hit, 'hierarchy')
})
// Group hits by category / subcategory
let groupedHits = utils.groupBy(hits, 'lvl0')
$.each(groupedHits, (level, collection) => {
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1')
const flattenedHits = utils.flattenAndFlagFirst(
groupedHitsByLvl1,
'isSubCategoryHeader'
)
groupedHits[level] = flattenedHits
})
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader')
// Translate hits into smaller objects to be send to the template
return groupedHits.map(hit => {
const url = DocSearch.formatURL(hit)
const category = utils.getHighlightedValue(hit, 'lvl0')
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category
const displayTitle = utils
.compact([
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
utils.getHighlightedValue(hit, 'lvl3'),
utils.getHighlightedValue(hit, 'lvl4'),
utils.getHighlightedValue(hit, 'lvl5'),
utils.getHighlightedValue(hit, 'lvl6')
])
.join(
'<span class="aa-suggestion-title-separator" aria-hidden="true"> </span>'
)
const text = utils.getSnippetedValue(hit, 'content')
const isTextOrSubcategoryNonEmpty =
(subcategory && subcategory !== '') ||
(displayTitle && displayTitle !== '')
const isLvl1EmptyOrDuplicate =
!subcategory || subcategory === '' || subcategory === category
const isLvl2 =
displayTitle && displayTitle !== '' && displayTitle !== subcategory
const isLvl1 =
!isLvl2 &&
(subcategory && subcategory !== '' && subcategory !== category)
const isLvl0 = !isLvl1 && !isLvl2
return {
isLvl0,
isLvl1,
isLvl2,
isLvl1EmptyOrDuplicate,
isCategoryHeader: hit.isCategoryHeader,
isSubCategoryHeader: hit.isSubCategoryHeader,
isTextOrSubcategoryNonEmpty,
category,
subcategory,
title: displayTitle,
text,
url
}
})
}
static formatURL (hit) {
const { url, anchor } = hit
if (url) {
const containsAnchor = url.indexOf('#') !== -1
if (containsAnchor) return url
else if (anchor) return `${hit.url}#${hit.anchor}`
return url
} else if (anchor) return `#${hit.anchor}`
/* eslint-disable */
console.warn("no anchor nor url for : ", JSON.stringify(hit));
/* eslint-enable */
return null
}
static getEmptyTemplate () {
return args => Hogan.compile(templates.empty).render(args)
}
static getSuggestionTemplate (isSimpleLayout) {
const stringTemplate = isSimpleLayout
? templates.suggestionSimple
: templates.suggestion
const template = Hogan.compile(stringTemplate)
return suggestion => template.render(suggestion)
}
handleSelected (input, event, suggestion, datasetNumber, context = {}) {
// Do nothing if click on the suggestion, as it's already a <a href>, the
// browser will take care of it. This allow Ctrl-Clicking on results and not
// having the main window being redirected as well
if (context.selectionMethod === 'click') {
return
}
input.setVal('')
window.location.assign(suggestion.url)
}
handleShown (input) {
const middleOfInput = input.offset().left + input.width() / 2
let middleOfWindow = $(document).width() / 2
if (isNaN(middleOfWindow)) {
middleOfWindow = 900
}
const alignClass =
middleOfInput - middleOfWindow >= 0
? 'algolia-autocomplete-right'
: 'algolia-autocomplete-left'
const otherAlignClass =
middleOfInput - middleOfWindow < 0
? 'algolia-autocomplete-right'
: 'algolia-autocomplete-left'
const autocompleteWrapper = $('.algolia-autocomplete')
if (!autocompleteWrapper.hasClass(alignClass)) {
autocompleteWrapper.addClass(alignClass)
}
if (autocompleteWrapper.hasClass(otherAlignClass)) {
autocompleteWrapper.removeClass(otherAlignClass)
}
}
}
export default DocSearch

View File

@@ -1,169 +0,0 @@
/* eslint-disable */
import lunr from 'lunr'
lunr.tokenizer.separator = /[\s\-/]+/
class LunrSearchAdapter {
constructor (searchData) {
this.searchData = searchData
this.init()
this.titleHitsRes = []
}
init () {
const { searchData } = this
this.lunrIndex = lunr(function () {
this.ref('id')
this.field('title', { boost: 200 })
this.field('content', { boost: 2 })
this.field('keywords', { boost: 100 })
this.metadataWhitelist = ['position']
searchData.forEach((d, i) => {
const doc = {
id: i,
title: d.title,
content: d.content,
keywords: d.keywords
}
this.add(doc)
})
})
}
getLunrResult (input) {
return this.lunrIndex.query(function (query) {
const tokens = lunr.tokenizer(input)
query.term(tokens, {
boost: 10
})
query.term(tokens, {
wildcard: lunr.Query.wildcard.TRAILING
})
})
}
getHit (doc, formattedTitle, formattedContent) {
return {
hierarchy: {
lvl0: doc.pageTitle || doc.title,
lvl1: doc.type === 0 ? null : doc.title
},
url: doc.url,
_snippetResult: formattedContent ? {
content: {
value: formattedContent,
matchLevel: 'full'
}
} : null,
_highlightResult: {
hierarchy: {
lvl0: {
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle
},
lvl1:
doc.type === 0
? null
: {
value: formattedTitle || doc.title
}
}
}
}
}
getTitleHit (doc, position, length) {
const start = position[0]
const end = position[0] + length
const formattedTitle = doc.title.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.title.substring(start, end) + '</span>' + doc.title.substring(end, doc.title.length)
return this.getHit(doc, formattedTitle)
}
getKeywordHit (doc, position, length) {
const start = position[0]
const end = position[0] + length
const formattedTitle = doc.title + '<br /><i>Keywords: ' + doc.keywords.substring(0, start) + '<span class="algolia-docsearch-suggestion--highlight">' + doc.keywords.substring(start, end) + '</span>' + doc.keywords.substring(end, doc.keywords.length) + '</i>'
return this.getHit(doc, formattedTitle)
}
getContentHit (doc, position) {
const start = position[0]
const end = position[0] + position[1]
let previewStart = start
let previewEnd = end
let ellipsesBefore = true
let ellipsesAfter = true
for (let k = 0; k < 3; k++) {
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2)
const nextDot = doc.content.lastIndexOf('.', previewStart - 2)
if ((nextDot > 0) && (nextDot > nextSpace)) {
previewStart = nextDot + 1
ellipsesBefore = false
break
}
if (nextSpace < 0) {
previewStart = 0
ellipsesBefore = false
break
}
previewStart = nextSpace + 1
}
for (let k = 0; k < 10; k++) {
const nextSpace = doc.content.indexOf(' ', previewEnd + 1)
const nextDot = doc.content.indexOf('.', previewEnd + 1)
if ((nextDot > 0) && (nextDot < nextSpace)) {
previewEnd = nextDot
ellipsesAfter = false
break
}
if (nextSpace < 0) {
previewEnd = doc.content.length
ellipsesAfter = false
break
}
previewEnd = nextSpace
}
let preview = doc.content.substring(previewStart, start)
if (ellipsesBefore) {
preview = '... ' + preview
}
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>'
preview += doc.content.substring(end, previewEnd)
if (ellipsesAfter) {
preview += ' ...'
}
return this.getHit(doc, null, preview)
}
search (input) {
return new Promise((resolve, rej) => {
const results = this.getLunrResult(input)
const hits = []
results.length > 5 && (results.length = 5)
this.titleHitsRes = []
this.contentHitsRes = []
results.forEach(result => {
const doc = this.searchData[result.ref]
const { metadata } = result.matchData
for (const i in metadata) {
if (metadata[i].title) {
if (!this.titleHitsRes.includes(result.ref)) {
const position = metadata[i].title.position[0]
hits.push(this.getTitleHit(doc, position, input.length))
this.titleHitsRes.push(result.ref)
}
} else if (metadata[i].content) {
const position = metadata[i].content.position[0]
hits.push(this.getContentHit(doc, position))
} else if (metadata[i].keywords) {
const position = metadata[i].keywords.position[0]
hits.push(this.getKeywordHit(doc, position, input.length))
this.titleHitsRes.push(result.ref)
}
}
})
hits.length > 5 && (hits.length = 5)
resolve(hits)
})
}
}
export default LunrSearchAdapter

View File

@@ -1,114 +0,0 @@
const prefix = 'algolia-docsearch'
const suggestionPrefix = `${prefix}-suggestion`
const footerPrefix = `${prefix}-footer`
/* eslint-disable max-len */
const templates = {
suggestion: `
<a class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
"
aria-label="Link to the result"
href="{{{url}}}"
>
<div class="${suggestionPrefix}--category-header">
<span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
</div>
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--subcategory-column">
<span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
</div>
{{#isTextOrSubcategoryNonEmpty}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
<div class="${suggestionPrefix}--title">{{{title}}}</div>
{{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
</div>
{{/isTextOrSubcategoryNonEmpty}}
</div>
</a>
`,
suggestionSimple: `
<div class="${suggestionPrefix}
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
suggestion-layout-simple
">
<div class="${suggestionPrefix}--category-header">
{{^isLvl0}}
<span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
{{^isLvl1}}
{{^isLvl1EmptyOrDuplicate}}
<span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
{{{subcategory}}}
</span>
{{/isLvl1EmptyOrDuplicate}}
{{/isLvl1}}
{{/isLvl0}}
<div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
{{#isLvl2}}
{{{title}}}
{{/isLvl2}}
{{#isLvl1}}
{{{subcategory}}}
{{/isLvl1}}
{{#isLvl0}}
{{{category}}}
{{/isLvl0}}
</div>
</div>
<div class="${suggestionPrefix}--wrapper">
{{#text}}
<div class="${suggestionPrefix}--content">
<div class="${suggestionPrefix}--text">{{{text}}}</div>
</div>
{{/text}}
</div>
</div>
`,
footer: `
<div class="${footerPrefix}">
</div>
`,
empty: `
<div class="${suggestionPrefix}">
<div class="${suggestionPrefix}--wrapper">
<div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
<div class="${suggestionPrefix}--title">
<div class="${suggestionPrefix}--text">
No results found for query <b>"{{query}}"</b>
</div>
</div>
</div>
</div>
</div>
`,
searchBox: `
<form novalidate="novalidate" onsubmit="return false;" class="searchbox">
<div role="search" class="searchbox__wrapper">
<input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
<button type="submit" title="Submit your search query." class="searchbox__submit" >
<svg width=12 height=12 role="img" aria-label="Search">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
</svg>
</button>
<button type="reset" title="Clear the search query." class="searchbox__reset hide">
<svg width=12 height=12 role="img" aria-label="Reset">
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
</svg>
</button>
</div>
</form>
<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
<symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
</svg>
</div>
`
}
export default templates

View File

@@ -1,270 +0,0 @@
import $ from './zepto'
const utils = {
/*
* Move the content of an object key one level higher.
* eg.
* {
* name: 'My name',
* hierarchy: {
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* }
* Will be converted to
* {
* name: 'My name',
* lvl0: 'Foo',
* lvl1: 'Bar'
* }
* @param {Object} object Main object
* @param {String} property Main object key to move up
* @return {Object}
* @throws Error when key is not an attribute of Object or is not an object itself
*/
mergeKeyWithParent (object, property) {
if (object[property] === undefined) {
return object
}
if (typeof object[property] !== 'object') {
return object
}
const newObject = $.extend({}, object, object[property])
delete newObject[property]
return newObject
},
/*
* Group all objects of a collection by the value of the specified attribute
* If the attribute is a string, use the lowercase form.
*
* eg.
* groupBy([
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexS', category: 'dev'},
* {name: 'AlexK', category: 'sales'}
* ], 'category');
* =>
* {
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* }
* @param {array} collection Array of objects to group
* @param {String} property The attribute on which apply the grouping
* @return {array}
* @throws Error when one of the element does not have the specified property
*/
groupBy (collection, property) {
const newCollection = {}
$.each(collection, (index, item) => {
if (item[property] === undefined) {
throw new Error(`[groupBy]: Object has no key ${property}`)
}
let key = item[property]
if (typeof key === 'string') {
key = key.toLowerCase()
}
// fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
// such as the constructor, so we need to do this check.
if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
newCollection[key] = []
}
newCollection[key].push(item)
})
return newCollection
},
/*
* Return an array of all the values of the specified object
* eg.
* values({
* foo: 42,
* bar: true,
* baz: 'yep'
* })
* =>
* [42, true, yep]
* @param {object} object Object to extract values from
* @return {array}
*/
values (object) {
return Object.keys(object).map(key => object[key])
},
/*
* Flattens an array
* eg.
* flatten([1, 2, [3, 4], [5, 6]])
* =>
* [1, 2, 3, 4, 5, 6]
* @param {array} array Array to flatten
* @return {array}
*/
flatten (array) {
const results = []
array.forEach(value => {
if (!Array.isArray(value)) {
results.push(value)
return
}
value.forEach(subvalue => {
results.push(subvalue)
})
})
return results
},
/*
* Flatten all values of an object into an array, marking each first element of
* each group with a specific flag
* eg.
* flattenAndFlagFirst({
* 'devs': [
* {name: 'Tim', category: 'dev'},
* {name: 'Vincent', category: 'dev'},
* {name: 'AlexS', category: 'dev'}
* ],
* 'sales': [
* {name: 'Ben', category: 'sales'},
* {name: 'Jeremy', category: 'sales'},
* {name: 'AlexK', category: 'sales'}
* ]
* , 'isTop');
* =>
* [
* {name: 'Tim', category: 'dev', isTop: true},
* {name: 'Vincent', category: 'dev', isTop: false},
* {name: 'AlexS', category: 'dev', isTop: false},
* {name: 'Ben', category: 'sales', isTop: true},
* {name: 'Jeremy', category: 'sales', isTop: false},
* {name: 'AlexK', category: 'sales', isTop: false}
* ]
* @param {object} object Object to flatten
* @param {string} flag Flag to set to true on first element of each group
* @return {array}
*/
flattenAndFlagFirst (object, flag) {
const values = this.values(object).map(collection =>
collection.map((item, index) => {
// eslint-disable-next-line no-param-reassign
item[flag] = index === 0
return item
})
)
return this.flatten(values)
},
/*
* Removes all empty strings, null, false and undefined elements array
* eg.
* compact([42, false, null, undefined, '', [], 'foo']);
* =>
* [42, [], 'foo']
* @param {array} array Array to compact
* @return {array}
*/
compact (array) {
const results = []
array.forEach(value => {
if (!value) {
return
}
results.push(value)
})
return results
},
/*
* Returns the highlighted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly
* eg.
* getHighlightedValue({
* _highlightResult: {
* text: {
* value: '<mark>foo</mark>'
* }
* },
* text: 'foo'
* }, 'text');
* =>
* '<mark>foo</mark>'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getHighlightedValue (object, property) {
if (
object._highlightResult &&
object._highlightResult.hierarchy_camel &&
object._highlightResult.hierarchy_camel[property] &&
object._highlightResult.hierarchy_camel[property].matchLevel &&
object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
object._highlightResult.hierarchy_camel[property].value
) {
return object._highlightResult.hierarchy_camel[property].value
}
if (
object._highlightResult &&
object._highlightResult &&
object._highlightResult[property] &&
object._highlightResult[property].value
) {
return object._highlightResult[property].value
}
return object[property]
},
/*
* Returns the snippeted value of the specified key in the specified object.
* If no highlighted value is available, will return the key value directly.
* Will add starting and ending ellipsis (…) if we detect that a sentence is
* incomplete
* eg.
* getSnippetedValue({
* _snippetResult: {
* text: {
* value: '<mark>This is an unfinished sentence</mark>'
* }
* },
* text: 'This is an unfinished sentence'
* }, 'text');
* =>
* '<mark>This is an unfinished sentence</mark>…'
* @param {object} object Hit object returned by the Algolia API
* @param {string} property Object key to look for
* @return {string}
**/
getSnippetedValue (object, property) {
if (
!object._snippetResult ||
!object._snippetResult[property] ||
!object._snippetResult[property].value
) {
return object[property]
}
let snippet = object._snippetResult[property].value
if (snippet[0] !== snippet[0].toUpperCase()) {
snippet = `${snippet}`
}
if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
snippet = `${snippet}`
}
return snippet
},
/*
* Deep clone an object.
* Note: This will not clone functions and dates
* @param {object} object Object to clone
* @return {object}
*/
deepClone (object) {
return JSON.parse(JSON.stringify(object))
}
}
export default utils

View File

@@ -1,2 +0,0 @@
import zepto from 'autocomplete.js/zepto'
export default zepto

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