Compare commits

...

93 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
Balázs Orbán
a388b44d0b Merge branch 'main' into next 2021-05-03 21:11:04 +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
255 changed files with 58242 additions and 23182 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 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),
},
}
}

View File

@@ -5,18 +5,25 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
plugins: [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
],
comments: false,
overrides: [
{
test: ["../src/client/**"],
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
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

@@ -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",
}

41060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,7 @@
"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"
@@ -37,8 +36,9 @@
"watch": "npm run watch:js | npm run watch:css",
"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"
@@ -62,47 +62,46 @@
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "canary",
"@next-auth/typeorm-legacy-adapter": "canary",
"crypto-js": "^4.0.0",
"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",
@@ -112,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
@@ -157,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

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

View File

@@ -15,7 +15,7 @@
* ...
*
* // pages/index
* import { signIn } from "next-auth/client"
* import { signIn } from "next-auth/react"
* ...
* <button onClick={() => signIn("dropbox")}>
* Sign in
@@ -29,26 +29,26 @@
*/
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',
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',
"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
email_verified: profile.email_verified,
}
},
protection: ["state", "pkce"],
...options
...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,64 +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':
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
@@ -207,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`)
}
@@ -225,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,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,60 +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'
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'
if (provider.id === "dropbox") {
httpMethod = "POST"
}
return new Promise((resolve, reject) => {
this._request(httpMethod, 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,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

@@ -77,7 +77,7 @@ export default async function callback(req, res) {
account,
OAuthProfile
)
if (signInCallbackResponse === false) {
if (!signInCallbackResponse) {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
@@ -85,16 +85,11 @@ export default async function callback(req, res) {
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
@@ -226,22 +221,17 @@ export default async function callback(req, res) {
account,
{ email }
)
if (signInCallbackResponse === false) {
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
@@ -336,7 +326,8 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials
credentials,
{ ...req, options: {}, cookies: {} }
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res
@@ -346,16 +337,13 @@ export default async function callback(req, res) {
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
@@ -367,20 +355,17 @@ export default async function callback(req, res) {
account,
credentials
)
if (signInCallbackResponse === false) {
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 = {

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

@@ -8,14 +8,8 @@ import adapterErrorHandler from "../../adapters/error-handler"
* @param {import("types/internals").NextAuthResponse} res
*/
export default async function signin(req, res) {
const {
provider,
baseUrl,
basePath,
adapter,
callbacks,
logger,
} = req.options
const { provider, baseUrl, basePath, adapter, callbacks, logger } =
req.options
if (!provider.type) {
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
@@ -62,14 +56,9 @@ export default async function signin(req, res) {
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 {

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

11
types/adapters.d.ts vendored
View File

@@ -1,15 +1,6 @@
import { AppOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig } from "./providers"
import { ConnectionOptions } from "typeorm"
/** Legacy */
declare const Adapters: {
Default: Adapter<ConnectionOptions>
TypeORM: { Adapter: Adapter<ConnectionOptions> }
Prisma: { Adapter: Adapter }
}
export default Adapters
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
@@ -26,7 +17,7 @@ export interface AdapterInstance<U = User, P = Profile, S = Session> {
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

23
types/index.d.ts vendored
View File

@@ -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.
@@ -113,18 +105,11 @@ export interface NextAuthOptions {
*/
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?: ReturnType<Adapter>
@@ -428,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>

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>

11
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"
@@ -57,12 +57,14 @@ export type OAuthProviderType =
| "Apple"
| "Atlassian"
| "Auth0"
| "AzureAD"
| "AzureADB2C"
| "Basecamp"
| "BattleNet"
| "Box"
| "Bungie"
| "Cognito"
| "Coinbase"
| "Discord"
| "Dropbox"
| "EVEOnline"
@@ -94,8 +96,10 @@ export type OAuthProviderType =
| "Twitter"
| "VK"
| "WordPress"
| "WorkOS"
| "Yandex"
| "Zoho"
| "Zoom"
export type OAuthProvider = (options: Partial<OAuthConfig>) => OAuthConfig
@@ -114,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 = (

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

@@ -135,7 +135,6 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
clientSecret: "123",
}),
],
database: "path/to/db",
debug: true,
secret: "my secret",
session: {

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

@@ -66,7 +66,7 @@ See the [providers documentation](/configuration/providers) for a list of suppor
#### 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, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
@@ -300,20 +300,12 @@ events: {
### adapter
- **Default value**: _Adapter.Default()_
- **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).
---

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

@@ -8,7 +8,7 @@ Authentication Providers in **NextAuth.js** are services that can be used to sig
There's four ways a user can be signed in:
- [Using a built-in OAuth Provider](#oauth-providers) (e.g Github, Twitter, Google, etc...)
- [Using a custom OAuth Provider](#-using-a-custom-provider)
- [Using a custom OAuth Provider](#using-a-custom-provider)
- [Using Email](#email-provider)
- [Using Credentials](#credentials-provider)
@@ -254,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
}
})
]
@@ -282,10 +288,10 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
### 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 `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials) => Promise<User>` | Yes |
| Name | Description | Type | Required |
| :---------: | :-----------------------------------------------: | :-----------------------------------: | :------: |
| id | Unique ID for the provider | `string` | Yes |
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
| credentials | The credentials to sign-in with | `Object` | Yes |
| authorize | Callback to execute once user is to be authorized | `(credentials, req) => Promise<User>` | Yes |

View File

@@ -15,13 +15,13 @@ If you are seeing any of these errors in the console, something is wrong.
These errors are returned from the client. As the client is [Universal JavaScript (or "Isomorphic JavaScript")](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) it can be run on the client or server, so these errors can occur in both in the terminal and in the browser console.
#### CLIENT_USE_SESSION_ERROR
#### CLIENT_SESSION_ERROR
This error occurs when the `useSession()` React Hook has a problem fetching session data.
This error occurs when the `SessionProvider` Context has a problem fetching session data.
#### CLIENT_FETCH_ERROR
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` envionment variable.
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` environment variable.
---
@@ -63,9 +63,9 @@ The Email authentication provider can only be used if a database is configured.
The Credentials Provider can only be used if JSON Web Tokens are used for sessions.
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explictly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and manged outside of NextAuth.js.
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and managed outside of NextAuth.js.
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
@@ -87,20 +87,20 @@ https://next-auth.js.org/errors#jwt_session_error JWKKeySupport: the key does no
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using
````
```
jose newkey -s 512 -t oct -a HS512
````
```
If you are unable to use an HS512 key (for example to interoperate with other services) you can define what is supported using
````
```
jwt: {
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
verificationOptions: {
algorithms: ["HS256"]
}
}
````
```
#### SESSION_ERROR

View File

@@ -23,13 +23,11 @@ You can use also NextAuth.js with any database using a custom database adapter,
### What authentication services does NextAuth.js support?
<p>NextAuth.js includes built-in support for signing in with&nbsp;
{Object.values(require("../providers.json")).sort().join(", ")}.
(See also: <a href="/configuration/providers">Providers</a>)
</p>
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
You can also use a custom based provider to support signing in with a username and password stored in an external database and/or using two factor authentication.
@@ -58,7 +56,6 @@ NextAuth.js is designed as a secure, confidential client and implements a server
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
### Is NextAuth.js supporting TypeScript?
Yes! Check out the [TypeScript docs](/getting-started/typescript)
@@ -83,9 +80,9 @@ If you are using a database with NextAuth.js, you can still explicitly enable JS
### Should I use a database?
* Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
- Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
* Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
- Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
### What database should I use?
@@ -93,10 +90,9 @@ Managed database solutions for MySQL, Postgres and MongoDB (and compatible datab
If you are deploying directly to a particular cloud platform you may also want to consider serverless database offerings they have (e.g. [Amazon Aurora Serverless on AWS](https://aws.amazon.com/rds/aurora/serverless/)).
---
## Security
## Security
### I think I've found a security problem, what should I do?
@@ -165,14 +161,14 @@ Ultimately if your request is not accepted or is not actively in development, yo
---
## JSON Web Tokens
## JSON Web Tokens
### Does NextAuth.js use JSON Web Tokens?
NextAuth.js supports both database session tokens and JWT session tokens.
* If a database is specified, database session tokens will be used by default.
* If no database is specified, JWT session tokens will be used by default.
- If a database is specified, database session tokens will be used by default.
- If no database is specified, JWT session tokens will be used by default.
You can also choose to use JSON Web Tokens as session tokens with using a database, by explicitly setting the `session: { jwt: true }` option.
@@ -180,33 +176,33 @@ You can also choose to use JSON Web Tokens as session tokens with using a databa
JSON Web Tokens can be used for session tokens, but are also used for lots of other things, such as sending signed objects between services in authentication flows.
* Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
- Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
* JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
- JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
* You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
- You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in an server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
- You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in a server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
### What are the disadvantages of JSON Web Tokens?
* You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
- You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
Shorter session expiry times are used when using JSON Web Tokens as session tokens to allow sessions to be invalidated sooner and simplify this problem.
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
- As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
- Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
Even if appropriately configured, information stored in an encrypted JWT should not be assumed to be impossible to decrypt at some point - e.g. due to the discovery of a defect or advances in technology.
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
* If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
- If you do not explicitly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
@@ -214,11 +210,11 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space available to store data (total cookie size for a domain is limited to 4KB).
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
- JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
- If encryption is enabled by setting `jwt: { encryption: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
NextAuth.js will generate keys for you, but this will generate a warning at start up.
@@ -228,14 +224,14 @@ Using explicit public/private keys for signing is strongly recommended.
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
* [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
* [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
* [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
* [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
* [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
- [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
- [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
- [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
- [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
- [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
This incorporates support for:
* [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
* [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
* [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)
- [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
- [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
- [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)

View File

@@ -22,29 +22,29 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
:::tip
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
You can use the [session callback](/configuration/callbacks#session-callback) to customize the session object returned to the client if you need to return additional data in the session object.
:::
---
## useSession()
* Client Side: **Yes**
* Server Side: No
- Client Side: **Yes**
- Server Side: No
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
It works best when the [`<SessionProvider>`](#sessionprovider) is added to `pages/_app.js`.
#### Example
```jsx
import { useSession } from 'next-auth/client'
import { useSession } from "next-auth/react"
export default function Component() {
const [ session, loading ] = useSession()
const [session, loading] = useSession()
if(session) {
if (session) {
return <p>Signed in as {session.user.email}</p>
}
@@ -56,8 +56,8 @@ export default function Component() {
## getSession()
* Client Side: **Yes**
* Server Side: **Yes**
- Client Side: **Yes**
- Server Side: **Yes**
NextAuth.js provides a `getSession()` method which can be called client or server side to return a session.
@@ -75,7 +75,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getSession } from 'next-auth/client'
import { getSession } from "next-auth/react"
export default async (req, res) => {
const session = await getSession({ req })
@@ -94,8 +94,8 @@ The tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-r
## getCsrfToken()
* Client Side: **Yes**
* Server Side: **Yes**
- Client Side: **Yes**
- Server Side: **Yes**
The `getCsrfToken()` method returns the current Cross Site Request Forgery Token (CSRF Token) required to make POST requests (e.g. for signing in and signing out).
@@ -113,7 +113,7 @@ async function myFunction() {
#### Server Side Example
```js
import { getCsrfToken } from 'next-auth/client'
import { getCsrfToken } from "next-auth/react"
export default async (req, res) => {
const csrfToken = await getCsrfToken({ req })
@@ -126,8 +126,8 @@ export default async (req, res) => {
## getProviders()
* Client Side: **Yes**
* Server Side: **Yes**
- Client Side: **Yes**
- Server Side: **Yes**
The `getProviders()` method returns the list of providers currently configured for sign in.
@@ -140,11 +140,11 @@ It can be useful if you are creating a dynamic custom sign in page.
#### API Route
```jsx title="pages/api/example.js"
import { getProviders } from 'next-auth/client'
import { getProviders } from "next-auth/react"
export default async (req, res) => {
const providers = await getProviders()
console.log('Providers', providers)
console.log("Providers", providers)
res.end()
}
```
@@ -157,8 +157,8 @@ Unlike `getSession()` and `getCsrfToken()`, when calling `getProviders()` server
## signIn()
* Client Side: **Yes**
* Server Side: No
- Client Side: **Yes**
- Server Side: No
Using the `signIn()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF Tokens for you automatically when signing in with email.
@@ -167,20 +167,18 @@ The `signIn()` method can be called from the client in different ways, as shown
#### Redirects to sign in page when clicked
```js
import { signIn } from 'next-auth/client'
import { signIn } from "next-auth/react"
export default () => (
<button onClick={() => signIn()}>Sign in</button>
)
export default () => <button onClick={() => signIn()}>Sign in</button>
```
#### Starts Google OAuth sign-in flow when clicked
```js
import { signIn } from 'next-auth/client'
import { signIn } from "next-auth/react"
export default () => (
<button onClick={() => signIn('google')}>Sign in with Google</button>
<button onClick={() => signIn("google")}>Sign in with Google</button>
)
```
@@ -189,10 +187,10 @@ export default () => (
When using it with the email flow, pass the target `email` as an option.
```js
import { signIn } from 'next-auth/client'
import { signIn } from "next-auth/react"
export default ({ email }) => (
<button onClick={() => signIn('email', { email })}>Sign in with Email</button>
<button onClick={() => signIn("email", { email })}>Sign in with Email</button>
)
```
@@ -204,11 +202,11 @@ You can specify a different `callbackUrl` by specifying it as the second argumen
e.g.
* `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
* `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
* `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
- `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
- `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
- `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -234,8 +232,8 @@ e.g.
error: string | undefined
/**
* HTTP status code,
* hints the kind of error that happened.
*/
* hints the kind of error that happened.
*/
status: number
/**
* `true` if the signin was successful
@@ -258,34 +256,32 @@ See the [Authorization Request OIDC spec](https://openid.net/specs/openid-connec
e.g.
* `signIn("identity-server4", null, { prompt: "login" })` *always ask the user to reauthenticate*
* `signIn("auth0", null, { login_hint: "info@example.com" })` *hints the e-mail address to the provider*
- `signIn("identity-server4", null, { prompt: "login" })` _always ask the user to reauthenticate_
- `signIn("auth0", null, { login_hint: "info@example.com" })` _hints the e-mail address to the provider_
:::note
You can also set these parameters through [`provider.authorizationParams`](/configuration/providers#oauth-provider-options).
:::
:::note
The following parameters are always overridden server-side: `redirect_uri`, `scope`, `state`
The following parameters are always overridden server-side: `redirect_uri`, `state`
:::
---
## signOut()
* Client Side: **Yes**
* Server Side: No
- Client Side: **Yes**
- Server Side: No
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
It reloads the page in the browser when complete.
```js
import { signOut } from 'next-auth/client'
import { signOut } from "next-auth/react"
export default () => (
<button onClick={() => signOut()}>Sign out</button>
)
export default () => <button onClick={() => signOut()}>Sign out</button>
```
#### Specifying a callbackUrl
@@ -294,7 +290,7 @@ As with the `signIn()` function, you can specify a `callbackUrl` parameter by pa
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
#### Using the redirect: false option
@@ -308,25 +304,44 @@ where `data.url` is the validated url you can redirect the user to without any f
---
## Provider
## SessionProvider
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
Using the supplied `<SessionProvider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
import { SessionProvider } from "next-auth/react"
export default function App ({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<Provider session={pageProps.session}>
<SessionProvider session={session}>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}
```
If you pass the `session` page prop to the `<Provider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
If you pass the `session` page prop to the `<SessionProvider>` as in the example above you can avoid checking the session twice on pages that support both server and client side rendering.
This only works on pages where you provide the correct `pageProps`, however. This is normally done in `getInitialProps` or `getServerSideProps` like so:
```js title="pages/index.js"
import { getSession } from "next-auth/react"
...
export async function getServerSideProps(ctx) {
return {
props: {
session: await getSession(ctx)
}
}
}
```
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each auth check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
### Options
@@ -334,21 +349,26 @@ The session state is automatically synchronized across all open tabs/windows and
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
However, if you need to customize the session behavior and/or are using short session expiry times, you can pass options to the provider to customize the behavior of the `useSession()` hook.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
import { SessionProvider } from "next-auth/react"
export default function App ({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<Provider session={pageProps.session}
options={{
clientMaxAge: 60 // Re-fetch session if cache is older than 60 seconds
keepAlive: 5 * 60 // Send keepAlive message every 5 minutes
}}
>
<SessionProvider
session={session}
// Re-fetch session if cache is older than 60 seconds
staleTime={60}
// Re-fetch session every 5 minutes
refetchInterval={5 * 60}
>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}
```
@@ -358,31 +378,105 @@ export default function App ({ Component, pageProps }) {
Every tab/window maintains its own copy of the local session state; the session is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
Using low values for `staleTime` or `refetchInterval` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
:::
#### Client Max Age
#### Stale time
The `clientMaxAge` option is the maximum age a session data can be on the client before it is considered stale.
The `staleTime` option is the maximum age a session data can be on the client before it is considered stale.
When `clientMaxAge` is set to `0` (the default) the cache will always be used when useSession is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
When `staleTime` is set to `0` (the default) the cache will always be used when `useSession` is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
If set to any value other than zero, it specifies in seconds the maxium age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
If set to any value other than zero, it specifies in seconds the maximum age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
The value for `clientMaxAge` should always be lower than the value of the session `maxAge` option.
The value for `staleTime` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
#### Keep Alive
#### Refetch interval
The `keepAlive` option is how often the client should contact the server to avoid a session expiring.
The `refetchInterval` option can be used to contact the server to avoid a session expiring.
When `keepAlive` is set to `0` (the default) it will not send a keep alive message.
When `refetchInterval` is set to `0` (the default) there will be no session polling.
If set to any value other than zero, it specifies in seconds how often the client should contact the server to update the session state. If the session state has expired when it is triggered, all open tabs/windows will be updated to reflect this.
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
The value for `refetchInterval` should always be lower than the value of the session `maxAge` [session option](/configuration/options#session).
:::note
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **\_app.js** in Next.js applications.
:::
## Alternatives
### Custom Client Session Handling
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side query to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
```js title="pages/admin.jsx"
export default function AdminDashboard() {
const [session] = useSession()
// session is always non-null inside this page, all the way down the React tree.
return "Some super secret dashboard"
}
AdminDashboard.auth = true
```
```jsx title="pages/_app.jsx"
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
)
}
function Auth({ children }) {
const [session, loading] = useSession()
const isUser = !!session?.user
React.useEffect(() => {
if (loading) return // Do nothing while loading
if (!isUser) signIn() // If not authenticated, force log in
}, [isUser, loading])
if (isUser) {
return children
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <div>Loading...</div>
}
```
It can be easily be extended/modified to support something like an options object for role based authentication on pages. An example:
```jsx title="pages/admin.jsx"
AdminDashboard.auth = {
role: "admin",
loading: <AdminLoadingSkeleton />,
unauthorized: "/login-with-different-user", // redirect to this url
}
```
Because of how \_app is done, it won't unnecessarily contact the /api/auth/session endpoint for pages that do not require auth.
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
### NextAuth.js + React-Query
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `SessionProvider` from `next-auth/react`.
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.

View File

@@ -5,10 +5,10 @@ title: Example Code
## Get started with NextAuth.js
The example code below describes to add authentication to a Next.js app.
The example code below describes how to add authentication to a Next.js app.
:::tip
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.now.sh](https://next-auth-example.now.sh)
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.vercel.app](https://next-auth-example.vercel.app)
:::
### Add API route
@@ -18,21 +18,18 @@ To add NextAuth.js to a project create a file called `[...nextauth].js` in `page
[Read more about how to add authentication providers.](/configuration/providers)
```javascript title="pages/api/auth/[...nextauth].js"
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({
// Configure one or more authentication providers
providers: [
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET
clientSecret: process.env.GITHUB_SECRET,
}),
// ...add more providers here
],
// A database is optional, but required to persist accounts in a database
database: process.env.DATABASE_URL,
})
```
@@ -47,21 +44,27 @@ See the [options documentation](/configuration/options) for how to configure pro
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```jsx title="pages/index.js"
import { signIn, signOut, useSession } from 'next-auth/client'
import { signIn, signOut, useSession } from "next-auth/react"
export default function Page() {
const [ session, loading ] = useSession()
const [session, loading] = useSession()
return <>
{!session && <>
Not signed in <br/>
<button onClick={() => signIn()}>Sign in</button>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>}
</>
return (
<>
{!session && (
<>
Not signed in <br />
<button onClick={() => signIn()}>Sign in</button>
</>
)}
{session && (
<>
Signed in as {session.user.email} <br />
<button onClick={() => signOut()}>Sign out</button>
</>
)}
</>
)
}
```
@@ -74,13 +77,16 @@ You can use the `useSession` hook from anywhere in your application (e.g. in a h
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `pages/_app.js`.
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
import { SessionProvider } from "next-auth/react"
export default function App ({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<Provider session={pageProps.session}>
<SessionProvider session={session}>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}
```

View File

@@ -3,7 +3,7 @@ id: typescript
title: TypeScript
---
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about how certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
https://github.com/nextauthjs/next-auth-typescript-example
@@ -72,7 +72,7 @@ export default NextAuth({
```
```ts title="pages/index.ts"
import { useSession } from "next-auth/client"
import { useSession } from "next-auth/react"
export default function IndexPage() {
// `session` should match `callbacks.session()` in `NextAuth()`
@@ -91,7 +91,7 @@ import NextAuth from "next-auth"
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
interface Session {
user: {

View File

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

View File

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

View File

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

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