Compare commits

...

41 Commits

Author SHA1 Message Date
Balázs Orbán
a2e5afa162 feat(react): make session requireable in useSession (#2236)
A living session could be a requirement for specific pages (like dashboards). If it doesn’t exist, the user should be redirected to a page asking them to sign in again.

Sometimes, a user might log out by accident, or by deleting cookies on purpose. If that happens (e.g. on a separate tab), then `useSession({ required: true })` should detect the absence of a session cookie and always return a non-nullable Session object type.

When `required: true` is set, the default behavior will be to redirect the user to the sign-in page. This can be overridden by an `action()` callback:

```js
const session = useSession({
  required: true,
  action() {
    // ....
  }
})
if (session.status === "Loading") return "Loading or not authenticated..."

// session.data is always defined here.
```

Co-authored-by: Kristóf Poduszló <kripod@protonmail.com>
Co-authored-by: Lluis Agusti <hi@llu.lu>

BREAKING CHANGE:

The `useSession` hook now returns an object. Here is how to accommodate for this change:

```diff
- const [ session, loading ] = useSession()
+ const { data: session, status } = useSession()
+ const loading = status === "loading"
```

With the new `status` option, you can test states much more clearly.
2021-07-05 16:03:55 +02:00
Nico Domino
53e5e37948 docs: update tutorials/faq structure (#2256)
* docs: cleanup css

* docs: upgrade deps

* docs: cleanup docusaurus config

* docs: reorganise tutorials page

* docs: fix github-counter css

* docs: update faq page structure

* docs(tutorials): copy

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

* docs(tutorials): copy

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

* docs(tutorials): copy

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

* docs(tutorials): copy

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

* docs(style): reorg css

Co-authored-by: Lluis Agusti <hi@llu.lu>
2021-07-05 11:15:45 +02:00
Balázs Orbán
8ff4b26014 Merge main into next 2021-07-02 13:10:34 +02:00
Balázs Orbán
2c35aa27f9 Merge main into next 2021-06-29 22:55:33 +02:00
Balázs Orbán
2833b661bd feat(core): use named params in callbacks (#2173)
Some of our user-facing callbacks come with a bunch of parameters, and it is not always the case that a user needs all of them. Picking out certain parameters from the end of the list would require the user to define params that they wouldn't even need.

Therefore this PR changes such callbacks so the user can only pick the necessary parameters.

This comes with the bonus of better TS support on the `session` and `signIn` callbacks, where some parameters historically could have been different types.

In the `session` callback, the second param could have been `token` (when using JWT sessions) or `user` (when using DB persisted sessions). Now they are separate parameters.

In the `signIn` callback, we now separate `profile` (OAuth), `email` (Email) and `credentials` (Credentials) provider params.

BREAKING CHANGE:

The `callbacks` method signatures are changing the following way:

```diff
- signIn(user, account, profileOrEmailOrCredentials)
+ signIn({ user, account, profile, email, credentials })
```
```diff
- redirect(url, baseUrl)
+ redirect({ url, baseUrl })
```
```diff
- session(session, tokenOrUser)
+ session({ session, token, user })
```
```diff
- jwt(token, user, account, OAuthProfile, isNewUser)
+ jwt({ token, user, account, profile, isNewUser })
```

> NOTE: You only need to define the params that you actually need (no more need  for `_` params.)

This way, if you only need `token` and `account` in the `jwt` callback, you can write:

```js
jwt({ token, account }) {
  if(account) {
    token.accessToken = account.access_token
  }
  return token
}
```
2021-06-26 14:54:13 +02:00
Balázs Orbán
6c1a0ec620 feat: bump dependencies (#2254)
Node 10 has reached end-of-life, Next.js stopped supporting it in Next 11. Since we are a Next.js library, it doesn't make sense for us to support anything lower than that either.

I also upgraded a bunch of dependencies and dropped some that weren't necessary anymore.

BREAKING CHANGE:

The lowest supported Node version is 12. (We still support IE11 in browsers, until that is not dropped by Next.js itself)
2021-06-26 11:39:18 +02:00
Balázs Orbán
988c9912b1 test(ts): fix ts tests 2021-06-26 00:32:35 +02:00
Balázs Orbán
a225324d4f Merge main into next 2021-06-25 22:10:09 +02:00
Nico Domino
3a48b8e467 docs: update errors page with more details (#2196) 2021-06-22 20:15:17 +02:00
Lluis Agusti
fb50b54466 test(client-provider): fix flaky test (#2216) 2021-06-20 20:04:34 +02:00
Sheldon Vaughn
fa89431573 docs(provider): import useState in WorkOS example (#2198)
Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Add useState dependency

I went to execute the signin.js file and received an error that useState was undefined. Was able to reconcile this by adding the dependency.

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Sangwon Park <dev.psw@gmail.com>
2021-06-17 01:23:50 +02:00
Balázs Orbán
3383857715 Merge branch 'main' into next 2021-06-17 01:21:49 +02:00
David Peherstorfer
bbc2d9b538 docs(provider): scope expects space separated string (#2188)
* fix(docs): scope expects space separated string

Currently the docs list string[] as possible type for scope. 
However, It only accepts a string (with space as separator).

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-16 07:38:39 +02:00
Hugh Boylan
d10bd9beba fix(react): publish react scripts with npm build (#2192) 2021-06-15 23:27:06 +02:00
Vikrant Bhat
c1c866f664 docs(provider): English language sentence fix (#2175)
* English language sentence fix

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2021-06-14 23:55:10 +02:00
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
Tom Richter
ea9b6e37a9 fix(provider): convert github profile id from int to string (#2108) 2021-06-09 17:02:52 +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
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
207 changed files with 24633 additions and 27030 deletions

1
.github/CODEOWNERS vendored
View File

@@ -1 +1,2 @@
/types/ @balazsorban44 @lluia
/__tests__/ @lluia

View File

@@ -28,10 +28,10 @@ See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen rec
## Environment 🖥
Please run this command:
Please run this command in your project's root folder:
```
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
```sh
npx envinfo --system --binaries --browsers --npmPackages "next,next-auth,react"
```
and paste the output here.

View File

@@ -1,4 +1,4 @@
name: Release Flow
name: Release
on:
push:
@@ -11,37 +11,66 @@ on:
jobs:
test:
name: Tests
name: Test
runs-on: ubuntu-latest
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Build
run: npm run build
- name: Run tests
run: npm test -- --coverage --verbose
run: npm test -- --coverage --verbose && npm run test:types
- 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
release-branch:
name: Publish branch
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'push' }}
environment: Production
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Release
- name: Publish to npm and GitHub
run: npx semantic-release@17
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
release-pr:
name: Publish PR
runs-on: ubuntu-latest
needs: test
if: ${{ github.event_name == 'pull_request' }}
environment: Preview
steps:
- name: Init
uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: 16
- name: Dependencies
uses: bahmutov/npm-install@v1
- name: Publish to npm
run: |
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc
npm run version:pr
npm publish --access public --tag experimental
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PR_NUMBER: ${{ github.event.number }}

4
.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
@@ -61,4 +63,4 @@ app/yarn.lock
/prisma/migrations
# Tests
/coverage
/coverage

View File

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

View File

@@ -11,43 +11,49 @@ 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:
The dev application requires you to use `npm@7`.
1. Clone the repo:
```sh
git clone git@github.com:nextauthjs/next-auth.git
cd next-auth
```
2. Install packages:
2. Install packages, set up the dev application:
```sh
npm i && npm run dev:setup
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 +70,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)
@@ -73,40 +80,27 @@ You can look at the existing built-in providers for inspiration.
#### Databases
Included is a Docker Compose file that starts up MySQL, PostgreSQL, and MongoDB databases on localhost.
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
You will need Docker and Docker Compose installed to be able to start / stop the databases.
When stopping the databases, it will reset their contents.
If you would like to contribute to an existing database adapter or help create a new one, head over to the [nextauthjs/adapters](https://www.github.com/nextauthjs/adapters) repository and follow the instructions provided there.
#### Testing
Tests can be run with `npm run test`.
Automated tests are currently crude and limited in functionality, but improvements are in development.
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
## 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 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
@@ -121,9 +115,8 @@ Some recommended scopes are:
- **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

110
README.md
View File

@@ -38,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.
@@ -48,36 +48,36 @@ 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 practices for safeguarding user data
- Uses Cross-Site Request Forgery (CSRF) 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 session polling 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
@@ -90,54 +90,76 @@ The package at `@types/next-auth` is now deprecated.
### Add API Route
```javascript
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
// pages/api/auth/[...nextauth].js
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
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
```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 { data: session } = 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>
</>
)
}
```
## Acknowledgements
### Share/configure session state
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
```jsx title="pages/_app.js"
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps }
}) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)
}
```
## Acknowledgments
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)

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 publicly.
- 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

@@ -23,8 +23,6 @@ TWITTER_SECRET=
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
EMAIL_FROM=user@gmail.com
# You can use any of these as the "DATABASE_URL" for
# databases started with Docker using `npm run db:start`.
# Note: If using with Prisma adapter, you need to use a `.env`
# file rather than a `.env.local` file to configure env vars.
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true

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,29 @@
import Link from 'next/link'
import styles from './footer.module.css'
import { version } from 'package.json'
import Link from "next/link"
import styles from "./footer.module.css"
import packageJSON from "package.json"
export default function Footer () {
export default function Footer() {
return (
<footer className={styles.footer}>
<hr />
<ul className={styles.navItems}>
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
<li className={styles.navItem}><em>{version}</em></li>
<li className={styles.navItem}>
<a href="https://next-auth.js.org">Documentation</a>
</li>
<li className={styles.navItem}>
<a href="https://www.npmjs.com/package/next-auth">NPM</a>
</li>
<li className={styles.navItem}>
<a href="https://github.com/nextauthjs/next-auth-example">GitHub</a>
</li>
<li className={styles.navItem}>
<Link href="/policy">
<a>Policy</a>
</Link>
</li>
<li className={styles.navItem}>
<em>{packageJSON.version}</em>
</li>
</ul>
</footer>
)

View File

@@ -1,22 +1,22 @@
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 () {
const [session, loading] = useSession()
export default function Header() {
const { data: session, status } = 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
className={`nojs-show ${
!session && loading ? styles.loading : styles.loaded
!session && status === "loading" ? styles.loading : styles.loaded
}`}
>
{!session && (
@@ -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

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

View File

@@ -1,31 +1,13 @@
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
// `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
// 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}
>
<SessionProvider session={session}>
<Component {...pageProps} />
</Provider>
</SessionProvider>
)
}

View File

@@ -5,10 +5,6 @@ import Auth0Provider from "next-auth/providers/auth0"
import TwitterProvider from "next-auth/providers/twitter"
import CredentialsProvider from "next-auth/providers/credentials"
// import Adapters from 'next-auth/adapters'
// import { PrismaClient } from '@prisma/client'
// const prisma = new PrismaClient()
export default NextAuth({
// Used to debug https://github.com/nextauthjs/next-auth/issues/1664
// cookies: {
@@ -79,13 +75,4 @@ export default NextAuth({
},
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 })
})

View File

@@ -1,5 +1,5 @@
// 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

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)
}
@@ -21,18 +21,22 @@ export default function Page () {
setResponse(response)
}
const [session] = useSession()
const { data: session } = useSession()
if (session) {
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)
}
@@ -29,18 +29,22 @@ export default function Page () {
setResponse(response)
}
const [session] = useSession()
const { data: session } = useSession()
if (session) {
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,35 @@
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"
export default function Page () {
const [session, loading] = useSession()
export default function Page() {
const { status } = useSession({
required: true,
})
const [content, setContent] = useState()
// Fetch content from protected route
useEffect(() => {
if (status === "loading") return
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])
}, [status])
// When rendering client side don't display anything until loading is complete
if (typeof window !== 'undefined' && loading) return null
// If no session exists, display access denied message
if (!session) { return <Layout><AccessDenied /></Layout> }
if (status === "loading") return <Layout>Loading...</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

@@ -3,7 +3,7 @@
// https://nextjs.org/docs/basic-features/supported-browsers-features
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
presets: [["@babel/preset-env", { targets: { node: "12" } }]],
plugins: [
"@babel/plugin-proposal-optional-catch-binding",
"@babel/plugin-transform-runtime",
@@ -12,7 +12,10 @@ module.exports = {
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/**"],
@@ -20,14 +23,7 @@ module.exports = {
},
{
test: ["../src/**/*.test.js"],
presets: [
[
"@babel/preset-react",
{
runtime: "automatic",
},
],
],
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`)

View File

@@ -8,4 +8,5 @@ module.exports = {
collectCoverageFrom: ["!client/__tests__/**"],
testMatch: ["**/*.test.js"],
coverageDirectory: "../coverage",
testEnvironment: "jsdom",
}

17
config/version-pr.js Normal file
View File

@@ -0,0 +1,17 @@
const fs = require("fs-extra")
const path = require("path")
try {
const packageJSONPath = path.join(process.cwd(), "package.json")
const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, "utf8"))
const sha8 = process.env.GITHUB_SHA.substr(0, 8)
const prNumber = process.env.PR_NUMBER
packageJSON.version = `0.0.0-pr.${prNumber}.${sha8}`
fs.writeFileSync(packageJSONPath, JSON.stringify(packageJSON))
} catch (error) {
console.error("Could not set PR version", error)
process.exit(1)
}

34605
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"
@@ -32,7 +31,7 @@
"build": "npm run build:js && npm run build:css",
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
"dev:setup": "npm run build:css && cd app && npm i",
"dev:setup": "npm i && npm run build:css && cd app && npm i",
"dev": "cd app && npm run dev",
"watch": "npm run watch:js | npm run watch:css",
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
@@ -42,7 +41,9 @@
"test:types": "dtslint types --onlyTestTsNext",
"prepublishOnly": "npm run build",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
"lint:fix": "eslint . --fix",
"version:pr": "node ./config/version-pr",
"website": "cd www && npm run start"
},
"files": [
"dist",
@@ -52,8 +53,8 @@
"providers.d.ts",
"adapters.js",
"adapters.d.ts",
"client.js",
"client.d.ts",
"react.js",
"react.d.ts",
"errors.js",
"errors.d.ts",
"jwt.js",
@@ -62,73 +63,67 @@
],
"license": "ISC",
"dependencies": {
"@babel/runtime": "^7.14.0",
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
"futoin-hkdf": "^1.3.2",
"@babel/runtime": "^7.14.6",
"futoin-hkdf": "^1.3.3",
"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"
"pkce-challenge": "^2.2.0",
"preact": "^10.5.13",
"preact-render-to-string": "^5.1.19"
},
"peerDependencies": {
"react": "^16.13.1 || ^17",
"react-dom": "^16.13.1 || ^17"
"nodemailer": "^6.6.2",
"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.6.2"
},
"peerDependenciesMeta": {
"nodemailer": {
"optional": true
}
},
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/core": "^7.9.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.9.6",
"@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",
"@babel/cli": "^7.14.5",
"@babel/core": "^7.14.6",
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.7",
"@babel/preset-react": "^7.14.5",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@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",
"@types/nodemailer": "^6.4.2",
"@types/react": "^17.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"autoprefixer": "^10.2.6",
"babel-jest": "^27.0.5",
"babel-preset-preact": "^2.0.0",
"conventional-changelog-conventionalcommits": "4.4.0",
"cssnano": "^4.1.10",
"dotenv": "^8.2.0",
"dtslint": "^4.0.8",
"eslint": "^7.19.0",
"eslint-config-prettier": "^8.2.0",
"eslint-config-standard-with-typescript": "^19.0.1",
"eslint-plugin-import": "^2.22.1",
"conventional-changelog-conventionalcommits": "4.6.0",
"cssnano": "^5.0.6",
"dtslint": "^4.1.0",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard-with-typescript": "^20.0.0",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-promise": "^5.1.0",
"fs-extra": "^10.0.0",
"husky": "^6.0.0",
"jest": "^26.6.3",
"msw": "^0.28.2",
"next": "^10.0.5",
"postcss-cli": "^7.1.1",
"postcss-nested": "^4.2.1",
"prettier": "^2.2.1",
"pretty-quick": "^3.1.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"typescript": "^4.1.3",
"jest": "^27.0.5",
"msw": "^0.30.0",
"next": "^11.0.1",
"postcss-cli": "^8.3.1",
"postcss-nested": "^5.0.5",
"prettier": "^2.3.1",
"pretty-quick": "^3.1.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"typescript": "^4.3.4",
"whatwg-fetch": "^3.6.2"
},
"prettier": {

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,7 @@
import { useState } from "react"
import { rest } from "msw"
import { render, screen, waitFor } from "@testing-library/react"
import { server, mockSession } from "./helpers/mocks"
import { Provider, useSession } from ".."
import userEvent from "@testing-library/user-event"
import { SessionProvider, useSession } from "../react"
beforeAll(() => {
server.listen()
@@ -18,6 +16,22 @@ afterAll(() => {
server.close()
})
test("it won't allow to fetch the session in isolation without a session context", () => {
function App() {
useSession()
return null
}
jest.spyOn(console, "error")
console.error.mockImplementation(() => {})
expect(() => render(<App />)).toThrow(
"useSession must be wrapped in a SessionProvider"
)
console.error.mockRestore()
})
test("fetches the session once and re-uses it for different consumers", async () => {
const sessionRouteCall = jest.fn()
@@ -30,6 +44,9 @@ test("fetches the session once and re-uses it for different consumers", async ()
render(<ProviderFlow />)
expect(screen.getByTestId("session-consumer-1")).toHaveTextContent("loading")
expect(screen.getByTestId("session-consumer-2")).toHaveTextContent("loading")
await waitFor(() => {
expect(sessionRouteCall).toHaveBeenCalledTimes(1)
@@ -40,25 +57,44 @@ test("fetches the session once and re-uses it for different consumers", async ()
})
})
test("when there's an existing session, it won't initialize as loading", 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 session={mockSession} />)
expect(await screen.findByTestId("session-consumer-1")).not.toHaveTextContent(
"loading"
)
expect(screen.getByTestId("session-consumer-2")).not.toHaveTextContent(
"loading"
)
expect(sessionRouteCall).not.toHaveBeenCalled()
})
function ProviderFlow({ options = {} }) {
return (
<>
<Provider options={options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</Provider>
</>
<SessionProvider {...options}>
<SessionConsumer />
<SessionConsumer testId="2" />
</SessionProvider>
)
}
function SessionConsumer({ testId = 1 }) {
const [session, loading] = useSession()
if (loading) return <span>loading</span>
const { data: session, status } = useSession()
return (
<div data-testid={`session-consumer-${testId}`}>
{JSON.stringify(session)}
{status === "loading" ? "loading" : JSON.stringify(session)}
</div>
)
}

View File

@@ -3,7 +3,7 @@ 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 ".."
import { getCsrfToken } from "../react"
import { rest } from "msw"
jest.mock("../../lib/logger", () => ({

View File

@@ -2,7 +2,7 @@ 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 ".."
import { getProviders } from "../react"
import logger from "../../lib/logger"
import { rest } from "msw"

View File

@@ -3,7 +3,7 @@ import { rest } from "msw"
import { server, mockSession } from "./helpers/mocks"
import logger from "../../lib/logger"
import { useState, useEffect } from "react"
import { getSession } from ".."
import { getSession } from "../react"
import { getBroadcastEvents } from "./helpers/utils"
jest.mock("../../lib/logger", () => ({

View File

@@ -8,7 +8,7 @@ import {
mockEmailResponse,
mockGithubResponse,
} from "./helpers/mocks"
import { signIn } from ".."
import { signIn } from "../react"
import { rest } from "msw"
const { location } = window

View File

@@ -2,7 +2,7 @@ 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 ".."
import { signOut } from "../react"
import { rest } from "msw"
import { getBroadcastEvents } from "./helpers/utils"

View File

@@ -1,418 +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 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?callbackUrl=${encodeURIComponent(callbackUrl)}`
)
}
const isCredentials = providers[provider].type === "credentials"
const isEmail = providers[provider].type === "email"
const isSupportingReturn = isCredentials || isEmail
const signInUrl = isCredentials
? `${baseUrl}/callback/${provider}`
: `${baseUrl}/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
}
// 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()
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/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,
}

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

@@ -0,0 +1,387 @@
// 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(options = {}) {
const value = React.useContext(SessionContext)
if (process.env.NODE_ENV !== "production" && !value) {
throw new Error("useSession must be wrapped in a SessionProvider")
}
const { required, onUnauthenticated } = options
const requiredAndNotLoading = required && value.status === "unauthenticated"
React.useEffect(() => {
if (requiredAndNotLoading) {
const url = `/api/auth/signin?${new URLSearchParams({
error: "SessionRequired",
callbackUrl: window.location.href,
})}`
if (onUnauthenticated) onUnauthenticated()
else window.location.replace(url)
}
}, [requiredAndNotLoading, onUnauthenticated])
if (requiredAndNotLoading) {
return { data: value.data, status: "loading" }
}
return value
}
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(
() => ({
data: session,
status: loading
? "loading"
: session
? "authenticated"
: "unauthenticated",
}),
[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
}
}

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

18
src/providers/naver.js Normal file
View File

@@ -0,0 +1,18 @@
export default function Naver(options) {
return {
id: "naver",
name: "Naver",
type: "oauth",
version: "2.0",
params: { grant_type: "authorization_code" },
protection: ["state"],
accessTokenUrl: "https://nid.naver.com/oauth2.0/token",
authorizationUrl:
"https://nid.naver.com/oauth2.0/authorize?response_type=code",
profileUrl: "https://openapi.naver.com/v1/nid/me",
profile(profile) {
return profile.response
},
...options,
}
}

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|png|gif)$/, ".$1"),
image: profile.profile_image_url_https.replace(
/_normal\.(jpg|png|gif)$/,
".$1"
),
}
},
...options,

View File

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

View File

@@ -1,4 +1,3 @@
import adapters from "../adapters"
import jwt from "../lib/jwt"
import parseUrl from "../lib/parse-url"
import logger, { setLogger } from "../lib/logger"
@@ -102,13 +101,6 @@ async function NextAuthHandler(req, res, userOptions) {
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 = {
@@ -119,7 +111,6 @@ async function NextAuthHandler(req, res, userOptions) {
...userOptions,
// These computed settings can have values in userOptions but we override them
// and are request-specific.
adapter,
baseUrl,
basePath,
action,
@@ -129,7 +120,7 @@ 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,
@@ -219,6 +210,7 @@ async function NextAuthHandler(req, res, userOptions) {
"OAuthAccountNotLinked",
"EmailSignin",
"CredentialsSignin",
"SessionRequired",
].includes(error)
) {
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)

View File

@@ -16,7 +16,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
* @param {import("types").Session} sessionToken
* @param {import("types").Profile} profile
* @param {import("types").Account} account
* @param {import("types/internals").AppOptions} options
* @param {import("types/internals").InternalOptions} options
*/
export default async function callbackHandler(
sessionToken,

View File

@@ -1,32 +1,42 @@
import * as cookie from '../lib/cookie'
// @ts-check
import * as cookie from "../lib/cookie"
/**
* Get callback URL based on query param / cookie + validation,
* and add it to `req.options.callbackUrl`.
* @note: `req.options` must already be defined when called.
* @type {import("types/internals").NextAuthApiHandler}
*/
export default async function callbackUrlHandler (req, res) {
export default async function callbackUrlHandler(req, res) {
const { query } = req
const { body } = req
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
const { cookies, baseUrl, callbacks } = req.options
// Handle preserving and validating callback URLs
// If no defaultCallbackUrl option specified, default to the homepage for the site
let callbackUrl = defaultCallbackUrl || baseUrl
let callbackUrl = baseUrl
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
if (callbackUrlParamValue) {
// If callbackUrl form field or query parameter is passed try to use it if allowed
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
callbackUrl = await callbacks.redirect({
url: callbackUrlParamValue,
baseUrl,
})
} else if (callbackUrlCookieValue) {
// If no callbackUrl specified, try using the value from the cookie if allowed
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
callbackUrl = await callbacks.redirect({
url: callbackUrlCookieValue,
baseUrl,
})
}
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
// Save callback URL in a cookie so that it can be used for subsequent requests in signin/signout/callback flow
if (callbackUrl && callbackUrl !== callbackUrlCookieValue) {
cookie.set(
res,
cookies.callbackUrl.name,
callbackUrl,
cookies.callbackUrl.options
)
}
req.options.callbackUrl = callbackUrl

View File

@@ -1,64 +1,24 @@
/**
* Use the signIn callback to control if a user is allowed to sign in or not.
*
* This is triggered before sign in flow completes, so the user profile may be
* a user object (with an ID) or it may be just their name and email address,
* depending on the sign in flow and if they have an account already.
*
* When using email sign in, this method is triggered both when the user
* requests to sign in and again when they activate the link in the sign in
* email.
*
* @param {object} profile User profile (e.g. user id, name, email)
* @param {object} account Account used to sign in (e.g. OAuth account)
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
* Return `false` to deny access
*/
export async function signIn () {
// @ts-check
/** @type {import("types").CallbacksOptions["signIn"]} */
export function signIn() {
return true
}
/**
* Redirect is called anytime the user is redirected on signin or signout.
* By default, for security, only Callback URLs on the same URL as the site
* are allowed, you can use this callback to customise that behaviour.
*
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {Promise<string>} URL the client will be redirect to
*/
export async function redirect (url, baseUrl) {
/** @type {import("types").CallbacksOptions["redirect"]} */
export function redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) {
return url
}
return baseUrl
}
/**
* The session callback is called whenever a session is checked.
* e.g. `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* @param {object} session Session object
* @param {object} token JSON Web Token (if enabled)
* @return {Promise<object>} Session that will be returned to the client
*/
export async function session (session) {
/** @type {import("types").CallbacksOptions["session"]} */
export function session({ session }) {
return session
}
/**
* This callback is called whenever a JSON Web Token is created / updated.
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
*
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
* with an OAuth provider. It is not avalible on subsequent calls. You can
* take advantage of this to persist additional data you need to in the JWT.
*
* @param {object} token Decrypted JSON Web Token
* @param {object} oAuthProfile OAuth profile - only available on sign in
* @return {Promise<object>} JSON Web Token that will be saved
*/
export async function jwt (token) {
/** @type {import("types").CallbacksOptions["jwt"]} */
export function jwt({ token }) {
return token
}

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

@@ -5,7 +5,7 @@ import adapterErrorHandler from "../../../adapters/error-handler"
*
* @param {string} email
* @param {import("types/providers").EmailConfig} provider
* @param {import("types/internals").AppOptions} options
* @param {import("types/internals").InternalOptions} options
* @returns
*/
export default async function email(email, provider, options) {

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
redirect_uri: provider.callbackUrl,
})
// If the authorizationUrl specified in the config has query parameters on it
@@ -27,13 +27,13 @@ export default async function getAuthorizationUrl (req) {
// which inadvertantly strips them.
//
// https://github.com/ciaranj/node-oauth/pull/193
if (provider.authorizationUrl.includes('?')) {
if (provider.authorizationUrl.includes("?")) {
const parseUrl = new URL(provider.authorizationUrl)
const baseUrl = `${parseUrl.origin}${parseUrl.pathname}?`
url = url.replace(baseUrl, provider.authorizationUrl + '&')
url = url.replace(baseUrl, provider.authorizationUrl + "&")
}
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
}
@@ -42,12 +42,12 @@ export default async function getAuthorizationUrl (req) {
const url = `${provider.authorizationUrl}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params
...tokens.params,
})}`
logger.debug('GET_AUTHORIZATION_URL', url)
logger.debug("GET_AUTHORIZATION_URL", url)
return url
} catch (error) {
logger.error('GET_AUTHORIZATION_URL_ERROR', error)
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
throw error
}
}

View File

@@ -1,12 +1,18 @@
import { h } from 'preact' // eslint-disable-line no-unused-vars
import { h } from "preact" // eslint-disable-line no-unused-vars
export default function signin ({ csrfToken, providers, callbackUrl, email, error: errorType }) {
export default function signin({
csrfToken,
providers,
callbackUrl,
email,
error: errorType,
}) {
// We only want to render providers
const providersToRender = providers.filter(provider => {
if (provider.type === 'oauth' || provider.type === 'email') {
const providersToRender = providers.filter((provider) => {
if (provider.type === "oauth" || provider.type === "email") {
// Always render oauth and email type providers
return true
} else if (provider.type === 'credentials' && provider.credentials) {
} else if (provider.type === "credentials" && provider.credentials) {
// Only render credentials type provider if credentials are defined
return true
}
@@ -15,70 +21,94 @@ export default function signin ({ csrfToken, providers, callbackUrl, email, erro
})
const errors = {
Signin: 'Try signing with a different account.',
OAuthSignin: 'Try signing with a different account.',
OAuthCallback: 'Try signing with a different account.',
OAuthCreateAccount: 'Try signing with a different account.',
EmailCreateAccount: 'Try signing with a different account.',
Callback: 'Try signing with a different account.',
OAuthAccountNotLinked: 'To confirm your identity, sign in with the same account you used originally.',
EmailSignin: 'Check your email address.',
CredentialsSignin: 'Sign in failed. Check the details you provided are correct.',
default: 'Unable to sign in.'
Signin: "Try signing in with a different account.",
OAuthSignin: "Try signing in with a different account.",
OAuthCallback: "Try signing in with a different account.",
OAuthCreateAccount: "Try signing in with a different account.",
EmailCreateAccount: "Try signing in with a different account.",
Callback: "Try signing in with a different account.",
OAuthAccountNotLinked:
"To confirm your identity, sign in with the same account you used originally.",
EmailSignin: "Check your email inbox.",
CredentialsSignin:
"Sign in failed. Check the details you provided are correct.",
SessionRequired: "Please sign in to access this page.",
default: "Unable to sign in.",
}
const error = errorType && (errors[errorType] ?? errors.default)
return (
<div className='signin'>
{error &&
<div className='error'>
<div className="signin">
{error && (
<div className="error">
<p>{error}</p>
</div>}
{providersToRender.map((provider, i) =>
<div key={provider.id} className='provider'>
{provider.type === 'oauth' &&
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
<button type='submit' className='button'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
<hr />}
{provider.type === 'email' &&
<form action={provider.signinUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
<label for={`input-email-for-${provider.id}-provider`}>Email</label>
<input id={`input-email-for-${provider.id}-provider`} autoFocus type='text' name='email' value={email} placeholder='email@example.com' />
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{provider.type === 'credentials' &&
<form action={provider.callbackUrl} method='POST'>
<input type='hidden' name='csrfToken' value={csrfToken} />
{Object.keys(provider.credentials).map(credential => {
</div>
)}
{providersToRender.map((provider, i) => (
<div key={provider.id} className="provider">
{provider.type === "oauth" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{callbackUrl && (
<input type="hidden" name="callbackUrl" value={callbackUrl} />
)}
<button type="submit" className="button">
Sign in with {provider.name}
</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i > 0 &&
providersToRender[i - 1].type !== "email" &&
providersToRender[i - 1].type !== "credentials" && <hr />}
{provider.type === "email" && (
<form action={provider.signinUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
<label for={`input-email-for-${provider.id}-provider`}>
Email
</label>
<input
id={`input-email-for-${provider.id}-provider`}
autoFocus
type="text"
name="email"
value={email}
placeholder="email@example.com"
/>
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{provider.type === "credentials" && (
<form action={provider.callbackUrl} method="POST">
<input type="hidden" name="csrfToken" value={csrfToken} />
{Object.keys(provider.credentials).map((credential) => {
return (
<div key={`input-group-${provider.id}`}>
<label
for={`input-${credential}-for-${provider.id}-provider`}
>{provider.credentials[credential].label || credential}
>
{provider.credentials[credential].label || credential}
</label>
<input
name={credential}
id={`input-${credential}-for-${provider.id}-provider`}
type={provider.credentials[credential].type || 'text'}
value={provider.credentials[credential].value || ''}
placeholder={provider.credentials[credential].placeholder || ''}
type={provider.credentials[credential].type || "text"}
value={provider.credentials[credential].value || ""}
placeholder={
provider.credentials[credential].placeholder || ""
}
/>
</div>
)
})}
<button type='submit'>Sign in with {provider.name}</button>
</form>}
{(provider.type === 'email' || provider.type === 'credentials') && ((i + 1) < providersToRender.length) &&
<hr />}
<button type="submit">Sign in with {provider.name}</button>
</form>
)}
{(provider.type === "email" || provider.type === "credentials") &&
i + 1 < providersToRender.length && <hr />}
</div>
)}
))}
</div>
)
}

View File

@@ -6,8 +6,7 @@ import adapterErrorHandler from "../../adapters/error-handler"
/**
* Handle callbacks from login services
* @param {import("types/internals").NextAuthRequest} req
* @param {import("types/internals").NextAuthResponse} res
* @type {import("types/internals").NextAuthApiHandler}
*/
export default async function callback(req, res) {
const {
@@ -72,12 +71,12 @@ export default async function callback(req, res) {
}
try {
const signInCallbackResponse = await callbacks.signIn(
userOrProfile,
const signInCallbackResponse = await callbacks.signIn({
user: userOrProfile,
account,
OAuthProfile
)
if (signInCallbackResponse === false) {
profile: OAuthProfile,
})
if (!signInCallbackResponse) {
return res.redirect(
`${baseUrl}${basePath}/error?error=AccessDenied`
)
@@ -85,16 +84,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
@@ -106,22 +100,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
OAuthProfile,
isNewUser
)
profile: OAuthProfile,
isNewUser,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -221,27 +215,22 @@ export default async function callback(req, res) {
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(
profile,
const signInCallbackResponse = await callbacks.signIn({
user: profile,
account,
{ email }
)
if (signInCallbackResponse === false) {
email: { email },
})
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
@@ -253,22 +242,22 @@ export default async function callback(req, res) {
)
if (useJwtSession) {
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
profile,
isNewUser
)
isNewUser,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()
@@ -336,7 +325,8 @@ export default async function callback(req, res) {
let userObjectReturnedFromAuthorizeHandler
try {
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
credentials, {...req, options: {}, cookies: {}}
credentials,
{ ...req, options: {}, cookies: {} }
)
if (!userObjectReturnedFromAuthorizeHandler) {
return res
@@ -346,59 +336,53 @@ 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
const account = { id: provider.id, type: "credentials" }
try {
const signInCallbackResponse = await callbacks.signIn(
const signInCallbackResponse = await callbacks.signIn({
user,
account,
credentials
)
if (signInCallbackResponse === false) {
credentials,
})
if (!signInCallbackResponse) {
return res
.status(403)
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
} else if (typeof signInCallbackResponse === "string") {
return res.redirect(signInCallbackResponse)
}
} catch (error) {
if (error instanceof Error) {
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
error.message
)}`
)
}
return res.redirect(error)
return res.redirect(
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`
)
}
const defaultJwtPayload = {
const defaultToken = {
name: user.name,
email: user.email,
picture: user.image,
sub: user.id?.toString(),
}
const jwtPayload = await callbacks.jwt(
defaultJwtPayload,
const token = await callbacks.jwt({
token: defaultToken,
user,
account,
userObjectReturnedFromAuthorizeHandler,
false
)
profile: userObjectReturnedFromAuthorizeHandler,
isNewUser: false,
})
// Sign and encrypt token
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newEncodedJwt = await jwt.encode({ ...jwt, token })
// Set cookie expiry date
const cookieExpires = new Date()

View File

@@ -22,7 +22,7 @@ export default async function session(req, res) {
if (useJwtSession) {
try {
// Decrypt and verify token
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
// Generate new session expiry date
const sessionExpiresDate = new Date()
@@ -33,38 +33,35 @@ export default async function session(req, res) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSessionPayload = {
const defaultSession = {
user: {
name: decodedJwt.name || null,
email: decodedJwt.email || null,
image: decodedJwt.picture || null,
name: decodedToken.name || null,
email: decodedToken.email || null,
image: decodedToken.picture || null,
},
expires: sessionExpires,
}
// Pass Session and JSON Web Token through to the session callback
const jwtPayload = await callbacks.jwt(decodedJwt)
const sessionPayload = await callbacks.session(
defaultSessionPayload,
jwtPayload
)
const token = await callbacks.jwt({ token: decodedToken })
const session = await callbacks.session({
session: defaultSession,
token,
})
// Return session payload as response
response = sessionPayload
response = session
// Refresh JWT expiry by re-signing it, with an updated expiry date
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
const newToken = await jwt.encode({ ...jwt, token })
// Set cookie, to also update expiry date on cookie
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
cookie.set(res, cookies.sessionToken.name, newToken, {
expires: sessionExpires,
...cookies.sessionToken.options,
})
await dispatchEvent(events.session, {
session: sessionPayload,
jwt: jwtPayload,
})
await dispatchEvent(events.session, { session, token })
} catch (error) {
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
logger.error("JWT_SESSION_ERROR", error)
@@ -88,7 +85,7 @@ export default async function session(req, res) {
// By default, only exposes a limited subset of information to the client
// as needed for presentation purposes (e.g. "you are logged in as…").
const defaultSessionPayload = {
const defaultSession = {
user: {
name: user.name,
email: user.email,
@@ -99,10 +96,10 @@ export default async function session(req, res) {
}
// Pass Session through to the session callback
const sessionPayload = await callbacks.session(
defaultSessionPayload,
user
)
const sessionPayload = await callbacks.session({
session: defaultSession,
user,
})
// Return session payload as response
response = sessionPayload

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}`)
@@ -47,14 +41,15 @@ export default async function signin(req, res) {
const email = req.body.email?.toLowerCase() ?? null
// If is an existing user return a user object (otherwise use placeholder)
const profile = (await getUserByEmail(email)) || { email }
const user = (await getUserByEmail(email)) || { email }
const account = { id: provider.id, type: "email", providerAccountId: email }
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn(profile, account, {
email,
verificationRequest: true,
const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { email, verificationRequest: true },
})
if (signInCallbackResponse === false) {
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
@@ -62,14 +57,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

@@ -6,9 +6,9 @@
"types": ["./types"],
"next-auth": ["./src/server"],
"next-auth/adapters": ["./src/adapters"],
"next-auth/client": ["./src/client"],
"next-auth/react": ["./src/client/react"],
"next-auth/jwt": ["./src/lib/jwt"],
"next-auth/providers": ["./src/providers"]
"next-auth/providers": ["./src/providers"],
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],

36
types/adapters.d.ts vendored
View File

@@ -1,39 +1,7 @@
import { AppOptions } from "./internals"
import { InternalOptions } from "./internals"
import { User, Profile, Session } from "."
import { EmailConfig } from "./providers"
/** Legacy */
export {
TypeORMAccountModel,
TypeORMSessionModel,
TypeORMUserModel,
TypeORMVerificationRequestModel,
} from "@next-auth/typeorm-legacy-adapter"
import {
TypeORMAdapter,
TypeORMAdapterModels,
} from "@next-auth/typeorm-legacy-adapter"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
export const TypeORM: {
Models: TypeORMAdapterModels
Adapter: TypeORMAdapter
}
export const Prisma: {
Adapter: PrismaLegacyAdapter
}
declare const Adapters: {
Default: TypeORMAdapter
TypeORM: typeof TypeORM
Prisma: typeof Prisma
}
export default Adapters
/**
* Using a custom adapter you can connect to any database backend or even several different databases.
* Custom adapters created and maintained by our community can be found in the adapters repository.
@@ -152,5 +120,5 @@ export type Adapter<
client: C,
options?: O
) => {
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
getAdapter(appOptions: InternalOptions): Promise<AdapterInstance<U, P, S>>
}

95
types/index.d.ts vendored
View File

@@ -2,10 +2,9 @@
/// <reference types="node" />
import { ConnectionOptions } from "typeorm"
import { Adapter } from "./adapters"
import { JWTOptions, JWT } from "./jwt"
import { AppProviders } from "./providers"
import { AppProviders, Credentials } from "./providers"
import {
Awaitable,
NextApiRequest,
@@ -29,14 +28,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.
@@ -82,7 +73,7 @@ export interface NextAuthOptions {
* signOut: '/auth/signout',
* error: '/auth/error',
* verifyRequest: '/auth/verify-request',
* newUser: null
* newUser: '/auth/new-user'
* }
* ```
*
@@ -98,7 +89,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#callbacks) | [Callbacks documentation](https://next-auth.js.org/configuration/callbacks)
*/
callbacks?: CallbacksOptions
callbacks?: Partial<CallbacksOptions>
/**
* Events are asynchronous functions that do not return a response, they are useful for audit logging.
* You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
@@ -113,18 +104,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>
@@ -212,7 +196,7 @@ export interface NextAuthOptions {
*
* [Documentation](https://next-auth.js.org/configuration/options#cookies) | [Usage example](https://next-auth.js.org/configuration/options#example)
*/
cookies?: CookiesOptions
cookies?: Partial<CookiesOptions>
}
/**
@@ -283,7 +267,29 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn?(user: User, account: A, profile: P): Awaitable<string | boolean>
signIn(params: {
user: User
account: A
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile: P & Record<string, unknown>
/**
* If Email provider is used, it contains the email, and optionally on the first call a
* `verificationRequest: true` property 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.
*/
email: {
email: string | null
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
credentials: Credentials
}): Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
* By default only URLs on the same URL as the site are allowed,
@@ -291,12 +297,19 @@ export interface CallbacksOptions<
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#redirect-callback)
*/
redirect?(url: string, baseUrl: string): Awaitable<string>
redirect(params: {
/** URL provided as callback URL by the client */
url: string
/** Default base URL of site (can be used as fallback) */
baseUrl: string
}): Awaitable<string>
/**
* This callback is called whenever a session is checked.
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
*
* - ⚠ By default, only a subset of the token is returned for increased security.
* ⚠ By default, only a subset (email, name, imgage)
* 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.
*
@@ -306,7 +319,11 @@ export interface CallbacksOptions<
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
*
*/
session?(session: Session, userOrToken: JWT | User): Awaitable<Session>
session(params: {
session: Session
user: User
token: JWT
}): Awaitable<Session>
/**
* This callback is called whenever a JSON Web Token is created (i.e. at sign in)
* or updated (i.e whenever a session is accessed in the client).
@@ -314,18 +331,18 @@ export interface CallbacksOptions<
* where you can control what should be returned to the client.
* Anything else will be kept from your front-end.
*
* - ⚠ By default the JWT is signed, but not encrypted.
* ⚠ By default the JWT is signed, but not encrypted.
*
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback)
*/
jwt?(
token: JWT,
user?: User,
account?: A,
profile?: P,
jwt(params: {
token: JWT
user?: User
account?: A
profile?: P
isNewUser?: boolean
): Awaitable<JWT>
}): Awaitable<JWT>
}
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
@@ -343,10 +360,10 @@ export interface CookieOption {
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
export interface CookiesOptions {
sessionToken?: CookieOption
callbackUrl?: CookieOption
csrfToken?: CookieOption
pkceCodeVerifier?: CookieOption
sessionToken: CookieOption
callbackUrl: CookieOption
csrfToken: CookieOption
pkceCodeVerifier: CookieOption
}
/** [Documentation](https://next-auth.js.org/configuration/events) */
@@ -428,11 +445,11 @@ export interface DefaultSession extends Record<string, unknown> {
/**
* Returned by `useSession`, `getSession`, returned by the `session` callback
* and also the shape received as a prop on the `Provider` React Context
* and also the shape received as a prop on the `SessionProvider` React Context
*
* [`useSession`](https://next-auth.js.org/getting-started/client#usesession) |
* [`getSession`](https://next-auth.js.org/getting-started/client#getsession) |
* [`Provider`](https://next-auth.js.org/getting-started/client#provider) |
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown>, DefaultSession {}

View File

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

View File

@@ -1,18 +1,18 @@
import { NextApiRequest, NextApiResponse } from "./utils"
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
import { Awaitable, NextApiRequest, NextApiResponse } from "./utils"
import {
CallbacksOptions,
CookiesOptions,
EventCallbacks,
LoggerInstance,
PagesOptions,
SessionOptions,
Theme,
} from ".."
import { AppProvider } from "../providers"
import { JWTOptions } from "next-auth/jwt"
import { Adapter } from "next-auth/adapters"
/** Options that are the same both in internal and user provided options. */
export type NextAuthSharedOptions =
| "pages"
| "jwt"
| "events"
| "callbacks"
| "cookies"
| "adapter"
export interface AppOptions
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
export interface InternalOptions {
providers: AppProvider[]
baseUrl: string
basePath: string
@@ -42,10 +42,22 @@ export interface AppOptions
debug: boolean
logger: LoggerInstance
session: Required<SessionOptions>
pages: PagesOptions
jwt: JWTOptions
events: EventCallbacks
adapter: ReturnType<Adapter>
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string
}
export interface NextAuthRequest extends NextApiRequest {
options: AppOptions
options: InternalOptions
}
export type NextAuthResponse = NextApiResponse
export type NextAuthApiHandler = (
req: NextAuthRequest,
res: NextAuthResponse
) => Awaitable<void>

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

@@ -0,0 +1,37 @@
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 SessionContextValue<R extends boolean = false> = R extends true
?
| { data: Session; status: "authenticated" }
| { data: null; status: "loading" }
:
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }
export type SessionContext = React.Context<SessionContextValue>

31
types/providers.d.ts vendored
View File

@@ -1,5 +1,6 @@
import { Profile, TokenSet, User } from "."
import { Awaitable, NextApiRequest } from "./internals/utils"
import { Options as SMTPConnectionOptions } from 'nodemailer/lib/smtp-connection'
export type ProviderType = "oauth" | "email" | "credentials"
@@ -57,6 +58,7 @@ export type OAuthProviderType =
| "Apple"
| "Atlassian"
| "Auth0"
| "AzureAD"
| "AzureADB2C"
| "Basecamp"
| "BattleNet"
@@ -83,6 +85,7 @@ export type OAuthProviderType =
| "Mailchimp"
| "MailRu"
| "Medium"
| "Naver"
| "Netlify"
| "Okta"
| "Osso"
@@ -113,30 +116,24 @@ interface CredentialInput {
placeholder?: string
}
interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
export type Credentials = Record<string, CredentialInput>
interface CredentialsConfig<C extends Credentials = {}>
extends CommonProviderOptions {
type: "credentials"
credentials: C
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
authorize(
credentials: Record<keyof C, string>,
req: NextApiRequest
): Awaitable<User | null>
}
export type CredentialsProvider = (
options: Partial<CredentialsConfig>
) => CredentialsConfig
export type CredentialsProvider = <C extends Record<string, CredentialInput>>(
options: Partial<CredentialsConfig<C>>
) => CredentialsConfig<C>
export type CredentialsProviderType = "Credentials"
/** Email Provider */
export interface EmailConfigServerOptions {
host: string
port: number
auth: {
user: string
pass: string
}
}
export type SendVerificationRequest = (params: {
identifier: string
url: string
@@ -148,7 +145,7 @@ export type SendVerificationRequest = (params: {
export interface EmailConfig extends CommonProviderOptions {
type: "email"
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
server: string | EmailConfigServerOptions
server: string | SMTPConnectionOptions
/** @default "NextAuth <no-reply@example.com>" */
from?: string
/**

View File

@@ -2,6 +2,7 @@ import * as React from "react"
import { IncomingMessage } from "http"
import { Session } from "."
import { ProviderType } from "./providers"
import { SessionContextValue } from "internals/react"
export interface CtxOrReq {
req?: IncomingMessage
@@ -17,29 +18,24 @@ export type GetSessionOptions = CtxOrReq & {
triggerEvent?: boolean
}
export interface UseSessionOptions<R extends boolean> {
required: R
/** Defaults to `signIn` */
action?(): void
}
/**
* React Hook that gives you access
* to the logged in user's session data.
*
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
*/
export function useSession(): [Session | null, boolean]
export function useSession<R extends boolean>(
options?: UseSessionOptions<R>
): SessionContextValue<R>
/**
* Can be called client or server side to return a session asynchronously.
* It calls `/api/auth/session` and returns a promise with a session object,
* or null if no session exists.
*
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
*/
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 +50,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 +74,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 +121,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 +147,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 +173,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 +0,0 @@
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",
},
})

View File

@@ -19,12 +19,12 @@ Providers.Email({
from: "path/from",
})
// $ExpectType CredentialsConfig<{}>
// $ExpectType CredentialsConfig<{ username: { label: string; type: string; }; password: { label: string; type: string; }; }>
Providers.Credentials({
id: "login",
name: "account",
credentials: {
user: {
username: {
label: "Password",
type: "password",
},
@@ -33,7 +33,7 @@ Providers.Credentials({
type: "password",
},
},
authorize: async (credentials) => {
authorize: async ({username, password}) => {
const user = {
/* fetched user */
}

View File

@@ -1,4 +1,4 @@
import * as client from "next-auth/client"
import * as client from "next-auth/react"
import { nextReq } from "./test-helpers"
const clientSession = {
@@ -11,87 +11,89 @@ const clientSession = {
expires: "1234",
}
// $ExpectType [Session | null, boolean]
/**
* $ExpectType
* | { data: Session; status: "authenticated"; }
* | { data: null; status: "unauthenticated" | "loading"; }
* | { //// data: Session; status: "authenticated"; }
* | { data: null; status: "loading"; }
*/
client.useSession()
// $ExpectType { data: Session; status: "authenticated"; } | { data: null; status: "loading"; }
const session = client.useSession({ required: true })
if (session.status === "loading") {
// $ExpectType null
session.data
} else {
// $ExpectType Session
session.data
}
// $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

@@ -4,7 +4,7 @@ import NextAuth, * as NextAuthTypes from "next-auth"
import { IncomingMessage, ServerResponse } from "http"
import { Socket } from "net"
import { NextApiRequest, NextApiResponse } from "internals/utils"
import { AppOptions } from "internals"
import { InternalOptions } from "internals"
const req: NextApiRequest = Object.assign(new IncomingMessage(new Socket()), {
query: {},
@@ -62,7 +62,7 @@ const exampleVerificationRequest = {
const MyAdapter: Adapter<Record<string, unknown>> = () => {
return {
async getAdapter(appOptions: AppOptions) {
async getAdapter(appOptions: InternalOptions) {
return {
async createUser(profile) {
return exampleUser
@@ -135,7 +135,6 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
clientSecret: "123",
}),
],
database: "path/to/db",
debug: true,
secret: "my secret",
session: {
@@ -154,16 +153,16 @@ const allConfig: NextAuthTypes.NextAuthOptions = {
},
pages: pageOptions,
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, email, credentials, profile }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return "path/to/foo"
},
async session(session, userOrToken) {
return { ...session }
async session({ session, user, token }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
},
},

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

@@ -13,10 +13,10 @@ You can find the full schema in the table structure section below.
## Getting Started
1. Install `next-auth` and `@next-auth/dynamodb-adapter@canary`
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
```js
npm install next-auth @next-auth/dynamodb-adapter@canary
npm install next-auth @next-auth/dynamodb-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.

View File

@@ -11,10 +11,10 @@ You can find the Fauna schema and seed information in the docs at [next-auth.js.
## Getting Started
1. Install `next-auth` and `@next-auth/fauna-adapter@canary`
1. Install `next-auth` and `@next-auth/fauna-adapter`
```js
npm install next-auth @next-auth/fauna-adapter@canary
npm install next-auth @next-auth/fauna-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.

View File

@@ -9,10 +9,10 @@ This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This p
## Getting Started
1. Install `next-auth` and `@next-auth/firebase-adapter@canary`
1. Install `next-auth` and `@next-auth/firebase-adapter`
```js
npm install next-auth @next-auth/firebase-adapter@canary
npm install next-auth @next-auth/firebase-adapter
```
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration 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

@@ -15,6 +15,7 @@ There you can find the following adapters:
- [`fauna`](./fauna)
- [`dynamodb`](./dynamodb)
- [`firebase`](./firebase)
- [`pouchdb`](./pouchdb)
## Custom Adapter

View File

@@ -13,10 +13,10 @@ Depending on your architecture you can use PouchDB's http adapter to reach any d
> **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`
1. Install `next-auth` and `@next-auth/pouchdb-adapter`
```js
npm install next-auth @next-auth/pouchdb-adapter@canary
npm install next-auth @next-auth/pouchdb-adapter
```
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
@@ -28,10 +28,9 @@ 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
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
@@ -49,7 +48,7 @@ export default NextAuth({
})
```
## Advanced
## Advanced
### Memory-First Caching Strategy
@@ -60,4 +59,3 @@ Use an in-memory PouchDB as your main authentication database, and synchronize i
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

@@ -23,7 +23,7 @@ Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import Providers from "next-auth/providers"
import Adapters from "next-auth/adapters"
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
@@ -35,7 +35,7 @@ export default NextAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
adapter: Adapters.Prisma.Adapter({ prisma }),
adapter: PrismaLegacyAdapter({ prisma }),
})
```
@@ -161,7 +161,7 @@ You can use custom model names by using the `modelMapping` option (shown here wi
```javascript title="pages/api/auth/[...nextauth].js"
...
adapter: Adapters.Prisma.Adapter({
adapter: PrismaLegacyAdapter({
prisma,
modelMapping: {
User: 'user',

View File

@@ -11,10 +11,10 @@ You can also use NextAuth.js with the new experimental Adapter for [Prisma](http
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:
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter` package:
```
npm install @prisma/client @next-auth/prisma-adapter@canary
npm install @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev
```

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

@@ -16,16 +16,16 @@ You can specify a handler for any of the callbacks below.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return baseUrl
},
async session(session, user) {
async session({ session, user, token }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
}
...
@@ -41,15 +41,7 @@ Use the `signIn()` callback to control if a user is allowed to sign in.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} user User object
* @param {object} account Provider account
* @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")
*/
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
const isAllowedToSignIn = true
if (isAllowedToSignIn) {
return true
@@ -64,18 +56,18 @@ 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 `email` 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 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.
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 `credentials` 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.
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
When using NextAuth.js without a database, the user object will always be a prototype user object, with information extracted from the profile.
:::
:::note
@@ -93,52 +85,36 @@ By default only URLs on the same URL as the site are allowed, you can use the re
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {string} url URL provided as callback URL by the client
* @param {string} baseUrl Default base URL of site (can be used as fallback)
* @return {string} URL the client will be redirect to
*/
async redirect(url, baseUrl) {
return url.startsWith(baseUrl)
? url
: baseUrl
redirect({ url, baseUrl }) {
return url.startsWith(baseUrl) ? url : baseUrl
}
}
...
```
:::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
in) or updated (i.e whenever a session is accessed in the client).
This callback is called whenever a JSON Web Token is created (i.e. at sign
in) or updated (i.e whenever a session is accessed in the client). The returned value will be [signed and optionally encrypted](/configuration/options#jwt), and it is stored in a cookie.
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`, `useSession()` will invoke this function, but only if you are using a [JWT session](/configuration/options#session). This method is not invoked when you persist sessions in a database.
* 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 persisted 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. In subsequent calls, only `token` will be available.
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. You can persist data such as User ID, OAuth Access Token in this token. To make it available in the browser, check out the [`session()` callback](#session-callback) as well.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
/**
* @param {object} token Decrypted JSON Web Token
* @param {object} user User object (only available on sign in)
* @param {object} account Provider account (only available on sign in)
* @param {object} profile Provider profile (only available on sign in)
* @param {boolean} isNewUser True if new user (only available on sign in)
* @return {object} JSON Web Token that will be saved
*/
async jwt(token, user, account, profile, isNewUser) {
// Add access_token to the token right after signin
if (account?.accessToken) {
token.accessToken = account.accessToken
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
if (account) {
token.accessToken = account.access_token
}
return token
}
@@ -147,18 +123,13 @@ callbacks: {
```
:::tip
Use an if branch in jwt with checking for existence of any other params than token. If any of those exist, you call jwt for the first time.
This is a good place to add for example an `access_token` to your jwt, if you want to.
:::
:::tip
Check out the content of all the params in addition `token`, to see what info you have available on signin.
Use an if branch to check for the existence of parameters (apart from `token`). If they exist, this means that the callback is being invoked for the first time (i.e. the user is being signed in). This is a good place to persist additional data like an `access_token` in the JWT. Subsequent invocations will only contain the `token` parameter.
:::
:::warning
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). 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.
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. Opt into database persisted sessions by setting [`session: {jwt: false}`](/configuration/options#session).
:::
## Session callback
@@ -167,20 +138,14 @@ The session callback is called whenever a session is checked. By default, only a
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"
...
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
*/
async session(session, token) {
// Add property to session, like an access_token from a provider.
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
session.accessToken = token.accessToken
return session
}
@@ -193,10 +158,6 @@ When using JSON Web Tokens the `jwt()` callback is invoked before the `session()
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
:::
:::tip
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
:::
:::warning
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.

View File

@@ -3,47 +3,51 @@ 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, PostgreSQL, MSSQL, SQLite and MongoDB databases._
- **Prisma**<br/>
_The Prisma 2 adapter supports MySQL, PostgreSQL and SQLite databases._
- **Fauna**<br/>
_The FaunaDB adapter only supports FaunaDB._
- **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)
> There are currently efforts in the [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters) repository to get community-based DynamoDB, Sanity, PouchDB and Sequelize Adapters merged. If you are interested in any of the above, feel free to check out the PRs in the `nextauthjs/adapters` repository!
**This document covers the default adapter (TypeORM).**
See the [documentation for adapters](/adapters/overview) 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).
@@ -51,22 +55,23 @@ You can pass in any valid [TypeORM configuration option](https://github.com/type
_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:_
```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_",
})
```
:::
@@ -87,19 +92,21 @@ _If you are running SQLite, MongoDB or a Document database you can skip this ste
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
@@ -128,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
@@ -139,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
@@ -152,30 +163,34 @@ Install module:
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)
@@ -190,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
@@ -201,7 +216,9 @@ 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
@@ -214,7 +231,7 @@ Install module:
#### Example
```js
database: "sqlite://localhost/:memory:"
adapter: TypeORMAdapter("sqlite://localhost/:memory:")
```
## Other databases

View File

@@ -229,7 +229,7 @@ pages: {
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
}
```
@@ -252,16 +252,16 @@ You can specify a handler for any of the callbacks below.
```js
callbacks: {
async signIn(user, account, profile) {
async signIn({ user, account, profile, email, credentials }) {
return true
},
async redirect(url, baseUrl) {
async redirect({ url, baseUrl }) {
return baseUrl
},
async session(session, user) {
async session({ session, token, user }) {
return session
},
async jwt(token, user, account, profile, isNewUser) {
async jwt({ token, user, account, profile, isNewUser }) {
return token
}
}
@@ -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](/adapters/overview) 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

@@ -16,15 +16,17 @@ To add a custom login page, you can use the `pages` option:
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: null // If set, new users will be directed here on first sign in
newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
}
...
```
## 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.
@@ -35,6 +37,7 @@ The following errors are passed as error query parameters to the default or over
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)),
@@ -45,8 +48,9 @@ The following errors are passed as error query parameters to the default or over
- **OAuthAccountNotLinked**: If the email on the account is already linked, but not with this OAuth account
- **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.
- **SessionRequired**: The content of this page requires you to be signed in at all times. See [useSession](/getting-started/client#require-session) for configuration.
- **Default**: Catch all, will apply, if none of the above matched
Example: `/auth/error?error=Default`
## Theming
@@ -60,14 +64,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 +81,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 +103,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 +139,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 +147,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 +170,8 @@ export default function SignIn({ csrfToken }) {
export async function getServerSideProps(context) {
return {
props: {
csrfToken: await getCsrfToken(context)
}
csrfToken: await getCsrfToken(context),
},
}
}
@@ -182,9 +188,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

@@ -83,7 +83,7 @@ providers: [
| name | Descriptive name for the provider | `string` | Yes |
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
| scope | OAuth access scopes (expects string with space as separator) | `string` | Yes |
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
@@ -260,14 +260,14 @@ providers: [
// 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)
// (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
@@ -288,10 +288,10 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
### Options
| Name | Description | Type | Required |
| 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 |
| 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,9 +15,9 @@ 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
@@ -33,28 +33,62 @@ These errors are displayed on the terminal.
#### OAUTH_GET_ACCESS_TOKEN_ERROR
This occurs when there was an error in the POST request to the OAuth provider and we were not able to retrieve the access token.
Please double check your provider settings.
#### OAUTH_V1_GET_ACCESS_TOKEN_ERROR
This error is explicitly related to older OAuth v1.x providers, if you are using one of these, please double check all available settings.
#### OAUTH_GET_PROFILE_ERROR
N/A
#### OAUTH_PARSE_PROFILE_ERROR
This error is a result of either a problem with the provider response or the user cancelling the action with the provider, unfortunately we can't discern which with the information we have.
This error should also log the exception and available `profileData` to further aid debugging.
#### OAUTH_CALLBACK_HANDLER_ERROR
This error will occur when there was an issue parsing the json request body, for example.
There should also be further details logged when this occurs, such as the error thrown, and the request body itself to aid in debugging.
---
### Signin / Callback
#### GET_AUTHORIZATION_URL_ERROR
This error can occur when we cannot get the OAuth v1 request token and generate the authorization URL.
Please double check your OAuth v1 provider settings, especially the OAuth token and OAuth token secret.
#### SIGNIN_OAUTH_ERROR
This error can occur in one of a few places, first during the redirect to the authorization URL of the provider. Next, in the signin flow while creating the PKCE code verifier. Finally, during the generation of the CSRF Token hash in internal state during signin.
Please check your OAuth provider settings and make sure your URLs and other options are correctly set on the provider side.
#### CALLBACK_OAUTH_ERROR
This can occur during handling of the callback if the `code_verifier` cookie was not found or an invalid state was returned from the OAuth provider.
#### SIGNIN_EMAIL_ERROR
This error can occur when a user tries to sign in via an email link; for example, if the email token could not be generated or the verification request failed.
Please double check your email settings.
#### CALLBACK_EMAIL_ERROR
This can occur during the email callback process. Specifically, if there was an error signing the user in via email, encoding the jwt, etc.
Please double check your Email settings.
#### EMAIL_REQUIRES_ADAPTER_ERROR
The Email authentication provider can only be used if a database is configured.
@@ -71,6 +105,8 @@ In _most cases_ it does not make sense to specify a database in NextAuth.js opti
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
This error occurs when there was no `authorize()` handler defined on the credential authentication provider.
#### PKCE_ERROR
The provider you tried to use failed when setting [PKCE or Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636#section-4.2).
@@ -87,20 +123,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
@@ -110,6 +146,8 @@ If you are unable to use an HS512 key (for example to interoperate with other se
#### SIGNOUT_ERROR
This error occurs when there was an issue deleting the session from the database, for example.
---
### Database
@@ -120,30 +158,56 @@ They all indicate a problem interacting with the database.
#### ADAPTER_CONNECTION_ERROR
This error can occur during the `createConnection()` function. Make sure your database connection string / settings are correct and the database is up and ready to receive connections.
#### CREATE_USER_ERROR
N/A
#### GET_USER_BY_ID_ERROR
N/A
#### GET_USER_BY_EMAIL_ERROR
N/A
#### GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR
N/A
#### LINK_ACCOUNT_ERROR
N/A
#### CREATE_SESSION_ERROR
N/A
#### GET_SESSION_ERROR
N/A
#### UPDATE_SESSION_ERROR
N/A
#### DELETE_SESSION_ERROR
N/A
#### CREATE_VERIFICATION_REQUEST_ERROR
N/A
#### GET_VERIFICATION_REQUEST_ERROR
N/A
#### DELETE_VERIFICATION_REQUEST_ERROR
N/A
---
### Other

View File

@@ -15,26 +15,42 @@ It is not commercial software and is not associated with a commercial organizati
## Compatibility
### What databases does NextAuth.js support?
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases does NextAuth.js support?</h3>
</summary>
<p>
You can use NextAuth.js with MySQL, MariaDB, Postgres, MongoDB and SQLite or without a database. (See also: [Databases](/configuration/databases))
You can use also NextAuth.js with any database using a custom database adapter, or by using a custom credentials authentication provider - e.g. to support signing in with a username and password stored in an existing database.
### What authentication services does NextAuth.js support?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What authentication services does NextAuth.js support?</h3>
</summary>
<p>
<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.
### Does NextAuth.js support signing in with a username and password?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js support signing in with a username and password?</h3>
</summary>
<p>
NextAuth.js is designed to avoid the need to store passwords for user accounts.
@@ -42,7 +58,14 @@ If you have an existing database of usernames and passwords, you can use a custo
_If you use a custom credentials provider user accounts will not be persisted in a database by NextAuth.js (even if one is configured). The option to use JSON Web Tokens for session tokens (which allow sign in without using a session database) must be enabled to use a custom credentials provider._
### Can I use NextAuth.js with a website that does not use Next.js?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with a website that does not use Next.js?</h3>
</summary>
<p>
NextAuth.js is designed for use with Next.js and Serverless.
@@ -52,28 +75,55 @@ If you use NextAuth.js on a website with a different subdomain then the rest of
NextAuth.js does not currently support automatically signing into sites on different top level domains (e.g. `www.example.com` vs `www.example.org`) using a single session.
### Can I use NextAuth.js with React Native?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Can I use NextAuth.js with React Native?</h3>
</summary>
<p>
NextAuth.js is designed as a secure, confidential client and implements a server side authentication flow.
It is not intended to be used in native applications on desktop or mobile applications, which typically implement public clients (e.g. with client / secrets embedded in the application).
</p>
</details>
### Is NextAuth.js supporting TypeScript?
<details>
<summary>
<h3 style={{display:"inline-block"}}>Is NextAuth.js supporting TypeScript?</h3>
</summary>
<p>
Yes! Check out the [TypeScript docs](/getting-started/typescript)
</p>
</details>
---
## Databases
### What databases are supported by NextAuth.js?
<details>
<summary>
<h3 style={{display:"inline-block"}}>What databases are supported by NextAuth.js?</h3>
</summary>
<p>
NextAuth.js can be used with MySQL, Postgres, MongoDB, SQLite and compatible databases (e.g. MariaDB, Amazon Aurora, Amazon DocumentDB…) or with no database.
It also provides an Adapter API which allows you to connect it to any database.
### What does NextAuth.js use databases for?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What does NextAuth.js use databases for?</h3>
</summary>
<p>
Databases in NextAuth.js are used for persisting users, OAuth accounts, email sign in tokens and sessions.
@@ -81,36 +131,70 @@ Specifying a database is optional if you don't need to persist user data or supp
If you are using a database with NextAuth.js, you can still explicitly enable JSON Web Tokens for sessions (instead of using database sessions).
### Should I use a database?
</p>
</details>
* 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.
<details>
<summary>
<h3 style={{display:"inline-block"}}>Should I use a database?</h3>
</summary>
<p>
* 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 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.
### What database should I use?
- 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).
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What database should I use?</h3>
</summary>
<p>
Managed database solutions for MySQL, Postgres and MongoDB (and compatible databases) are well supported from cloud providers such as Amazon, Google, Microsoft and Atlas.
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/)).
</p>
</details>
---
## Security
## Security
### I think I've found a security problem, what should I do?
<details>
<summary>
<h3 style={{display:"inline-block"}}>I think I've found a security problem, what should I do?</h3>
</summary>
<p>
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
### What is the disclosure policy for NextAuth.js?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What is the disclosure policy for NextAuth.js?</h3>
</summary>
<p>
We practice responsible disclosure.
If you contact us regarding a potentially serious issue, we will endeavor to get back to you within 72 hours and to publish a fix within 30 days. We will responsibly disclose the issue (and credit you with your consent) once a fix to resolve the issue has been released - or after 90 days, which ever is sooner.
### How do I get Refresh Tokens and Access Tokens for an OAuth account?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>How do I get Refresh Tokens and Access Tokens for an OAuth account?</h3>
</summary>
<p>
NextAuth.js provides a solution for authentication, session management and user account creation.
@@ -123,7 +207,14 @@ You can then look them up from the database or persist them to the JSON Web Toke
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
### When I sign in with another account with the same email address, why are accounts not linked automatically?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>When I sign in with another account with the same email address, why are accounts not linked automatically?</h3>
</summary>
<p>
Automatic account linking on sign in is not secure between arbitrary providers - with the exception of allowing users to sign in via an email addresses as a fallback (as they must verify their email address as part of the flow).
@@ -141,11 +232,18 @@ Automatic account linking is not a planned feature of NextAuth.js, however there
Providing support for secure account linking and unlinking of additional providers - which can only be done if a user is already signed in already - was originally a feature in v1.x but has not been present since v2.0, is planned to return in a future release.
</p>
</details>
---
## Feature Requests
### Why doesn't NextAuth.js support [a particular feature]?
<details>
<summary>
<h3 style={{display:"inline-block"}}>Why doesn't NextAuth.js support [a particular feature]?</h3>
</summary>
<p>
NextAuth.js is an open source project built by individual contributors who are volunteers writing code and providing support in their spare time.
@@ -153,7 +251,14 @@ If you would like NextAuth.js to support a particular feature, the best way to h
If you are not able to develop a feature yourself, you can offer to sponsor someone to work on it.
### I disagree with a design decision, how can I change your mind?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>I disagree with a design decision, how can I change your mind?</h3>
</summary>
<p>
Product design decisions on NextAuth.js are made by core team members.
@@ -163,79 +268,117 @@ Requests that provide the detail requested in the template and follow the format
Ultimately if your request is not accepted or is not actively in development, you are always free to fork the project under the terms of the ISC License.
</p>
</details>
---
## JSON Web Tokens
## JSON Web Tokens
### Does NextAuth.js use JSON Web Tokens?
<details>
<summary>
<h3 style={{display:"inline-block"}}>Does NextAuth.js use JSON Web Tokens?</h3>
</summary>
<p>
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.
### What are the advantages of JSON Web Tokens?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the advantages of JSON Web Tokens?</h3>
</summary>
<p>
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 a 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?
</p>
</details>
* 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.
<details>
<summary>
<h3 style={{display:"inline-block"}}>What are the disadvantages of JSON Web Tokens?</h3>
</summary>
<p>
- 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 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/))
- 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 ~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.
### Are JSON Web Tokens secure?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>Are JSON Web Tokens secure?</h3>
</summary>
<p>
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: { encryption: 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.
Using explicit public/private keys for signing is strongly recommended.
### What signing and encryption standards does NextAuth.js support?
</p>
</details>
<details>
<summary>
<h3 style={{display:"inline-block"}}>What signing and encryption standards does NextAuth.js support?</h3>
</summary>
<p>
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)
</p>
</details>

View File

@@ -29,22 +29,22 @@ You can use the [session callback](/configuration/callbacks#session-callback) to
## 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`.
Make sure that [`<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 { data: session, status } = useSession()
if(session) {
if (status === "authenticated") {
return <p>Signed in as {session.user.email}</p>
}
@@ -52,12 +52,47 @@ export default function Component() {
}
```
`useSession()` returns an object containing two values: `data` and `status`:
- **`data`**: This can be three values: [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444) / `undefined` / `null`.
- when the session hasn't been fetched yet, `data` will `undefined`
- in case it failed to retrieve the session, `data` will be `null`
- in case of success, `data` will be [`Session`](https://github.com/nextauthjs/next-auth/blob/8ff4b260143458c5d8a16b80b11d1b93baa0690f/types/index.d.ts#L437-L444).
- **`status`**: enum mapping to three possible session states: `"loading" | "authenticated" | "unauthenticated"`
### Require session
Due to the way how Next.js handles `getServerSideProps` and `getInitialProps`, every protected page load has to make a server-side request to check if the session is valid and then generate the requested page (SSR). This increases server load, and if you are good with making the requests from the client, there is an alternative. You can use `useSession` in a way that makes sure you always have a valid session. If after the initial loading state there was no session found, you can define the appropriate action to respond.
The default behavior is to redirect the user to the sign-in page, from where - after a successful login - they will be sent back to the page they started on. You can also define an `onFail()` callback, if you would like to do something else:
#### Example
```jsx title="pages/protected.jsx"
import { useSession } from "next-auth/react"
export default function Admin() {
const { status } = useSession({
required: true,
onUnauthenticated() {
// The user is not authenticated, handle it here.
}
})
const if (status === "loading") {
return "Loading or not authenticated..."
}
return "User is logged in"
}
```
---
## 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 +110,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 +129,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 +148,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 +161,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 +175,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 +192,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 +202,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 +222,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 +237,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-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.
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 host name, 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 +267,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
@@ -250,7 +283,7 @@ e.g.
}
```
#### Additional params
#### Additional parameters
It is also possible to pass additional parameters to the `/authorize` endpoint through the third argument of `signIn()`.
@@ -258,8 +291,8 @@ 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 re-authenticate_
- `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).
@@ -273,19 +306,17 @@ The following parameters are always overridden server-side: `redirect_uri`, `sta
## 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 +325,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-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.
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 host name (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
@@ -303,35 +334,36 @@ If you pass `redirect: false` to `signOut`, the page will not reload. The sessio
:::tip
If you need to redirect to another page but you want to avoid a page reload, you can try:
`const data = await signOut({redirect: false, callbackUrl: "/foo"})`
where `data.url` is the validated url you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
where `data.url` is the validated URL you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
:::
---
## 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/client"
import { getSession } from "next-auth/react"
...
@@ -344,7 +376,7 @@ export async function getServerSideProps(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).
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 authentication check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
### Options
@@ -352,21 +384,25 @@ 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>
)
}
```
@@ -376,44 +412,44 @@ 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 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.
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side request 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()
export default function AdminDashboard() {
const { data: session } = useSession()
// session is always non-null inside this page, all the way down the React tree.
return "Some super secret dashboard"
}
@@ -422,19 +458,25 @@ AdminDashboard.auth = true
```
```jsx title="pages/_app.jsx"
export default function App({ Component, pageProps }) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={pageProps.session}>
{Component.auth
? <Auth><Component {...pageProps} /></Auth>
: <Component {...pageProps} />
}
<SessionProvider session={session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
)
}
function Auth({ children }) {
const [session, loading] = useSession()
const { data: session, loading } = useSession()
const isUser = !!session?.user
React.useEffect(() => {
if (loading) return // Do nothing while loading
@@ -444,7 +486,7 @@ function Auth({ children }) {
if (isUser) {
return children
}
// Session is being fetched, or no user.
// If no user, useEffect() will redirect.
return <div>Loading...</div>
@@ -456,20 +498,19 @@ It can be easily be extended/modified to support something like an options objec
```jsx title="pages/admin.jsx"
AdminDashboard.auth = {
role: "admin",
loading: <AdminLoadingSkeleton/>,
unauthorized: "/login-with-different-user" // redirect to this url
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.
Because of how `_app` is written, it won't unnecessarily contact the `/api/auth/session` endpoint for pages that do not require authentication.
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
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).
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 `Provider` from `next-auth/client`.
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

@@ -18,25 +18,22 @@ 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,
})
```
All requests to `/api/auth/*` (signin, callback, signout, etc) will automatically be handed by NextAuth.js.
All requests to `/api/auth/*` (`signIn`, callback, `signOut`, etc.) will automatically be handled by NextAuth.js.
:::tip
See the [options documentation](/configuration/options) for how to configure providers, databases and other options.
@@ -44,24 +41,27 @@ See the [options documentation](/configuration/options) for how to configure pro
### Add React Hook
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
The [`useSession()`](http://localhost:3000/getting-started/client#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'
```javascript
import { useSession, signIn, signOut } from "next-auth/react"
export default function Page() {
const [ session, loading ] = useSession()
return <>
{!session && <>
Not signed in <br/>
export default function Component() {
const { data: session } = 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>
</>}
{session && <>
Signed in as {session.user.email} <br/>
<button onClick={() => signOut()}>Sign out</button>
</>}
</>
</>
)
}
```
@@ -69,22 +69,28 @@ export default function Page() {
You can use the `useSession` hook from anywhere in your application (e.g. in a header component).
:::
### Add session state
### Share/configure session state
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`.
To be able to use `useSession` first you'll need to expose the session context, [`<SessionProvider />`](http://localhost:3000/getting-started/client#sessionprovider), at the top level of your application:
```jsx title="pages/_app.js"
import { Provider } from 'next-auth/client'
```javascript
// pages/_app.js
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>
)
}
```
In this way instances of `useSession` can have access to the session data and status, otherwise they'll throw an error... `<SessionProvider />` also takes care of keeping the session updated and synced between browser tabs and windows.
:::tip
Check out the [client documentation](/getting-started/client) to see how you can improve the user experience and page performance by using the NextAuth.js client.
:::

View File

@@ -64,19 +64,19 @@ import NextAuth from "next-auth"
export default NextAuth({
callbacks: {
session(session, token) {
return session // The type here should match the one returned in `useSession()`
session({ session, token, user }) {
return session // The return type will match the one returned in `useSession()`
},
},
})
```
```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()`
const [session] = useSession()
// `session` will match the returned value of `callbacks.session()` from `NextAuth()`
const { data: session } = useSession()
return (
// Your component
@@ -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

@@ -5,11 +5,7 @@ title: Azure Active Directory B2C
## Documentation
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
## Configuration
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
https://docs.microsoft.com/en-us/azure/active-directory-b2c/
## Options
@@ -19,34 +15,86 @@ The **Azure Active Directory Provider** comes with a set of default options:
You can override any of the options to suit your own use case.
## Example
## Configuration (Basic)
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
- Make sure to remember / copy
- Application (client) ID
- Directory (tenant) ID
- When asked for a redirection URL, use http://localhost:3000/api/auth/callback/azure-ad-b2c
- Create a new secret and remember / copy its value immediately, it will disappear.
Basic configuration sets up Azure AD B2C to return an ID Token. This should be done as a prerequisite prior to running through the Advanced configuration.
Step 1: Azure AD B2C Tenant
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
Step 2: App Registration
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications
Step 3: User Flow
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows
Note: For the step "User attributes and token claims" you might minimally:
- Collect attribute:
- Email Address
- Display Name
- Given Name
- Surname
- Return claim:
- Email Addresses
- Display Name
- Given Name
- Surname
- Identity Provider
- Identity Provider Access Token
- User's Object ID
## Example
In `.env.local` create the following entries:
```
AZURE_CLIENT_ID=<copy Application (client) ID here>
AZURE_CLIENT_SECRET=<copy generated secret value here>
AZURE_TENANT_ID=<copy the tenant id here>
AZURE_AD_B2C_TENANT_NAME=<copy the B2C tenant name here from Step 1>
AZURE_AD_B2C_CLIENT_ID=<copy Application (client) ID here from Step 2>
AZURE_AD_B2C_CLIENT_SECRET=<copy generated secret value here from Step 2>
AZURE_AD_B2C_PRIMARY_USER_FLOW=<copy the name of the signin user flow you created from Step 3>
```
In `pages/api/auth/[...nextauth].js` find or add the AZURE entries:
In `pages/api/auth/[...nextauth].js` find or add the AZURE_AD_B2C entries:
```js
import Providers from 'next-auth/providers';
...
providers: [
Providers.AzureADB2C({
clientId: process.env.AZURE_CLIENT_ID,
clientSecret: process.env.AZURE_CLIENT_SECRET,
scope: 'offline_access User.Read',
tenantId: process.env.AZURE_TENANT_ID,
tenantName: process.env.AZURE_AD_B2C_TENANT_NAME,
clientId: process.env.AZURE_AD_B2C_CLIENT_ID,
clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET,
primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
scope: `offline_access openid`,
}),
]
...
```
## Configuration (Advanced)
Advanced configuration sets up Azure AD B2C to return an Authorization Token. This builds on the steps completed in the Basic configuration above.
Step 4: Add a Web API application
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-single-page-app-webapi?tabs=app-reg-ga
Note: this is a second app registration (similar to Step 2) but with different setup and configuration.
## Example
Nothing in `.env.local` needs to change here. The only update is in `pages/api/auth/[...nextauth].js` where you will need to add the additional scopes that were created in Step 4 above:
```js
import Providers from 'next-auth/providers';
...
providers: [
Providers.AzureADB2C({
tenantName: process.env.AZURE_AD_B2C_TENANT_NAME,
clientId: process.env.AZURE_AD_B2C_CLIENT_ID,
clientSecret: process.env.AZURE_AD_B2C_CLIENT_SECRET,
primaryUserFlow: process.env.AZURE_AD_B2C_PRIMARY_USER_FLOW,
scope: `https://${process.env.AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/api/demo.read https://${process.env.AZURE_AD_B2C_TENANT_NAME}.onmicrosoft.com/api/demo.write offline_access openid`,
}),
]
...

View File

@@ -0,0 +1,53 @@
---
id: azure-ad
title: Azure Active Directory
---
## Documentation
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
## Configuration
https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
## Example
### To allow specific Active Directory users access:
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
- Pay close attention to "Who can use this application or access this API?"
- This allows you to scope access to specific types of user accounts.
- Make sure to remember / copy
- Application (client) ID
- Directory (tenant) ID
- When asked for a redirection URL, use http://localhost:3000/api/auth/callback/azure-ad
- Create a new secret and remember / copy its value immediately, it will disappear.
In `.env.local` create the follwing entries:
```
AZURE_AD_CLIENT_ID=<copy Application (client) ID here>
AZURE_AD_CLIENT_SECRET=<copy generated secret value here>
AZURE_AD_TENANT_ID=<copy the tenant id here>
```
**Note**: Omit the `AZURE_TENANT_ID` if you created the App Registration for:
> Accounts in any organizational directory (Any Azure AD directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)
That will default the tenant to use the `common` authorization endpoint. [For more details see here](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints).
In `pages/api/auth/[...nextauth].js` find or add the `AzureAD` entries:
```js
import Providers from 'next-auth/providers';
...
providers: [
Providers.AzureAD({
clientId: process.env.AZURE_AD_CLIENT_ID,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET,
tenantId: process.env.AZURE_AD_TENANT_ID, // omit this if it was omitted above.
scope: 'offline_access User.Read',
}),
]
...
```

View File

@@ -29,15 +29,15 @@ The Credentials provider is specified like other providers, except that you need
1. A `user` object, which indicates the credentials are valid.
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
2. Either `false` or `null`, which indicates failure.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
If you return `false` or `null` then an error will be displayed advising the user to check their details.
3. You can throw an Error or a URL (a string).
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
The Credentials provider's `authorize()` method also provides the request object as the second parameter (see example below).
@@ -88,34 +88,43 @@ You can also use them in conjunction with other provider options.
As with all providers, the order you specify them is the order they are displayed on the sign in page.
```js
providers: [
Providers.Credentials({
id: 'domain-login',
name: "Domain Account",
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
providers: [
Providers.Credentials({
id: "domain-login",
name: "Domain Account",
async authorize(credentials, req) {
const user = {
/* add function to get user */
}
return user
},
credentials: {
domain: {
label: "Domain",
type: "text ",
placeholder: "CORPNET",
value: "CORPNET",
},
credentials: {
domain: { label: "Domain", type: "text ", placeholder: "CORPNET", value: "CORPNET" },
username: { label: "Username", type: "text ", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
}
}),
Providers.Credentials({
id: 'intranet-credentials',
name: "Two Factor Auth",
async authorize(credentials, req) {
const user = { /* add function to get user */ }
return user
},
credentials: {
email: { label: "Username", type: "text ", placeholder: "jsmith" },
"2fa-key": { label: "2FA Key" }
},
}),
/* ... additional providers ... /*/
]
username: { label: "Username", type: "text ", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
}),
Providers.Credentials({
id: "intranet-credentials",
name: "Two Factor Auth",
async authorize(credentials, req) {
const user = {
/* add function to get user */
}
return user
},
credentials: {
email: { label: "Username", type: "text ", placeholder: "jsmith" },
"2fa-key": { label: "2FA Key" },
},
}),
/* ... additional providers ... /*/
]
```
### Example UI
@@ -126,7 +135,7 @@ You can also [use a custom sign in page](/configuration/pages#credentials-sign-i
<Image src="/img/signin-complex.png"/>
export const Image = ({ children, src, alt = '' }) => (
export const Image = ({ children, src, alt = '' }) => (
<div
style={{
padding: '0.2rem',
@@ -136,4 +145,4 @@ export const Image = ({ children, src, alt = '' }) => (
}}>
<img alt={alt} src={src} />
</div>
)
)

View File

@@ -13,7 +13,7 @@ The Email provider can be used in conjunction with (or instead of) one or more O
### How it works
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used within that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
If someone provides the email address of an _existing account_ when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.

View File

@@ -34,7 +34,7 @@ providers: [
```
:::warning
Google only provide the Refresh Token to an application the first time a user signs in.
Google only provides Refresh Token to an application the first time a user signs in.
To force Google to re-issue a Refresh Token, the user needs to remove the application from their account and sign in again:
https://myaccount.google.com/permissions
@@ -60,7 +60,7 @@ const options = {
:::
:::tip
Google also return an `verified_email` boolean property in the OAuth profile.
Google also returns a `verified_email` boolean property in the OAuth profile.
You can use this property to restrict access to people with verified accounts at a particular domain.

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