mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
960ca85907 | ||
|
|
f960cc0f6f | ||
|
|
0f64f3eea7 | ||
|
|
71c78e8e24 | ||
|
|
d86609a2dc | ||
|
|
d0c3400d30 | ||
|
|
172e79cb04 | ||
|
|
46d5c76605 | ||
|
|
438efd8a9b | ||
|
|
d8d497cc91 | ||
|
|
6152c8afbb | ||
|
|
5ae6f6118c | ||
|
|
96ff048b59 | ||
|
|
e80f6e936d | ||
|
|
6b5a215fb2 | ||
|
|
782482b9f4 | ||
|
|
2d364f246a | ||
|
|
564b342f69 | ||
|
|
63638d81dc | ||
|
|
28683015f1 | ||
|
|
726c49603d | ||
|
|
a7113c6d3e | ||
|
|
910514c6e2 | ||
|
|
b7cca484cf | ||
|
|
e293e786a8 | ||
|
|
82dd6ba3e4 | ||
|
|
6e28a07746 | ||
|
|
61047e3c14 | ||
|
|
dc5f3f481d | ||
|
|
0343344802 | ||
|
|
134a95a4bd | ||
|
|
52a4bd97cd | ||
|
|
87d43e4038 | ||
|
|
68695af1f3 | ||
|
|
76df2b5e70 | ||
|
|
8bd9d87633 | ||
|
|
6af40e3fe2 |
@@ -1,8 +1,13 @@
|
||||
# Rename file to .env.local and populate values
|
||||
# Rename file to .env.local (or .env) and populate values
|
||||
# to be able to run the dev app
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
SECRET= Linux: `openssl rand -hex 32` or https://generate-secret.now.sh/32
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_DOMAIN=
|
||||
@@ -12,4 +17,17 @@ GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
TWITTER_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
# You can use any of these as the "DATABASE_URL" for
|
||||
# databases started with Docker using `npm run db:start`.
|
||||
# Note: If using with Prisma adapter, you need to use a `.env`
|
||||
# file rather than a `.env.local` file to configure env vars.
|
||||
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true
|
||||
# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true
|
||||
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
|
||||
DATABASE_URL=
|
||||
14
.github/labeler.yml
vendored
14
.github/labeler.yml
vendored
@@ -19,3 +19,17 @@ databases:
|
||||
- test/docker/databases/**/*
|
||||
- www/docs/configuration/databases.md
|
||||
- test/fixtures/**/*
|
||||
|
||||
core:
|
||||
- src/**/*
|
||||
|
||||
style:
|
||||
- src/css/**/*
|
||||
|
||||
client:
|
||||
- src/client/**/*
|
||||
- www/docs/getting-started/client.md
|
||||
|
||||
pages:
|
||||
- src/server/pages/**/*
|
||||
- www/docs/configuration/pages.md
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -7,6 +7,7 @@ exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- priority
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -1,31 +1,30 @@
|
||||
# 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: Build Test
|
||||
name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
- next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
node-version: [10, 12, 14]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
67
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# 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"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, next ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
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
|
||||
|
||||
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
|
||||
11
.github/workflows/integration.yml
vendored
11
.github/workflows/integration.yml
vendored
@@ -2,9 +2,10 @@ name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, canary ]
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
pull_request:
|
||||
branches: [ main, canary ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -28,7 +29,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [12.x]
|
||||
node-version: [10, 12, 14]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -37,8 +38,8 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
# Install dependencies
|
||||
- run: npm ci
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
|
||||
1
.github/workflows/labeler.yml
vendored
1
.github/workflows/labeler.yml
vendored
@@ -9,4 +9,3 @@ jobs:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
sync-labels: true
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -2,29 +2,25 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- canary
|
||||
- 'main'
|
||||
- 'next'
|
||||
- '3.x'
|
||||
pull_request:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
name: 'Release'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run build
|
||||
- run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,6 +11,8 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
yarn.lock
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
@@ -24,6 +26,7 @@ node_modules
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.next
|
||||
www/providers.json
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
@@ -33,4 +36,7 @@ node_modules
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{ "name": "canary", "prerelease": true }
|
||||
],
|
||||
"plugins": [
|
||||
["@semantic-release/commit-analyzer", {
|
||||
"preset": "conventionalcommits",
|
||||
"releaseRules": [
|
||||
{ "breaking": true, "release": "major" },
|
||||
{ "revert": true, "release": "patch" },
|
||||
{ "type": "feat", "release": "minor" },
|
||||
{ "type": "fix", "release": "patch" },
|
||||
{ "type": "perf", "release": "patch" },
|
||||
{ "type": "docs", "release": "patch" }
|
||||
]
|
||||
}],
|
||||
["@semantic-release/release-notes-generator", {
|
||||
"preset": "conventionalcommits",
|
||||
"presetConfig": {
|
||||
"types": [
|
||||
{ "type": "feat", "section": "Features", "hidden": false },
|
||||
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
||||
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
||||
{ "type": "revert", "section": "Reverts", "hidden": false },
|
||||
{ "type": "docs", "section": "Documentation", "hidden": false },
|
||||
{ "type": "style", "section": "Styles", "hidden": false },
|
||||
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
|
||||
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
|
||||
{ "type": "test", "section": "Tests", "hidden": false },
|
||||
{ "type": "build", "section": "Build System", "hidden": false },
|
||||
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
|
||||
]
|
||||
}
|
||||
}],
|
||||
"@semantic-release/github",
|
||||
"@semantic-release/npm"
|
||||
]
|
||||
}
|
||||
@@ -13,10 +13,9 @@ Please raise any significant new functionality or breaking change an issue for d
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
### Pull Requests
|
||||
|
||||
* The latest changes are always in `canary`, so please make your Pull Request against that branch.
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* Rebasing in Pull Requests is preferred to keep a clean commit history (see below)
|
||||
* Run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this extension](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) to fix lint issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
@@ -89,7 +88,7 @@ We use [semantic-release](https://github.com/semantic-release/semantic-release)
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
* Use "Squash and merge"
|
||||
* Make sure you merge contributor PRs into `canary`
|
||||
* Make sure you merge contributor PRs into `main`
|
||||
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
|
||||
|
||||
3
FUNDING.yml
Normal file
3
FUNDING.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
|
||||
|
||||
github: [balazsorban44]
|
||||
25
README.md
25
README.md
@@ -7,12 +7,25 @@
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
|
||||
<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>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
</a>
|
||||
<a href="https://www.npmtrends.com/next-auth">
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/next-auth">
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ export default function AccessDenied () {
|
||||
<>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a href="/api/auth/signin"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}>You must be signed in to view this page</a>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>You must be signed in to view this page
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Link from 'next/link'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import * as client from 'next-auth/client'
|
||||
import styles from './header.module.css'
|
||||
|
||||
// The approach used in this component shows how to built a sign in and sign out
|
||||
@@ -26,7 +25,7 @@ export default function Header () {
|
||||
You are not signed in
|
||||
</span>
|
||||
<a
|
||||
href="/api/auth/signin"
|
||||
href='/api/auth/signin'
|
||||
className={styles.buttonPrimary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -51,7 +50,7 @@ export default function Header () {
|
||||
<strong>{session.user.email || session.user.name}</strong>
|
||||
</span>
|
||||
<a
|
||||
href="/api/auth/signout"
|
||||
href='/api/auth/signout'
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
@@ -96,6 +95,16 @@ export default function Header () {
|
||||
<a>API</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/credentials'>
|
||||
<a>Credentials</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/email'>
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
2
next-env.d.ts
vendored
Normal file
2
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
859
package-lock.json
generated
859
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -29,8 +29,8 @@
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix"
|
||||
"lint": "ts-standard",
|
||||
"lint:fix": "ts-standard --fix"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -64,21 +64,24 @@
|
||||
"mysql": "^2.18.1",
|
||||
"mssql": "^6.2.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.12.0"
|
||||
"@prisma/client": "^2.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@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",
|
||||
"@types/react": "^17.0.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.19.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
@@ -87,18 +90,29 @@
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"standard": "^16.0.3"
|
||||
"ts-standard": "^10.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"standard": {
|
||||
"ts-standard": {
|
||||
"project": "./tsconfig.json",
|
||||
"ignore": [
|
||||
"test/",
|
||||
"pages/",
|
||||
"components/"
|
||||
"next-env.d.ts"
|
||||
],
|
||||
"globals": [
|
||||
"fetch"
|
||||
]
|
||||
}
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/balazsorban44"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ export default function Page () {
|
||||
<p><em>You must be signed in to see responses.</em></p>
|
||||
<h2>Session</h2>
|
||||
<p>/api/examples/session</p>
|
||||
<iframe src="/api/examples/session"/>
|
||||
<iframe src='/api/examples/session' />
|
||||
<h2>JSON Web Token</h2>
|
||||
<p>/api/examples/jwt</p>
|
||||
<iframe src="/api/examples/jwt"/>
|
||||
<iframe src='/api/examples/jwt' />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,61 @@
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
protection: "pkce"
|
||||
protection: 'pkce'
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
],
|
||||
// Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
|
||||
// https://next-auth.js.org/configuration/databases
|
||||
//
|
||||
// Notes:
|
||||
// * You must to install an appropriate node_module for your database
|
||||
// * The Email provider requires a database (OAuth providers do not)
|
||||
|
||||
// The secret should be set to a reasonably long random string.
|
||||
// It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
|
||||
// a separate secret is defined explicitly for encrypting the JWT.
|
||||
|
||||
session: {
|
||||
// Use JSON Web Tokens for session instead of database sessions.
|
||||
// This option can be used with or without a database for users/accounts.
|
||||
// Note: `jwt` is automatically set to `true` if no database is specified.
|
||||
jwt: true,
|
||||
|
||||
// Seconds - How long until an idle session expires and is no longer valid.
|
||||
// maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
|
||||
// Seconds - Throttle how frequently to write to database to extend a session.
|
||||
// Use it to limit write operations. Set to 0 to always update the database.
|
||||
// Note: This option is ignored if using JSON Web Tokens
|
||||
// updateAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
|
||||
// JSON Web tokens are only used for sessions if the `jwt: true` session
|
||||
// option is set - or by default if no database is specified.
|
||||
// https://next-auth.js.org/configuration/options#jwt
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET,
|
||||
// A secret to use for key generation (you should set this explicitly)
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
// Set to true to use encryption (default: false)
|
||||
// encryption: true,
|
||||
// You can define your own encode/decode functions for signing and encryption
|
||||
// if you want to override the default behaviour.
|
||||
// encode: async ({ secret, token, maxAge }) => {},
|
||||
// decode: async ({ secret, token, maxAge }) => {},
|
||||
secret: process.env.SECRET
|
||||
},
|
||||
|
||||
// You can define custom pages to override the built-in pages.
|
||||
// The routes shown here are the default URLs that will be used when a custom
|
||||
// pages is not specified for that route.
|
||||
// https://next-auth.js.org/configuration/pages
|
||||
pages: {
|
||||
// signIn: '/api/auth/signin', // Displays signin buttons
|
||||
// signOut: '/api/auth/signout', // Displays form with sign out button
|
||||
// error: '/api/auth/error', // Error code passed in query string as ?error=
|
||||
// verifyRequest: '/api/auth/verify-request', // Used for check email page
|
||||
// newUser: null // If set, new users will be directed here on first sign in
|
||||
},
|
||||
|
||||
// Callbacks are asynchronous functions you can use to control what happens
|
||||
// when an action is performed.
|
||||
// https://next-auth.js.org/configuration/callbacks
|
||||
callbacks: {
|
||||
// signIn: async (user, account, profile) => { return Promise.resolve(true) },
|
||||
// redirect: async (url, baseUrl) => { return Promise.resolve(baseUrl) },
|
||||
// session: async (session, user) => { return Promise.resolve(session) },
|
||||
// jwt: async (token, user, account, profile, isNewUser) => { return Promise.resolve(token) }
|
||||
},
|
||||
|
||||
// Events are useful for logging
|
||||
// https://next-auth.js.org/configuration/events
|
||||
events: {},
|
||||
|
||||
// Enable debug messages in the console if you are having problems
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev --preview-feature
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
import jwt from "next-auth/jwt"
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@ export default async (req, res) => {
|
||||
} else {
|
||||
res.send({ error: 'You must be sign in to view the protected content on this page.' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ import { getSession } from 'next-auth/client'
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +19,4 @@ export default function Page () {
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
52
pages/credentials.js
Normal file
52
pages/credentials.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const handleLogin = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signIn('credentials', options)
|
||||
}
|
||||
const response = await signIn('credentials', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials login</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
66
pages/email.js
Normal file
66
pages/email.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const [email, setEmail] = React.useState('')
|
||||
|
||||
const handleChange = (event) => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handleLogin = (options) => async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (options.redirect) {
|
||||
return signIn('email', options)
|
||||
}
|
||||
const response = await signIn('email', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async (event) => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email login</h1>
|
||||
<label className='spacing'>
|
||||
Email address:{' '}
|
||||
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
|
||||
</label><br />
|
||||
<form onSubmit={handleLogin({ redirect: true, email })}>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin({ redirect: false, email })}>
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href={`https://next-auth.js.org`}>NextAuth.js</a> for authentication.
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<h2>Terms of Service</h2>
|
||||
<p>
|
||||
@@ -27,4 +27,4 @@ export default function Page () {
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page ({ content, session }) {
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied/></Layout> }
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
@@ -16,7 +16,7 @@ export default function Page ({ content, session }) {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
export async function getServerSideProps (context) {
|
||||
const session = await getSession(context)
|
||||
let content = null
|
||||
|
||||
|
||||
@@ -4,24 +4,24 @@ import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page () {
|
||||
const [ session, loading ] = useSession()
|
||||
const [ content , setContent ] = useState()
|
||||
const [session, loading] = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(()=>{
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch('/api/examples/protected')
|
||||
const json = await res.json()
|
||||
if (json.content) { setContent(json.content) }
|
||||
}
|
||||
fetchData()
|
||||
},[session])
|
||||
}, [session])
|
||||
|
||||
// When rendering client side don't display anything until loading is complete
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied/></Layout> }
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
@@ -30,4 +30,4 @@ export default function Page () {
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useSession, getSession } from 'next-auth/client'
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
@@ -6,7 +6,6 @@ export default function Page () {
|
||||
// populated on render without needing to go through a loading stage.
|
||||
// This is possible because of the shared context configured in `_app.js` that
|
||||
// is used by `useSession()`.
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
@@ -29,7 +28,7 @@ export default function Page () {
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps(context) {
|
||||
export async function getServerSideProps (context) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(context)
|
||||
|
||||
63
prisma/schema.prisma
Normal file
63
prisma/schema.prisma
Normal file
@@ -0,0 +1,63 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
providerId String @map(name: "provider_id")
|
||||
providerAccountId String @map(name: "provider_account_id")
|
||||
refreshToken String? @map(name: "refresh_token")
|
||||
accessToken String? @map(name: "access_token")
|
||||
accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
accessToken String @unique @map(name: "access_token")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
image String?
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
7
release.config.js
Normal file
7
release.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
branches: [
|
||||
'+([0-9])?(.{+([0-9]),x}).x',
|
||||
'main',
|
||||
{ name: 'next', prerelease: true }
|
||||
]
|
||||
}
|
||||
@@ -1,84 +1,83 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function _debug (...args) {
|
||||
if (appOptions.debug) {
|
||||
console.log('[next-auth][debug]', ...args)
|
||||
}
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
_debug('createUser', profile)
|
||||
debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
_debug('getUser', id)
|
||||
debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
_debug('getUserByEmail', email)
|
||||
debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
_debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
_debug('updateUser', user)
|
||||
debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
_debug('deleteUser', userId)
|
||||
debug('deleteUser', userId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
_debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
_debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
_debug('createSession', user)
|
||||
debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
_debug('getSession', sessionToken)
|
||||
debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
_debug('updateSession', session)
|
||||
debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
_debug('deleteSession', sessionToken)
|
||||
debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
_debug('createVerificationRequest', identifier)
|
||||
debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('getVerificationRequest', identifier, token)
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('deleteVerification', identifier, token)
|
||||
debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
@@ -21,6 +20,7 @@ const Adapter = (config) => {
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
@@ -280,11 +280,15 @@ const Adapter = (config) => {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await prisma[VerificationRequest].findUnique({ where: { token: hashedToken } })
|
||||
|
||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
token: hashedToken
|
||||
}
|
||||
})
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -300,7 +304,7 @@ const Adapter = (config) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
import adapterTransform from './lib/transform'
|
||||
import Models from './models'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
@@ -41,6 +41,12 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
let connection = null
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// Helper function to reuse / restablish connections
|
||||
// (useful if they drop when after being idle)
|
||||
async function _connect () {
|
||||
@@ -77,12 +83,6 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
|
||||
const { manager } = connection
|
||||
|
||||
// Display debug output if debug option enabled
|
||||
// @TODO Refactor logger so is passed in appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
// flexiblity is required in the adapter to support non-SQL databases such
|
||||
// as MongoDB which have different pragmas.
|
||||
@@ -331,7 +331,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
/* global fetch:false */
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import logger from '../lib/logger'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
@@ -38,6 +37,8 @@ const __NEXTAUTH = {
|
||||
_getSession: () => {}
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined') {
|
||||
if (__NEXTAUTH._eventListenersAdded === false) {
|
||||
@@ -115,12 +116,10 @@ const setOptions = ({
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
export const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getSession() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
if (!req && ctx && ctx.req) { req = ctx.req }
|
||||
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getSession() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
export async function getSession ({ ctx, req = ctx?.req, triggerEvent = true } = {}) {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
|
||||
@@ -131,12 +130,10 @@ export const getSession = async ({ req, ctx, triggerEvent = true } = {}) => {
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getCsrfToken = async ({ req, ctx } = {}) => {
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getCsrfToken() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
if (!req && ctx && ctx.req) { req = ctx.req }
|
||||
|
||||
// If passed 'appContext' via getInitialProps() in _app.js then get the req
|
||||
// object from ctx and use that for the req value to allow getCsrfToken() to
|
||||
// work seemlessly in getInitialProps() on server side pages *and* in _app.js.
|
||||
async function getCsrfToken ({ ctx, req = ctx?.req } = {}) {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
|
||||
@@ -233,62 +230,119 @@ const _useSessionHook = (session) => {
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
// Client side method
|
||||
export const signIn = async (provider, args = {}, authorizationParams = {}) => {
|
||||
/**
|
||||
* Client-side method to initiate a signin flow
|
||||
* or send the user to the signin page listing all possible providers.
|
||||
* (Automatically adds the CSRF token to the request)
|
||||
* @see https://next-auth.js.org/getting-started/client#signin
|
||||
* @param {string} [provider]
|
||||
* @param {SignInOptions} [options]
|
||||
* @param {object} [authorizationParams]
|
||||
* @return {Promise<SignInResponse | undefined>}
|
||||
* @typedef {{callbackUrl?: string; redirect?: boolean}} SignInOptions
|
||||
* @typedef {{error: string | null; status: number; ok: boolean}} SignInResponse
|
||||
*/
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const callbackUrl = args?.callbackUrl ?? window.location
|
||||
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)}`
|
||||
} else {
|
||||
const signInUrl = (providers[provider].type === 'credentials')
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
return
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
...args,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${_encodedForm(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
window.location = data.url ?? callbackUrl
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
}
|
||||
}
|
||||
|
||||
// Client side method
|
||||
export const signOut = async (args = {}) => {
|
||||
const callbackUrl = args.callbackUrl ?? window.location
|
||||
|
||||
/**
|
||||
* Signs the user out, by removing the session cookie.
|
||||
* (Automatically adds the CSRF token to the request)
|
||||
* @param {SignOutOptions} [options]
|
||||
* @returns {Promise<{url?: string} | undefined>}
|
||||
* @typedef {{callbackUrl?: string; redirect?: boolean;}} SignOutOptions
|
||||
*/
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
|
||||
window.location = data.url ?? callbackUrl
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Provider to wrap the app in to make session data available globally
|
||||
@@ -321,12 +375,6 @@ const _apiBaseUrl = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const _encodedForm = (formData) => {
|
||||
return Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
}
|
||||
|
||||
const _sendMessage = (message) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
--border-radius: .3rem;
|
||||
--color-error: #c94b4b;
|
||||
--color-info: #157efb;
|
||||
--color-info-text: #fff;
|
||||
}
|
||||
|
||||
.__next-auth-theme-auto,
|
||||
@@ -23,7 +24,6 @@
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
|
||||
--color-seperator: #444;
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ input[type] {
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0, 0, 0, .2);
|
||||
color: var(--color-text);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
@@ -202,13 +203,13 @@ a.site {
|
||||
font-weight: 500;
|
||||
border-radius: 0.3rem;
|
||||
background: var(--color-info);
|
||||
color: var(--color-text);
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2rem;
|
||||
color: var(--color-info-text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
src/lib/logger.d.ts
vendored
Normal file
5
src/lib/logger.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface LoggerInstance {
|
||||
warn: (code?: string, ...message: unknown[]) => void
|
||||
error: (code?: string, ...message: unknown[]) => void
|
||||
debug: (code?: string, ...message: unknown[]) => void
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
const logger = {
|
||||
/** @type {import("./logger").LoggerInstance} */
|
||||
const _logger = {
|
||||
error (code, ...message) {
|
||||
console.error(
|
||||
`[next-auth][error][${code.toLowerCase()}]`,
|
||||
@@ -22,4 +23,60 @@ const logger = {
|
||||
}
|
||||
}
|
||||
|
||||
export default logger
|
||||
/**
|
||||
* Override the built-in logger.
|
||||
* Any `undefined` level will use the default logger.
|
||||
* @param {Partial<import("./logger").LoggerInstance>} newLogger
|
||||
*/
|
||||
export function setLogger (newLogger = {}) {
|
||||
if (newLogger.error) _logger.error = newLogger.error
|
||||
if (newLogger.warn) _logger.warn = newLogger.warn
|
||||
if (newLogger.debug) _logger.debug = newLogger.debug
|
||||
}
|
||||
|
||||
export default _logger
|
||||
|
||||
/**
|
||||
* Serializes client-side log messages and sends them to the server
|
||||
* @param {import("./logger").LoggerInstance} logger
|
||||
* @param {string} basePath
|
||||
* @return {import("./logger").LoggerInstance}
|
||||
*/
|
||||
export function proxyLogger (logger = _logger, basePath) {
|
||||
try {
|
||||
if (typeof window === 'undefined') {
|
||||
return logger
|
||||
}
|
||||
|
||||
const clientLogger = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, ...message) => {
|
||||
_logger[level](code, ...message) // Log on client as usual
|
||||
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({
|
||||
level,
|
||||
code,
|
||||
message: JSON.stringify(message.map(m => {
|
||||
if (m instanceof Error) {
|
||||
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
return { name: m.name, message: m.message, stack: m.stack }
|
||||
}
|
||||
return m
|
||||
}))
|
||||
})
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(url, body)
|
||||
}
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body
|
||||
})
|
||||
}
|
||||
}
|
||||
return clientLogger
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default (options) => {
|
||||
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
|
||||
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
|
||||
} else {
|
||||
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
|
||||
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'
|
||||
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
|
||||
}
|
||||
return {
|
||||
|
||||
21
src/providers/eveonline.js
Normal file
21
src/providers/eveonline.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'eveonline',
|
||||
name: 'EVE Online',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.eveonline.com/oauth/token',
|
||||
authorizationUrl: 'https://login.eveonline.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://login.eveonline.com/oauth/verify',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
email: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import Cognito from './cognito'
|
||||
import Credentials from './credentials'
|
||||
import Discord from './discord'
|
||||
import Email from './email'
|
||||
import EVEOnline from './eveonline'
|
||||
import Facebook from './facebook'
|
||||
import Foursquare from './foursquare'
|
||||
import FusionAuth from './fusionauth'
|
||||
@@ -46,6 +47,7 @@ export default {
|
||||
Credentials,
|
||||
Discord,
|
||||
Email,
|
||||
EVEOnline,
|
||||
Facebook,
|
||||
Foursquare,
|
||||
FusionAuth,
|
||||
|
||||
@@ -3,7 +3,7 @@ export default (options) => {
|
||||
|
||||
return {
|
||||
id: 'vk',
|
||||
name: 'vk.com',
|
||||
name: 'VK',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'email',
|
||||
|
||||
94
src/server/index.d.ts
vendored
Normal file
94
src/server/index.d.ts
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import { LoggerInstance } from 'src/lib/logger'
|
||||
import { CallbacksOptions } from './lib/callbacks'
|
||||
import { CookiesOptions } from './lib/cookie'
|
||||
import { EventsOptions } from './lib/events'
|
||||
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
params: Record<string, unknown>
|
||||
scope: string
|
||||
accessTokenUrl: string
|
||||
authorizationUrl: string
|
||||
profileUrl?: string
|
||||
grant_type?: string
|
||||
profile?: (profile: any) => Promise<any>
|
||||
}
|
||||
|
||||
/** @docs https://next-auth.js.org/configuration/options */
|
||||
export interface NextAuthOptions {
|
||||
/** @docs https://next-auth.js.org/configuration/options#theme */
|
||||
theme?: 'auto' | 'dark' | 'light'
|
||||
/** @docs https://next-auth.js.org/configuration/options#providers */
|
||||
providers: Provider[]
|
||||
/** @docs https://next-auth.js.org/configuration/options#database */
|
||||
database?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#secret */
|
||||
secret?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#session */
|
||||
session?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#jwt */
|
||||
jwt?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#pages */
|
||||
pages?: {
|
||||
signIn?: string
|
||||
signOut?: string
|
||||
/** Error code passed in query string as ?error= */
|
||||
error?: string
|
||||
verifyRequest?: string
|
||||
/** If set, new users will be directed here on first sign in */
|
||||
newUser?: string
|
||||
}
|
||||
/**
|
||||
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
|
||||
* Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as
|
||||
* they allow you to implement access controls without a database and
|
||||
* to integrate with external databases or APIs.
|
||||
* @docs https://next-auth.js.org/configuration/options#callbacks
|
||||
*/
|
||||
callbacks?: CallbacksOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#events */
|
||||
events?: EventsOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#adapter */
|
||||
adapter?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#debug */
|
||||
debug?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#usesecurecookies */
|
||||
useSecureCookies?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#cookies */
|
||||
cookies?: CookiesOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#logger */
|
||||
logger: LoggerInstance
|
||||
}
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug' | 'logger'
|
||||
|
||||
export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthSharedOptions> {
|
||||
pkce?: {
|
||||
code_verifier?: string
|
||||
/**
|
||||
* Could be `"plain"`, but not recommended.
|
||||
* We ignore it for now.
|
||||
* @spec https://tools.ietf.org/html/rfc7636#section-4.2.
|
||||
*/
|
||||
code_challenge_method?: 'S256'
|
||||
}
|
||||
provider?: Provider
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
action?: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: NextAuthInternalOptions
|
||||
}
|
||||
|
||||
export interface NextAuthResponse extends NextApiResponse {}
|
||||
|
||||
export declare function NextAuthHandler (req: NextAuthRequest, res: NextAuthResponse, options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
export declare function NextAuthHandler (options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
@@ -1,7 +1,7 @@
|
||||
import adapters from '../adapters'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import logger from '../lib/logger'
|
||||
import logger, { setLogger } from '../lib/logger'
|
||||
import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
@@ -21,7 +21,15 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("next").NextApiRequest} req
|
||||
* @param {import("next").NextApiResponse} res
|
||||
* @param {import(".").NextAuthOptions} userOptions
|
||||
*/
|
||||
async function NextAuthHandler (req, res, userOptions) {
|
||||
if (userOptions.logger) {
|
||||
setLogger(userOptions.logger)
|
||||
}
|
||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||
if (userOptions.debug) {
|
||||
process.env._NEXTAUTH_DEBUG = true
|
||||
@@ -122,7 +130,8 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
...defaultCallbacks,
|
||||
...userOptions.callbacks
|
||||
},
|
||||
pkce: {}
|
||||
pkce: {},
|
||||
logger
|
||||
}
|
||||
|
||||
await callbackUrlHandler(req, res)
|
||||
@@ -215,6 +224,21 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case '_log':
|
||||
try {
|
||||
if (!userOptions.logger) return
|
||||
const {
|
||||
code = 'CLIENT_ERROR',
|
||||
level = 'error',
|
||||
message = '[]'
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error('LOGGER_ERROR', error)
|
||||
}
|
||||
return res.end()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
7
src/server/lib/callbacks.d.ts
vendored
Normal file
7
src/server/lib/callbacks.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
export interface CallbacksOptions {
|
||||
signIn?: (user: any, account: any, profile: any) => Promise<never | string>
|
||||
jwt?: (token: any, user: any, account: any, profile: any, isNewUser?: boolean) => Promise<any>
|
||||
session?: (session: any, userOrToken: any) => Promise<any>
|
||||
redirect?: (url: string, baseUrl: string) => Promise<string>
|
||||
}
|
||||
16
src/server/lib/cookie.d.ts
vendored
Normal file
16
src/server/lib/cookie.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface CookieOption {
|
||||
name: string
|
||||
options: {
|
||||
httpOnly: boolean
|
||||
sameSite: string
|
||||
path?: string
|
||||
secure: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface CookiesOptions {
|
||||
sessionToken: CookieOption
|
||||
callbackUrl: CookieOption
|
||||
csrfToken: CookieOption
|
||||
pkceCodeVerifier: CookieOption
|
||||
}
|
||||
@@ -110,6 +110,7 @@ function _serialize (name, val, options) {
|
||||
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
|
||||
*
|
||||
* @TODO Review cookie settings (names, options)
|
||||
* @return {import("./cookie").CookiesOptions}
|
||||
*/
|
||||
export function defaultCookies (useSecureCookies) {
|
||||
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
||||
|
||||
12
src/server/lib/events.d.ts
vendored
Normal file
12
src/server/lib/events.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export type EventType=
|
||||
| 'signIn'
|
||||
| 'signOut'
|
||||
| 'createUser'
|
||||
| 'updateUser'
|
||||
| 'linkAccount'
|
||||
| 'session'
|
||||
| 'error'
|
||||
|
||||
export type EventCallback = (message: any) => Promise<void>
|
||||
|
||||
export type EventsOptions = Partial<Record<EventType, EventCallback>>
|
||||
@@ -3,6 +3,7 @@ import oAuthClient from './client'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
|
||||
/** @param {import("../..").NextAuthRequest} req */
|
||||
export default async function oAuthCallback (req) {
|
||||
const { provider, pkce } = req.options
|
||||
const client = oAuthClient(provider)
|
||||
@@ -89,7 +90,7 @@ export default async function oAuthCallback (req) {
|
||||
* refresh_token?: string
|
||||
* id_token?: string
|
||||
* }
|
||||
* provider: object
|
||||
* provider: import("../..").Provider
|
||||
* user?: object
|
||||
* }} profileParams
|
||||
*/
|
||||
@@ -107,7 +108,7 @@ async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
|
||||
logger.debug('PROFILE_DATA', profileData)
|
||||
|
||||
const profile = await provider.profile(profileData)
|
||||
const profile = await provider.profile(profileData, tokens)
|
||||
// Return profile, raw profile and auth provider details
|
||||
return {
|
||||
profile: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { sign as jwtSign } from 'jsonwebtoken'
|
||||
* @TODO Refactor to remove dependancy on 'oauth' package
|
||||
* It is already quite monkey patched, we don't use all the features and and it
|
||||
* would be easier to maintain if all the code was native to next-auth.
|
||||
* @param {import("../..").Provider} provider
|
||||
*/
|
||||
export default function oAuthClient (provider) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
@@ -86,6 +87,9 @@ export default function oAuthClient (provider) {
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
* @param {string} code
|
||||
* @param {import("../..").Provider} provider
|
||||
* @param {string | undefined} codeVerifier
|
||||
*/
|
||||
async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
const url = provider.accessTokenUrl
|
||||
@@ -128,7 +132,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
|
||||
}
|
||||
|
||||
if ((provider.id === 'okta' || provider.id === 'identity-server4') && !headers.Authorization) {
|
||||
if (provider.id === 'identity-server4' && !headers.Authorization) {
|
||||
headers.Authorization = `Bearer ${code}`
|
||||
}
|
||||
|
||||
@@ -184,6 +188,9 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
*
|
||||
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
|
||||
* e.g. see providers/bungie
|
||||
* @param {import("../..").Provider} provider
|
||||
* @param {string} accessToken
|
||||
* @param {any} results
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
|
||||
@@ -8,7 +8,11 @@ const PKCE_LENGTH = 64
|
||||
const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/** Adds `code_verifier` to `req.options.pkce`, and removes the corresponding cookie */
|
||||
/**
|
||||
* Adds `code_verifier` to `req.options.pkce`, and removes the corresponding cookie
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
@@ -38,7 +42,11 @@ export async function handleCallback (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds `code_challenge` and `code_challenge_method` to `req.options.pkce`. */
|
||||
/**
|
||||
* Adds `code_challenge` and `code_challenge_method` to `req.options.pkce`.
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { OAuthCallbackError } from '../../../lib/errors'
|
||||
* For OAuth 2.0 flows, if the provider supports state,
|
||||
* check if state matches the one sent on signin
|
||||
* (a hash of the NextAuth.js CSRF token).
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
const { csrfToken, provider, baseUrl, basePath } = req.options
|
||||
@@ -31,7 +33,11 @@ export async function handleCallback (req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds CSRF token to the authorizationParams. */
|
||||
/**
|
||||
* Adds CSRF token to the authorizationParams.
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
const { provider, baseUrl, basePath, csrfToken } = req.options
|
||||
try {
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function email (email, provider, options) {
|
||||
const secret = provider.secret || options.secret
|
||||
|
||||
// Generate token
|
||||
const token = provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
|
||||
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
|
||||
|
||||
// Send email with link containing token (the unhashed version)
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import oAuthClient from '../oauth/client'
|
||||
import logger from '../../../lib/logger'
|
||||
|
||||
/** @param {import("../..").NextAuthRequest} req */
|
||||
export default async function getAuthorizationUrl (req) {
|
||||
const { provider } = req.options
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
// @ts-check
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import render from 'preact-render-to-string'
|
||||
|
||||
/** Renders an error page. */
|
||||
export default function error ({ baseUrl, basePath, error, res }) {
|
||||
/**
|
||||
* Renders an error page.
|
||||
* @param {{
|
||||
* baseUrl: string
|
||||
* basePath: string
|
||||
* error?: string
|
||||
* res: import("..").NextAuthResponse
|
||||
* }} params
|
||||
*/
|
||||
export default function error ({ baseUrl, basePath, error = 'default', res }) {
|
||||
const signinPageUrl = `${baseUrl}${basePath}/signin`
|
||||
|
||||
const errors = {
|
||||
@@ -37,14 +46,14 @@ export default function error ({ baseUrl, basePath, error, res }) {
|
||||
message: (
|
||||
<div>
|
||||
<p>The sign in link is no longer valid.</p>
|
||||
<p>It may have be used already or it may have expired.</p>
|
||||
<p>It may have been used already or it may have expired.</p>
|
||||
</div>
|
||||
),
|
||||
signin: <p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
}
|
||||
}
|
||||
|
||||
const { statusCode, heading, message, signin } = errors[error.toLowerCase()] || errors.default
|
||||
const { statusCode, heading, message, signin } = errors[error.toLowerCase()]
|
||||
|
||||
res.status(statusCode)
|
||||
|
||||
|
||||
@@ -9,14 +9,34 @@ export default function renderPage (req, res) {
|
||||
const { baseUrl, basePath, callbackUrl, csrfToken, providers, theme } = req.options
|
||||
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
function send (html) {
|
||||
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body class="__next-auth-theme-${theme}"><div class="page">${html}</div></body></html>`)
|
||||
function send ({ html, title }) {
|
||||
res.send(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${theme}"><div class="page">${html}</div></body></html>`)
|
||||
}
|
||||
|
||||
return {
|
||||
signin (props) { send(signin({ csrfToken, providers, callbackUrl, ...req.query, ...props })) },
|
||||
signout (props) { send(signout({ csrfToken, baseUrl, basePath, ...props })) },
|
||||
verifyRequest (props) { send(verifyRequest({ baseUrl, ...props })) },
|
||||
error (props) { send(error({ basePath, baseUrl, res, ...props })) }
|
||||
signin (props) {
|
||||
send({
|
||||
html: signin({ csrfToken, providers, callbackUrl, ...req.query, ...props }),
|
||||
title: 'Sign In'
|
||||
})
|
||||
},
|
||||
signout (props) {
|
||||
send({
|
||||
html: signout({ csrfToken, baseUrl, basePath, ...props }),
|
||||
title: 'Sign Out'
|
||||
})
|
||||
},
|
||||
verifyRequest (props) {
|
||||
send({
|
||||
html: verifyRequest({ baseUrl, ...props }),
|
||||
title: 'Verify Request'
|
||||
})
|
||||
},
|
||||
error (props) {
|
||||
send({
|
||||
html: error({ basePath, baseUrl, res, ...props }),
|
||||
title: 'Error'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
|
||||
/** Handle callbacks from login services */
|
||||
/**
|
||||
* Handle callbacks from login services
|
||||
* @param {import("..").NextAuthRequest} req
|
||||
* @param {import("..").NextAuthResponse} res
|
||||
*/
|
||||
export default async function callback (req, res) {
|
||||
const {
|
||||
provider,
|
||||
@@ -68,7 +72,7 @@ export default async function callback (req, res) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
@@ -164,7 +168,7 @@ export default async function callback (req, res) {
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
@@ -217,12 +221,12 @@ export default async function callback (req, res) {
|
||||
} else if (provider.type === 'credentials' && req.method === 'POST') {
|
||||
if (!useJwtSession) {
|
||||
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
if (!provider.authorize) {
|
||||
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const credentials = req.body
|
||||
@@ -231,11 +235,11 @@ export default async function callback (req, res) {
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
@@ -246,11 +250,11 @@ export default async function callback (req, res) {
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* Return a JSON object with a list of all OAuth providers currently configured
|
||||
* and their signin and callback URLs. This makes it possible to automatically
|
||||
* generate buttons for all providers when rendering client side.
|
||||
* @param {import("..").NextAuthRequest} req
|
||||
* @param {import("..").NextAuthResponse} res
|
||||
*/
|
||||
export default function providers (req, res) {
|
||||
const { providers } = req.options
|
||||
|
||||
@@ -44,7 +44,7 @@ export default async function signin (req, res) {
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
|
||||
5339
test/docker/app/package-lock.json
generated
5339
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,8 +11,8 @@
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^9.5.4",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
"next": "^10.0.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
48
tsconfig.json
Normal file
48
tsconfig.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"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"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -112,52 +112,6 @@ callbacks: {
|
||||
The redirect callback may be invoked more than once in the same flow.
|
||||
:::
|
||||
|
||||
## Session callback
|
||||
|
||||
The session callback is called whenever a session is checked.
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
* When using database sessions, the User object is passed as an argument.
|
||||
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {object} session Session object
|
||||
* @param {object} token User object (if using database sessions)
|
||||
* JSON Web Token (if not using database sessions)
|
||||
* @return {object} Session that will be returned to the client
|
||||
*/
|
||||
async session(session, token) {
|
||||
if(token?.accessToken) {
|
||||
// Add property to session, like an access_token from a provider
|
||||
session.accessToken = token.accessToken
|
||||
}
|
||||
return session
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
|
||||
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt` callback). If you use a database, call it `user`.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
|
||||
|
||||
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
|
||||
|
||||
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
|
||||
:::
|
||||
|
||||
## JWT callback
|
||||
|
||||
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
|
||||
@@ -206,3 +160,47 @@ NextAuth.js does not limit how much data you can store in a JSON Web Token, howe
|
||||
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
|
||||
:::
|
||||
|
||||
## 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.
|
||||
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
* When using database sessions, the User object is passed as an argument.
|
||||
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {object} session Session object
|
||||
* @param {object} token User object (if using database sessions)
|
||||
* JSON Web Token (if not using database sessions)
|
||||
* @return {object} Session that will be returned to the client
|
||||
*/
|
||||
async session(session, token) {
|
||||
// Add property to session, like an access_token from a provider.
|
||||
session.accessToken = token.accessToken
|
||||
return session
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
|
||||
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
To better represent its value, when using a JWT session, the second parameter should be called `token` (This is the same thing you return from the `jwt()` callback). If you use a database, call it `user`.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
The session object is not persisted server side, even when using database sessions - only data such as the session token, the user, and the expiry time is stored in the session table.
|
||||
|
||||
If you need to persist session data server side, you can use the `accessToken` returned for the session as a key - and connect to the database in the `session()` callback to access it. Session `accessToken` values do not rotate and are valid as long as the session is valid.
|
||||
|
||||
If using JSON Web Tokens instead of database sessions, you should use the User ID or a unique key stored in the token (you will need to generate a key for this yourself on sign in, as access tokens for sessions are not generated when using JSON Web Tokens).
|
||||
:::
|
||||
|
||||
@@ -307,6 +307,42 @@ Set debug to `true` to enable debug messages for authentication and database ope
|
||||
|
||||
---
|
||||
|
||||
### logger
|
||||
|
||||
* **Default value**: `console`
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service.
|
||||
|
||||
Example:
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
import log from "logging-service"
|
||||
|
||||
export default NextAuth({
|
||||
...
|
||||
logger: {
|
||||
error(code, ...message) {
|
||||
log.error(code, message)
|
||||
},
|
||||
warn(code, ...message) {
|
||||
log.warn(code, message)
|
||||
},
|
||||
debug(code, ...message) {
|
||||
log.debug(code, message)
|
||||
}
|
||||
}
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
:::note
|
||||
If the `debug` level is defined by the user, it will be called regardless of the `debug: false` [option](#debug).
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### theme
|
||||
|
||||
* **Default value**: `"auto"`
|
||||
|
||||
@@ -7,7 +7,7 @@ NextAuth.js automatically creates simple, unbranded authentication pages for han
|
||||
|
||||
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
|
||||
|
||||
To add a custom login page, for example. You can use the `pages` option:
|
||||
To add a custom login page, you can use the `pages` option:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
@@ -42,9 +42,9 @@ export default function SignIn({ providers }) {
|
||||
)
|
||||
}
|
||||
|
||||
SignIn.getInitialProps = async (context) => {
|
||||
SignIn.getInitialProps = async () => {
|
||||
return {
|
||||
providers: await providers(context)
|
||||
providers: await providers()
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -118,3 +118,7 @@ You can also use the `signIn()` function which will handle obtaining the CSRF to
|
||||
```js
|
||||
signIn('credentials', { username: 'jsmith', password: '1234' })
|
||||
```
|
||||
|
||||
:::tip
|
||||
Remember to put any custom pages in a folder outside **/pages/api** which is reserved for API code. As per the examples above, a location convention suggestion is `pages/auth/...`.
|
||||
:::
|
||||
|
||||
@@ -78,7 +78,10 @@ As an example of what this looks like, this is the the provider object returned
|
||||
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
|
||||
authorizationUrl: "https://accounts.google.com/o/oauth2/auth?response_type=code",
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
async profile(profile) {
|
||||
async profile(profile, tokens) {
|
||||
// You can use the tokens, in case you want to fetch more profile information
|
||||
// For example several OAuth provider does not return e-mail by default.
|
||||
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
@@ -112,6 +115,16 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add three changes:
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)
|
||||
2. Re-export your config: at [`src/providers/index.js`](https://github.com/nextauthjs/next-auth/blob/main/src/providers/index.js)
|
||||
3. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
:::
|
||||
|
||||
|
||||
### OAuth provider options
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
|
||||
@@ -5,14 +5,14 @@ title: Contributors
|
||||
|
||||
## Core Team
|
||||
|
||||
* <a href="https://github.com/iaincollins">Iain Collins</a>
|
||||
* <a href="https://github.com/LoriKarikari">Lori Karikari</a>
|
||||
* <a href="https://github.com/ndom91">Nico Domino</a>
|
||||
* <a href="https://github.com/Fumler">Fredrik Pettersen</a>
|
||||
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
|
||||
* <a href="https://github.com/lluia">Lluis Agusti</a>
|
||||
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
|
||||
* <a href="https://github.com/balazsorban44">Balázs Orbán</a>
|
||||
* [Iain Collins](https://github.com/iaincollins)
|
||||
* [Lori Karikari](https://github.com/LoriKarikari)
|
||||
* [Nico Domino](https://github.com/ndom91)
|
||||
* [Fredrik Pettersen](https://github.com/Fumler)
|
||||
* [Gerald Nolan](https://github.com/geraldnolan)
|
||||
* [Lluis Agusti](https://github.com/lluia)
|
||||
* [Jefferson Bledsoe](https://github.com/JeffersonBledsoe)
|
||||
* [Balázs Orbán](https://github.com/sponsors/balazsorban44)
|
||||
|
||||
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
|
||||
|
||||
You can then look them up from the database or persist them to the JSON Web Token.
|
||||
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, if this is something you need, currently you will need to write the logic to handle that yourself.
|
||||
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, however you can check out [this tutorial](/tutorials/refresh-token-rotation) if you want to implement it.
|
||||
|
||||
### When I sign in with another account with the same email address, why are accounts not linked automatically?
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ The `getProviders()` method returns the list of providers currently configured f
|
||||
|
||||
It calls `/api/auth/providers` and returns a list of the currently configured authentication providers.
|
||||
|
||||
It can be use useful if you are creating a dynamic custom sign in page.
|
||||
It can be useful if you are creating a dynamic custom sign in page.
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +150,7 @@ export default async (req, res) => {
|
||||
```
|
||||
|
||||
:::note
|
||||
Unlike `getSession()` and `getCsrfToken()`, when calling `getSession()` server side, you don't need to pass anything, just as calling it client side.
|
||||
Unlike `getSession()` and `getCsrfToken()`, when calling `getProviders()` server side, you don't need to pass anything, just as calling it client side.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -210,6 +210,46 @@ e.g.
|
||||
|
||||
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.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
:::note
|
||||
The redirect option is only available for `credentials` and `email` providers.
|
||||
:::
|
||||
|
||||
In some cases, you might want to deal with the sign in response on the same page and disable the default redirection. For example, if an error occurs (like wrong credentials given by the user), you might want to handle the error on the same page. For that, you can pass `redirect: false` in the second parameter object.
|
||||
|
||||
e.g.
|
||||
|
||||
- `signIn('credentials', { redirect: false, password: 'password' })`
|
||||
- `signIn('email', { redirect: false, email: 'bill@fillmurray.com' })`
|
||||
|
||||
`signIn` will then return a Promise, that resolves to the following:
|
||||
|
||||
```ts
|
||||
{
|
||||
/**
|
||||
* Will be different error codes,
|
||||
* depending on the type of error.
|
||||
*/
|
||||
error: string | undefined
|
||||
/**
|
||||
* HTTP status code,
|
||||
* hints the kind of error that happened.
|
||||
*/
|
||||
status: number
|
||||
/**
|
||||
* `true` if the signin was successful
|
||||
*/
|
||||
ok: boolean
|
||||
/**
|
||||
* `null` if there was an error,
|
||||
* otherwise the url the user
|
||||
* should have been redirected to.
|
||||
*/
|
||||
url: string | null
|
||||
}
|
||||
```
|
||||
|
||||
#### Additional params
|
||||
|
||||
It is also possible to pass additional parameters to the `/authorize` endpoint through the third argument of `signIn()`.
|
||||
@@ -256,6 +296,16 @@ 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.
|
||||
|
||||
#### Using the redirect: false option
|
||||
|
||||
If you pass `redirect: false` to `signOut`, the page will not reload. The session will be deleted, and the `useSession` hook is notified, so any indication about the user will be shown as logged out automatically. It can give a very nice experience for the user.
|
||||
|
||||
:::tip
|
||||
If you need to redirect to another page but you want to avoid a page reload, you can try:
|
||||
`const data = await signOut({redirect: false, callbackUrl: "/foo"})`
|
||||
where `data.url` is the validated url you can redirect the user to without any flicker by using Next.js's `useRouter().push(data.url)`
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Provider
|
||||
|
||||
@@ -9,8 +9,6 @@ https://developer.atlassian.com/cloud/jira/platform/oauth-2-authorization-code-g
|
||||
|
||||
## Example
|
||||
|
||||
For Jira Platform API access:
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
@@ -18,8 +16,7 @@ providers: [
|
||||
Providers.Atlassian({
|
||||
clientId: process.env.ATLASSIAN_CLIENT_ID,
|
||||
clientSecret: process.env.ATLASSIAN_CLIENT_SECRET,
|
||||
scope:
|
||||
'write:jira-work read:jira-work read:jira-user offline_access read:me',
|
||||
scope: 'write:jira-work read:jira-work read:jira-user offline_access read:me'
|
||||
})
|
||||
]
|
||||
...
|
||||
@@ -33,7 +30,7 @@ providers: [
|
||||
An app can be created at https://developer.atlassian.com/apps/
|
||||
:::
|
||||
|
||||
Under "Apis and features" side menu, configure the following for the "OAuth 2.0 (3LO)"
|
||||
Under "Apis and features" in the side menu, configure the following for "OAuth 2.0 (3LO)":
|
||||
|
||||
- Redirect URL
|
||||
- http://localhost:3000/api/auth/callback/atlassian
|
||||
|
||||
@@ -12,7 +12,23 @@ https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-c
|
||||
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
|
||||
|
||||
## Example
|
||||
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
|
||||
- Make sure to remember / copy
|
||||
- Application (client) ID
|
||||
- Directory (tenant) ID
|
||||
- When asked for a redirection URL, use http://localhost:3000/api/auth/callback/azure-ad-b2c
|
||||
- Create a new secret and remember / copy its value immediately, it will disappear.
|
||||
|
||||
In `.env.local` create the follwing entries:
|
||||
|
||||
```
|
||||
AZURE_CLIENT_ID=<copy Application (client) ID here>
|
||||
AZURE_CLIENT_SECRET=<copy generated secret value here>
|
||||
AZURE_TENANT_ID=<copy the tenant id here>
|
||||
```
|
||||
|
||||
In `pages/api/auth/[...nextauth].js` find or add the AZURE entries:
|
||||
|
||||
```js
|
||||
import Providers from 'next-auth/providers';
|
||||
...
|
||||
@@ -25,4 +41,5 @@ providers: [
|
||||
}),
|
||||
]
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
@@ -184,3 +184,17 @@ const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
|
||||
:::tip
|
||||
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
|
||||
:::
|
||||
|
||||
|
||||
## Customising the Verification Token
|
||||
|
||||
By default, we are generating a random verification token. You can define a `generateVerificationToken` method in your provider options if you want to override it:
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
async generateVerificationToken() {
|
||||
return "ABC123"
|
||||
}
|
||||
})
|
||||
],
|
||||
57
www/docs/providers/eveonline.md
Normal file
57
www/docs/providers/eveonline.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
id: eveonline
|
||||
title: EVE Online
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developers.eveonline.com/blog/article/sso-to-authenticated-calls
|
||||
|
||||
## Configuration
|
||||
|
||||
https://developers.eveonline.com/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.EVEOnline({
|
||||
clientId: process.env.EVE_CLIENT_ID,
|
||||
clientSecret: process.env.EVE_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
:::tip When creating your application, make sure to select `Authentication Only` as the connection type.
|
||||
|
||||
:::tip If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example:
|
||||
|
||||
```js
|
||||
...
|
||||
options: {
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET,
|
||||
},
|
||||
callbacks: {
|
||||
jwt: async (token, user, account, profile, isNewUser) => {
|
||||
if (profile) {
|
||||
token = {
|
||||
...token,
|
||||
id: profile.CharacterID,
|
||||
}
|
||||
}
|
||||
return token;
|
||||
},
|
||||
session: async (session, token) => {
|
||||
if (token) {
|
||||
session.user.id = token.id;
|
||||
}
|
||||
return session;
|
||||
}
|
||||
}
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -11,6 +11,8 @@ https://dev.twitch.tv/docs/authentication
|
||||
|
||||
https://dev.twitch.tv/console/apps
|
||||
|
||||
Add the following redirect URL into the console `http://<your-next-app-url>/api/auth/callback/twitch`
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
@@ -23,4 +25,4 @@ providers: [
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
id: vk
|
||||
title: vk.com
|
||||
title: VK
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -55,13 +55,13 @@ However, it should not be enabled against production databases as it may cause d
|
||||
|
||||
## Prisma Adapter
|
||||
|
||||
You can also use NextAuth.js with [Prisma](https://www.prisma.io/docs/).
|
||||
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:
|
||||
|
||||
```
|
||||
npm i @prisma/client
|
||||
npm add -D @prisma/cli
|
||||
npm install @prisma/client
|
||||
npm install prisma --save-dev
|
||||
```
|
||||
|
||||
Configure your NextAuth.js to use the Prisma adapter:
|
||||
@@ -91,7 +91,7 @@ While Prisma includes an experimental feature in the migration command that is a
|
||||
|
||||
### Prisma Schema
|
||||
|
||||
Create a `schema.prisma` file similar to this one:
|
||||
Create a schema file in `prisma/schema.prisma` similar to this one:
|
||||
|
||||
```json title="schema.prisma"
|
||||
generator client {
|
||||
@@ -179,12 +179,22 @@ datasource db {
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, you can run the Prisma CLI to generate the Prisma Client:
|
||||
Once you have saved your schema, use the Prisma CLI to generate the Prisma Client:
|
||||
|
||||
```
|
||||
npx @prisma/cli generate
|
||||
npx prisma generate
|
||||
```
|
||||
|
||||
To configure you database to use the new schema (i.e. create tables and columns) use the `primsa migrate` command:
|
||||
|
||||
```
|
||||
npx prisma migrate dev --preview-feature
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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`.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -9,6 +9,10 @@ _These tutorials are contributed by the community and hosted on this site._
|
||||
|
||||
_New submissions and edits are welcome!_
|
||||
|
||||
### [Refresh Token Rotation](tutorials/refresh-token-rotation)
|
||||
|
||||
How to implement refresh token rotation.
|
||||
|
||||
### [Securing pages and API routes](tutorials/securing-pages-and-api-routes)
|
||||
|
||||
How to restrict access to pages and API routes.
|
||||
@@ -67,7 +71,6 @@ 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.
|
||||
|
||||
|
||||
### [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.
|
||||
|
||||
139
www/docs/tutorials/refresh-token-rotation.md
Normal file
139
www/docs/tutorials/refresh-token-rotation.md
Normal file
@@ -0,0 +1,139 @@
|
||||
---
|
||||
id: refresh-token-rotation
|
||||
title: Refresh Token Rotation
|
||||
---
|
||||
|
||||
While NextAuth.js doesn't automatically handle access token rotation for OAuth providers yet, this functionality can be implemented using [callbacks](https://next-auth.js.org/configuration/callbacks).
|
||||
|
||||
## Source Code
|
||||
|
||||
_A working example can be accessed [here](https://github.com/lawrencecchen/next-auth-refresh-tokens)._
|
||||
|
||||
## Implementation
|
||||
|
||||
### Server Side
|
||||
|
||||
Using a [JWT callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) and a [session callback](https://next-auth.js.org/configuration/callbacks#session-callback), we can persist OAuth tokens and refresh them when they expire.
|
||||
|
||||
Below is a sample implementation using Google's Identity Provider. Please note that the OAuth 2.0 request in the `refreshAccessToken()` function will vary between different providers, but the core logic should remain similar.
|
||||
|
||||
```js title="pages/auth/[...nextauth.js]"
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
|
||||
const GOOGLE_AUTHORIZATION_URL =
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?" +
|
||||
new URLSearchParams({
|
||||
prompt: "consent",
|
||||
access_type: "offline",
|
||||
response_type: "code",
|
||||
});
|
||||
|
||||
/**
|
||||
* Takes a token, and returns a new token with updated
|
||||
* `accessToken` and `accessTokenExpires`. If an error occurs,
|
||||
* returns the old token and an error property
|
||||
*/
|
||||
async function refreshAccessToken(token) {
|
||||
try {
|
||||
const url =
|
||||
"https://oauth2.googleapis.com/token?" +
|
||||
new URLSearchParams({
|
||||
client_id: process.env.GOOGLE_CLIENT_ID,
|
||||
client_secret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: token.refreshToken,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const refreshedTokens = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw refreshedTokens;
|
||||
}
|
||||
|
||||
return {
|
||||
...token,
|
||||
accessToken: refreshedTokens.access_token,
|
||||
accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
|
||||
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
return {
|
||||
...token,
|
||||
error: "RefreshAccessTokenError",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
authorizationUrl: GOOGLE_AUTHORIZATION_URL,
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt(token, user, account) {
|
||||
// Initial sign in
|
||||
if (account && user) {
|
||||
return {
|
||||
accessToken: account.accessToken,
|
||||
accessTokenExpires: Date.now() + account.expires_in * 1000,
|
||||
refreshToken: account.refresh_token,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
// Return previous token if the access token has not expired yet
|
||||
if (Date.now() < token.accessTokenExpires) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Access token has expired, try to update it
|
||||
return refreshAccessToken(token);
|
||||
},
|
||||
async session(session, token) {
|
||||
if (token) {
|
||||
session.user = token.user;
|
||||
session.accessToken = token.accessToken;
|
||||
session.error = token.error;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Client Side
|
||||
|
||||
The `RefreshAccessTokenError` error that is caught in the `refreshAccessToken()` method is passed all the way to the client. This means that you can direct the user to the sign in flow if we cannot refresh their token.
|
||||
|
||||
We can handle this functionality as a side effect:
|
||||
|
||||
```js title="pages/auth/[...nextauth.js]"
|
||||
import { signIn, useSession } from "next-auth/client";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const HomePage() {
|
||||
const [session] = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.error === "RefreshAccessTokenError") {
|
||||
signIn(); // Force sign in to hopefully resolve error
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
return (...)
|
||||
}
|
||||
```
|
||||
@@ -68,7 +68,7 @@ module.exports = {
|
||||
to: '/contributors'
|
||||
},
|
||||
{
|
||||
label: 'Canary docs',
|
||||
label: 'Canary documentation',
|
||||
to: 'https://next-auth-git-canary.nextauthjs.vercel.app/'
|
||||
}
|
||||
]
|
||||
|
||||
6
www/package-lock.json
generated
6
www/package-lock.json
generated
@@ -10900,9 +10900,9 @@
|
||||
"integrity": "sha512-MgMhSdHuHymNRqD6KM3eGS0PNqgK9q4QF5P0yoQQvpB6jNjeSAi3jcSAz0Sua/t9fa4xDOMar9HJbLa08gl9ug=="
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.22.0.tgz",
|
||||
"integrity": "sha512-lLJ/Wt9yy0AiSYBf212kK3mM5L8ycwlyTlSxHBAneXLR0nzFMlZ5y7riFPF3E33zXOF2IH95xdY5jIyZbM9z/w==",
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
|
||||
"integrity": "sha512-c29LVsqOaLbBHuIbsTxaKENh1N2EQBOHaWv7gkHN4dgRbxSREqDnDbtFJYdpPauS4YCplMSNCABQ6Eeor69bAA==",
|
||||
"requires": {
|
||||
"clipboard": "^2.0.0"
|
||||
}
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
"name": "next-auth-docs",
|
||||
"version": "0.1.1",
|
||||
"scripts": {
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"start": "npm run generate-providers && docusaurus start",
|
||||
"build": "npm run generate-providers && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix"
|
||||
"lint:fix": "standard --fix",
|
||||
"generate-providers": "node ./scripts/generate-providers.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^2.0.0-alpha.66",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.66",
|
||||
"@docusaurus/core": "^2.0.0-alpha.70",
|
||||
"@docusaurus/preset-classic": "^2.0.0-alpha.70",
|
||||
"classnames": "^2.2.6",
|
||||
"docusaurus-lunr-search": "^2.1.7",
|
||||
"docusaurus-lunr-search": "^2.1.10",
|
||||
"jose": "^2.0.2",
|
||||
"lodash.times": "^4.3.2",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-marquee-slider": "^1.1.2",
|
||||
"styled-components": "^5.2.0"
|
||||
"styled-components": "^5.2.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -34,6 +35,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^15.0.0"
|
||||
"standard": "^16.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"apple": "Apple",
|
||||
"atlassian": "Atlassian",
|
||||
"auth0": "Auth0",
|
||||
"azure-ad-b2c": "Azure Active Directory B2C",
|
||||
"basecamp": "Basecamp",
|
||||
"battle.net": "Battle.net",
|
||||
"box": "Box",
|
||||
"bungie": "Bungie",
|
||||
"cognito": "Amazon Cognito",
|
||||
"credentials": "Credentials",
|
||||
"discord": "Discord",
|
||||
"email": "Email",
|
||||
"facebook": "Facebook",
|
||||
"foursquare": "Foursquare",
|
||||
"fusionauth": "FusionAuth",
|
||||
"github": "GitHub",
|
||||
"gitlab": "GitLab",
|
||||
"google": "Google",
|
||||
"identity-server4": "IdentityServer4",
|
||||
"line": "LINE",
|
||||
"linkedin": "LinkedIn",
|
||||
"mailru": "Mail.ru",
|
||||
"medium": "Medium",
|
||||
"netlify": "Netlify",
|
||||
"okta": "Okta",
|
||||
"reddit": "Reddit",
|
||||
"salesforce": "Salesforce",
|
||||
"slack": "Slack",
|
||||
"spotify": "Spotify",
|
||||
"strava": "Strava",
|
||||
"twitch": "Twitch",
|
||||
"twitter": "Twitter",
|
||||
"vk": "VK",
|
||||
"yandex": "Yandex"
|
||||
}
|
||||
15
www/scripts/generate-providers.js
Normal file
15
www/scripts/generate-providers.js
Normal file
@@ -0,0 +1,15 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const providersPath = path.join(process.cwd(), '/docs/providers')
|
||||
|
||||
const files = fs.readdirSync(providersPath, 'utf8')
|
||||
|
||||
const result = files.reduce((acc, file) => {
|
||||
const provider = fs.readFileSync(path.join(providersPath, file), 'utf8')
|
||||
const { id, title } = provider.match(/id: (?<id>.+)\ntitle: (?<title>.+)\n/).groups
|
||||
acc[id] = title
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), 'providers.json'), JSON.stringify(result, null, 2))
|
||||
Reference in New Issue
Block a user