mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29862ac887 | ||
|
|
5aa2b61b88 | ||
|
|
929c644653 | ||
|
|
2657e72e81 | ||
|
|
8ff7dbb18f | ||
|
|
748d576a5a | ||
|
|
9f16e3f0fb | ||
|
|
1042e9a93d | ||
|
|
aa57f2dd7e | ||
|
|
1817286ce3 | ||
|
|
b942dd34f3 | ||
|
|
4d9622e1cc | ||
|
|
a7eadf80e5 | ||
|
|
75c7dbc3e7 | ||
|
|
d36b89cb12 | ||
|
|
349cd03fbd | ||
|
|
5cd130669b | ||
|
|
638233f4a0 | ||
|
|
37e175195f | ||
|
|
e8a9e8aeb6 | ||
|
|
1fb308a6f4 | ||
|
|
613c303315 | ||
|
|
d24fe1cebb | ||
|
|
885b02ca95 | ||
|
|
f218697fd6 | ||
|
|
dbead0ad85 | ||
|
|
704ded5310 | ||
|
|
25fbcb4648 | ||
|
|
53a439b44b | ||
|
|
16a2e37fd6 | ||
|
|
0392a8df9a | ||
|
|
a459b95c5b | ||
|
|
13df7eb81d | ||
|
|
62f261209c | ||
|
|
da43d0d896 | ||
|
|
4b1271ba75 | ||
|
|
d30da0170f | ||
|
|
887b2985fc | ||
|
|
d2bbac1164 |
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# Simple check that the build is valid and no linting errors.
|
||||
# Currently is run as a seperate workflow as it's fast to fail.
|
||||
name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
68
.github/workflows/codeql-analysis.yml
vendored
68
.github/workflows/codeql-analysis.yml
vendored
@@ -1,67 +1,27 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
name: Code Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, beta, next ]
|
||||
branches: [main, beta, next]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
- cron: "43 17 * * 2"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
name: Verify
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
language: ["javascript"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
|
||||
57
.github/workflows/integration.yml
vendored
57
.github/workflows/integration.yml
vendored
@@ -1,57 +0,0 @@
|
||||
name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# Only run tests integration against Pull Requests from branches in
|
||||
# this repository. We do this as integration tests require access to
|
||||
# secrets in GitHub and they are not exposed to tests run against
|
||||
# forks (for security reasons), so integration test against
|
||||
# Pull Requests from external repos just fail and generate noise.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
|
||||
# We use self-hosted runners as cloud based runnners (e.g. AWS, GPC)
|
||||
# fail due to IP Address checks done by providers, which enforce
|
||||
# CAPTCHA checks on login request from cloud compute IP addresses to
|
||||
# prevent abuse.
|
||||
runs-on: self-hosted
|
||||
|
||||
# Target time is under 5 minutes to run all tests. If it takes longer than
|
||||
# 10 minutes should look at running tests in parallel. No individual flow
|
||||
# should take longer than 5 minutes to build and run.
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
# TODO Tests should exit out if env vars not set (currently hangs)
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
NEXTAUTH_TWITTER_ID: ${{secrets.NEXTAUTH_TWITTER_ID}}
|
||||
NEXTAUTH_TWITTER_SECRET: ${{secrets.NEXTAUTH_TWITTER_SECRET}}
|
||||
NEXTAUTH_TWITTER_USERNAME: ${{secrets.NEXTAUTH_TWITTER_USERNAME}}
|
||||
NEXTAUTH_TWITTER_PASSWORD: ${{secrets.NEXTAUTH_TWITTER_PASSWORD}}
|
||||
NEXTAUTH_GITHUB_ID: ${{secrets.NEXTAUTH_GITHUB_ID}}
|
||||
NEXTAUTH_GITHUB_SECRET: ${{secrets.NEXTAUTH_GITHUB_SECRET}}
|
||||
NEXTAUTH_GITHUB_USERNAME: ${{secrets.NEXTAUTH_GITHUB_USERNAME}}
|
||||
NEXTAUTH_GITHUB_PASSWORD: ${{secrets.NEXTAUTH_GITHUB_PASSWORD}}
|
||||
12
.github/workflows/labeler.yml
vendored
12
.github/workflows/labeler.yml
vendored
@@ -1,11 +1,13 @@
|
||||
name: "Pull Request Labeler"
|
||||
name: PR Labeler
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
name: Triage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
34
.github/workflows/release.yml
vendored
34
.github/workflows/release.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Release
|
||||
name: Release Flow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -7,20 +8,35 @@ on:
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: "Release"
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npx semantic-release@17
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- name: Build
|
||||
run: npm run build
|
||||
release:
|
||||
name: Release
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v1
|
||||
- name: Dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Release
|
||||
run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
|
||||
27
.github/workflows/types.yml
vendored
27
.github/workflows/types.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Types
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
run: npm run test:types
|
||||
1
.husky/.gitignore
vendored
Normal file
1
.husky/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_
|
||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx pretty-quick --staged
|
||||
@@ -95,7 +95,7 @@ The databases can take a few seconds to start up, so you might need to give it a
|
||||
|
||||
## For maintainers
|
||||
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
@@ -113,9 +113,9 @@ type(scope): title
|
||||
body
|
||||
```
|
||||
|
||||
Scope is the part that will help groupping the different commit types in the release notes.
|
||||
Scope is the part that will help grouping the different commit types in the release notes.
|
||||
|
||||
Some recommened scopes are:
|
||||
Some recommended scopes are:
|
||||
|
||||
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/actions/workflows/release.yml/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
@@ -84,7 +81,7 @@ Advanced options allow you to define your own routines to handle controlling wha
|
||||
|
||||
### TypeScript
|
||||
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentaion.
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
|
||||
|
||||
The package at `@types/next-auth` is now deprecated.
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ If you contact us regarding a serious issue:
|
||||
* We will endeavor to get back to you within 72 hours.
|
||||
* We will aim to publish a fix within 30 days.
|
||||
* We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
|
||||
* If 90 days has elapsed and we still don't have a fix, we will disclose the issue publically.
|
||||
* 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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
@@ -18,5 +18,16 @@ module.exports = {
|
||||
test: ["../src/server/pages/**"],
|
||||
presets: ["preact"],
|
||||
},
|
||||
{
|
||||
test: ["../src/**/*.test.js"],
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
2
config/jest-setup.js
Normal file
2
config/jest-setup.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import "whatwg-fetch"
|
||||
8
config/jest.config.js
Normal file
8
config/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
|
||||
},
|
||||
roots: ["../src"],
|
||||
setupFilesAfterEnv: ["./jest-setup.js"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
}
|
||||
41063
package-lock.json
generated
41063
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -37,7 +37,8 @@
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test": "jest --config ./config/jest.config.js",
|
||||
"test:ci": "npm run lint && npm run test:types && npm run test -- --ci",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"lint": "eslint .",
|
||||
@@ -90,15 +91,20 @@
|
||||
"@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.14.2",
|
||||
"@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",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
@@ -108,16 +114,22 @@
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.6",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"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"
|
||||
"typescript": "^4.1.3",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
@@ -144,7 +156,23 @@
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*test.js"
|
||||
],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
87
src/client/__tests__/mocks.js
Normal file
87
src/client/__tests__/mocks.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { setupServer } from "msw/node"
|
||||
import { rest } from "msw"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export const mockSession = {
|
||||
user: {
|
||||
image: null,
|
||||
name: "John",
|
||||
email: "john@email.com",
|
||||
},
|
||||
expires: 123213139,
|
||||
}
|
||||
|
||||
export const mockProviders = {
|
||||
github: {
|
||||
id: "github",
|
||||
name: "Github",
|
||||
type: "oauth",
|
||||
signinUrl: "path/to/signin",
|
||||
callbackUrl: "path/to/callback",
|
||||
},
|
||||
credentials: {
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
},
|
||||
email: {
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCSRFToken = {
|
||||
csrfToken: randomBytes(32).toString("hex"),
|
||||
}
|
||||
|
||||
export const mockGithubResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/github/url",
|
||||
}
|
||||
|
||||
export const mockCredentialsResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/credentials/url",
|
||||
}
|
||||
|
||||
export const mockEmailResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/email/url",
|
||||
}
|
||||
|
||||
export const mockSignOutResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/signout/url",
|
||||
}
|
||||
|
||||
export const server = setupServer(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSignOutResponse))
|
||||
),
|
||||
rest.get("/api/auth/session", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
),
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCSRFToken))
|
||||
),
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockProviders))
|
||||
),
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
),
|
||||
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCredentialsResponse))
|
||||
),
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmailResponse))
|
||||
),
|
||||
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
|
||||
)
|
||||
97
src/client/__tests__/session.test.js
Normal file
97
src/client/__tests__/session.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from ".."
|
||||
import { getBroadcastEvents } from "./utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => server.close())
|
||||
|
||||
test("if it can fetch the session, it should store it in `localStorage`", async () => {
|
||||
render(<SessionFlow />)
|
||||
|
||||
// In the start, there is no session
|
||||
const noSession = await screen.findByText("No session")
|
||||
expect(noSession).toBeInTheDocument()
|
||||
|
||||
// After we fetched the session, it should have been rendered by `<SessionFlow />`
|
||||
const session = await screen.findByText(new RegExp(mockSession.user.name))
|
||||
expect(session).toBeInTheDocument()
|
||||
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "getSession",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
|
||||
test("if there's an error fetching the session, it should log it", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.body("Server error"))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SessionFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"session",
|
||||
new SyntaxError("Unexpected token S in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SessionFlow() {
|
||||
const [session, setSession] = useState(null)
|
||||
useEffect(() => {
|
||||
async function fetchUserSession() {
|
||||
try {
|
||||
const result = await getSession({})
|
||||
setSession(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
fetchUserSession()
|
||||
}, [])
|
||||
|
||||
if (session) {
|
||||
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
}
|
||||
return <p>No session</p>
|
||||
}
|
||||
290
src/client/__tests__/sign-in.test.js
Normal file
290
src/client/__tests__/sign-in.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../lib/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./mocks"
|
||||
import { signIn } from ".."
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider, it redirects to the default sign-in page",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider supplied and no callback URL, redirects using the current location",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | mockUrl
|
||||
${`email`} | ${mockEmailResponse.url}
|
||||
${`credentials`} | ${mockCredentialsResponse.url}
|
||||
`(
|
||||
"$provider provider redirects if `redirect` is `true`",
|
||||
async ({ provider, mockUrl }) => {
|
||||
render(<SignInFlow providerId={provider} redirect={true} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test("redirection can't be stopped using an oauth provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="github"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'credentials' provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="credentials"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockCredentialsResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn`
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/credentials/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'email' provider", async () => {
|
||||
render(
|
||||
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockEmailResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn` oauth
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/email/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockEmailResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test("params are propagated to the signin URL when supplied", async () => {
|
||||
let matchedParams = ""
|
||||
const authParams = "foo=bar&bar=foo"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) => {
|
||||
matchedParams = req.url.search
|
||||
return res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(matchedParams).toEqual(`?${authParams}`)
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
|
||||
const errorMsg = "Error when retrieving providers"
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(errorMsg))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
errorMsg
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SignInFlow({
|
||||
providerId,
|
||||
callbackUrl,
|
||||
redirect = true,
|
||||
authorizationParams = {},
|
||||
}) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignIn() {
|
||||
const result = await signIn(
|
||||
providerId,
|
||||
{
|
||||
callbackUrl,
|
||||
redirect,
|
||||
},
|
||||
authorizationParams
|
||||
)
|
||||
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signin-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={() => handleSignIn()}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
129
src/client/__tests__/sign-out.test.js
Normal file
129
src/client/__tests__/sign-out.test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSignOutResponse } from "./mocks"
|
||||
import { signOut } from ".."
|
||||
import { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./utils"
|
||||
|
||||
const { location } = window
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test("by default it redirects to the current URL if the server did not provide one", async () => {
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
|
||||
})
|
||||
})
|
||||
|
||||
test("it redirects to the URL allowed by the server", async () => {
|
||||
render(<SignOutFlow callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
mockSignOutResponse.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("if url contains a hash during redirection a page reload happens", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockSignOutResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
})
|
||||
})
|
||||
|
||||
test("will broadcast the signout event to other tabs", async () => {
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "signout",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignOutFlow({ callbackUrl, redirect = true }) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function setSignOutRes() {
|
||||
const result = await signOut({ callbackUrl, redirect })
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signout-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={() => setSignOutRes()}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
src/client/__tests__/utils.js
Normal file
8
src/client/__tests__/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getBroadcastEvents() {
|
||||
return window.localStorage.setItem.mock.calls
|
||||
.filter((call) => call[0] === "nextauth.message")
|
||||
.map(([eventName, value]) => {
|
||||
const { timestamp, ...rest } = JSON.parse(value)
|
||||
return { eventName, value: rest }
|
||||
})
|
||||
}
|
||||
@@ -8,9 +8,15 @@
|
||||
//
|
||||
// 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'
|
||||
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
|
||||
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
|
||||
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,
|
||||
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
|
||||
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
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)
|
||||
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
// 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' }))
|
||||
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)
|
||||
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) {
|
||||
export function useSession(session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
function _useSessionHook(session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
const triggeredByStorageEvent = event === "storage"
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
} 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 }
|
||||
if (clientSession === undefined) {
|
||||
__NEXTAUTH._clientSession = null
|
||||
}
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
triggerEvent: !triggeredByStorageEvent,
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
logger.error("CLIENT_USE_SESSION_ERROR", error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -137,114 +158,112 @@ function _useSessionHook (session) {
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
export async function getCsrfToken(ctx) {
|
||||
return (await _fetchData("csrf", ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
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}`
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
|
||||
const res = await fetch(_signInUrl, {
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
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')
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
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()
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -252,13 +271,18 @@ export async function signOut (options = {}) {
|
||||
// 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 } = {}) {
|
||||
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
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
@@ -269,12 +293,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
await __NEXTAUTH._getSession({ event: "timer" })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider ({ children, session, options }) {
|
||||
export function Provider({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
@@ -290,24 +314,25 @@ export function Provider ({ children, session, options }) {
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
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)
|
||||
logger.error("CLIENT_FETCH_ERROR", path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
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')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
@@ -318,7 +343,7 @@ function _apiBaseUrl () {
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
@@ -328,35 +353,34 @@ function _now () {
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
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) => {
|
||||
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
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
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
|
||||
@@ -368,7 +392,7 @@ export {
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout
|
||||
signOut as signout,
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -390,5 +414,5 @@ export default {
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
signout: signOut,
|
||||
}
|
||||
|
||||
24
src/providers/coinbase.js
Normal file
24
src/providers/coinbase.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function Coinbase(options) {
|
||||
return {
|
||||
id: "coinbase",
|
||||
name: "Coinbase",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "wallet:user:email wallet:user:read",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
requestTokenUrl: "https://api.coinbase.com/oauth/token",
|
||||
authorizationUrl:
|
||||
"https://www.coinbase.com/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.coinbase.com/v2/user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.name,
|
||||
email: profile.data.email,
|
||||
image: profile.data.avatar_url,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export default function WorkOS(options) {
|
||||
const domain = options.domain || 'api.workos.com';
|
||||
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
@@ -10,9 +12,9 @@ export default function WorkOS(options) {
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: 'https://api.workos.com/sso/token/',
|
||||
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
|
||||
profileUrl: 'https://api.workos.com/sso/profile/',
|
||||
accessTokenUrl: `https://${domain}/sso/token`,
|
||||
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/sso/profile`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
|
||||
20
src/providers/zoom.js
Normal file
20
src/providers/zoom.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function Zoom(options) {
|
||||
return {
|
||||
id: "zoom",
|
||||
name: "Zoom",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://zoom.us/oauth/token",
|
||||
authorizationUrl: "https://zoom.us/oauth/authorize?response_type=code",
|
||||
profileUrl: "https://api.zoom.us/v2/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
email: profile.email,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
scope: provider.scope,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope
|
||||
redirect_uri: provider.callbackUrl
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default function providers (req, res) {
|
||||
export default function providers(req, res) {
|
||||
const { providers } = req.options
|
||||
|
||||
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
}, {})
|
||||
const result = providers.reduce(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
@@ -3,31 +3,15 @@
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": [
|
||||
"./types"
|
||||
],
|
||||
"next-auth": [
|
||||
"./src/server"
|
||||
],
|
||||
"next-auth/adapters": [
|
||||
"./src/adapters"
|
||||
],
|
||||
"next-auth/client": [
|
||||
"./src/client"
|
||||
],
|
||||
"next-auth/jwt": [
|
||||
"./src/lib/jwt"
|
||||
],
|
||||
"next-auth/providers": [
|
||||
"./src/providers"
|
||||
]
|
||||
"types": ["./types"],
|
||||
"next-auth": ["./src/server"],
|
||||
"next-auth/adapters": ["./src/adapters"],
|
||||
"next-auth/client": ["./src/client"],
|
||||
"next-auth/jwt": ["./src/lib/jwt"],
|
||||
"next-auth/providers": ["./src/providers"]
|
||||
},
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
@@ -44,9 +28,8 @@
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js"
|
||||
"**/*.js",
|
||||
".eslintrc.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
2
types/providers.d.ts
vendored
2
types/providers.d.ts
vendored
@@ -63,6 +63,7 @@ export type OAuthProviderType =
|
||||
| "Box"
|
||||
| "Bungie"
|
||||
| "Cognito"
|
||||
| "Coinbase"
|
||||
| "Discord"
|
||||
| "Dropbox"
|
||||
| "EVEOnline"
|
||||
@@ -97,6 +98,7 @@ export type OAuthProviderType =
|
||||
| "WorkOS"
|
||||
| "Yandex"
|
||||
| "Zoho"
|
||||
| "Zoom"
|
||||
|
||||
export type OAuthProvider = (options: Partial<OAuthConfig>) => OAuthConfig
|
||||
|
||||
|
||||
71
www/docs/adapters/dynamodb.md
Normal file
71
www/docs/adapters/dynamodb.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: dynamodb
|
||||
title: DynamoDB Adapter
|
||||
---
|
||||
|
||||
# DynamoDB
|
||||
|
||||
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
|
||||
|
||||
You need a table with a partition key `pk` and a sort key `sk`. Your table also needs a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. You can set whatever you want as the table name and the billing method.
|
||||
|
||||
You can find the full schema in the table structure section below.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter@canary`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/dynamodb-adapter@canary
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
You need to pass `DocumentClient` instance from `aws-sdk` to the adapter.
|
||||
The default table name is `next-auth`, but you can customise that by passing `{ tableName: 'your-table-name' }` as the second parameter in the adapter.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import AWS from "aws-sdk";
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
|
||||
|
||||
AWS.config.update({
|
||||
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY,
|
||||
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY,
|
||||
region: process.env.NEXT_AUTH_AWS_REGION,
|
||||
});
|
||||
|
||||
export default NextAuth({
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
adapter: DynamoDBAdapter(
|
||||
new AWS.DynamoDB.DocumentClient()
|
||||
),
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
|
||||
|
||||
## Schema
|
||||
|
||||
The table respects the single table design pattern. This has many advantages:
|
||||
|
||||
- Only one table to manage, monitor and provision.
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retrieving all sessions for a user).
|
||||
- Only one table needs to be replicated, if you want to go multi-region.
|
||||
|
||||
Here is a schema of the table :
|
||||
|
||||

|
||||
84
www/docs/adapters/fauna.md
Normal file
84
www/docs/adapters/fauna.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
id: fauna
|
||||
title: FaunaDB Adapter
|
||||
---
|
||||
|
||||
# FaunaDB
|
||||
|
||||
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/fauna-adapter@canary`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/fauna-adapter@canary
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import * as Fauna from "faunadb"
|
||||
import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
|
||||
const client = new Fauna.Client({
|
||||
secret: "secret",
|
||||
scheme: "http",
|
||||
domain: "localhost",
|
||||
port: 8443,
|
||||
})
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FaunaAdapter({ faunaClient: client})
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
```javascript
|
||||
CreateCollection({ name: "accounts" })
|
||||
CreateCollection({ name: "sessions" })
|
||||
CreateCollection({ name: "users" })
|
||||
CreateCollection({ name: "verification_requests" })
|
||||
CreateIndex({
|
||||
name: "account_by_provider_account_id",
|
||||
source: Collection("accounts"),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ["data", "providerId"] },
|
||||
{ field: ["data", "providerAccountId"] },
|
||||
],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "session_by_token",
|
||||
source: Collection("sessions"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "sessionToken"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "user_by_email",
|
||||
source: Collection("users"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "email"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "verification_request_by_token",
|
||||
source: Collection("verification_requests"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],
|
||||
})
|
||||
```
|
||||
73
www/docs/adapters/firebase.md
Normal file
73
www/docs/adapters/firebase.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: firebase
|
||||
title: Firebase Adapter
|
||||
---
|
||||
|
||||
# Firebase
|
||||
|
||||
This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter@canary`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/firebase-adapter@canary
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
import firebase from "firebase/app"
|
||||
import "firebase/firestore"
|
||||
|
||||
const firestore = (
|
||||
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
|
||||
).firestore()
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirebaseAdapter(firestore),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
:::tip **From Firebase**
|
||||
|
||||
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
|
||||
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
:::
|
||||
42
www/docs/adapters/overview.md
Normal file
42
www/docs/adapters/overview.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
id: overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
|
||||
The adapters can be found in their own repository under [`nextauthjs/adapters`](https://github.com/nextauthjs/adapters).
|
||||
|
||||
There you can find the following adapters:
|
||||
|
||||
- [`typeorm-legacy`](./typeorm/typeorm-overview)
|
||||
- [`prisma`](./prisma)
|
||||
- [`prisma-legacy`](./prisma-legacy)
|
||||
- [`fauna`](./fauna)
|
||||
- [`dynamodb`](./dynamodb)
|
||||
- [`firebase`](./firebase)
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
|
||||
|
||||
### Editor integration
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @type { import("next-auth/adapters").Adapter } */
|
||||
const MyAdapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
63
www/docs/adapters/pouchdb.md
Normal file
63
www/docs/adapters/pouchdb.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
id: pouchdb
|
||||
title: PouchDB Adapter
|
||||
---
|
||||
|
||||
# PouchDB
|
||||
|
||||
This is the PouchDB Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
Depending on your architecture you can use PouchDB's http adapter to reach any database compliant with the CouchDB protocol (CouchDB, Cloudant, ...) or use any other PouchDB compatible adapter (leveldb, in-memory, ...)
|
||||
|
||||
## Getting Started
|
||||
|
||||
> **Prerequesite**: Your PouchDB instance MUST provide the `pouchdb-find` plugin since it is used internally by the adapter to build and manage indexes
|
||||
|
||||
1. Install `next-auth` and `@next-auth/pouchdb-adapter@canary`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/pouchdb-adapter@canary
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/auth/[...nextauth].js` next-auth configuration object
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { PouchDBAdapter } from "@next-auth/pouchdb-adapter"
|
||||
import PouchDB from "pouchdb"
|
||||
|
||||
// Setup your PouchDB instance and database
|
||||
PouchDB
|
||||
.plugin(require("pouchdb-adapter-leveldb")) // Any other adapter
|
||||
.plugin(require("pouchdb-find")) // Don't forget the `pouchdb-find` plugin
|
||||
|
||||
const pouchdb = new PouchDB("auth_db", { adapter: "leveldb" })
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: PouchDBAdapter(pouchdb),
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
### Memory-First Caching Strategy
|
||||
|
||||
If you need to boost your authentication layer performance, you may use PouchDB's powerful sync features and various adapters, to build a memory-first caching strategy.
|
||||
|
||||
Use an in-memory PouchDB as your main authentication database, and synchronize it with any other persisted PouchDB. You may do a one way, one-off replication at startup from the persisted PouchDB into the in-memory PouchDB, then two-way, continuous, retriable sync.
|
||||
|
||||
This will most likely not increase performance much in a serverless environment due to various reasons such as concurrency, function startup time increases, etc.
|
||||
|
||||
For more details, please see https://pouchdb.com/api.html#sync
|
||||
|
||||
@@ -1,62 +1,16 @@
|
||||
---
|
||||
id: adapters
|
||||
title: Database Adapters
|
||||
id: prisma-legacy
|
||||
title: Prisma Adapter (Legacy)
|
||||
---
|
||||
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
# Prisma (Legacy)
|
||||
|
||||
You do not need to specify an Adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma Adapter instead of the default TypeORM Adapter, or if you are creating a custom Adapter to connect to a database that is not one of the supported databases.
|
||||
You can also use NextAuth.js with the built-in Adapter for [Prisma](https://www.prisma.io/docs/). This is included in the core `next-auth` package at the moment. The other adapter needs to be installed from its own additional package.
|
||||
|
||||
### Database Schemas
|
||||
|
||||
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
|
||||
|
||||
- [MySQL Schema](/schemas/mysql)
|
||||
- [Postgres Schema](/schemas/postgres)
|
||||
- [Microsoft SQL Server Schema](/schemas/mssql)
|
||||
|
||||
## TypeORM Adapter
|
||||
|
||||
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
|
||||
|
||||
The default Adapter is the TypeORM Adapter, the following configuration options are exactly equivalent.
|
||||
|
||||
```javascript
|
||||
database: {
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.Default({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.TypeORM.Adapter({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
|
||||
|
||||
:::tip
|
||||
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
|
||||
|
||||
However, it should not be enabled against production databases as it may cause data loss if the configured schema does not match the expected schema!
|
||||
:::info
|
||||
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
|
||||
:::
|
||||
|
||||
## Prisma Adapter
|
||||
|
||||
You can also use NextAuth.js with the experimental Adapter for [Prisma 2](https://www.prisma.io/docs/).
|
||||
|
||||
To use this Adapter, you need to install Prisma Client and Prisma CLI:
|
||||
|
||||
```
|
||||
@@ -88,8 +42,9 @@ export default NextAuth({
|
||||
:::tip
|
||||
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
|
||||
:::
|
||||
Schema for the Prisma Adapter
|
||||
|
||||
### Prisma Schema
|
||||
## Setup
|
||||
|
||||
Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
@@ -104,7 +59,7 @@ datasource db {
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
@@ -119,12 +74,11 @@ model Account {
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
@@ -136,7 +90,7 @@ model Session {
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
@@ -148,36 +102,19 @@ model User {
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
id Int @id @default(autoincrement())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
:::note
|
||||
Set the `datasource` option appropriately for your database:
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
@@ -192,10 +129,28 @@ To configure you database to use the new schema (i.e. create tables and columns)
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
To generate a schema in this way with the above example code, you will need to specify your datbase connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
|
||||
|
||||
:::tip
|
||||
If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
let prisma
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient()
|
||||
}
|
||||
prisma = global.prisma
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Custom Models
|
||||
|
||||
You can add properties to the schema and map them to any database column names you wish, but you should not change the base properties or types defined in the example schema.
|
||||
@@ -217,46 +172,3 @@ adapter: Adapters.Prisma.Adapter({
|
||||
})
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you experience issues with Prisma opening too many database connections in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
let prisma
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient()
|
||||
}
|
||||
prisma = global.prisma
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
See the tutorial for [creating a database Adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom Adapter. Have a look at the [Adapter repository](https://github.com/nextauthjs/adapters) to see community maintained custom Adapter or add your own.
|
||||
|
||||
### Editor integration
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @type { import("next-auth/adapters").Adapter } */
|
||||
const MyAdapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
218
www/docs/adapters/prisma.md
Normal file
218
www/docs/adapters/prisma.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
id: prisma
|
||||
title: Prisma Adapter
|
||||
---
|
||||
|
||||
# Prisma
|
||||
|
||||
You can also use NextAuth.js with the new experimental Adapter for [Prisma](https://www.prisma.io/docs/). This version of the Prisma Adapter is not included in the core `next-auth` package, and must be installed separately.
|
||||
|
||||
:::info
|
||||
You may have noticed there is a `prisma` and `prisma-legacy` adapter. This is due to historical reasons, but the code has mostly converged so that there is no longer much difference between the two. The legacy adapter, however, does have the ability to rename tables which the newer version does not.
|
||||
:::
|
||||
|
||||
To use this Adapter, you need to install Prisma Client, Prisma CLI, and the separate `@next-auth/prisma-adapter@canary` package:
|
||||
|
||||
```
|
||||
npm install @prisma/client @next-auth/prisma-adapter@canary
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
Configure your NextAuth.js to use the Prisma Adapter:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: PrismaAdapter(prisma),
|
||||
})
|
||||
```
|
||||
|
||||
:::tip
|
||||
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
|
||||
:::
|
||||
Schema for the Prisma Adapter (`@next-auth/prisma-adapter`)
|
||||
|
||||
## Setup
|
||||
|
||||
Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
```json title="schema.prisma"
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
providerType String
|
||||
providerId String
|
||||
providerAccountId String
|
||||
refreshToken String?
|
||||
accessToken String?
|
||||
accessTokenExpires DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([providerId, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
expires DateTime
|
||||
sessionToken String @unique
|
||||
accessToken String @unique
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id String @id @default(cuid())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
|
||||
```
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `prisma migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev
|
||||
```
|
||||
|
||||
To generate a schema in this way with the above example code, you will need to specify your database connection string in the environment variable `DATABASE_URL`. You can do this by setting it in a `.env` file at the root of your project.
|
||||
|
||||
As this feature is experimental in Prisma, it is behind a feature flag. You should check your database schema manually after using this option. See the [Prisma documentation](https://www.prisma.io/docs/) for information on how to use `prisma migrate`.
|
||||
|
||||
## Schema History
|
||||
|
||||
Changes from the original Prisma Adapter
|
||||
|
||||
```diff
|
||||
model Account {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
- compoundId String @unique @map(name: "compound_id")
|
||||
- userId Int @map(name: "user_id")
|
||||
+ userId String
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
- providerType String @map(name: "provider_type")
|
||||
+ providerType String
|
||||
- providerId String @map(name: "provider_id")
|
||||
+ providerId String
|
||||
- providerAccountId String @map(name: "provider_account_id")
|
||||
+ providerAccountId String
|
||||
- refreshToken String? @map(name: "refresh_token")
|
||||
+ refreshToken String?
|
||||
- accessToken String? @map(name: "access_token")
|
||||
+ accessToken String?
|
||||
- accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
+ accessTokenExpires DateTime?
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@index([providerAccountId], name: "providerAccountId")
|
||||
- @@index([providerId], name: "providerId")
|
||||
- @@index([userId], name: "userId")
|
||||
- @@map(name: "accounts")
|
||||
+ @@unique([providerId, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
- userId Int @map(name: "user_id")
|
||||
+ userId String
|
||||
+ user User @relation(fields: [userId], references: [id])
|
||||
expires DateTime
|
||||
- sessionToken String @unique @map(name: "session_token")
|
||||
+ sessionToken String @unique
|
||||
- accessToken String @unique @map(name: "access_token")
|
||||
+ accessToken String @unique
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
-
|
||||
- @@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
- emailVerified DateTime? @map(name: "email_verified")
|
||||
+ emailVerified DateTime?
|
||||
image String?
|
||||
+ accounts Account[]
|
||||
+ sessions Session[]
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
- id Int @default(autoincrement()) @id
|
||||
+ id String @id @default(cuid())
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
- createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
+ createdAt DateTime @default(now())
|
||||
- updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
+ updatedAt DateTime @updatedAt
|
||||
|
||||
- @@map(name: "verification_requests")
|
||||
+ @@unique([identifier, token])
|
||||
}
|
||||
```
|
||||
49
www/docs/adapters/typeorm/overview.md
Normal file
49
www/docs/adapters/typeorm/overview.md
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: typeorm-overview
|
||||
title: Overview
|
||||
---
|
||||
|
||||
## TypeORM Adapter
|
||||
|
||||
NextAuth.js comes with a default Adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use in your project and pass a database connection string to NextAuth.js.
|
||||
|
||||
### Database Schemas
|
||||
|
||||
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
|
||||
|
||||
- [MySQL Schema](./mysql)
|
||||
- [Postgres Schema](./postgres)
|
||||
- [Microsoft SQL Server Schema](./mssql)
|
||||
- [MongoDB](./mongodb)
|
||||
|
||||
The default Adapter is the TypeORM Adapter and the default database type for TypeORM is SQLite, the following configuration options are exactly equivalent.
|
||||
|
||||
```javascript
|
||||
database: {
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.Default({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
```javascript
|
||||
adapter: Adapters.TypeORM.Adapter({
|
||||
type: "sqlite",
|
||||
database: ":memory:",
|
||||
synchronize: true,
|
||||
})
|
||||
```
|
||||
|
||||
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM Adapter. You can use these models in your own code.
|
||||
|
||||
:::tip
|
||||
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres. This will automatically apply any changes it finds in the entity model, therefore it **should not be enabled against production databases** as it may cause data loss if the configured schema does not match the expected schema!
|
||||
:::
|
||||
@@ -66,7 +66,7 @@ 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.
|
||||
|
||||
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include an property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
|
||||
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).
|
||||
|
||||
@@ -78,10 +78,10 @@ When using NextAuth.js with a database, the User object will be either a user ob
|
||||
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you only want to allow users who already have accounts in the database to sign in, you can check for the existence of a `user.id` property and reject any sign in attempts from accounts that do not have one.
|
||||
:::note
|
||||
Redirects returned by this callback cancel the authentication flow. Only redirect to error pages that, for example, tell the user why they're not allowed to sign in.
|
||||
|
||||
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
|
||||
To redirect to a page after a successful sign in, please use [the `callbackUrl` option](/getting-started/client#specifying-a-callbackurl) or [the redirect callback](/configuration/callbacks#redirect-callback).
|
||||
:::
|
||||
|
||||
## Redirect callback
|
||||
@@ -163,7 +163,7 @@ If you need to persist a large amount of data, you will need to persist it elsew
|
||||
|
||||
## Session callback
|
||||
|
||||
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitely forward it here to make it available to the client.
|
||||
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
|
||||
@@ -5,16 +5,20 @@ title: Databases
|
||||
|
||||
NextAuth.js comes with multiple ways of connecting to a database:
|
||||
|
||||
* **TypeORM** (default)<br/>
|
||||
_The TypeORM adapter supports MySQL, Postgres, MsSql, SQLite and MongoDB databases._
|
||||
* **Prisma**<br/>
|
||||
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
|
||||
* **Custom Adapter**<br/>
|
||||
- **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._
|
||||
|
||||
> 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](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
|
||||
See the [documentation for adapters](/adapters/overview) to learn more about using Prisma adapter or using a custom adapter.
|
||||
|
||||
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
|
||||
|
||||
@@ -27,7 +31,7 @@ You can specify database credentials as as a connection string or a [TypeORM con
|
||||
The following approaches are exactly equivalent:
|
||||
|
||||
```js
|
||||
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name'
|
||||
database: "mysql://nextauth:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -44,13 +48,14 @@ database: {
|
||||
:::tip
|
||||
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
|
||||
|
||||
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
|
||||
_e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:_
|
||||
|
||||
```js
|
||||
'mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
|
||||
"mysql://nextauth:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_"
|
||||
|
||||
```
|
||||
|
||||
*…or as a database configuration object:*
|
||||
_…or as a database configuration object:_
|
||||
|
||||
```js
|
||||
database: {
|
||||
@@ -63,6 +68,7 @@ database: {
|
||||
entityPrefix: 'nextauth_'
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -73,15 +79,15 @@ Using SQL to create tables and columns is the recommended way to set up an SQL d
|
||||
|
||||
Check out the links below for SQL you can run to set up a database for NextAuth.js.
|
||||
|
||||
* [MySQL Schema](/schemas/mysql)
|
||||
* [Postgres Schema](/schemas/postgres)
|
||||
- [MySQL Schema](/adapters/typeorm/mysql)
|
||||
- [Postgres Schema](/adapters/typeorm/postgres)
|
||||
|
||||
_If you are running SQLite, MongoDB or a Document database you can skip this step._
|
||||
|
||||
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
|
||||
|
||||
```js
|
||||
database: 'mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
database: "mysql://nextauth:password@127.0.0.1:3306/database_name?synchronize=true"
|
||||
```
|
||||
|
||||
```js
|
||||
@@ -122,7 +128,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mysql://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
@@ -133,7 +139,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mariadb://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### Postgres / CockroachDB
|
||||
@@ -144,13 +150,15 @@ Install module:
|
||||
#### Example
|
||||
|
||||
PostgresDB
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:5432/database_name'
|
||||
database: "postgres://username:password@127.0.0.1:5432/database_name"
|
||||
```
|
||||
|
||||
CockroachDB
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:26257/database_name'
|
||||
database: "postgres://username:password@127.0.0.1:26257/database_name"
|
||||
```
|
||||
|
||||
If the node is using Self-signed cert
|
||||
@@ -182,7 +190,7 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mssql://sa:password@localhost:1433/database_name'
|
||||
database: "mssql://sa:password@localhost:1433/database_name"
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
@@ -193,12 +201,12 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
|
||||
database: "mongodb://username:password@127.0.0.1:3306/database_name"
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
*SQLite is intended only for development / testing and not for production use.*
|
||||
_SQLite is intended only for development / testing and not for production use._
|
||||
|
||||
Install module:
|
||||
`npm i sqlite3`
|
||||
@@ -206,9 +214,9 @@ Install module:
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'sqlite://localhost/:memory:'
|
||||
database: "sqlite://localhost/:memory:"
|
||||
```
|
||||
|
||||
## Other databases
|
||||
|
||||
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
See the [documentation for adapters](/adapters/overview) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
|
||||
@@ -66,7 +66,7 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
|
||||
#### Description
|
||||
|
||||
A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
|
||||
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
|
||||
|
||||
@@ -309,7 +309,7 @@ By default NextAuth.js uses a database adapter that uses TypeORM and supports My
|
||||
|
||||
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
|
||||
|
||||
See the [adapter documentation](/schemas/adapters) for more information.
|
||||
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.
|
||||
|
||||
@@ -28,7 +28,7 @@ We purposefully restrict the returned error codes for increased security.
|
||||
The following errors are passed as error query parameters to the default or overriden error page:
|
||||
|
||||
- **Configuration**: There is a problem with the server configuration. Check if your [options](/configuration/options#options) is correct.
|
||||
- **AccessDenied**: Usually occurs, when you restriected access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
|
||||
- **AccessDenied**: Usually occurs, when you restricted access through the [`signIn` callback](/configuration/callbacks#sign-in-callback), or [`redirect` callback](/configuration/callbacks#redirect-callback)
|
||||
- **Verification**: Related to the Email provider. The token has expired or has already been used
|
||||
- **Default**: Catch all, will apply, if none of the above matched
|
||||
|
||||
|
||||
@@ -254,22 +254,26 @@ providers: [
|
||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
const user = (credentials, req) => {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// that is false/null if the credentials are invalid.
|
||||
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
// You can also use the request object to obtain additional parameters
|
||||
// (i.e., the request IP address)
|
||||
return null
|
||||
}
|
||||
if (user) {
|
||||
// Any user object returned here will be saved in the JSON Web Token
|
||||
async authorize(credentials, req) {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// that is false/null if the credentials are invalid.
|
||||
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
// You can also use the `req` object to obtain additional parameters
|
||||
// (i.e., the request IP address)
|
||||
const res = await fetch("/your/endpoint", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(credentials),
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
const user = await res.json()
|
||||
|
||||
// If no error and we have user data, return it
|
||||
if (res.ok && user) {
|
||||
return user
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
// Return null if user data could not be retrieved
|
||||
return null
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ This error occurs when the `useSession()` React Hook has a problem fetching sess
|
||||
|
||||
#### CLIENT_FETCH_ERROR
|
||||
|
||||
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` envionment variable.
|
||||
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` environment variable.
|
||||
|
||||
---
|
||||
|
||||
@@ -63,9 +63,9 @@ The Email authentication provider can only be used if a database is configured.
|
||||
|
||||
The Credentials Provider can only be used if JSON Web Tokens are used for sessions.
|
||||
|
||||
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explictly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
|
||||
JSON Web Tokens are used for Sessions by default if you have not specified a database. However if you are using a database, then Database Sessions are enabled by default and you need to [explicitly enable JWT Sessions](https://next-auth.js.org/configuration/options#session) to use the Credentials Provider.
|
||||
|
||||
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and manged outside of NextAuth.js.
|
||||
If you are using a Credentials Provider, NextAuth.js will not persist users or sessions in a database - user accounts used with the Credentials Provider must be created and managed outside of NextAuth.js.
|
||||
|
||||
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
|
||||
|
||||
* You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
|
||||
|
||||
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in an server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
|
||||
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in a server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
|
||||
|
||||
### What are the disadvantages of JSON Web Tokens?
|
||||
|
||||
@@ -216,7 +216,7 @@ By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds a
|
||||
|
||||
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
|
||||
|
||||
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
|
||||
* If encryption is enabled by setting `jwt: { encryption: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
|
||||
|
||||
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ The NextAuth.js client library makes it easy to interact with sessions from Reac
|
||||
:::tip
|
||||
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
|
||||
|
||||
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
|
||||
You can use the [session callback](/configuration/callbacks#session-callback) to customize the session object returned to the client if you need to return additional data in the session object.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -208,7 +208,7 @@ e.g.
|
||||
* `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own redirect callback to allow other URLs, including supporting relative URLs.
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default it requires the URL to be an absolute URL at the same hostname, or else it will redirect to the homepage. You can define your own [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
@@ -266,7 +266,7 @@ You can also set these parameters through [`provider.authorizationParams`](/conf
|
||||
:::
|
||||
|
||||
:::note
|
||||
The following parameters are always overridden server-side: `redirect_uri`, `scope`, `state`
|
||||
The following parameters are always overridden server-side: `redirect_uri`, `state`
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -294,7 +294,7 @@ As with the `signIn()` function, you can specify a `callbackUrl` parameter by pa
|
||||
|
||||
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect-callback). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom [redirect callback](/configuration/callbacks#redirect-callback) to allow other URLs, including supporting relative URLs.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
@@ -328,6 +328,24 @@ export default function App ({ Component, pageProps }) {
|
||||
|
||||
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.
|
||||
|
||||
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"
|
||||
|
||||
...
|
||||
|
||||
export async function getServerSideProps(ctx) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If every one of your pages needs to be protected, you can do this in `_app`, otherwise you can do it on a page-by-page basis. Alternatively, you can do per page authentication checks client side, instead of having each auth check be blocking (SSR) by using the method described below in [alternative client session handling](#custom-client-session-handling).
|
||||
|
||||
### Options
|
||||
|
||||
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
|
||||
@@ -367,7 +385,7 @@ The `clientMaxAge` option is the maximum age a session data can be on the client
|
||||
|
||||
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.
|
||||
|
||||
If set to any value other than zero, it specifies in seconds the maxium age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
|
||||
If set to any value other than zero, it specifies in seconds the maximum age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
|
||||
|
||||
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
|
||||
|
||||
@@ -386,3 +404,72 @@ The value for `keepAlive` should always be lower than the value of the 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.
|
||||
:::
|
||||
|
||||
## Alternatives
|
||||
|
||||
### Custom Client Session Handling
|
||||
|
||||
Due to the way Next.js handles `getServerSideProps` / `getInitialProps`, every protected page load has to make a server-side query to check if the session is valid and then generate the requested page. This alternative solution allows for showing a loading state on the initial check and every page transition afterward will be client-side, without having to check with the server and regenerate pages.
|
||||
|
||||
```js title="pages/admin.jsx"
|
||||
export default function AdminDashboard () {
|
||||
const [session] = useSession()
|
||||
// session is always non-null inside this page, all the way down the React tree.
|
||||
return "Some super secret dashboard"
|
||||
}
|
||||
|
||||
AdminDashboard.auth = true
|
||||
```
|
||||
|
||||
```jsx title="pages/_app.jsx"
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<SessionProvider session={pageProps.session}>
|
||||
{Component.auth
|
||||
? <Auth><Component {...pageProps} /></Auth>
|
||||
: <Component {...pageProps} />
|
||||
}
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function Auth({ children }) {
|
||||
const [session, loading] = useSession()
|
||||
const isUser = !!session?.user
|
||||
React.useEffect(() => {
|
||||
if (loading) return // Do nothing while loading
|
||||
if (!isUser) signIn() // If not authenticated, force log in
|
||||
}, [isUser, loading])
|
||||
|
||||
if (isUser) {
|
||||
return children
|
||||
}
|
||||
|
||||
// Session is being fetched, or no user.
|
||||
// If no user, useEffect() will redirect.
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
```
|
||||
|
||||
It can be easily be extended/modified to support something like an options object for role based authentication on pages. An example:
|
||||
|
||||
```jsx title="pages/admin.jsx"
|
||||
AdminDashboard.auth = {
|
||||
role: "admin",
|
||||
loading: <AdminLoadingSkeleton/>,
|
||||
unauthorized: "/login-with-different-user" // redirect to this url
|
||||
}
|
||||
```
|
||||
|
||||
Because of how _app is done, it won't unnecessarily contact the /api/auth/session endpoint for pages that do not require auth.
|
||||
|
||||
More information can be found in the following [Github Issue](https://github.com/nextauthjs/next-auth/issues/1210).
|
||||
|
||||
### NextAuth.js + React-Query
|
||||
|
||||
There is also an alternative client-side API library based upon [`react-query`](https://www.npmjs.com/package/react-query) available under [`nextauthjs/react-query`](https://github.com/nextauthjs/react-query).
|
||||
|
||||
If you use `react-query` in your project already, you can leverage it with NextAuth.js to handle the client-side session management for you as well. This replaces NextAuth.js's native `useSession` and `Provider` from `next-auth/client`.
|
||||
|
||||
See repository [`README`](https://github.com/nextauthjs/react-query) for more details.
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ title: Example Code
|
||||
|
||||
## Get started with NextAuth.js
|
||||
|
||||
The example code below describes to add authentication to a Next.js app.
|
||||
The example code below describes how to add authentication to a Next.js app.
|
||||
|
||||
:::tip
|
||||
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.now.sh](https://next-auth-example.now.sh)
|
||||
The easiest way to get started is to clone the [example app](https://github.com/nextauthjs/next-auth-example) and follow the instructions in README.md. You can try out a live demo at [next-auth-example.vercel.app](https://next-auth-example.vercel.app)
|
||||
:::
|
||||
|
||||
### Add API route
|
||||
|
||||
@@ -45,7 +45,7 @@ providers: [
|
||||
|
||||
:::tip
|
||||
|
||||
You can convert your Apple key to a single line to use it in a environment variable.
|
||||
You can convert your Apple key to a single line to use it in an environment variable.
|
||||
|
||||
**Mac**
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ You can override any of the options to suit your own use case.
|
||||
- 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.
|
||||
|
||||
In `.env.local` create the follwing entries:
|
||||
In `.env.local` create the following entries:
|
||||
|
||||
```
|
||||
AZURE_CLIENT_ID=<copy Application (client) ID here>
|
||||
|
||||
38
www/docs/providers/coinbase.md
Normal file
38
www/docs/providers/coinbase.md
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: coinbase
|
||||
title: Coinbase
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developers.coinbase.com/api/v2
|
||||
|
||||
## Configuration
|
||||
|
||||
https://www.coinbase.com/settings/api
|
||||
|
||||
## Options
|
||||
|
||||
The **Coinbase Provider** comes with a set of default options:
|
||||
|
||||
- [Coinbase Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/coinbase.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Coinbase({
|
||||
clientId: process.env.COINBASE_CLIENT_ID,
|
||||
clientSecret: process.env.COINBASE_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
This Provider template has a 2 hour access token to it. A refresh token is also returned.
|
||||
:::
|
||||
@@ -83,7 +83,7 @@ See the [callbacks documentation](/configuration/callbacks) for more information
|
||||
|
||||
You can specify more than one credentials provider by specifying a unique `id` for each one.
|
||||
|
||||
You can also use them in conjuction with other provider options.
|
||||
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.
|
||||
|
||||
|
||||
@@ -46,5 +46,5 @@ Email address is not returned by the Instagram API.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrock](https://ngrok.com/docs).
|
||||
Instagram display app required callback URL to be configured in your Facebook app and Facebook required you to use **https** even for localhost! In order to do that, you either need to [add an SSL to your localhost](https://www.freecodecamp.org/news/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec/) or use a proxy such as [ngrok](https://ngrok.com/docs).
|
||||
:::
|
||||
|
||||
@@ -7,6 +7,10 @@ title: WorkOS
|
||||
|
||||
https://workos.com/docs/sso/guide
|
||||
|
||||
## Configuration
|
||||
|
||||
https://dashboard.workos.com
|
||||
|
||||
## Options
|
||||
|
||||
The **WorkOS Provider** comes with a set of default options:
|
||||
@@ -22,10 +26,87 @@ import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.WorkOS({
|
||||
clientId: process.env.WORKOS_ID,
|
||||
clientSecret: process.env.WORKOS_SECRET,
|
||||
domain: process.env.WORKOS_DOMAIN
|
||||
clientId: process.env.WORKOS_CLIENT_ID,
|
||||
clientSecret: process.env.WORKOS_API_KEY,
|
||||
}),
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
WorkOS is not an identity provider itself, but, rather, a bridge to multiple single sign-on (SSO) providers. As a result, we need to make some additional changes to authenticate users using WorkOS.
|
||||
|
||||
In order to sign a user in using WorkOS, we need to specify which WorkOS Connection to use. A common way to do this is to collect the user's email address and extract the domain.
|
||||
|
||||
This can be done using a custom login page.
|
||||
|
||||
To add a custom login page, you can use the `pages` option:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
pages: {
|
||||
signIn: '/auth/signin',
|
||||
}
|
||||
```
|
||||
|
||||
We can then add a custom login page that displays an input where the user can enter their email address. We then extract the domain from the user's email address and pass it to the `authorizationParams` parameter on the `signIn` function:
|
||||
|
||||
```jsx title="pages/auth/signin.js"
|
||||
import { getProviders, signIn } from 'next-auth/client'
|
||||
|
||||
export default function SignIn({ providers }) {
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(providers).map((provider) => {
|
||||
if (provider.id === 'workos') {
|
||||
return (
|
||||
<div key={provider.id}>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder="Email"
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
signIn(provider.id, undefined, {
|
||||
domain: email.split('@')[1],
|
||||
})
|
||||
}
|
||||
>
|
||||
Sign in with SSO
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={provider.id}>
|
||||
<button onClick={() => signIn(provider.id)}>
|
||||
Sign in with {provider.name}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// This is the recommended way for Next.js 9.3 or newer
|
||||
export async function getServerSideProps(context){
|
||||
const providers = await getProviders()
|
||||
return {
|
||||
props: { providers }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// If older than Next.js 9.3
|
||||
SignIn.getInitialProps = async () => {
|
||||
return {
|
||||
providers: await getProviders()
|
||||
}
|
||||
}
|
||||
*/
|
||||
```
|
||||
|
||||
34
www/docs/providers/zoom.md
Normal file
34
www/docs/providers/zoom.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: zoom
|
||||
title: Zoom
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://marketplace.zoom.us/docs/guides/auth/oauth
|
||||
|
||||
## Configuration
|
||||
|
||||
https://marketplace.zoom.us
|
||||
|
||||
## Options
|
||||
|
||||
The **Zoom Provider** comes with a set of default options:
|
||||
|
||||
- [Zoom Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/zoom.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Zoom({
|
||||
clientId: process.env.ZOOM_CLIENT_ID,
|
||||
clientSecret: process.env.ZOOM_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -83,6 +83,14 @@ This example shows how to implement a fullstack app in TypeScript with Next.js u
|
||||
|
||||
This `dev.to` tutorial walks one through adding NextAuth.js to an existing project. Including setting up the OAuth client id and secret, adding the API routes for authentication, protecting pages and API routes behind that authentication, etc.
|
||||
|
||||
### [Introduction to NextAuth.js](https://www.youtube.com/watch?v=npZsJxWntJM)
|
||||
|
||||
This is an introductory video to NextAuth.js for beginners. In this video, it is explained how to set up authentication in a few easy steps and add different configurations to make it more robust and secure.
|
||||
|
||||
### [Adding Sign in With Apple Next JS](https://thesiddd.com/blog/apple-auth)
|
||||
|
||||
This tutorial walks step by step on how to get Sign In with Apple working (both locally and on a deployed website) using NextAuth.js.
|
||||
|
||||
### [How to Authenticate Next.js Apps with Twitter & NextAuth.js](https://spacejelly.dev/posts/how-to-authenticate-next-js-apps-with-twitter-nextauth-js/)
|
||||
|
||||
Learn how to add Twitter authentication and login to a Next.js app both clientside and serverside with NextAuth.js.
|
||||
|
||||
@@ -3,7 +3,7 @@ id: typeorm-custom-models
|
||||
title: Custom models with TypeORM
|
||||
---
|
||||
|
||||
NextAuth.js provides a set of [models and schemas](/schemas/models) for the built-in TypeORM adapter that you can easily extend.
|
||||
NextAuth.js provides a set of [models and schemas](/adapters/models) for the built-in TypeORM adapter that you can easily extend.
|
||||
|
||||
You can use these models with MySQL, MariaDB, Postgres, MongoDB and SQLite.
|
||||
|
||||
@@ -79,5 +79,3 @@ export default NextAuth({
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -27,15 +27,29 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Models & Schemas",
|
||||
label: "Database Adapters",
|
||||
collapsed: true,
|
||||
items: [
|
||||
"schemas/models",
|
||||
"schemas/mysql",
|
||||
"schemas/postgres",
|
||||
"schemas/mssql",
|
||||
"schemas/mongodb",
|
||||
"schemas/adapters",
|
||||
"adapters/overview",
|
||||
"adapters/models",
|
||||
{
|
||||
type: "category",
|
||||
label: "TypeORM",
|
||||
collapsed: true,
|
||||
items: [
|
||||
"adapters/typeorm/typeorm-overview",
|
||||
"adapters/typeorm/mysql",
|
||||
"adapters/typeorm/postgres",
|
||||
"adapters/typeorm/mssql",
|
||||
"adapters/typeorm/mongodb",
|
||||
],
|
||||
},
|
||||
"adapters/fauna",
|
||||
"adapters/prisma",
|
||||
"adapters/prisma-legacy",
|
||||
"adapters/dynamodb",
|
||||
"adapters/firebase",
|
||||
"adapters/pouchdb",
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -49,5 +63,7 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
},
|
||||
"warnings",
|
||||
"errors",
|
||||
],
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function Home() {
|
||||
"button button--outline button--secondary button--lg rounded-pill",
|
||||
styles.button
|
||||
)}
|
||||
href="https://next-auth-example.now.sh"
|
||||
href="https://next-auth-example.vercel.app"
|
||||
>
|
||||
Live Demo
|
||||
</a>
|
||||
|
||||
10
www/vercel.json
Normal file
10
www/vercel.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"redirects": [
|
||||
{ "source": "/schemas/models", "destination": "/adapters/models", "permanent": true },
|
||||
{ "source": "/schemas/mysql", "destination": "/adapters/typeorm/mysql", "permanent": true },
|
||||
{ "source": "/schemas/postgres", "destination": "/adapters/typeorm/postgres", "permanent": true },
|
||||
{ "source": "/schemas/mssql", "destination": "/adapters/typeorm/mssql", "permanent": true },
|
||||
{ "source": "/schemas/mongodb", "destination": "/adapters/typeorm/mongodb", "permanent": true },
|
||||
{ "source": "/schemas/adapters", "destination": "/adapters/overview", "permanent": true }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user