mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
41 Commits
next-auth@
...
v4.0.0-nex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e5afa162 | ||
|
|
53e5e37948 | ||
|
|
8ff4b26014 | ||
|
|
2c35aa27f9 | ||
|
|
2833b661bd | ||
|
|
6c1a0ec620 | ||
|
|
988c9912b1 | ||
|
|
a225324d4f | ||
|
|
3a48b8e467 | ||
|
|
fb50b54466 | ||
|
|
fa89431573 | ||
|
|
3383857715 | ||
|
|
bbc2d9b538 | ||
|
|
d10bd9beba | ||
|
|
c1c866f664 | ||
|
|
86ff89e296 | ||
|
|
dd12181378 | ||
|
|
47c17a89ae | ||
|
|
c07fe1b9a7 | ||
|
|
abaa5aed65 | ||
|
|
ca0ed1e2a8 | ||
|
|
ed345346db | ||
|
|
5ac1db741a | ||
|
|
0c17af969e | ||
|
|
ea9b6e37a9 | ||
|
|
960bc1e9c0 | ||
|
|
d29e3e9c9d | ||
|
|
a388b44d0b | ||
|
|
b6a3a72db4 | ||
|
|
edcb10a823 | ||
|
|
2acabe19e0 | ||
|
|
a6f5f4c184 | ||
|
|
9fa93e3b5e | ||
|
|
cb4342fdda | ||
|
|
5f717b3914 | ||
|
|
d09a45ec7c | ||
|
|
930f58eba3 | ||
|
|
c20b7f2930 | ||
|
|
e418cddd96 | ||
|
|
111e7aabdf | ||
|
|
a113ef6fab |
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +1,2 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
/__tests__/ @lluia
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||
|
||||
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
# npx pretty-quick --staged
|
||||
|
||||
@@ -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
110
README.md
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" }]],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -8,4 +8,5 @@ module.exports = {
|
||||
collectCoverageFrom: ["!client/__tests__/**"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
coverageDirectory: "../coverage",
|
||||
testEnvironment: "jsdom",
|
||||
}
|
||||
|
||||
17
config/version-pr.js
Normal file
17
config/version-pr.js
Normal 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
34605
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
113
package.json
113
package.json
@@ -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": {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as TypeORM from "./typeorm"
|
||||
import * as Prisma from "./prisma"
|
||||
|
||||
export { TypeORM, Prisma }
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma,
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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", () => ({
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./helpers/mocks"
|
||||
import { signIn } from ".."
|
||||
import { signIn } from "../react"
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
387
src/client/react.js
vendored
Normal 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() })
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
24
src/providers/azure-ad.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
18
src/providers/naver.js
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
36
types/adapters.d.ts
vendored
@@ -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
95
types/index.d.ts
vendored
@@ -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 {}
|
||||
|
||||
34
types/internals/client.d.ts
vendored
34
types/internals/client.d.ts
vendored
@@ -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>
|
||||
40
types/internals/index.d.ts
vendored
40
types/internals/index.d.ts
vendored
@@ -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
37
types/internals/react.d.ts
vendored
Normal 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
31
types/providers.d.ts
vendored
@@ -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
|
||||
/**
|
||||
|
||||
88
types/client.d.ts → types/react-client.d.ts
vendored
88
types/client.d.ts → types/react-client.d.ts
vendored
@@ -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>
|
||||
@@ -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",
|
||||
},
|
||||
})
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -15,6 +15,7 @@ There you can find the following adapters:
|
||||
- [`fauna`](./fauna)
|
||||
- [`dynamodb`](./dynamodb)
|
||||
- [`firebase`](./firebase)
|
||||
- [`pouchdb`](./pouchdb)
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -84,4 +84,4 @@ CREATE UNIQUE INDEX email
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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/...`.
|
||||
:::
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
237
www/docs/faq.md
237
www/docs/faq.md
@@ -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
|
||||
{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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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`,
|
||||
}),
|
||||
]
|
||||
...
|
||||
|
||||
53
www/docs/providers/azure-ad.md
Normal file
53
www/docs/providers/azure-ad.md
Normal 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',
|
||||
}),
|
||||
]
|
||||
...
|
||||
|
||||
```
|
||||
@@ -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>
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user