Compare commits

...

113 Commits

Author SHA1 Message Date
Balázs Orbán
86ff89e296 fix(react): initialize session without loading state (#2180)
See commit dd12181378
2021-06-14 21:16:42 +02:00
Hugh Boylan
dd12181378 initialize session without loading state (#2180) 2021-06-14 20:11:56 +02:00
Balázs Orbán
47c17a89ae Merge branch 'main' into next
commit 665445818e
Author: Balázs Orbán <info@balazsorban.com>
Date:   Sat Jun 12 17:11:53 2021 +0200

    docs(config): link to next documentation instead of canary

commit 67cf2a11bb
Author: ndom91 <yo@ndo.dev>
Date:   Sat Jun 12 16:42:48 2021 +0200

    docs: fix alt client provider example
2021-06-12 17:15:35 +02:00
Nico Domino
c07fe1b9a7 docs: add versioned docs for unreleased v4.0.0-next.* (#2167)
* docs: add versioned docs for unreleased canary 4.x

* docs(fix): prettier broke custom image component without .mdx extension

* docs(feat): automatically fetch latest stable version label

* docs: cleanup script

* docs: cleanup next version name

* docs(chore): cleanup version leftovers

* docs: fix .gitignore

* docs: v4 default

* docs: remove auto version generation

* docs: fix missing import

* docs: rm node-fetch
2021-06-12 17:04:32 +02:00
Balázs Orbán
abaa5aed65 fix(react): don't use localStorage on server side (#2166) 2021-06-11 22:11:50 +02:00
Balázs Orbán
ca0ed1e2a8 feat(react): create client tailored to React (#1473)
**What**:

These changes ensure that we work more tightly with React that can also result in unforeseen performance boosts. In case we would decide on expanding to other libraries/frameworks, a new file per framework could be added.

**Why**:

Some performance issues (https://github.com/nextauthjs/next-auth/issues/844) could only be fixed by moving more of the client code into the `Provider`.

**How**:

Refactoring `next-auth/client`

Related: #1461, #1084, #1462

BREAKING CHANGE:
**1.** `next-auth/client` is renamed to `next-auth/react`.

**2.** In the past, we exposed most of the functions with different names for convenience. To simplify our source code, the new React specific client code exports only the following functions, listed with the necessary changes:

- `setOptions`: Not exposed anymore, use `SessionProvider` props
- `options`: Not exposed anymore, use `SessionProvider` props
- `session`: Rename to `getSession`
- `providers`: Rename to `getProviders`
- `csrfToken`: Rename to `getCsrfToken`
- `signin`: Rename to `signIn`
- `signout`: Rename to `signOut`
- `Provider`: Rename to `SessionProvider`

**3.** `Provider` changes.
- `Provider` is renamed to `SessionProvider`
- The `options` prop is now flattened as the props of `SessionProvider`.
- `clientMaxAge` has been renamed to `staleTime`.
- `keepAlive` has been renamed to `refetchInterval`.
An example of the changes:
```diff
- <Provider options={{clientMaxAge: 0, keepAlive: 0}}>{children}</Provider>
+ <SessionProvider staleTime={0} refetchInterval={0}>{children}</SessionProvider> 
```

**4.** It is now **required** to wrap the part of your application that uses `useSession` into a `SessionProvider`.

Usually, the best place for this is in your `pages/_app.jsx` file:

```jsx
import { SessionProvider } from "next-auth/react"

export default function App({
  Component,
  pageProps: { session, ...pageProps }
}) {
  return (
    // `session` comes from `getServerSideProps` or `getInitialProps`.
    // Avoids flickering/session loading on first load.
    <SessionProvider session={session}>
      <Component {...pageProps} />
    </SessionProvider>
  )
}
```
2021-06-11 21:59:36 +02:00
Balázs Orbán
ed345346db fix(ts): add AzureAD to OAuthProviderType 2021-06-10 20:11:12 +02:00
Ben
5ac1db741a feat(provider): refactor Azure AD, B2C providers (#1591)
BREAKING CHANGE: 

If you currently use `AzureADB2C`, you will need to update it to to `AzureAD` There should be no other changes needed.
2021-06-10 20:09:34 +02:00
Balázs Orbán
0c17af969e Merge branch 'main' into next 2021-06-10 14:49:36 +02:00
Lluis Agusti
832d51f10e test(client): add more tests (#2135)
Contains the following squashed commits:

* test(client): verify CSRF Token fetch
* test(client): verify `getProviders` logic
* test(client): verify `useSession` happy path
* test(coverage): initial coverage setup (trial)
* chore(test): fix coverage reporting
* chore(test): define report directory for codecov

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-10 11:42:58 +02:00
Balázs Orbán
29862ac887 fix(build): do not run husky on postinstall (#2158) 2021-06-10 00:24:06 +02:00
Christopher Betz
5aa2b61b88 feat(provider): add Coinbase provider (#2153)
Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-09 22:46:12 +02:00
Nicholas Chiang
929c644653 docs(client): fix callback anchor links (#2151) 2021-06-09 22:19:26 +02:00
Nicholas Chiang
2657e72e81 docs(callbacks): don't use signIn for redirects (#2150)
Specifies that you shouldn't use the `signIn` callback for arbitrary redirects. Instead, use the `callbackUrl` option or the redirect callback.
2021-06-09 22:17:45 +02:00
Apoorv Taneja
8ff7dbb18f docs(tutorial): Adding a YouTube link for NextAuth.js introduction (#2047)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-06-09 17:04:41 +02:00
Tom Richter
ea9b6e37a9 fix(provider): convert github profile id from int to string (#2108) 2021-06-09 17:02:52 +02:00
Manish Kumar
748d576a5a docs(adapter): align DynamoDB docs with source code (#2125)
* Updated DynamoDB Adaptor documentation

* Update dynamodb.md

* Update dynamodb.md

* Update dynamodb.md
2021-06-09 17:01:00 +02:00
Balázs Orbán
960bc1e9c0 feat(adapter): remove adapters from core (#1919)
* feat(adapter): remove built-in adapters and database

BREAKING CHANGE:

From now on, you will have to import your own adapter

Check out https://github.com/nextauthjs/adapters

The migration is super easy and has HUGE advantages for those not using TypeORM.

```diff
// [...nextauth].js
+ import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"

...
export default NextAuth({
-  database: "yourconnectionstring",
+ adapter: TypeORMAdapter("yourconnectionstring")
})
```


Co-authored-by: Lluis Agusti <hi@llu.lu>
Co-authored-by: Giovanni Carnel <479046+g10@users.noreply.github.com>
2021-06-09 14:45:13 +02:00
Balázs Orbán
d29e3e9c9d Merge branch 'main'
Conflicts:
	config/babel.config.json
	package-lock.json
	package.json
	src/server/index.js
	src/server/routes/callback.js
	src/server/routes/signin.js
2021-06-09 02:16:11 +02:00
Camille Gabrieli
9f16e3f0fb docs(client): fix typo (#2139) 2021-06-08 09:02:41 +02:00
Adrian Artiles
1042e9a93d docs: fix typos (#2136) 2021-06-08 08:57:13 +02:00
Nico Domino
aa57f2dd7e docs(prisma-legacy): update tip location
Move client tip up to client section of docs
2021-06-07 22:44:04 +02:00
Nico Domino
1817286ce3 Update pouchdb.md 2021-06-07 22:21:39 +02:00
Nico Domino
b942dd34f3 docs(pouchdb): add pouchdb page (#2140) 2021-06-07 17:10:42 +02:00
Lluis Agusti
4d9622e1cc chore(git): fix git hooks (#2130)
Contains the following squashed commits:

* chore(git): fix husky pre-commit
* chore(husky): install git hooks on `postinstall`
2021-06-04 12:55:41 +02:00
sanctuxm
a7eadf80e5 docs(provider): fix ngrok typo on instagram provider docs (#2121) 2021-06-03 10:35:07 +02:00
Manish Kumar
75c7dbc3e7 docs(adapter): fix file location in DynamoDB docs (#2120) 2021-06-03 10:11:45 +02:00
Yi-Ru Lin
d36b89cb12 feat(provider): add Zoom provider (#2110)
* feat(provider): add Zoom provider

* Update src/providers/zoom.js

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

* Update src/providers/zoom.js

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

* Update www/docs/providers/zoom.md

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

* fix: syntax error

* Update www/docs/providers/zoom.md

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

* Update www/docs/providers/zoom.md

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

* Update www/docs/providers/zoom.md

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

* remove the default protection setting of Zoom for now

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-03 00:44:22 +02:00
Nico Domino
349cd03fbd docs(adapters): update adapter install instructions to canary branch (#2119) 2021-06-02 23:50:01 +02:00
Lluis Agusti
5cd130669b chore(lint): format files on pre-commit (#2117)
Contains the following squashed commits:

* chore(lint): run prettier on pre-commit
* chore(lint): format files on pre-commit
* chore(npm): update lock file
2021-06-02 13:59:53 +02:00
Lluis Agusti
638233f4a0 docs(readme): update release flow badge 2021-06-01 18:01:22 +02:00
Lluis Agusti
37e175195f chore(github): re-organize workflows (#2109)
Contains:

* chore(github): re-organize workflows
* chore(github): rename workflows structure
2021-06-01 17:52:17 +02:00
Lluis Agusti
e8a9e8aeb6 fix(client): unit tests setup and providers error handling (#1992)
* test(client): initial Jest + RTL setup

* test(client): add tests for `getSession`

* test(client): document expect cases and fix regex

* test(client): small refactors

* chore(npm): re-generate package-lock.json

* test(client): initial test for `signIn`

* test(client): refactor session tests for consistency

* test(client): credentials/email signin scenarios

* test(client): finish sign-in tests

* chore(github): add test to ci

* test(client): refactor and extend use cases

* test(client): sign-out tests

* refactor(client): code review suggestions (1)

* test(client): add few more sign-in/sign-out cases

* test(client): broadcasting session events

* fix(client): handle fetch providers error
2021-06-01 17:12:13 +02:00
Balázs Orbán
1fb308a6f4 docs(adapter): correct npm install script 2021-06-01 00:44:07 +02:00
Paul van Dyk
613c303315 docs: fix spelling in docs (#2105)
`restriected` => `restricted`
2021-05-31 19:22:39 +02:00
Nico Domino
d24fe1cebb docs: add error + warning pages to sidebar (#2100) 2021-05-31 02:14:27 +02:00
Manten
885b02ca95 chore(dev): add property to decrypt JWT (#2095) 2021-05-31 01:07:46 +02:00
Balázs Orbán
f218697fd6 docs(adapter): remove unnecessary section from prisma 2021-05-30 23:22:00 +02:00
Balázs Orbán
dbead0ad85 docs(adapter): fix API mixup in legacy adapter 2021-05-30 23:17:57 +02:00
Nico Domino
704ded5310 docs(prisma): add prisma-legacy separate docs page (#2097) 2021-05-30 21:44:58 +02:00
Manten
25fbcb4648 docs(FAQ): fix typo (#2088) 2021-05-29 16:47:06 +02:00
Nico Domino
53a439b44b docs(firebase): update firebase usage and options (#2076)
* docs(firebase): update firebase usage and options

* docs(firebase): add firebase tips/warnings

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-28 16:05:15 +02:00
Ben Orozco
16a2e37fd6 feat: Allow client to override scope (#2079)
* Ref[Signin]: Allow client to override scope

Allow client to override `scope` via query params

* Doc[Client]: Signin no longer overrides scope server-side
2021-05-28 10:07:42 +02:00
Colby Fayock
0392a8df9a docs(website): add Twitter Provider tutorial 2021-05-26 09:29:08 +02:00
Olav Fosse
a459b95c5b docs(website): fix typo (#2061) 2021-05-25 20:53:37 +02:00
Balázs Orbán
13df7eb81d docs: update urls to .vercel.app (#2039) 2021-05-25 00:35:57 +02:00
Kiran Paul
62f261209c docs(provider): improve authorize code example (#2046)
* Updated user fetch code as per review comments
2021-05-24 16:54:50 +02:00
Nico Domino
da43d0d896 docs(adapters): reorganise adapter docs for new pkg (#2051)
* docs(adapters): reorganise adapter docs for new pkg

* docs(adapters): fix link typos

* docs(adapters): add vercel.json redirects for new adapters URLs
2021-05-23 22:16:14 +02:00
Ben West
4b1271ba75 docs: Remove claim that new users do not have an ID (#1737)
I'm not sure when this changed, but it's no longer true. If the person logging in doesn't have a stored user account, the ID will be the provider_account_id
2021-05-22 13:47:48 +02:00
Marshall Bowers
d30da0170f fix(provider): make WorkOS domain configurable from signIn (#2038)
* Don't pass `domain` to the WorkOS provider

* Update docs

* Change `apiUrl` to `domain`
2021-05-22 13:40:48 +02:00
Nico Domino
887b2985fc docs(adapters): update copy regarding adapters (#2026)
* docs(adapters): update copy regarding adapters

* docs(adapters): add prisma schema page

* docs(adapters): add fauna schema/setup page

* docs(adapters): address PR comments

* Update www/docs/schemas/adapters.md

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

* docs(adapters): update adapters.md

* docs(adapters): update adapters.md

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-22 12:10:26 +02:00
Nico Domino
d2bbac1164 docs: explain where pageProps come from in Provider docs (#2016)
* docs: explain where pageProps come from in Provider docs

* chore: formatting

* docs(getting-started): add alternative client session handling methods

* docs(getting-started): update alternative client api docs
2021-05-22 11:30:38 +02:00
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
Balázs Orbán
a388b44d0b Merge branch 'main' into next 2021-05-03 21:11:04 +02:00
Kristóf Poduszló
aa9c1e7c96 fix(ts): unset generics defaults for overriding (#1891)
Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-05-03 14:31:56 +02:00
Lluis Agusti
66473054f5 docs(provider): update providers documentation (#1900)
* docs(providers): update providers documentation

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

* docs(providers): instructions on new provider types

* docs(providers): remove emojis

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

* docs(providers): reword sentence

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

* docs(providers): add tip on overriding options

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

* docs(providers): make names list inline

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

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

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

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

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

* fix typo in custom provider code example
2021-04-28 20:49:27 +02:00
Balázs Orbán
b6a3a72db4 Merge branch 'main' into next 2021-04-24 23:20:41 +02:00
Balázs Orbán
edcb10a823 Merge branch 'main' into next 2021-04-23 15:43:20 +02:00
Balázs Orbán
2acabe19e0 Merge main into next 2021-04-23 15:28:26 +02:00
Balázs Orbán
a6f5f4c184 fix: use upgraded require optional (#1743)
* chore(deps): switch back to (updated) require_optional

* fix: use @balazsorban/require-optional
2021-04-16 16:05:44 +02:00
Balázs Orbán
9fa93e3b5e fix(build): use optional-require dependency (#1736)
* chore(deps): add optional-require

* refactor: use optional-require
2021-04-16 00:23:29 +02:00
Balázs Orbán
cb4342fdda feat(build): modernize how we bundle next-auth (#1682)
* feat(build): optionally include TypeORM

If the user doesn't use databases,
it shouldn't be necessary to iclude it in the bundle.
This can more than half the package size!

* feat(build): clean up in dependencies

Remove unused dependencies, move optional ones to be optional

* feat(build): add exports field

* fix: use peerDependenciesMeta instead of non-standard peerOptionalDependecns field

* fix: ts-standard string quotes

* fix: ts-standard string quotes

* refactor: use asnyc/await for sendVerificationRequest

* chore(deps): upgrade mongodb, remove require_optional

Co-authored-by: ndom91 <yo@ndo.dev>

BREAKING CHANGE:
`typeorm`, and `nodemailer` are no longer dependencies added by default.
If you need any of them, you will have to install them yourself in your project directory.
TypeOrm is the default adapter, so if you only provide an `adapter` configuration or a `database`, you will need `typeorm`. You could also check out `@next-auth/typeorm-adapter`. In case you are using the Email provider, you will have to install `nodemailer` (or you can use the choice of your library in the `sendVerificationRequest` callback to send out the e-mail.)
2021-04-15 23:40:33 +02:00
Balázs Orbán
5f717b3914 chore: merge main into next 2021-04-12 00:46:27 +02:00
Balázs Orbán
d09a45ec7c chore: merge main into next 2021-03-26 16:23:35 +01:00
Balázs Orbán
930f58eba3 chore: merge main into next 2021-03-08 01:05:54 +01:00
Balázs Orbán
c20b7f2930 feat: use IE11 as client code bundle target (#1402) 2021-03-03 20:25:42 +01:00
Balázs Orbán
e418cddd96 chore: merge main into next 2021-03-03 20:25:42 +01:00
Balázs Orbán
111e7aabdf feat(provider): remove state property
BREAKING CHANGE: adding `state: true` is already redundant
as `protection: "state` is the default value. `state: false`
can be substituted with `protection: "state"`
2021-02-15 21:47:47 +01:00
Balázs Orbán
a113ef6fab feat: encourage returning strings instead of throwing
BREAKING CHANGE: We have supported throwing strings
for redirections, while we were showing a waring.
From now on, it is not possible. The user MUST return a string,
rather than throw it.
2021-02-15 21:47:35 +01:00
264 changed files with 59170 additions and 23131 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

@@ -1,32 +0,0 @@
# Simple check that the build is valid and no linting errors.
# Currently is run as a seperate workflow as it's fast to fail.
name: Lint/Build
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
- run: npm run lint
- run: npm run build

View File

@@ -1,67 +1,27 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
name: Code Analysis
on:
push:
branches: [ main, beta, next ]
branches: [main, beta, next]
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
branches: [main]
schedule:
- cron: '43 17 * * 2'
- cron: "43 17 * * 2"
jobs:
analyze:
name: Analyze
name: Verify
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
language: ["javascript"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,57 +0,0 @@
name: Integration Test
on:
push:
branches:
- main
- beta
- next
pull_request:
jobs:
test:
# Only run tests integration against Pull Requests from branches in
# this repository. We do this as integration tests require access to
# secrets in GitHub and they are not exposed to tests run against
# forks (for security reasons), so integration test against
# Pull Requests from external repos just fail and generate noise.
if: github.event.pull_request.head.repo.full_name == github.repository
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
# fail due to IP Address checks done by providers, which enforce
# CAPTCHA checks on login request from cloud compute IP addresses to
# prevent abuse.
runs-on: self-hosted
# Target time is under 5 minutes to run all tests. If it takes longer than
# 10 minutes should look at running tests in parallel. No individual flow
# should take longer than 5 minutes to build and run.
timeout-minutes: 10
strategy:
matrix:
node-version: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
uses: bahmutov/npm-install@v1
# Run tests (build library, build + start test app in Docker, run tests)
- run: npm test
# TODO Tests should exit out if env vars not set (currently hangs)
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}

View File

@@ -1,11 +1,13 @@
name: "Pull Request Labeler"
name: PR Labeler
on:
- pull_request_target
- pull_request_target
jobs:
triage:
name: Triage
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- uses: actions/labeler@main
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,4 +1,5 @@
name: Release
name: Release Flow
on:
push:
branches:
@@ -7,20 +8,44 @@ on:
- "next"
- "3.x"
pull_request:
jobs:
release:
name: "Release"
test:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Init
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
- name: Install dependencies
node-version: "16"
- name: Dependencies
uses: bahmutov/npm-install@v1
- run: npx semantic-release@17
- name: Run tests
run: npm test -- --coverage --verbose
- name: Coverage
uses: codecov/codecov-action@v1
with:
directory: ./coverage
fail_ci_if_error: false
- name: Build
run: npm run build
release:
name: Release
needs: test
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: "16"
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Release
run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}

View File

@@ -1,27 +0,0 @@
name: Types
on:
push:
branches:
- main
- beta
- next
pull_request:
branches:
- main
- beta
- next
jobs:
lint-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install dependencies
uses: bahmutov/npm-install@v1
- name: Check types
run: npm run test:types

7
.gitignore vendored
View File

@@ -40,6 +40,8 @@ src/providers/index.js
/providers.js
/errors.js
/errors.d.ts
/react.js
/react.d.ts
# Development app
app/next-auth
@@ -58,4 +60,7 @@ app/yarn.lock
/_work
# Prisma migrations
/prisma/migrations
/prisma/migrations
# Tests
/coverage

1
.husky/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# npx pretty-quick --staged

View File

@@ -11,43 +11,47 @@ Please raise any significant new functionality or breaking change an issue for d
## For contributors
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
### Pull Requests
* The latest changes are always in `main`, so please make your Pull Request against that branch.
* Pull Requests should be raised for any change
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
* We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
- The latest changes are always in `main`, so please make your Pull Request against that branch.
- Pull Requests should be raised for any change
- Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
- We use ESLint/Prettier for linting/formatting, so please run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [this Prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) to fix lint and formatting issues in development)
- We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
- If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
### Setting up local environment
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
A quick guide on how to setup _next-auth_ locally to work on it and test out any changes:
1. Clone the repo:
```sh
git clone git@github.com:nextauthjs/next-auth.git
cd next-auth
```
2. Install packages:
```sh
npm i && npm dev:setup
npm i && npm run dev:setup
```
3. Populate `.env.local`:
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
1. Start the dev application/server:
```sh
npm run dev
```
Your dev application will be available on ```http://localhost:3000```
Your dev application will be available on `http://localhost:3000`
That's it! 🎉
@@ -64,6 +68,7 @@ When running `npm run dev`, you start a Next.js dev server on `http://localhost:
#### Providers
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
@@ -95,35 +100,35 @@ The databases can take a few seconds to start up, so you might need to give it a
## For maintainers
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
When accepting Pull Requests, make sure the following:
* Use "Squash and merge"
* Make sure you merge contributor PRs into `main`
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
- Use "Squash and merge"
- Make sure you merge contributor PRs into `main`
- Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
- Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
### Recommended Scopes
A typical conventional commit looks like this:
```
type(scope): title
body
```
Scope is the part that will help groupping the different commit types in the release notes.
Scope is the part that will help grouping the different commit types in the release notes.
Some recommened scopes are:
Some recommended scopes are:
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
### Skipping a release

View File

@@ -7,11 +7,8 @@
Open Source. Full Stack. Own Your Data.
</p>
<p align="center" style="align: center;">
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
</a>
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
</a>
<a href="https://bundlephobia.com/result?p=next-auth">
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
@@ -41,7 +38,7 @@ It is designed from the ground up to support Next.js and Serverless.
npm install --save next-auth
```
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
@@ -51,40 +48,40 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
### Flexible and easy to use
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
* Supports email / passwordless authentication
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
* Supports both JSON Web Tokens and database sessions
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
- Supports email / passwordless authentication
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
- Supports both JSON Web Tokens and database sessions
- Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
### Own your own data
NextAuth.js can be used with or without a database.
* An open source solution that allows you to keep control of your data
* Supports Bring Your Own Database (BYOD) and can be used with any database
* Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
* Works great with databases from popular hosting providers
* Can also be used *without a database* (e.g. OAuth + JWT)
- An open source solution that allows you to keep control of your data
- Supports Bring Your Own Database (BYOD) and can be used with any database
- Built-in support for [MySQL, MariaDB, Postgres, Microsoft SQL Server, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
- Works great with databases from popular hosting providers
- Can also be used _without a database_ (e.g. OAuth + JWT)
### Secure by default
* Promotes the use of passwordless sign in mechanisms
* Designed to be secure by default and encourage best practice for safeguarding user data
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
* Auto-generates symmetric signing and encryption keys for developer convenience
* Features tab/window syncing and keepalive messages to support short lived sessions
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
- Promotes the use of passwordless sign in mechanisms
- Designed to be secure by default and encourage best practice for safeguarding user data
- Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
- Default cookie policy aims for the most restrictive policy appropriate for each cookie
- When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
- Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
- Auto-generates symmetric signing and encryption keys for developer convenience
- Features tab/window syncing and keepalive messages to support short lived sessions
- Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
### TypeScript
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
The package at `@types/next-auth` is now deprecated.
@@ -93,50 +90,50 @@ The package at `@types/next-auth` is now deprecated.
### Add API Route
```javascript
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
export default NextAuth({
providers: [
// OAuth authentication providers
Providers.Apple({
clientId: process.env.APPLE_ID,
clientSecret: process.env.APPLE_SECRET
clientSecret: process.env.APPLE_SECRET,
}),
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET
clientSecret: process.env.GOOGLE_SECRET,
}),
// Sign in with passwordless email link
Providers.Email({
server: process.env.MAIL_SERVER,
from: '<no-reply@example.com>'
from: "<no-reply@example.com>",
}),
],
// SQL or MongoDB database (or leave empty)
database: process.env.DATABASE_URL
})
```
### Add React Component
```javascript
import {
useSession, signIn, signOut
} from 'next-auth/client'
import { useSession, signIn, signOut } from "next-auth/react"
export default function Component() {
const [ session, loading ] = useSession()
if(session) {
return <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>
const [session, loading] = useSession()
if (session) {
return (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)
}
return <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>
return (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)
}
```

View File

@@ -14,10 +14,10 @@ We request that you contact us directly to report serious issues that might impa
If you contact us regarding a serious issue:
* We will endeavor to get back to you within 72 hours.
* We will aim to publish a fix within 30 days.
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publically.
- We will endeavor to get back to you within 72 hours.
- We will aim to publish a fix within 30 days.
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
Currently, the best way to report an issue is by emailing me@iaincollins.com

View File

@@ -4,7 +4,7 @@
NEXTAUTH_URL=http://localhost:3000
# You can use `openssl rand -hex 32` or
# https://generate-secret.now.sh/32 to generate a secret.
# https://generate-secret.vercel.app/32 to generate a secret.
# Note: Changing a secret may invalidate existing sessions
# and/or verificaion tokens.
SECRET=

View File

@@ -1,17 +1,18 @@
import { signIn } from 'next-auth/client'
import { signIn } from "next-auth/react"
export default function AccessDenied () {
export default function AccessDenied() {
return (
<>
<h1>Access Denied</h1>
<p>
<a
href='/api/auth/signin'
href="/api/auth/signin"
onClick={(e) => {
e.preventDefault()
signIn()
}}
>You must be signed in to view this page
>
You must be signed in to view this page
</a>
</p>
</>

View File

@@ -1,17 +1,17 @@
import Link from 'next/link'
import { signIn, signOut, useSession } from 'next-auth/client'
import styles from './header.module.css'
import Link from "next/link"
import { signIn, signOut, useSession } from "next-auth/react"
import styles from "./header.module.css"
// The approach used in this component shows how to built a sign in and sign out
// component that works on pages which support both client and server side
// rendering, and avoids any flash incorrect content on initial page load.
export default function Header () {
export default function Header() {
const [session, loading] = useSession()
return (
<header>
<noscript>
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
<style>{".nojs-show { opacity: 1; top: 0; }"}</style>
</noscript>
<div className={styles.signedInStatus}>
<p
@@ -25,7 +25,7 @@ export default function Header () {
You are not signed in
</span>
<a
href='/api/auth/signin'
href="/api/auth/signin"
className={styles.buttonPrimary}
onClick={(e) => {
e.preventDefault()
@@ -50,7 +50,7 @@ export default function Header () {
<strong>{session.user.email || session.user.name}</strong>
</span>
<a
href='/api/auth/signout'
href="/api/auth/signout"
className={styles.button}
onClick={(e) => {
e.preventDefault()
@@ -66,42 +66,42 @@ export default function Header () {
<nav>
<ul className={styles.navItems}>
<li className={styles.navItem}>
<Link href='/'>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/client'>
<Link href="/client">
<a>Client</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/server'>
<Link href="/server">
<a>Server</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected'>
<Link href="/protected">
<a>Protected</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/protected-ssr'>
<Link href="/protected-ssr">
<a>Protected(SSR)</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/api-example'>
<Link href="/api-example">
<a>API</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/credentials'>
<Link href="/credentials">
<a>Credentials</a>
</Link>
</li>
<li className={styles.navItem}>
<Link href='/email'>
<Link href="/email">
<a>Email</a>
</Link>
</li>

View File

@@ -7,7 +7,7 @@ module.exports = {
alias: {
...config.resolve.alias,
"next-auth$": path.join(process.cwd(), "next-auth/server"),
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
"next-auth/react$": path.join(process.cwd(), "next-auth/client/react"),
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),

View File

@@ -15,6 +15,7 @@
"license": "ISC",
"dependencies": {
"next": "^10.1.3",
"nodemailer": "^6.6.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},

View File

@@ -1,31 +1,32 @@
import { Provider } from "next-auth/client"
import { SessionProvider } from "next-auth/react"
import "./styles.css"
// Use the <Provider> to improve performance and allow components that call
// Use the <SessionProvider> to improve performance and allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<Provider
// Provider options are not required but can be useful in situations where
<SessionProvider
// SessionProvider options are not required but can be useful in situations where
// you have a short session maxAge time. Shown here with default values.
options={{
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
clientMaxAge: 0,
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
keepAlive: 0,
}}
session={pageProps.session}
// Client Max Age controls how often the useSession in the client should
// contact the server to sync the session state. Value in seconds.
// e.g.
// * 0 - Disabled (always use cache value)
// * 60 - Sync session state with server if it's older than 60 seconds
staleTime={0}
// Keep Alive tells windows / tabs that are signed in to keep sending
// a keep alive request (which extends the current session expiry) to
// prevent sessions in open windows from expiring. Value in seconds.
//
// Note: If a session has expired when keep alive is triggered, all open
// windows / tabs will be updated to reflect the user is signed out.
refetchInterval={0}
session={session}
>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}

View File

@@ -1,9 +1,9 @@
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()
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"
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
@@ -28,15 +28,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,43 +45,34 @@ 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'
// Default Database Adapter (TypeORM)
// database: process.env.DATABASE_URL
// Prisma Database Adapter
// To configure this app to use the schema in `prisma/schema.prisma` run:
// npx prisma generate
// npx prisma migrate dev
// adapter: Adapters.Prisma.Adapter({ prisma })
theme: "auto",
})

View File

@@ -1,9 +1,9 @@
// This is an example of how to read a JSON Web Token from an API route
import jwt from 'next-auth/jwt'
import jwt from "next-auth/jwt"
const secret = process.env.SECRET
export default async (req, res) => {
const token = await jwt.getToken({ req, secret })
const token = await jwt.getToken({ req, secret, encryption: true })
res.send(JSON.stringify(token, null, 2))
}

View File

@@ -1,12 +1,17 @@
// This is an example of to protect an API route
import { getSession } from 'next-auth/client'
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
res.send({
content:
"This is protected content. You can access this content because you are signed in.",
})
} else {
res.send({ error: 'You must be sign in to view the protected content on this page.' })
res.send({
error: "You must be sign in to view the protected content on this page.",
})
}
}

View File

@@ -1,5 +1,5 @@
// This is an example of how to access a session from an API route
import { getSession } from 'next-auth/client'
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })

View File

@@ -1,15 +1,15 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page () {
export default function Page() {
const [response, setResponse] = React.useState(null)
const handleLogin = (options) => async () => {
if (options.redirect) {
return signIn('credentials', options)
return signIn("credentials", options)
}
const response = await signIn('credentials', options)
const response = await signIn("credentials", options)
setResponse(response)
}
@@ -27,12 +27,16 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Credentials logout</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
@@ -40,14 +44,24 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Credentials login</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
<span className='spacing'>No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
<span className="spacing">Default:</span>
<button onClick={handleLogin({ redirect: true, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogin({ redirect: false, password: "password" })}>
Login
</button>
<br />
<span className="spacing">No redirect, wrong password:</span>
<button onClick={handleLogin({ redirect: false, password: "" })}>
Login
</button>
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -1,11 +1,11 @@
// eslint-disable-next-line no-use-before-define
import * as React from 'react'
import { signIn, signOut, useSession } from 'next-auth/client'
import Layout from 'components/layout'
import * as React from "react"
import { signIn, signOut, useSession } from "next-auth/react"
import Layout from "components/layout"
export default function Page () {
export default function Page() {
const [response, setResponse] = React.useState(null)
const [email, setEmail] = React.useState('')
const [email, setEmail] = React.useState("")
const handleChange = (event) => {
setEmail(event.target.value)
@@ -15,9 +15,9 @@ export default function Page () {
event.preventDefault()
if (options.redirect) {
return signIn('email', options)
return signIn("email", options)
}
const response = await signIn('email', options)
const response = await signIn("email", options)
setResponse(response)
}
@@ -35,12 +35,16 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Email logout</h1>
<span className='spacing'>Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
<span className='spacing'>No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
<span className="spacing">Default:</span>
<button onClick={handleLogout({ redirect: true })}>Logout</button>
<br />
<span className="spacing">No redirect:</span>
<button onClick={handleLogout({ redirect: false })}>Logout</button>
<br />
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}
@@ -48,20 +52,29 @@ export default function Page () {
return (
<Layout>
<h1>Test different flows for Email login</h1>
<label className='spacing'>
Email address:{' '}
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
</label><br />
<label className="spacing">
Email address:{" "}
<input
type="text"
id="email"
name="email"
value={email}
onChange={handleChange}
/>
</label>
<br />
<form onSubmit={handleLogin({ redirect: true, email })}>
<span className='spacing'>Default:</span>
<button type='submit'>Sign in with Email</button>
<span className="spacing">Default:</span>
<button type="submit">Sign in with Email</button>
</form>
<form onSubmit={handleLogin({ redirect: false, email })}>
<span className='spacing'>No redirect:</span>
<button type='submit'>Sign in with Email</button>
<span className="spacing">No redirect:</span>
<button type="submit">Sign in with Email</button>
</form>
<p>Response:</p>
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
<pre style={{ background: "#eee", padding: 16 }}>
{JSON.stringify(response, null, 2)}
</pre>
</Layout>
)
}

View File

@@ -1,37 +1,47 @@
// This is an example of how to protect content using server rendering
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page ({ content, session }) {
export default function Page({ content, session }) {
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
<p>
<strong>{content}</strong>
</p>
</Layout>
)
}
export async function getServerSideProps (context) {
export async function getServerSideProps(context) {
const session = await getSession(context)
let content = null
if (session) {
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
const hostname = process.env.NEXTAUTH_URL || "http://localhost:3000"
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/examples/protected`, options)
const json = await res.json()
if (json.content) { content = json.content }
if (json.content) {
content = json.content
}
}
return {
props: {
session,
content
}
content,
},
}
}

View File

@@ -1,33 +1,43 @@
import { useState, useEffect } from 'react'
import { useSession } from 'next-auth/client'
import Layout from '../components/layout'
import AccessDenied from '../components/access-denied'
import { useState, useEffect } from "react"
import { useSession } from "next-auth/react"
import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function Page () {
export default function Page() {
const [session, loading] = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
const fetchData = async () => {
const res = await fetch('/api/examples/protected')
const res = await fetch("/api/examples/protected")
const json = await res.json()
if (json.content) { setContent(json.content) }
if (json.content) {
setContent(json.content)
}
}
fetchData()
}, [session])
// When rendering client side don't display anything until loading is complete
if (typeof window !== 'undefined' && loading) return null
if (typeof window !== "undefined" && loading) return null
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
if (!session) {
return (
<Layout>
<AccessDenied />
</Layout>
)
}
// If session exists, display content
return (
<Layout>
<h1>Protected Page</h1>
<p><strong>{content}</strong></p>
<p>
<strong>{content}</strong>
</p>
</Layout>
)
}

View File

@@ -1,7 +1,7 @@
import { getSession } from 'next-auth/client'
import Layout from '../components/layout'
import { getSession } from "next-auth/react"
import Layout from "../components/layout"
export default function Page () {
export default function Page() {
// As this page uses Server Side Rendering, the `session` will be already
// populated on render without needing to go through a loading stage.
// This is possible because of the shared context configured in `_app.js` that
@@ -11,27 +11,31 @@ export default function Page () {
<Layout>
<h1>Server Side Rendering</h1>
<p>
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
This page uses the universal <strong>getSession()</strong> method in{" "}
<strong>getServerSideProps()</strong>.
</p>
<p>
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
support Server Side Rendering with authentication.
Using <strong>getSession()</strong> in{" "}
<strong>getServerSideProps()</strong> is the recommended approach if you
need to support Server Side Rendering with authentication.
</p>
<p>
The advantage of Server Side Rendering is this page does not require client side JavaScript.
The advantage of Server Side Rendering is this page does not require
client side JavaScript.
</p>
<p>
The disadvantage of Server Side Rendering is that this page is slower to render.
The disadvantage of Server Side Rendering is that this page is slower to
render.
</p>
</Layout>
)
}
// Export the `session` prop to use sessions with Server Side Rendering
export async function getServerSideProps (context) {
export async function getServerSideProps(context) {
return {
props: {
session: await getSession(context)
}
session: await getSession(context),
},
}
}

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

@@ -0,0 +1,29 @@
// 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" } }],
["@babel/preset-react", { runtime: "automatic" }],
],
},
{
test: ["../src/server/pages/**"],
presets: ["preact"],
},
{
test: ["../src/**/*.test.js"],
presets: [["@babel/preset-react", { runtime: "automatic" }]],
},
],
}

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

View File

@@ -3,7 +3,7 @@ const path = require("path")
const MODULE_ENTRIES = {
SERVER: "index",
CLIENT: "client",
REACT: "react",
PROVIDERS: "providers",
ADAPTERS: "adapters",
JWT: "jwt",
@@ -13,12 +13,16 @@ const MODULE_ENTRIES = {
// Building submodule entries
const BUILD_TARGETS = {
[`${MODULE_ENTRIES.SERVER}.js`]: "module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.CLIENT}.js`]: "module.exports = require('./dist/client').default\n",
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
[`${MODULE_ENTRIES.SERVER}.js`]:
"module.exports = require('./dist/server').default\n",
[`${MODULE_ENTRIES.REACT}.js`]:
"module.exports = require('./dist/client/react').default\n",
[`${MODULE_ENTRIES.PROVIDERS}.js`]:
"module.exports = require('./dist/providers').default\n",
[`${MODULE_ENTRIES.JWT}.js`]:
"module.exports = require('./dist/lib/jwt').default\n",
[`${MODULE_ENTRIES.ERRORS}.js`]:
"module.exports = require('./dist/lib/errors').default\n",
}
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
@@ -32,7 +36,7 @@ Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
const TYPES_TARGETS = [
`${MODULE_ENTRIES.SERVER}.d.ts`,
`${MODULE_ENTRIES.CLIENT}.d.ts`,
`${MODULE_ENTRIES.REACT}-client.d.ts`,
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
`${MODULE_ENTRIES.JWT}.d.ts`,
@@ -43,7 +47,10 @@ const TYPES_TARGETS = [
TYPES_TARGETS.forEach((target) => {
fs.copy(
path.resolve("types", target),
path.join(process.cwd(), target),
path.join(
process.cwd(),
target.startsWith("react-client") ? "react.d.ts" : target
),
(err) => {
if (err) throw err
console.log(`[build-types] copying "${target}" to root folder`)

2
config/jest-setup.js Normal file
View File

@@ -0,0 +1,2 @@
import "@testing-library/jest-dom"
import "whatwg-fetch"

11
config/jest.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
transform: {
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
},
rootDir: "../src",
setupFilesAfterEnv: ["../config/jest-setup.js"],
collectCoverageFrom: ["!client/__tests__/**"],
testMatch: ["**/*.test.js"],
coverageDirectory: "../coverage",
}

40743
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,23 +22,23 @@
"exports": {
".": "./dist/server/index.js",
"./jwt": "./dist/lib/jwt.js",
"./adapters": "./dist/adapters/index.js",
"./client": "./dist/client/index.js",
"./react": "./dist/client/react.js",
"./providers": "./dist/providers/index.js",
"./providers/*": "./dist/providers/*.js",
"./errors": "./dist/lib/errors.js"
},
"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": "jest --config ./config/jest.config.js",
"test:ci": "npm run lint && npm run test:types && npm run test -- --ci",
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
@@ -61,46 +61,47 @@
],
"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",
"futoin-hkdf": "^1.3.2",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"nodemailer": "^6.4.16",
"oauth": "^0.9.15",
"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"
"preact-render-to-string": "^5.1.14"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "16.13.1 || ^17"
"nodemailer": "^6.4.16",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"peerOptionalDependencies": {
"mongodb": "^3.5.9",
"mysql": "^2.18.1",
"mssql": "^6.2.1",
"pg": "^8.2.1",
"@prisma/client": "^2.16.1"
"nodemailer": "^6.4.16"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@prisma/client": "^2.16.1",
"@babel/preset-react": "^7.13.13",
"@semantic-release/commit-analyzer": "^8.0.1",
"@semantic-release/github": "^7.2.0",
"@semantic-release/npm": "7.0.8",
"@semantic-release/release-notes-generator": "^9.0.1",
"@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^13.1.9",
"@types/react": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"autoprefixer": "^9.7.6",
"babel-jest": "^26.6.3",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
@@ -110,25 +111,22 @@
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jest": "^24.3.6",
"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",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"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"
"pretty-quick": "^3.1.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.1.3",
"whatwg-fetch": "^3.6.2"
},
"prettier": {
"semi": false
@@ -155,7 +153,23 @@
"localStorage": "readonly",
"location": "readonly",
"fetch": "readonly"
}
},
"overrides": [
{
"files": [
"./**/*test.js"
],
"env": {
"jest/globals": true
},
"extends": [
"plugin:jest/recommended"
],
"plugins": [
"jest"
]
}
]
},
"release": {
"branches": [

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 +0,0 @@
import TypeORM from './typeorm'
import Prisma from './prisma'
export default {
Default: TypeORM.Adapter,
TypeORM,
Prisma
}

View File

@@ -1,8 +0,0 @@
/*
* Source code is now at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
*/
import PrismaLegacyAdapter from "@next-auth/prisma-legacy-adapter"
export default PrismaLegacyAdapter

View File

@@ -1,8 +0,0 @@
/*
* Source code is now at:
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
*/
import TypeORMLegacyAdapter from "@next-auth/typeorm-legacy-adapter"
export default TypeORMLegacyAdapter

View File

@@ -0,0 +1,76 @@
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { SessionProvider, useSession } from "../react"
beforeAll(() => {
server.listen()
})
afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
server.close()
})
test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
sessionRouteCall()
res(ctx.status(200), ctx.json(mockSession))
})
)
render(<ProviderFlow />)
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
const session1 = screen.getByTestId("session-consumer-1").textContent
const session2 = screen.getByTestId("session-consumer-2").textContent
expect(session1).toEqual(session2)
})
})
test("initialize provider with session", () => {
render(<ProviderFlow session={mockSession} />)
expect(screen.getByTestId("session-consumer-1")).not.toHaveTextContent(
"loading"
)
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
"loading"
)
})
function ProviderFlow({ options = {} }) {
return (
<>
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
</>
)
}
function SessionConsumer({ testId = 1 }) {
const [session, loading] = useSession()
if (loading)
return <span data-testid={`session-consumer-${testId}`}>loading</span>
return (
<div data-testid={`session-consumer-${testId}`}>
{JSON.stringify(session)}
</div>
)
}

View File

@@ -0,0 +1,105 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockCSRFToken } from "./helpers/mocks"
import logger from "../../lib/logger"
import { getCsrfToken } from "../react"
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("returns the Cross Site Request Forgery Token (CSRF Token) required to make POST requests", async () => {
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toEqual(
mockCSRFToken.csrfToken
)
})
})
test("when there's no CSRF token returned, it'll reflect that", async () => {
server.use(
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
...mockCSRFToken,
csrfToken: null,
})
)
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("csrf-result").textContent).toBe("null-response")
})
})
test("when the fetch fails it'll throw a client fetch error", async () => {
server.use(
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<CSRFFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"csrf",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})
function CSRFFlow() {
const [response, setResponse] = useState()
async function handleCSRF() {
const result = await getCsrfToken()
setResponse(result)
}
return (
<>
<p data-testid="csrf-result">
{response === null ? "null-response" : response || "no response"}
</p>
<button onClick={handleCSRF}>Get CSRF</button>
</>
)
}

View File

@@ -0,0 +1,90 @@
import { setupServer } from "msw/node"
import { rest } from "msw"
import { randomBytes } from "crypto"
export const mockSession = {
ok: true,
user: {
image: null,
name: "John",
email: "john@email.com",
},
expires: 123213139,
}
export const mockProviders = {
ok: true,
github: {
id: "github",
name: "Github",
type: "oauth",
signinUrl: "path/to/signin",
callbackUrl: "path/to/callback",
},
credentials: {
id: "credentials",
name: "Credentials",
type: "credentials",
authorize: null,
credentials: null,
},
email: {
id: "email",
type: "email",
name: "Email",
},
}
export const mockCSRFToken = {
ok: true,
csrfToken: randomBytes(32).toString("hex"),
}
export const mockGithubResponse = {
ok: true,
status: 200,
url: "https://path/to/github/url",
}
export const mockCredentialsResponse = {
ok: true,
status: 200,
url: "https://path/to/credentials/url",
}
export const mockEmailResponse = {
ok: true,
status: 200,
url: "https://path/to/email/url",
}
export const mockSignOutResponse = {
ok: true,
status: 200,
url: "https://path/to/signout/url",
}
export const server = setupServer(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSignOutResponse))
),
rest.get("/api/auth/session", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockSession))
),
rest.get("/api/auth/csrf", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCSRFToken))
),
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockProviders))
),
rest.post("/api/auth/signin/github", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockGithubResponse))
),
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockCredentialsResponse))
),
rest.post("/api/auth/signin/email", (req, res, ctx) =>
res(ctx.status(200), ctx.json(mockEmailResponse))
),
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
)

View File

@@ -0,0 +1,8 @@
export function getBroadcastEvents() {
return window.localStorage.setItem.mock.calls
.filter((call) => call[0] === "nextauth.message")
.map(([eventName, value]) => {
const { timestamp, ...rest } = JSON.parse(value)
return { eventName, value: rest }
})
}

View File

@@ -0,0 +1,85 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockProviders } from "./helpers/mocks"
import { getProviders } from "../react"
import logger from "../../lib/logger"
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("when called it'll return the currently configured providers for sign in", async () => {
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(screen.getByTestId("providers-result").textContent).toEqual(
JSON.stringify(mockProviders)
)
})
})
test("when failing to fetch the providers, it'll log the error", async () => {
server.use(
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.text("some error happened"))
)
)
render(<ProvidersFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
new SyntaxError("Unexpected token s in JSON at position 0")
)
})
})
function ProvidersFlow() {
const [response, setResponse] = useState()
async function handleGerProviders() {
const result = await getProviders()
setResponse(result)
}
return (
<>
<p data-testid="providers-result">
{response === null
? "null-response"
: JSON.stringify(response) || "no response"}
</p>
<button onClick={handleGerProviders}>Get Providers</button>
</>
)
}

View File

@@ -0,0 +1,98 @@
import { render, screen, waitFor } from "@testing-library/react"
import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from "../react"
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => server.listen())
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
server.resetHandlers()
jest.clearAllMocks()
})
afterAll(() => {
server.close()
})
test("if it can fetch the session, it should store it in `localStorage`", async () => {
render(<SessionFlow />)
// In the start, there is no session
const noSession = await screen.findByText("No session")
expect(noSession).toBeInTheDocument()
// After we fetched the session, it should have been rendered by `<SessionFlow />`
const session = await screen.findByText(new RegExp(mockSession.user.name))
expect(session).toBeInTheDocument()
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "getSession",
},
event: "session",
})
})
test("if there's an error fetching the session, it should log it", async () => {
server.use(
rest.get("/api/auth/session", (req, res, ctx) => {
return res(ctx.status(500), ctx.body("Server error"))
})
)
render(<SessionFlow />)
await waitFor(() => {
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"session",
new SyntaxError("Unexpected token S in JSON at position 0")
)
})
})
function SessionFlow() {
const [session, setSession] = useState(null)
useEffect(() => {
async function fetchUserSession() {
try {
const result = await getSession()
setSession(result)
} catch (e) {
console.error(e)
}
}
fetchUserSession()
}, [])
if (session) return <pre>{JSON.stringify(session, null, 2)}</pre>
return <p>No session</p>
}

View File

@@ -0,0 +1,290 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import logger from "../../lib/logger"
import {
server,
mockCredentialsResponse,
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from "../react"
import { rest } from "msw"
const { location } = window
jest.mock("../../lib/logger", () => ({
__esModule: true,
default: {
warn: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
},
proxyLogger(logger) {
return logger
},
}))
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider, it redirects to the default sign-in page",
async ({ provider }) => {
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
})
}
)
test.each`
provider | type
${""} | ${"no"}
${"foo"} | ${"unknown"}
`(
"if $type provider supplied and no callback URL, redirects using the current location",
async ({ provider }) => {
render(<SignInFlow providerId={provider} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
`/api/auth/signin?callbackUrl=${encodeURIComponent(
window.location.href
)}`
)
})
}
)
test.each`
provider | mockUrl
${`email`} | ${mockEmailResponse.url}
${`credentials`} | ${mockCredentialsResponse.url}
`(
"$provider provider redirects if `redirect` is `true`",
async ({ provider, mockUrl }) => {
render(<SignInFlow providerId={provider} redirect={true} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
})
}
)
test("redirection can't be stopped using an oauth provider", async () => {
render(
<SignInFlow
providerId="github"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
})
})
test("redirection can be stopped using the 'credentials' provider", async () => {
render(
<SignInFlow
providerId="credentials"
callbackUrl={callbackUrl}
redirect={false}
/>
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockCredentialsResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn`
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/credentials/url",
}
`)
})
test("redirection can be stopped using the 'email' provider", async () => {
render(
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).not.toHaveBeenCalledWith(
mockEmailResponse.url
)
expect(screen.getByTestId("signin-result").textContent).not.toBe(
"no response"
)
})
// snapshot the expected return shape from `signIn` oauth
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
.toMatchInlineSnapshot(`
Object {
"error": null,
"ok": true,
"status": 200,
"url": "https://path/to/email/url",
}
`)
})
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signin/email", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockEmailResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
expect(window.location.reload).toHaveBeenCalledTimes(1)
})
})
test("params are propagated to the signin URL when supplied", async () => {
let matchedParams = ""
const authParams = "foo=bar&bar=foo"
server.use(
rest.post("/api/auth/signin/github", (req, res, ctx) => {
matchedParams = req.url.search
return res(ctx.status(200), ctx.json(mockGithubResponse))
})
)
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(matchedParams).toEqual(`?${authParams}`)
})
})
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
const errorMsg = "Error when retrieving providers"
server.use(
rest.get("/api/auth/providers", (req, res, ctx) =>
res(ctx.status(500), ctx.json(errorMsg))
)
)
render(<SignInFlow providerId="github" />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
expect(logger.error).toHaveBeenCalledTimes(1)
expect(logger.error).toBeCalledWith(
"CLIENT_FETCH_ERROR",
"providers",
errorMsg
)
})
})
function SignInFlow({
providerId,
callbackUrl,
redirect = true,
authorizationParams = {},
}) {
const [response, setResponse] = useState(null)
async function handleSignIn() {
const result = await signIn(
providerId,
{
callbackUrl,
redirect,
},
authorizationParams
)
setResponse(result)
}
return (
<>
<p data-testid="signin-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignIn}>Sign in</button>
</>
)
}

View File

@@ -0,0 +1,129 @@
import { useState } from "react"
import userEvent from "@testing-library/user-event"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSignOutResponse } from "./helpers/mocks"
import { signOut } from "../react"
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"
const { location } = window
beforeAll(() => {
server.listen()
delete window.location
window.location = {
...location,
replace: jest.fn(),
reload: jest.fn(),
}
})
beforeEach(() => {
// eslint-disable-next-line no-proto
jest.spyOn(window.localStorage.__proto__, "setItem")
})
afterEach(() => {
jest.clearAllMocks()
server.resetHandlers()
})
afterAll(() => {
window.location = location
server.close()
})
const callbackUrl = "https://redirects/to"
test("by default it redirects to the current URL if the server did not provide one", async () => {
server.use(
rest.post("/api/auth/signout", (req, res, ctx) =>
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
)
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
})
})
test("it redirects to the URL allowed by the server", async () => {
render(<SignOutFlow callbackUrl={callbackUrl} />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.replace).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(
mockSignOutResponse.url
)
})
})
test("if url contains a hash during redirection a page reload happens", async () => {
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
server.use(
rest.post("/api/auth/signout", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
...mockSignOutResponse,
url: mockUrlWithHash,
})
)
})
)
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
expect(window.location.reload).toHaveBeenCalledTimes(1)
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
})
})
test("will broadcast the signout event to other tabs", async () => {
render(<SignOutFlow />)
userEvent.click(screen.getByRole("button"))
await waitFor(() => {
const broadcastCalls = getBroadcastEvents()
const [broadcastedEvent] = broadcastCalls
expect(broadcastCalls).toHaveLength(1)
expect(broadcastedEvent.eventName).toBe("nextauth.message")
expect(broadcastedEvent.value).toStrictEqual({
data: {
trigger: "signout",
},
event: "session",
})
})
})
function SignOutFlow({ callbackUrl, redirect = true }) {
const [response, setResponse] = useState(null)
async function handleSignOut() {
const result = await signOut({ callbackUrl, redirect })
setResponse(result)
}
return (
<>
<p data-testid="signout-result">
{response ? JSON.stringify(response) : "no response"}
</p>
<button onClick={handleSignOut}>Sign out</button>
</>
)
}

View File

@@ -1,378 +0,0 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
import { useState, useEffect, useContext, createContext, createElement } from 'react'
import _logger, { proxyLogger } from '../lib/logger'
import parseUrl from '../lib/parse-url'
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/client").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
keepAlive: 0,
clientMaxAge: 0,
// Properties starting with _ are used for tracking internal app state
_clientLastSync: 0,
_clientSyncTimer: null,
_eventListenersAdded: false,
_clientSession: undefined,
_getSession: () => {}
}
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
const broadcast = BroadcastChannel()
// Add event listners on load
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
__NEXTAUTH._eventListenersAdded = true
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
document.addEventListener('visibilitychange', () => {
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
}, false)
}
// Context to store session data globally
/** @type {import("types/internals/client").SessionContext} */
const SessionContext = createContext()
export function useSession (session) {
const context = useContext(SessionContext)
if (context) return context
return _useSessionHook(session)
}
function _useSessionHook (session) {
const [data, setData] = useState(session)
const [loading, setLoading] = useState(!data)
useEffect(() => {
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
try {
const triggredByEvent = event !== null
const triggeredByStorageEvent = event === 'storage'
const clientMaxAge = __NEXTAUTH.clientMaxAge
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
const currentTime = _now()
const clientSession = __NEXTAUTH._clientSession
// Updates triggered by a storage event *always* trigger an update and we
// always update if we don't have any value for the current session state.
if (!triggeredByStorageEvent && clientSession !== undefined) {
if (clientMaxAge === 0 && triggredByEvent !== true) {
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it.
return
} else if (clientMaxAge > 0 && clientSession === null) {
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a triggeredByStorageEvent
// event and will skip this logic)
return
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
// If the session freshness is within clientMaxAge then don't request
// it again on this call (avoids too many invokations).
return
}
}
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
// Update clientLastSync before making response to avoid repeated
// invokations that would otherwise be triggered while we are still
// waiting for a response.
__NEXTAUTH._clientLastSync = _now()
// If this call was invoked via a storage event (i.e. another window) then
// tell getSession not to trigger an event when it calls to avoid an
// infinate loop.
const newClientSessionData = await getSession({
triggerEvent: !triggeredByStorageEvent
})
// Save session state internally, just so we can track that we've checked
// if a session exists at least once.
__NEXTAUTH._clientSession = newClientSessionData
setData(newClientSessionData)
setLoading(false)
} catch (error) {
logger.error('CLIENT_USE_SESSION_ERROR', error)
setLoading(false)
}
}
__NEXTAUTH._getSession()
})
return [data, loading]
}
export async function getSession (ctx) {
const session = await _fetchData('session', ctx)
if (ctx?.triggerEvent ?? true) {
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
}
return session
}
export async function getCsrfToken (ctx) {
return (await _fetchData('csrf', ctx))?.csrfToken
}
export async function getProviders () {
return _fetchData('providers')
}
export async function signIn (provider, options = {}, authorizationParams = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
// Redirect to sign in page if no valid provider specified
if (!(provider in providers)) {
// If Provider not recognized, redirect to sign in page
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
return
}
const isCredentials = providers[provider].type === 'credentials'
const isEmail = providers[provider].type === 'email'
const canRedirectBeDisabled = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/signin/${provider}`
// If is any other provider type, POST to provider URL with CSRF Token,
// callback URL and any other parameters supplied.
const fetchOptions = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
}
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, fetchOptions)
const data = await res.json()
if (redirect || !canRedirectBeDisabled) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get('error')
if (res.ok) {
await __NEXTAUTH._getSession({ event: 'storage' })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url
}
}
export async function signOut (options = {}) {
const {
callbackUrl = window.location,
redirect = true
} = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true
})
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location = url
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes('#')) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: 'storage' })
return data
}
// Method to set options. The documented way is to use the provider, but this
// method is being left in as an alternative, that will be helpful if/when we
// expose a vanilla JavaScript version that doesn't depend on React.
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
if (keepAlive) {
__NEXTAUTH.keepAlive = keepAlive
if (typeof window === 'undefined') return
// Clear existing timer (if there is one)
if (__NEXTAUTH._clientSyncTimer !== null) {
clearTimeout(__NEXTAUTH._clientSyncTimer)
}
// Set next timer to trigger in number of seconds
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
// Only invoke keepalive when a session exists
if (!__NEXTAUTH._clientSession) return
await __NEXTAUTH._getSession({ event: 'timer' })
}, keepAlive * 1000)
}
}
export function Provider ({ children, session, options }) {
setOptions(options)
return createElement(
SessionContext.Provider,
{ value: useSession(session) },
children
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error('CLIENT_FETCH_ERROR', path, error)
return null
}
}
function _apiBaseUrl () {
if (typeof window === 'undefined') {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now () {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel (name = 'nextauth.message') {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
*/
receive (onReceive) {
if (typeof window === 'undefined') return
window.addEventListener('storage', async (event) => {
if (event.key !== name) return
/** @type {import("types/internals/client").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== 'session' || !message?.data) return
onReceive(message)
})
},
/** Notify other tabs/windows. */
post (message) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(name,
JSON.stringify({ ...message, timestamp: _now() })
)
}
}
}
export default {
getSession,
getCsrfToken,
getProviders,
useSession,
signIn,
signOut,
Provider,
/* Deprecated / unsupported features below this line */
// Use setOptions() set options globally in the app.
setOptions,
// Some methods are exported with more than one name. This provides some
// flexibility over how they can be invoked and backwards compatibility
// with earlier releases.
options: setOptions,
session: getSession,
providers: getProviders,
csrfToken: getCsrfToken,
signin: signIn,
signout: signOut
}

352
src/client/react.js vendored Normal file
View File

@@ -0,0 +1,352 @@
// Note about signIn() and signOut() methods:
//
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
// instead of HTTP as redirect URLs on other domains are not returned to
// requests made using the fetch API in the browser, and we need to ask the API
// to return the response as a JSON object (the end point still defaults to
// returning an HTTP response with a redirect for non-JavaScript clients).
//
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
// eslint-disable-next-line no-use-before-define
import * as React from "react"
import _logger, { proxyLogger } from "../lib/logger"
import parseUrl from "../lib/parse-url"
// This behaviour mirrors the default behaviour for getting the site name that
// happens server side in server/index.js
// 1. An empty value is legitimate when the code is being invoked client side as
// relative URLs are valid in that context and so defaults to empty.
// 2. When invoked server side the value is picked up from an environment
// variable and defaults to 'http://localhost:3000'.
/** @type {import("types/internals/react").NextAuthConfig} */
const __NEXTAUTH = {
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
baseUrlServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL ||
process.env.NEXTAUTH_URL ||
process.env.VERCEL_URL
).baseUrl,
basePathServer: parseUrl(
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
).basePath,
_lastSync: 0,
_session: undefined,
_getSession: () => {},
}
const broadcast = BroadcastChannel()
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
/** @type {import("types/internals/react").SessionContext} */
const SessionContext = React.createContext()
export function useSession() {
return React.useContext(SessionContext)
}
export async function getSession(ctx) {
const session = await _fetchData("session", ctx)
if (ctx?.broadcast ?? true) {
broadcast.post({ event: "session", data: { trigger: "getSession" } })
}
return session
}
export async function getCsrfToken(ctx) {
const response = await _fetchData("csrf", ctx)
return response?.csrfToken
}
export async function getProviders() {
return await _fetchData("providers")
}
export async function signIn(provider, options = {}, authorizationParams = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const providers = await getProviders()
if (!providers) {
return window.location.replace(`${baseUrl}/error`)
}
if (!(provider in providers)) {
return window.location.replace(
`${baseUrl}/signin?${new URLSearchParams({ callbackUrl })}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = `${baseUrl}/${
isCredentials ? "callback" : "signin"
}/${provider}`
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
const res = await fetch(_signInUrl, {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
...options,
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
})
const data = await res.json()
if (redirect || !isSupportingReturn) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
const error = new URL(data.url).searchParams.get("error")
if (res.ok) {
await __NEXTAUTH._getSession({ event: "storage" })
}
return {
error,
status: res.status,
ok: res.ok,
url: error ? null : data.url,
}
}
export async function signOut(options = {}) {
const { callbackUrl = window.location.href, redirect = true } = options
const baseUrl = _apiBaseUrl()
const fetchOptions = {
method: "post",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
csrfToken: await getCsrfToken(),
callbackUrl,
json: true,
}),
}
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
const data = await res.json()
broadcast.post({ event: "session", data: { trigger: "signout" } })
if (redirect) {
const url = data.url ?? callbackUrl
window.location.replace(url)
// If url contains a hash, the browser does not reload the page. We reload manually
if (url.includes("#")) window.location.reload()
return
}
await __NEXTAUTH._getSession({ event: "storage" })
return data
}
/** @param {import("types/react-client").SessionProviderProps} props */
export function SessionProvider(props) {
const { children, baseUrl, basePath, staleTime = 0 } = props
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
if (basePath) __NEXTAUTH.basePath = basePath
/**
* If session was `null`, there was an attempt to fetch it,
* but it failed, but we still treat it as a valid initial value.
*/
const hasInitialSession = props.session !== undefined
/** If session was passed, initialize as already synced */
__NEXTAUTH._lastSync = hasInitialSession ? _now() : 0
const [session, setSession] = React.useState(() => {
if (hasInitialSession) __NEXTAUTH._session = props.session
return props.session
})
/** If session was passed, initialize as not loading */
const [loading, setLoading] = React.useState(!hasInitialSession)
React.useEffect(() => {
__NEXTAUTH._getSession = async ({ event } = {}) => {
try {
const storageEvent = event === "storage"
// We should always update if we don't have a client session yet
// or if there are events from other tabs/windows
if (storageEvent || __NEXTAUTH._session === undefined) {
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession({
broadcast: !storageEvent,
})
setSession(__NEXTAUTH._session)
return
}
if (
// If there is no time defined for when a session should be considered
// stale, then it's okay to use the value we have until an event is
// triggered which updates it
(staleTime === 0 && !event) ||
// If the client doesn't have a session then we don't need to call
// the server to check if it does (if they have signed in via another
// tab or window that will come through as a "stroage" event
// event anyway)
(staleTime > 0 && __NEXTAUTH._session === null) ||
// Bail out early if the client session is not stale yet
(staleTime > 0 && _now() < __NEXTAUTH._lastSync + staleTime)
) {
return
}
// An event or session staleness occurred, update the client session.
__NEXTAUTH._lastSync = _now()
__NEXTAUTH._session = await getSession()
setSession(__NEXTAUTH._session)
} catch (error) {
logger.error("CLIENT_SESSION_ERROR", error)
} finally {
setLoading(false)
}
}
__NEXTAUTH._getSession()
}, [staleTime])
React.useEffect(() => {
// Listen for storage events and update session if event fired from
// another window (but suppress firing another event to avoid a loop)
// Fetch new session data but tell it to not to fire another event to
// avoid an infinite loop.
// Note: We could pass session data through and do something like
// `setData(message.data)` but that can cause problems depending
// on how the session object is being used in the client; it is
// more robust to have each window/tab fetch it's own copy of the
// session object rather than share it across instances.
const unsubscribe = broadcast.receive(
async () => await __NEXTAUTH._getSession({ event: "storage" })
)
return () => unsubscribe()
}, [])
React.useEffect(() => {
// Set up visibility change
// Listen for document visibility change events and
// if visibility of the document changes, re-fetch the session.
const visibilityHandler = () => {
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
}
document.addEventListener("visibilitychange", visibilityHandler, false)
return () =>
document.removeEventListener("visibilitychange", visibilityHandler, false)
}, [])
React.useEffect(() => {
const { refetchInterval } = props
// Set up polling
if (refetchInterval) {
const refetchIntervalTimer = setInterval(async () => {
if (__NEXTAUTH._session) {
await __NEXTAUTH._getSession({ event: "poll" })
}
}, refetchInterval * 1000)
return () => clearInterval(refetchIntervalTimer)
}
}, [props.refetchInterval])
const value = React.useMemo(() => [session, loading], [session, loading])
return (
<SessionContext.Provider value={value}>{children}</SessionContext.Provider>
)
}
/**
* If passed 'appContext' via getInitialProps() in _app.js
* then get the req object from ctx and use that for the
* req value to allow _fetchData to
* work seemlessly in getInitialProps() on server side
* pages *and* in _app.js.
*/
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
try {
const baseUrl = await _apiBaseUrl()
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
const res = await fetch(`${baseUrl}/${path}`, options)
const data = await res.json()
if (!res.ok) throw data
return Object.keys(data).length > 0 ? data : null // Return null if data empty
} catch (error) {
logger.error("CLIENT_FETCH_ERROR", path, error)
return null
}
}
function _apiBaseUrl() {
if (typeof window === "undefined") {
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
if (!process.env.NEXTAUTH_URL) {
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
}
// Return absolute path when called server side
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
}
// Return relative path when called client side
return __NEXTAUTH.basePath
}
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
function _now() {
return Math.floor(Date.now() / 1000)
}
/**
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
* Only not using it directly, because Safari does not support it.
*
* https://caniuse.com/?search=broadcastchannel
*/
function BroadcastChannel(name = "nextauth.message") {
return {
/**
* Get notified by other tabs/windows.
* @param {(message: import("types/internals/react").BroadcastMessage) => void} onReceive
*/
receive(onReceive) {
const handler = (event) => {
if (event.key !== name) return
/** @type {import("types/internals/react").BroadcastMessage} */
const message = JSON.parse(event.newValue)
if (message?.event !== "session" || !message?.data) return
onReceive(message)
}
window.addEventListener("storage", handler)
return () => window.removeEventListener("storage", handler)
},
/** Notify other tabs/windows. */
post(message) {
if (typeof window === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: _now() })
)
},
}
}

View File

@@ -1,5 +1,7 @@
export default function AzureADB2C(options) {
const tenant = options.tenantId ? options.tenantId : "common"
const { tenantName, primaryUserFlow } = options
const authorizeUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/authorize`
const tokenUrl = `https://${tenantName}.b2clogin.com/${tenantName}.onmicrosoft.com/${primaryUserFlow}/oauth2/v2.0/token`
return {
id: "azure-ad-b2c",
@@ -9,14 +11,29 @@ export default function AzureADB2C(options) {
params: {
grant_type: "authorization_code",
},
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
profileUrl: "https://graph.microsoft.com/v1.0/me/",
profile(profile) {
accessTokenUrl: tokenUrl,
requestTokenUrl: tokenUrl,
authorizationUrl: `${authorizeUrl}?response_type=code+id_token&response_mode=query`,
profileUrl: 'https://graph.microsoft.com/oidc/userinfo',
idToken: true,
profile: (profile) => {
let name = ''
if (profile.name) {
// B2C "Display Name"
name = profile.name
} else if (profile.given_name && profile.family_name) {
// B2C "Given Name" & "Surname"
name = `${profile.given_name} ${profile.family_name}`
} else if (profile.given_name) {
// B2C "Given Name"
name = `${profile.given_name}`
}
return {
id: profile.id,
name: profile.displayName,
email: profile.userPrincipalName,
name,
id: profile.oid,
email: profile.emails[0]
}
},
...options,

24
src/providers/azure-ad.js Normal file
View File

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

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

@@ -0,0 +1,24 @@
export default function Coinbase(options) {
return {
id: "coinbase",
name: "Coinbase",
type: "oauth",
version: "2.0",
scope: "wallet:user:email wallet:user:read",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://api.coinbase.com/oauth/token",
requestTokenUrl: "https://api.coinbase.com/oauth/token",
authorizationUrl:
"https://www.coinbase.com/oauth/authorize?response_type=code",
profileUrl: "https://api.coinbase.com/v2/user",
profile(profile) {
return {
id: profile.data.id,
name: profile.data.name,
email: profile.data.email,
image: profile.data.avatar_url,
}
},
...options,
}
}

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

@@ -0,0 +1,54 @@
/**
* @param {import("../server").Provider} options
* @example
*
* ```js
* // pages/api/auth/[...nextauth].js
* import Providers from `next-auth/providers`
* ...
* providers: [
* Providers.Dropbox({
* clientId: process.env.DROPBOX_CLIENT_ID,
* clientSecret: process.env.DROPBOX_CLIENT_SECRET
* })
* ]
* ...
*
* // pages/index
* import { signIn } from "next-auth/react"
* ...
* <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

@@ -1,5 +1,5 @@
import logger from '../lib/logger'
import nodemailer from "nodemailer"
import logger from "../lib/logger"
export default function Email(options) {
return {
@@ -22,34 +22,24 @@ export default function Email(options) {
}
}
const sendVerificationRequest = ({
identifier: email,
url,
baseUrl,
provider,
}) => {
return new Promise((resolve, reject) => {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, "")
nodemailer.createTransport(server).sendMail(
{
async function sendVerificationRequest ({ identifier: email, url, baseUrl, provider }) {
const { server, from } = provider
// Strip protocol from URL and use domain as site name
const site = baseUrl.replace(/^https?:\/\//, '')
try {
await nodemailer
.createTransport(server)
.sendMail({
to: email,
from,
subject: `Sign in to ${site}`,
text: text({ url, site, email }),
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))
}
return resolve()
}
)
})
html: html({ url, site, email })
})
} catch (error) {
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
throw new Error('SEND_VERIFICATION_EMAIL_ERROR')
}
}
// Email HTML body

View File

@@ -10,7 +10,7 @@ export default function GitHub(options) {
profileUrl: "https://api.github.com/user",
profile(profile) {
return {
id: profile.id,
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* import { signIn } from "next-auth/react"
* ...
* <button onClick={() => signIn("instagram")}>
* Sign in

View File

@@ -15,7 +15,10 @@ 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,

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

@@ -0,0 +1,26 @@
export default function WorkOS(options) {
const domain = options.domain || "api.workos.com"
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://${domain}/sso/token`,
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
profileUrl: `https://${domain}/sso/profile`,
profile: (profile) => {
return {
...profile,
name: `${profile.first_name} ${profile.last_name}`,
}
},
...options,
}
}

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

@@ -0,0 +1,20 @@
export default function Zoom(options) {
return {
id: "zoom",
name: "Zoom",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
accessTokenUrl: "https://zoom.us/oauth/token",
authorizationUrl: "https://zoom.us/oauth/authorize?response_type=code",
profileUrl: "https://api.zoom.us/v2/users/me",
profile(profile) {
return {
id: profile.id,
name: `${profile.first_name} ${profile.last_name}`,
email: profile.email,
}
},
...options,
}
}

View File

@@ -1,24 +1,23 @@
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 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 +25,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 +38,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,52 +54,63 @@ 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"]
}
}
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
// 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))
// 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
// and are request-specific.
adapter,
baseUrl,
basePath,
action,
@@ -108,10 +120,10 @@ async function NextAuthHandler (req, res, userOptions) {
providers,
// Session options
session: {
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
jwt: !userOptions.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 +131,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 +153,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 +229,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 +250,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

@@ -1,7 +1,7 @@
import { OAuth, OAuth2 } from 'oauth'
import querystring from 'querystring'
import logger from '../../../lib/logger'
import { sign as jwtSign } from 'jsonwebtoken'
import { OAuth, OAuth2 } from "oauth"
import querystring from "querystring"
import logger from "../../../lib/logger"
import { sign as jwtSign } from "jsonwebtoken"
/**
* @TODO Refactor to remove dependancy on 'oauth' package
@@ -9,8 +9,8 @@ import { sign as jwtSign } from 'jsonwebtoken'
* would be easier to maintain if all the code was native to next-auth.
* @param {import("types/providers").OAuthConfig} provider
*/
export default function oAuthClient (provider) {
if (provider.version?.startsWith('2.')) {
export default function oAuthClient(provider) {
if (provider.version?.startsWith("2.")) {
// Handle OAuth v2.x
const authorizationUrl = new URL(provider.authorizationUrl)
const basePath = authorizationUrl.origin
@@ -34,9 +34,9 @@ export default function oAuthClient (provider) {
provider.accessTokenUrl,
provider.clientId,
provider.clientSecret,
provider.version || '1.0',
provider.version || "1.0",
provider.callbackUrl,
provider.encoding || 'HMAC-SHA1'
provider.encoding || "HMAC-SHA1"
)
// Promisify get() and getOAuth2AccessToken() for OAuth1
@@ -51,40 +51,48 @@ export default function oAuthClient (provider) {
})
})
}
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
const originalGetOAuth1AccessToken =
oauth1Client.getOAuthAccessToken.bind(oauth1Client)
oauth1Client.getOAuthAccessToken = (...args) => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase
originalGetOAuth1AccessToken(...args, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
originalGetOAuth1AccessToken(
...args,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params,
})
}
resolve({
// TODO: Remove, this is only kept for backward compativility
// These are not in the OAuth 1.x spec
accessToken: oauth_token,
refreshToken: oauth_token_secret,
results: params,
oauth_token,
oauth_token_secret,
params
})
})
)
})
}
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
const originalGetOAuthRequestToken =
oauth1Client.getOAuthRequestToken.bind(oauth1Client)
oauth1Client.getOAuthRequestToken = (params = {}) => {
return new Promise((resolve, reject) => {
// eslint-disable-next-line camelcase
originalGetOAuthRequestToken(params, (error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
originalGetOAuthRequestToken(
params,
(error, oauth_token, oauth_token_secret, params) => {
if (error) {
return reject(error)
}
resolve({ oauth_token, oauth_token_secret, params })
}
resolve({ oauth_token, oauth_token_secret, params })
})
)
})
}
return oauth1Client
@@ -104,52 +112,68 @@ export default function oAuthClient (provider) {
* @param {import("types/providers").OAuthConfig} provider
* @param {string | undefined} codeVerifier
*/
async function getOAuth2AccessToken (code, provider, codeVerifier) {
async function getOAuth2AccessToken(code, provider, codeVerifier) {
const url = provider.accessTokenUrl
const params = { ...provider.params }
const headers = { ...provider.headers }
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
const codeParam =
params.grant_type === "refresh_token" ? "refresh_token" : "code"
if (!params[codeParam]) { params[codeParam] = code }
if (!params[codeParam]) {
params[codeParam] = code
}
if (!params.client_id) { params.client_id = provider.clientId }
if (!params.client_id) {
params.client_id = provider.clientId
}
// For Apple the client secret must be generated on-the-fly.
// Using the properties in clientSecret to create a JWT.
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') {
if (provider.id === "apple" && typeof provider.clientSecret === "object") {
const { keyId, teamId, privateKey } = provider.clientSecret
const clientSecret = jwtSign({
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
aud: 'https://appleid.apple.com',
sub: provider.clientId
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, '\n'),
{ algorithm: 'ES256', keyid: keyId }
const clientSecret = jwtSign(
{
iss: teamId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
aud: "https://appleid.apple.com",
sub: provider.clientId,
},
// Automatically convert \\n into \n if found in private key. If the key
// is passed in an environment variable \n can get escaped as \\n
privateKey.replace(/\\n/g, "\n"),
{ algorithm: "ES256", keyid: keyId }
)
params.client_secret = clientSecret
} else {
params.client_secret = provider.clientSecret
}
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
// Added as a fix for Reddit Authentication
if (provider.id === 'reddit') {
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
if (!params.redirect_uri) {
params.redirect_uri = provider.callbackUrl
}
if (provider.id === 'identity-server4' && !headers.Authorization) {
if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/x-www-form-urlencoded"
}
// Added as a fix to accomodate change in Twitch OAuth API
if (!headers["Client-ID"]) {
headers["Client-ID"] = provider.clientId
}
// Added as a fix for Reddit Authentication
if (provider.id === "reddit") {
headers.Authorization =
"Basic " +
Buffer.from(provider.clientId + ":" + provider.clientSecret).toString(
"base64"
)
}
if (provider.id === "identity-server4" && !headers.Authorization) {
headers.Authorization = `Bearer ${code}`
}
if (provider.protection.includes('pkce')) {
if (provider.protection.includes("pkce")) {
params.code_verifier = codeVerifier
}
@@ -157,14 +181,14 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
return new Promise((resolve, reject) => {
this._request(
'POST',
"POST",
url,
headers,
postData,
null,
(error, data, response) => {
if (error) {
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
logger.error("OAUTH_GET_ACCESS_TOKEN_ERROR", error, data, response)
return reject(error)
}
@@ -181,7 +205,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
}
let accessToken
if (provider.id === 'slack') {
if (provider.id === "slack") {
const { ok, error } = raw
if (!ok) {
return reject(error)
@@ -197,7 +221,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
accessTokenExpires: null,
refreshToken: raw.refresh_token,
idToken: raw.id_token,
...raw
...raw,
})
}
)
@@ -213,52 +237,69 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
* @param {string} accessToken
* @param {any} results
*/
async function getOAuth2 (provider, accessToken, results) {
async function getOAuth2(provider, accessToken, results) {
let url = provider.profileUrl
let httpMethod = "GET"
const headers = { ...provider.headers }
if (this._useAuthorizationHeaderForGET) {
headers.Authorization = this.buildAuthHeader(accessToken)
// Mail.ru & vk.com require 'access_token' as URL request parameter
if (['mailru', 'vk'].includes(provider.id)) {
if (["mailru", "vk"].includes(provider.id)) {
const safeAccessTokenURL = new URL(url)
safeAccessTokenURL.searchParams.append('access_token', accessToken)
safeAccessTokenURL.searchParams.append("access_token", accessToken)
url = safeAccessTokenURL.href
}
// This line is required for Twitch
if (provider.id === 'twitch') {
headers['Client-ID'] = provider.clientId
if (provider.id === "twitch") {
headers["Client-ID"] = provider.clientId
}
accessToken = null
}
if (provider.id === 'bungie') {
if (provider.id === "bungie") {
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) => {
if (error) {
return reject(error)
this._request(
httpMethod,
url,
headers,
null,
accessToken,
(error, profileData) => {
if (error) {
return reject(error)
}
resolve(profileData)
}
resolve(profileData)
})
)
})
}
/** Bungie needs special handling */
function prepareProfileUrl ({ provider, url, results }) {
function prepareProfileUrl({ provider, url, results }) {
if (!results.membership_id) {
// internal error
// @TODO: handle better
throw new Error('Expected membership_id to be passed.')
throw new Error("Expected membership_id to be passed.")
}
if (!provider.headers?.['X-API-Key']) {
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
if (!provider.headers?.["X-API-Key"]) {
throw new Error(
'The Bungie provider requires the X-API-Key option to be present in "headers".'
)
}
return url.replace('{membershipId}', results.membership_id)
return url.replace("{membershipId}", results.membership_id)
}

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,23 +1,23 @@
import oAuthClient from '../oauth/client'
import logger from '../../../lib/logger'
import oAuthClient from "../oauth/client"
import logger from "../../../lib/logger"
/** @param {import("types/internals").NextAuthRequest} req */
export default async function getAuthorizationUrl (req) {
export default async function getAuthorizationUrl(req) {
const { provider } = req.options
delete req.query?.nextauth
const params = {
...provider.authorizationParams,
...req.query
...req.query,
}
const client = oAuthClient(provider)
if (provider.version?.startsWith('2.')) {
if (provider.version?.startsWith("2.")) {
// Handle OAuth v2.x
let url = client.getAuthorizeUrl({
scope: provider.scope,
...params,
redirect_uri: provider.callbackUrl,
scope: provider.scope
})
// If the authorizationUrl specified in the config has query parameters on it
@@ -27,13 +27,13 @@ export default async function getAuthorizationUrl (req) {
// which inadvertantly strips them.
//
// https://github.com/ciaranj/node-oauth/pull/193
if (provider.authorizationUrl.includes('?')) {
if (provider.authorizationUrl.includes("?")) {
const parseUrl = new URL(provider.authorizationUrl)
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
url = url.replace(baseUrl, provider.authorizationUrl + '&')
url = url.replace(baseUrl, provider.authorizationUrl + "&")
}
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
}
@@ -42,12 +42,12 @@ export default async function getAuthorizationUrl (req) {
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
...tokens.params,
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
} catch (error) {
logger.error('GET_AUTHORIZATION_URL_ERROR', error)
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
throw error
}
}

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,80 @@ 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)
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === 'string') {
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
account,
OAuthProfile
)
if (!signInCallbackResponse) {
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)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// 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 +140,140 @@ 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 })
if (signInCallbackResponse === false) {
const signInCallbackResponse = await callbacks.signIn(
profile,
account,
{ email }
)
if (!signInCallbackResponse) {
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)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
// 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,79 +282,123 @@ 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
)}`
)
} else if (typeof userObjectReturnedFromAuthorizeHandler === "string") {
return res.redirect(userObjectReturnedFromAuthorizeHandler)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
}
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
}
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)
if (signInCallbackResponse === false) {
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
const signInCallbackResponse = await callbacks.signIn(
user,
account,
credentials
)
if (!signInCallbackResponse) {
return res
.status(403)
.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(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
}
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, 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

@@ -5,13 +5,16 @@
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
*/
export default function providers (req, res) {
export default function providers(req, res) {
const { providers } = req.options
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
}, {})
const result = providers.reduce(
(acc, { id, name, type, signinUrl, callbackUrl }) => {
acc[id] = { id, name, type, signinUrl, callbackUrl }
return acc
},
{}
)
res.json(result)
}

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,37 @@
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) {
const {
provider,
baseUrl,
basePath,
adapter,
callbacks
} = req.options
/**
* 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, 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 +41,38 @@ 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)}`)
}
// TODO: Remove in a future major release
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(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)

View File

@@ -3,31 +3,15 @@
"strictNullChecks": true,
"baseUrl": ".",
"paths": {
"types": [
"./types"
],
"next-auth": [
"./src/server"
],
"next-auth/adapters": [
"./src/adapters"
],
"next-auth/client": [
"./src/client"
],
"next-auth/jwt": [
"./src/lib/jwt"
],
"next-auth/providers": [
"./src/providers"
]
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/react": ["./src/client/react"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"],
},
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
@@ -44,9 +28,8 @@
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.js"
"**/*.js",
".eslintrc.js"
],
"exclude": [
"node_modules"
]
"exclude": ["node_modules"]
}

35
types/adapters.d.ts vendored
View File

@@ -1,15 +1,6 @@
import { AppOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig, SendVerificationRequest } from "./providers"
import { ConnectionOptions } from "typeorm"
/** Legacy */
declare const Adapters: {
Default: Adapter<ConnectionOptions>
TypeORM: { Adapter: Adapter<ConnectionOptions> }
Prisma: { Adapter: Adapter }
}
export default Adapters
import { EmailConfig } from "./providers"
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
@@ -22,9 +13,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
@@ -49,9 +42,15 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
): Promise<void>
createSession(user: U): Promise<S>
getSession(sessionToken: string): Promise<S | null>
updateSession(session: S, force?: boolean): Promise<S>
updateSession(session: S, force?: boolean): Promise<S | null>
deleteSession(sessionToken: string): Promise<void>
createVerificationRequest?: SendVerificationRequest
createVerificationRequest?(
identifier: string,
url: string,
token: string,
secret: string,
provider: EmailConfig & { maxAge: number; from: string }
): Promise<void>
getVerificationRequest?(
identifier: string,
verificationToken: string,
@@ -112,13 +111,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>>

104
types/index.d.ts vendored
View File

@@ -1,4 +1,4 @@
// Minimum TypeScript Version: 3.5
// Minimum TypeScript Version: 3.6
/// <reference types="node" />
@@ -29,14 +29,6 @@ export interface NextAuthOptions {
* [Documentation](https://next-auth.js.org/configuration/options#providers) | [Providers documentation](https://next-auth.js.org/configuration/providers)
*/
providers: AppProviders
/**
* A database connection string or configuration object.
* * **Default value**: `null`
* * **Required**: *No (unless using email provider)*
*
* [Documentation](https://next-auth.js.org/configuration/options#database) | [Databases](https://next-auth.js.org/configuration/databases)
*/
database?: string | Record<string, any> | ConnectionOptions
/**
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
@@ -111,23 +103,16 @@ 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.
* You can use the adapter option to use the Prisma adapter - or pass in your own adapter
* if you want to use a database that is not supported by one of the built-in adapters.
* * **Default value**: TypeORM adapter
* You can use the adapter option to pass in your database adapter.
*
* * **Required**: *No*
*
* - ⚠ If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
* - ⚠ Adapters are being migrated to their own home in a Community maintained repository.
*
* [Documentation](https://next-auth.js.org/configuration/options#adapter) |
* [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 +165,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 +200,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 +335,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 {
@@ -379,11 +413,11 @@ export interface DefaultSession extends Record<string, unknown> {
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `Provider` React Context
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown>, DefaultSession {}

View File

@@ -1,34 +0,0 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** 0 means disabled (don't send); 60 means send every 60 seconds */
keepAlive: number
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
clientMaxAge: number
/** Used for timestamp since last sycned (in seconds) */
_clientLastSync: number
/** Stores timer for poll interval */
_clientSyncTimer: ReturnType<typeof setTimeout>
/** Tracks if event listeners have been added */
_eventListenersAdded: boolean
/** Stores last session response from hook */
_clientSession: Session | null | undefined
/** Used to store to function export by getSession() hook */
_getSession: any
}
export type SessionContext = React.Context<Session>

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 {

29
types/internals/react.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
import * as React from "react"
import { Session } from ".."
export interface BroadcastMessage {
event?: "session"
data?: {
trigger?: "signout" | "getSession"
}
clientId: string
timestamp: number
}
export interface NextAuthConfig {
baseUrl: string
basePath: string
baseUrlServer: string
basePathServer: string
/** Stores last session response */
_session?: Session | null
/** Used for timestamp since last sycned (in seconds) */
_lastSync: number
/**
* Stores the `SessionProvider`'s session update method to be able to
* trigger session updates from places like `signIn` or `signOut`
*/
_getSession: any
}
export type SessionContext = React.Context<Session>

26
types/providers.d.ts vendored
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 }>
@@ -57,13 +57,16 @@ export type OAuthProviderType =
| "Apple"
| "Atlassian"
| "Auth0"
| "AzureAD"
| "AzureADB2C"
| "Basecamp"
| "BattleNet"
| "Box"
| "Bungie"
| "Cognito"
| "Coinbase"
| "Discord"
| "Dropbox"
| "EVEOnline"
| "Facebook"
| "FACEIT"
@@ -93,8 +96,10 @@ export type OAuthProviderType =
| "Twitter"
| "VK"
| "WordPress"
| "WorkOS"
| "Yandex"
| "Zoho"
| "Zoom"
export type OAuthProvider = (options: Partial<OAuthConfig>) => OAuthConfig
@@ -113,7 +118,10 @@ 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 = (
@@ -133,13 +141,13 @@ export interface EmailConfigServerOptions {
}
}
export type SendVerificationRequest = (
identifier: string,
url: string,
baseUrl: string,
token: string,
export type SendVerificationRequest = (params: {
identifier: string
url: string
baseUrl: string
token: string
provider: EmailConfig
) => Awaitable<void>
}) => Awaitable<void>
export interface EmailConfig extends CommonProviderOptions {
type: "email"

View File

@@ -34,12 +34,6 @@ export function useSession(): [Session | null, boolean]
*/
export function getSession(options?: GetSessionOptions): Promise<Session | null>
/**
* Alias for `getSession`
* @docs https://next-auth.js.org/getting-started/client#getsession
*/
export const session: typeof getSession
/*******************
* CSRF Token types
******************/
@@ -54,12 +48,6 @@ export const session: typeof getSession
*/
export function getCsrfToken(ctxOrReq?: CtxOrReq): Promise<string | null>
/**
* Alias for `getCsrfToken`
* @docs https://next-auth.js.org/getting-started/client#getcsrftoken
*/
export const csrfToken: typeof getCsrfToken
/******************
* Providers types
*****************/
@@ -84,12 +72,6 @@ export function getProviders(): Promise<Record<
ClientSafeProvider
> | null>
/**
* Alias for `getProviders`
* @docs https://next-auth.js.org/getting-started/client#getproviders
*/
export const providers: typeof getProviders
/****************
* Sign in types
***************/
@@ -137,12 +119,6 @@ export function signIn<P extends SignInProvider = undefined>(
P extends RedirectableProvider ? SignInResponse | undefined : undefined
>
/**
* Alias for `signIn`
* @docs https://next-auth.js.org/getting-started/client#signin
*/
export const signin: typeof signIn
/****************
* Sign out types
****************/
@@ -169,21 +145,25 @@ export function signOut<R extends boolean = true>(
params?: SignOutParams<R>
): Promise<R extends true ? undefined : SignOutResponse>
/**
* @docs https://next-auth.js.org/getting-started/client#signout
* Alias for `signOut`
*/
export const signout: typeof signOut
/************************
* SessionProvider types
***********************/
/** @docs: https://next-auth.js.org/getting-started/client#options */
export interface SessionProviderOptions {
export interface SessionProviderProps {
session?: Session
baseUrl?: string
basePath?: string
clientMaxAge?: number
keepAlive?: number
/**
* The amount of time (in seconds) after a session should be considered stale.
* If set to `0` (default), the session will never be re-fetched.
*/
staleTime?: number
/**
* A time interval (in seconds) after which the session will be re-fetched.
* If set to `0` (default), the session is not polled.
*/
refetchInterval?: number
}
/**
@@ -191,28 +171,6 @@ export interface SessionProviderOptions {
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
* [Documentation](https://next-auth.js.org/getting-started/client#sessionprovider)
*/
export type SessionProvider = React.FC<{
children: React.ReactNode
session?: Session
options?: SessionProviderOptions
}>
/**
* Provider to wrap the app in to make session data available globally.
* Can also be used to throttle the number of requests to the endpoint
* `/api/auth/session`.
*
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
*/
export const Provider: SessionProvider
/** @docs: https://next-auth.js.org/getting-started/client#options */
export function setOptions(options: SessionProviderOptions): void
/**
* Alias for `setOptions`
* @docs: https://next-auth.js.org/getting-started/client#options
*/
export const options: typeof setOptions
export const SessionProvider: React.FC<SessionProviderProps>

View File

@@ -1,26 +1 @@
import Adapters from "next-auth/adapters"
// ExpectType TypeORMAdapter["Adapter"]
Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType TypeORMAdapter
Adapters.TypeORM.Adapter({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
// ExpectType PrismaAdapter
Adapters.Prisma.Adapter({
prisma: {},
modelMapping: {
User: "foo",
Account: "bar",
Session: "session",
VerificationRequest: "foo",
},
})
// TODO:

View File

@@ -1,4 +1,4 @@
import * as client from "next-auth/client"
import * as client from "../react-client"
import { nextReq } from "./test-helpers"
const clientSession = {
@@ -17,81 +17,67 @@ client.useSession()
// $ExpectType Promise<Session | null>
client.getSession({ req: nextReq })
// $ExpectType Promise<Session | null>
client.session({ req: nextReq })
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.getProviders()
// $ExpectType Promise<Record<string, ClientSafeProvider> | null>
client.providers()
// $ExpectType Promise<string | null>
client.getCsrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.csrfToken({ req: nextReq })
// $ExpectType Promise<string | null>
client.csrfToken({ ctx: { req: nextReq } })
client.getCsrfToken({ ctx: { req: nextReq } })
// $ExpectType Promise<undefined>
client.signin("github", { callbackUrl: "foo" }, { login: "username" })
client.signIn("github", { callbackUrl: "foo" }, { login: "username" })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("credentials", { callbackUrl: "foo", redirect: true })
client.signIn("credentials", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("credentials", { redirect: false })
client.signIn("credentials", { redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("email", { callbackUrl: "foo", redirect: false })
client.signIn("email", { callbackUrl: "foo", redirect: false })
// $ExpectType Promise<SignInResponse | undefined>
client.signin("email", { callbackUrl: "foo", redirect: true })
client.signIn("email", { callbackUrl: "foo", redirect: true })
// $ExpectType Promise<undefined>
client.signout()
client.signOut()
// $ExpectType Promise<undefined>
client.signout({ callbackUrl: "https://foo.com/callback", redirect: true })
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: true })
// $ExpectType Promise<SignOutResponse>
client.signOut({ callbackUrl: "https://foo.com/callback", redirect: false })
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: clientSession,
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
},
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: clientSession,
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
options: {},
})
// $ExpectType ReactElement<any, any> | null
client.Provider({
client.SessionProvider({
children: null,
session: {
expires: "",
},
options: {
baseUrl: "https://foo.com",
basePath: "/",
clientMaxAge: 1234,
keepAlive: 4321,
},
baseUrl: "https://foo.com",
basePath: "/",
staleTime: 1234,
refetchInterval: 4321,
})

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({
@@ -138,7 +135,6 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
clientSecret: "123",
}),
],
database: "path/to/db",
debug: true,
secret: "my secret",
session: {
@@ -171,26 +167,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

@@ -15,7 +15,7 @@
"next-auth": ["."],
"next-auth/providers": ["./providers"],
"next-auth/adapters": ["./adapters"],
"next-auth/client": ["./client"],
"next-auth/react": ["./react-client"],
"next-auth/jwt": ["./jwt"]
}
}

View File

@@ -0,0 +1,71 @@
---
id: dynamodb
title: DynamoDB Adapter
---
# DynamoDB
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
You need a table with a partition key `pk` and a sort key `sk`. Your table also needs a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. You can set whatever you want as the table name and the billing method.
You can find the full schema in the table structure section below.
## Getting Started
1. Install `next-auth` and `@next-auth/dynamodb-adapter@canary`
```js
npm install next-auth @next-auth/dynamodb-adapter@canary
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
You need to pass `DocumentClient` instance from `aws-sdk` to the adapter.
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
```javascript title="pages/api/auth/[...nextauth].js"
import AWS from "aws-sdk";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
AWS.config.update({
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY,
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY,
region: process.env.NEXT_AUTH_AWS_REGION,
});
export default NextAuth({
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Providers.Email({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
// ...add more providers here
],
adapter: DynamoDBAdapter(
new AWS.DynamoDB.DocumentClient()
),
...
});
```
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
## Schema
The table respects the single table design pattern. This has many advantages:
- Only one table to manage, monitor and provision.
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
- Only one table needs to be replicated, if you want to go multi-region.
Here is a schema of the table :
![DynamoDB Table](https://i.imgur.com/hGZtWDq.png)

View File

@@ -0,0 +1,84 @@
---
id: fauna
title: FaunaDB Adapter
---
# FaunaDB
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
## Getting Started
1. Install `next-auth` and `@next-auth/fauna-adapter@canary`
```js
npm install next-auth @next-auth/fauna-adapter@canary
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import * as Fauna from "faunadb"
import { FaunaAdapter } from "@next-auth/fauna-adapter"
const client = new Fauna.Client({
secret: "secret",
scheme: "http",
domain: "localhost",
port: 8443,
})
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FaunaAdapter({ faunaClient: client})
...
})
```
## Schema
```javascript
CreateCollection({ name: "accounts" })
CreateCollection({ name: "sessions" })
CreateCollection({ name: "users" })
CreateCollection({ name: "verification_requests" })
CreateIndex({
name: "account_by_provider_account_id",
source: Collection("accounts"),
unique: true,
terms: [
{ field: ["data", "providerId"] },
{ field: ["data", "providerAccountId"] },
],
})
CreateIndex({
name: "session_by_token",
source: Collection("sessions"),
unique: true,
terms: [{ field: ["data", "sessionToken"] }],
})
CreateIndex({
name: "user_by_email",
source: Collection("users"),
unique: true,
terms: [{ field: ["data", "email"] }],
})
CreateIndex({
name: "verification_request_by_token",
source: Collection("verification_requests"),
unique: true,
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],
})
```

View File

@@ -0,0 +1,73 @@
---
id: firebase
title: Firebase Adapter
---
# Firebase
This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
## Getting Started
1. Install `next-auth` and `@next-auth/firebase-adapter@canary`
```js
npm install next-auth @next-auth/firebase-adapter@canary
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
import firebase from "firebase/app"
import "firebase/firestore"
const firestore = (
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
).firestore()
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: FirebaseAdapter(firestore),
...
})
```
## Options
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
An example firebase config looks like this:
```js
const firebaseConfig = {
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
authDomain: "myapp-project-123.firebaseapp.com",
databaseURL: "https://myapp-project-123.firebaseio.com",
projectId: "myapp-project-123",
storageBucket: "myapp-project-123.appspot.com",
messagingSenderId: "65211879809",
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
measurementId: "G-8GSGZQ44ST",
}
```
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
:::tip **From Firebase**
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
:::

View File

@@ -11,7 +11,6 @@ All table/collection names in the built in models are plural, and all table name
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
:::
---
## User
@@ -30,7 +29,7 @@ If a user first signs in with OAuth then their email address is automatically po
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured).
:::
## Account
## Account
Table: `accounts`
@@ -60,4 +59,4 @@ The Verification Request model is used to store tokens for passwordless sign in
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).

View File

@@ -0,0 +1,43 @@
---
id: overview
title: Overview
---
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.
The adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters).
There you can find the following adapters:
- [`typeorm-legacy`](./typeorm/typeorm-overview)
- [`prisma`](./prisma)
- [`prisma-legacy`](./prisma-legacy)
- [`fauna`](./fauna)
- [`dynamodb`](./dynamodb)
- [`firebase`](./firebase)
- [`pouchdb`](./pouchdb)
## 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 [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

@@ -0,0 +1,61 @@
---
id: pouchdb
title: PouchDB Adapter
---
# PouchDB
This is the PouchDB Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
Depending on your architecture you can use PouchDB's http adapter to reach any database compliant with the CouchDB protocol (CouchDB, Cloudant, ...) or use any other PouchDB compatible adapter (leveldb, in-memory, ...)
## Getting Started
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
1. Install `next-auth` and `@next-auth/pouchdb-adapter@canary`
```js
npm install next-auth @next-auth/pouchdb-adapter@canary
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { PouchDBAdapter } from "@next-auth/pouchdb-adapter"
import PouchDB from "pouchdb"
// Setup your PouchDB instance and database
PouchDB.plugin(require("pouchdb-adapter-leveldb")) // Any other adapter
.plugin(require("pouchdb-find")) // Don't forget the `pouchdb-find` plugin
const pouchdb = new PouchDB("auth_db", { adapter: "leveldb" })
// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export default NextAuth({
// https://next-auth.js.org/configuration/providers
providers: [
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
],
adapter: PouchDBAdapter(pouchdb),
// ...
})
```
## Advanced
### Memory-First Caching Strategy
If you need to boost your authentication layer performance, you may use PouchDB's powerful sync features and various adapters, to build a memory-first caching strategy.
Use an in-memory PouchDB as your main authentication database, and synchronize it with any other persisted PouchDB. You may do a one way, one-off replication at startup from the persisted PouchDB into the in-memory PouchDB, then two-way, continuous, retriable sync.
This will most likely not increase performance much in a serverless environment due to various reasons such as concurrency, function startup time increases, etc.
For more details, please see https://pouchdb.com/api.html#sync

View File

@@ -0,0 +1,174 @@
---
id: prisma-legacy
title: Prisma Adapter (Legacy)
---
# Prisma (Legacy)
You can also use NextAuth.js with the built-in Adapter for [Prisma](https://www.prisma.io/docs/). This is included in the core `next-auth` package at the moment. The other adapter needs to be installed from its own additional package.
:::info
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
:::
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:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: PrismaLegacyAdapter({ prisma }),
})
```
:::tip
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
:::
Schema for the Prisma Adapter
## Setup
Create a schema file in `prisma/schema.prisma` similar to this one:
```json title="schema.prisma"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Account {
id Int @id @default(autoincrement())
compoundId String @unique @map(name: "compound_id")
userId Int @map(name: "user_id")
providerType String @map(name: "provider_type")
providerId String @map(name: "provider_id")
providerAccountId String @map(name: "provider_account_id")
refreshToken String? @map(name: "refresh_token")
accessToken String? @map(name: "access_token")
accessTokenExpires DateTime? @map(name: "access_token_expires")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@index([providerAccountId], name: "providerAccountId")
@@index([providerId], name: "providerId")
@@index([userId], name: "userId")
@@map(name: "accounts")
}
model Session {
id Int @id @default(autoincrement())
userId Int @map(name: "user_id")
expires DateTime
sessionToken String @unique @map(name: "session_token")
accessToken String @unique @map(name: "access_token")
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "sessions")
}
model User {
id Int @id @default(autoincrement())
name String?
email String? @unique
emailVerified DateTime? @map(name: "email_verified")
image String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "users")
}
model VerificationRequest {
id Int @id @default(autoincrement())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
@@map(name: "verification_requests")
}
```
### Generate Client
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
```
npx prisma generate
```
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
```
npx prisma migrate dev
```
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
:::tip
If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
```javascript title="pages/api/auth/[...nextauth].js"
let prisma
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
```
:::
### Custom Models
You can add properties to the schema and map them to any database column names you wish, but you should not change the base properties or types defined in the example schema.
The model names themselves can be changed with a configuration option, and the datasource can be changed to anything supported by Prisma.
You can use custom model names by using the `modelMapping` option (shown here with default values).
```javascript title="pages/api/auth/[...nextauth].js"
...
adapter: PrismaLegacyAdapter({
prisma,
modelMapping: {
User: 'user',
Account: 'account',
Session: 'session',
VerificationRequest: 'verificationRequest'
}
})
...
```

218
www/docs/adapters/prisma.md Normal file
View File

@@ -0,0 +1,218 @@
---
id: prisma
title: Prisma Adapter
---
# Prisma
You can also use NextAuth.js with the new experimental Adapter for [Prisma](https://www.prisma.io/docs/). This version of the Prisma Adapter is not included in the core `next-auth` package, and must be installed separately.
:::info
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
:::
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter@canary` package:
```
npm install @prisma/client @next-auth/prisma-adapter@canary
npm install prisma --save-dev
```
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 { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export default NextAuth({
providers: [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: PrismaAdapter(prisma),
})
```
:::tip
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
:::
Schema for the Prisma Adapter (`@next-auth/prisma-adapter`)
## Setup
Create a schema file in `prisma/schema.prisma` similar to this one:
```json title="schema.prisma"
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Account {
id String @id @default(cuid())
userId String
providerType String
providerId String
providerAccountId String
refreshToken String?
accessToken String?
accessTokenExpires DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([providerId, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
expires DateTime
sessionToken String @unique
accessToken String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
sessions Session[]
}
model VerificationRequest {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, token])
}
```
### Generate Client
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
```
npx prisma generate
```
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
```
npx prisma migrate dev
```
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
## Schema History
Changes from the original Prisma Adapter
```diff
model Account {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
- compoundId String @unique @map(name: "compound_id")
- userId Int @map(name: "user_id")
+ userId String
+ user User @relation(fields: [userId], references: [id])
- providerType String @map(name: "provider_type")
+ providerType String
- providerId String @map(name: "provider_id")
+ providerId String
- providerAccountId String @map(name: "provider_account_id")
+ providerAccountId String
- refreshToken String? @map(name: "refresh_token")
+ refreshToken String?
- accessToken String? @map(name: "access_token")
+ accessToken String?
- accessTokenExpires DateTime? @map(name: "access_token_expires")
+ accessTokenExpires DateTime?
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@index([providerAccountId], name: "providerAccountId")
- @@index([providerId], name: "providerId")
- @@index([userId], name: "userId")
- @@map(name: "accounts")
+ @@unique([providerId, providerAccountId])
}
model Session {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
- userId Int @map(name: "user_id")
+ userId String
+ user User @relation(fields: [userId], references: [id])
expires DateTime
- sessionToken String @unique @map(name: "session_token")
+ sessionToken String @unique
- accessToken String @unique @map(name: "access_token")
+ accessToken String @unique
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
-
- @@map(name: "sessions")
}
model User {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
name String?
email String? @unique
- emailVerified DateTime? @map(name: "email_verified")
+ emailVerified DateTime?
image String?
+ accounts Account[]
+ sessions Session[]
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@map(name: "users")
}
model VerificationRequest {
- id Int @default(autoincrement()) @id
+ id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
- createdAt DateTime @default(now()) @map(name: "created_at")
+ createdAt DateTime @default(now())
- updatedAt DateTime @default(now()) @map(name: "updated_at")
+ updatedAt DateTime @updatedAt
- @@map(name: "verification_requests")
+ @@unique([identifier, token])
}
```

View File

@@ -19,4 +19,4 @@ Objects stored in MongoDB use similar datatypes to SQL, with some differences:
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.

View File

@@ -1,88 +1,88 @@
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::
---
id: mssql
title: Microsoft SQL Server
---
Schema for a Microsoft SQL Server (mssql) database.
:::note
When using a Microsoft SQL Server database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `datetime`.
This transform is also applied to any properties of type `timestamp` when using custom models.
:::
```sql
CREATE TABLE accounts
(
id int IDENTITY(1,1) NOT NULL,
compound_id varchar(255) NOT NULL,
user_id int NOT NULL,
provider_type varchar(255) NOT NULL,
provider_id varchar(255) NOT NULL,
provider_account_id varchar(255) NOT NULL,
refresh_token text NULL,
access_token text NULL,
access_token_expires datetime NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE sessions
(
id int IDENTITY(1,1) NOT NULL,
user_id int NOT NULL,
expires datetime NOT NULL,
session_token varchar(255) NOT NULL,
access_token varchar(255) NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE users
(
id int IDENTITY(1,1) NOT NULL,
name varchar(255) NULL,
email varchar(255) NULL,
email_verified datetime NULL,
image varchar(255) NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE TABLE verification_requests
(
id int IDENTITY(1,1) NOT NULL,
identifier varchar(255) NOT NULL,
token varchar(255) NOT NULL,
expires datetime NOT NULL,
created_at datetime NOT NULL DEFAULT getdate(),
updated_at datetime NOT NULL DEFAULT getdate()
);
CREATE UNIQUE INDEX compound_id
ON accounts(compound_id);
CREATE INDEX provider_account_id
ON accounts(provider_account_id);
CREATE INDEX provider_id
ON accounts(provider_id);
CREATE INDEX user_id
ON accounts(user_id);
CREATE UNIQUE INDEX session_token
ON sessions(session_token);
CREATE UNIQUE INDEX access_token
ON sessions(access_token);
CREATE UNIQUE INDEX email
ON users(email);
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
:::

View File

@@ -84,4 +84,4 @@ CREATE UNIQUE INDEX email
CREATE UNIQUE INDEX token
ON verification_requests(token);
```
```

View File

@@ -0,0 +1,49 @@
---
id: typeorm-overview
title: Overview
---
## 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 in your project and pass a database connection string to NextAuth.js.
### Database Schemas
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
- [MySQL Schema](./mysql)
- [Postgres Schema](./postgres)
- [Microsoft SQL Server Schema](./mssql)
- [MongoDB](./mongodb)
The default Adapter is the TypeORM Adapter and the default database type for TypeORM is SQLite, the following configuration options are exactly equivalent.
```javascript
database: {
type: 'sqlite',
database: ':memory:',
synchronize: true
}
```
```javascript
adapter: Adapters.Default({
type: "sqlite",
database: ":memory:",
synchronize: true,
})
```
```javascript
adapter: Adapters.TypeORM.Adapter({
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.
:::tip
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres. This will automatically apply any changes it finds in the entity model, therefore it **should not be enabled against production databases** as it may cause data loss if the configured schema does not match the expected schema!
:::

View File

@@ -44,7 +44,7 @@ callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @param {object} profile Provider profile
* @param {object} profile Provider profile
* @return {boolean|string} Return `true` to allow sign in
* Return `false` to deny access
* Return `string` to redirect to (eg.: "/unauthorized")
@@ -64,13 +64,13 @@ callbacks: {
...
```
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
- When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again _after_ they activate the link in the sign in email.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include a property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked _after_ a user has clicked on a sign in link, this property will not be present.
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include an property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
* When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `profile` object is the raw body of the `HTTP POST` submission.
- When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `profile` object is the raw body of the `HTTP POST` submission.
:::note
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
@@ -78,10 +78,10 @@ When using NextAuth.js with a database, the User object will be either a user ob
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
:::
:::tip
If you only want to allow users who already have accounts in the database to sign in, you can check for the existence of a `user.id` property and reject any sign in attempts from accounts that do not have one.
:::note
Redirects returned by this callback cancel the authentication flow. Only redirect to error pages that, for example, tell the user why they're not allowed to sign in.
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
To redirect to a page after a successful sign in, please use [the `callbackUrl` option](/getting-started/client#specifying-a-callbackurl) or [the redirect callback](/configuration/callbacks#redirect-callback).
:::
## Redirect callback
@@ -107,22 +107,21 @@ callbacks: {
...
```
:::note
The redirect callback may be invoked more than once in the same flow.
:::
## JWT callback
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client).
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
* As with database session expiry times, token expiry time is extended whenever a session is active.
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
- As with database session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in.
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
```js title="pages/api/auth/[...nextauth].js"
...
@@ -156,19 +155,19 @@ 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
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitely forward it here to make it available to the client.
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
* When using database sessions, the User object is passed as an argument.
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
- When using database sessions, the User object is passed as an argument.
- When using JSON Web Tokens for sessions, the JWT payload is provided instead.
```js title="pages/api/auth/[...nextauth].js"
...
@@ -177,7 +176,7 @@ callbacks: {
* @param {object} session Session object
* @param {object} token User object (if using database sessions)
* JSON Web Token (if not using database sessions)
* @return {object} Session that will be returned to the client
* @return {object} Session that will be returned to the client
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.

View File

@@ -3,66 +3,77 @@ id: databases
title: Databases
---
NextAuth.js comes with multiple ways of connecting to a database:
NextAuth.js offers multiple database adapters:
* **TypeORM** (default)<br/>
_The TypeORM adapter supports MySQL, Postgres, MsSql, SQLite and MongoDB databases._
* **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
* **Custom Adapter**<br/>
_A custom Adapter can be used to connect to any database._
- [`typeorm-legacy`](./../adapters/typeorm/typeorm-overview)
- [`prisma`](./../adapters/prisma)
- [`prisma-legacy`](./../adapters/prisma-legacy)
- [`fauna`](./../adapters/fauna)
- [`dynamodb`](./../adapters/dynamodb)
- [`firebase`](./../adapters/firebase)
- [`pouchdb`](./../adapters/pouchdb)
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
> As of **v4.0.0** NextAuth.js no longer ships with an adapter included by default. If you would like to persist any information, you need to install one of the many available adapters yourself. See the individual adapter documentation pages for more details.
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
---
**The rest of this document covers the old default adapter (TypeORM).**
## How to use a database
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
## How to use a database
The following approaches are exactly equivalent:
You can specify database credentials as a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object or connection string:
```js
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name'
```js title="pages/api/auth/[...nextauth].js"
import TypeORMAdapter from "@next-auth/typeorm-legacy-adapter"
import NextAuth from "next-auth"
export default NextAuth({
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name"
),
// or...
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: "nextauth",
password: "password",
database: "database_name",
}),
})
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name'
}
```
Both approaches are exactly equivalent:
:::tip
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
_e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:_
```js
'mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
)
```
*…or as a database configuration object:*
_…or as a database configuration object:_
```js
database: {
type: 'mysql',
host: '127.0.0.1',
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name',
entityPrefix: 'nextauth_'
}
username: "nextauth",
password: "password",
database: "database_name",
entityPrefix: "nextauth_",
})
```
:::
---
@@ -73,27 +84,29 @@ Using SQL to create tables and columns is the recommended way to set up an SQL d
Check out the links below for SQL you can run to set up a database for NextAuth.js.
* [MySQL Schema](/schemas/mysql)
* [Postgres Schema](/schemas/postgres)
- [MySQL Schema](/adapters/typeorm/mysql)
- [Postgres Schema](/adapters/typeorm/postgres)
_If you are running SQLite, MongoDB or a Document database you can skip this step._
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
```js
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true'
adapter: TypeORMAdapter(
"mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
)
```
```js
database: {
type: 'mysql',
host: '127.0.0.1',
adapter: TypeORMAdapter({
type: "mysql",
host: "127.0.0.1",
port: 3306,
username: 'nextauth',
password: 'password',
database: 'database_name',
synchronize: true
}
username: "nextauth",
password: "password",
database: "database_name",
synchronize: true,
})
```
:::warning
@@ -122,7 +135,9 @@ Install module:
#### Example
```js
database: 'mysql://username:password@127.0.0.1:3306/database_name'
adapter: TypeORMAdapter(
"mysql://username:password@127.0.0.1:3306/database_name"
)
```
### MariaDB
@@ -133,7 +148,9 @@ Install module:
#### Example
```js
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
adapter: TypeORMAdapter(
"mariadb://username:password@127.0.0.1:3306/database_name"
)
```
### Postgres / CockroachDB
@@ -144,30 +161,36 @@ Install module:
#### Example
PostgresDB
```js
database: 'postgres://username:password@127.0.0.1:5432/database_name'
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:5432/database_name"
)
```
CockroachDB
```js
database: 'postgres://username:password@127.0.0.1:26257/database_name'
adapter: TypeORMAdapter(
"postgres://username:password@127.0.0.1:26257/database_name"
)
```
If the node is using Self-signed cert
```js
database: {
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync('/path/to/server-certificates/root.crt').toString()
},
adapter: TypeORMAdapter({
type: "cockroachdb",
host: process.env.DATABASE_HOST,
port: 26257,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
ssl: {
rejectUnauthorized: false,
ca: fs.readFileSync("/path/to/server-certificates/root.crt").toString(),
},
})
```
Read more: [https://node-postgres.com/features/ssl](https://node-postgres.com/features/ssl)
@@ -182,7 +205,7 @@ Install module:
#### Example
```js
database: 'mssql://sa:password@localhost:1433/database_name'
adapter: TypeORMAdapter("mssql://sa:password@localhost:1433/database_name")
```
### MongoDB
@@ -193,12 +216,14 @@ Install module:
#### Example
```js
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
adapter: TypeORMAdapter(
"mongodb://username:password@127.0.0.1:3306/database_name"
)
```
### SQLite
*SQLite is intended only for development / testing and not for production use.*
_SQLite is intended only for development / testing and not for production use._
Install module:
`npm i sqlite3`
@@ -206,9 +231,9 @@ Install module:
#### Example
```js
database: 'sqlite://localhost/:memory:'
adapter: TypeORMAdapter("sqlite://localhost/:memory:")
```
## Other databases
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).

View File

@@ -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.
A random string used to hash tokens, sign cookies and generate cryptographic keys.
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
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,46 +282,37 @@ 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**: none
- **Required**: _No_
#### Description
By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases. An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
See the [adapter documentation](/schemas/adapters) for more information.
:::note
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
:::
By default NextAuth.js does not include an adapter any longer. If you would like to persist user / account data, please install one of the many available adapters. More information can be found in the [adapter documentation](/adapters/overview).
---
### debug
* **Default value**: `false`
* **Required**: *No*
- **Default value**: `false`
- **Required**: _No_
#### Description
@@ -335,14 +322,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 +359,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 +376,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 +392,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

@@ -22,19 +22,22 @@ To add a custom login page, you can use the `pages` option:
```
## Error codes
We purposefully restrict the returned error codes for increased security.
### Error page
The following errors are passed as error query parameters to the default or overriden error page:
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
- **AccessDenied**: Usually occurs, when you restriected access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
- **AccessDenied**: Usually occurs, when you restricted access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
- **Verification**: Related to the Email provider. The token has expired or has already been used
- **Default**: Catch all, will apply, if none of the above matched
Example: `/auth/error?error=Configuration`
### Sign-in page
The following errors are passed as error query parameters to the default or overriden sign-in page:
- **OAuthSignin**: Error in constructing an authorization URL ([1](https://github.com/nextauthjs/next-auth/blob/457952bb5abf08b09861b0e5da403080cd5525be/src/server/lib/signin/oauth.js), [2](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/pkce-handler.js), [3](https://github.com/nextauthjs/next-auth/blob/main/src/server/lib/oauth/state-handler.js)),
@@ -46,7 +49,7 @@ The following errors are passed as error query parameters to the default or over
- **EmailSignin**: Sending the e-mail with the verification token failed
- **CredentialsSignin**: The `authorize` callback returned `null` in the [Credentials provider](/providers/credentials). We don't recommend providing information about which part of the credentials were wrong, as it might be abused by malicious hackers.
- **Default**: Catch all, will apply, if none of the above matched
Example: `/auth/error?error=Default`
## Theming
@@ -60,14 +63,16 @@ By default, the built-in pages will follow the system theme, utilizing the [`pre
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
```jsx title="pages/auth/signin.js"
import { getProviders, signIn } from 'next-auth/client'
import { getProviders, signIn } from "next-auth/react"
export default function SignIn({ providers }) {
export default function SignIn({ Providers }) {
return (
<>
{Object.values(providers).map(provider => (
{Object.values(providers).map((provider) => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</>
@@ -75,10 +80,10 @@ export default function SignIn({ providers }) {
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
export async function getServerSideProps(context) {
const providers = await getProviders()
return {
props: { providers }
props: { providers },
}
}
@@ -97,26 +102,26 @@ SignIn.getInitialProps = async () => {
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
```jsx title="pages/auth/email-signin.js"
import { getCsrfToken } from 'next-auth/client'
import { getCsrfToken } from "next-auth/react"
export default function SignIn({ csrfToken }) {
return (
<form method='post' action='/api/auth/signin/email'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<form method="post" action="/api/auth/signin/email">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
Email address
<input type='email' id='email' name='email'/>
<input type="email" id="email" name="email" />
</label>
<button type='submit'>Sign in with Email</button>
<button type="submit">Sign in with Email</button>
</form>
)
}
// This is the recommended way for Next.js 9.3 or newer
export async function getServerSideProps(context){
export async function getServerSideProps(context) {
const csrfToken = await getCsrfToken(context)
return {
props: { csrfToken }
props: { csrfToken },
}
}
@@ -133,7 +138,7 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('email', { email: 'jsmith@example.com' })
signIn("email", { email: "jsmith@example.com" })
```
### Credentials Sign in
@@ -141,21 +146,21 @@ signIn('email', { email: 'jsmith@example.com' })
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
```jsx title="pages/auth/credentials-signin.js"
import { getCsrfToken } from 'next-auth/client'
import { getCsrfToken } from "next-auth/react"
export default function SignIn({ csrfToken }) {
return (
<form method='post' action='/api/auth/callback/credentials'>
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
<form method="post" action="/api/auth/callback/credentials">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} />
<label>
Username
<input name='username' type='text'/>
<input name="username" type="text" />
</label>
<label>
Password
<input name='password' type='password'/>
<input name="password" type="password" />
</label>
<button type='submit'>Sign in</button>
<button type="submit">Sign in</button>
</form>
)
}
@@ -164,8 +169,8 @@ export default function SignIn({ csrfToken }) {
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context)
}
csrfToken: await getCsrfToken(context),
},
}
}
@@ -182,9 +187,9 @@ SignIn.getInitialProps = async (context) => {
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
```js
signIn('credentials', { username: 'jsmith', password: '1234' })
signIn("credentials", { username: "jsmith", password: "1234" })
```
:::tip
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
:::

View File

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

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