mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3474d3e250 | ||
|
|
a35c3a424c | ||
|
|
6e65ba87a6 | ||
|
|
ae7247f14f | ||
|
|
12a5d6b1f4 | ||
|
|
19da066b04 | ||
|
|
8115a7c66c | ||
|
|
1ab029c60a | ||
|
|
d0dbacfc4b | ||
|
|
f6b7e0aad9 | ||
|
|
4a23f88180 | ||
|
|
f8dbd67a16 | ||
|
|
9406f8b332 | ||
|
|
b0410ed9d4 | ||
|
|
8c0d0c4dea | ||
|
|
9446c26419 | ||
|
|
cdfa6008c7 | ||
|
|
b4bb8bda26 | ||
|
|
2c32504cc9 | ||
|
|
bd188ff410 | ||
|
|
89aedb1285 | ||
|
|
db8c0820b6 | ||
|
|
d86464c822 | ||
|
|
fcfeb0ce88 | ||
|
|
01e472912e | ||
|
|
bbfeac408e | ||
|
|
364de1fc6c | ||
|
|
52af06cd33 | ||
|
|
8f472c5987 | ||
|
|
dcbd7a6703 | ||
|
|
e6fd4c2edc | ||
|
|
e19ca19a82 | ||
|
|
7b1b68e1c4 | ||
|
|
56d848c868 | ||
|
|
100eece7a2 | ||
|
|
278ecc1e48 | ||
|
|
a3d379554b | ||
|
|
983dd98a66 | ||
|
|
ca3f26b8d2 | ||
|
|
d2a2352e9a | ||
|
|
3043a9525a | ||
|
|
e1c6632b6f | ||
|
|
56e64e322e | ||
|
|
cbd056f225 | ||
|
|
22ab66f9d8 | ||
|
|
3597733dae | ||
|
|
cb9ce69ba3 | ||
|
|
c19a79cbca | ||
|
|
e97e090b65 | ||
|
|
eda4a6d18b | ||
|
|
94f66b60d8 | ||
|
|
9a85e27c0c | ||
|
|
7fb7e3d1bc | ||
|
|
90066fdbec | ||
|
|
475f0e7b51 | ||
|
|
a9131724d6 | ||
|
|
55bfb6d9dc | ||
|
|
af3da3abf8 | ||
|
|
339d9f2d03 | ||
|
|
a24fb8b380 | ||
|
|
65319e3927 | ||
|
|
19917972ef | ||
|
|
c1b412814a | ||
|
|
53ea8407ea | ||
|
|
66f46e8cc7 | ||
|
|
fec69a21be | ||
|
|
505ebb8ae1 | ||
|
|
fb4381d8eb | ||
|
|
4772f5b571 | ||
|
|
481db425d6 | ||
|
|
b886729bb8 | ||
|
|
3a21a9c9f1 | ||
|
|
9e4a6fec59 | ||
|
|
86921022dc | ||
|
|
f57f11e6ff | ||
|
|
77ad6bd97e | ||
|
|
78c7041b3f | ||
|
|
99edead0f2 | ||
|
|
b0b3dbc0fc | ||
|
|
8b5af54e1c | ||
|
|
0b5b04a22f | ||
|
|
890be1de0d | ||
|
|
40ae747bc1 | ||
|
|
5a8022e9a2 | ||
|
|
3e512b5cf5 | ||
|
|
81071d7776 | ||
|
|
fc05140c1f | ||
|
|
faec6824ba | ||
|
|
b91bfef16d | ||
|
|
ba9dc17e44 | ||
|
|
c220bcc57e | ||
|
|
f8a4808aa7 | ||
|
|
495d0a47db | ||
|
|
8cda627fe6 | ||
|
|
d0a0ccc6bc | ||
|
|
999222cd97 | ||
|
|
72eb7fda3f | ||
|
|
3c94940ae6 | ||
|
|
1a8ed2aec1 | ||
|
|
0e2321dc14 | ||
|
|
78d1983f9a | ||
|
|
5435df110c | ||
|
|
32853b8d1e | ||
|
|
9737b4c6ab | ||
|
|
e9bdd5c355 | ||
|
|
9728567296 | ||
|
|
ef6579a7ee | ||
|
|
8e810aa765 | ||
|
|
37596edf2b | ||
|
|
229a3e430e | ||
|
|
1d80f595c5 | ||
|
|
189a2c8e0e | ||
|
|
97096fb811 | ||
|
|
e8b75e40b1 | ||
|
|
d41c38e002 | ||
|
|
966bc7b433 | ||
|
|
e7b06d3362 | ||
|
|
d5d8eb8d7c | ||
|
|
8ec07f0224 | ||
|
|
558536db1e | ||
|
|
0c2fe054d1 | ||
|
|
b5a69fd787 | ||
|
|
9b29ed347d | ||
|
|
c5c4ff4d51 | ||
|
|
008b1a9f8d | ||
|
|
4a6f153aa6 | ||
|
|
9eccc78e3a | ||
|
|
09938cc368 | ||
|
|
5db05e1031 | ||
|
|
f6ba72b4fa | ||
|
|
bf7e555cfa | ||
|
|
26abc70a99 | ||
|
|
d38cd54dee | ||
|
|
200690ad6c | ||
|
|
52b69a6d68 | ||
|
|
f319b2af05 | ||
|
|
b80a005733 | ||
|
|
34936aecc0 | ||
|
|
b021f26f03 | ||
|
|
fcf7197120 | ||
|
|
bec8d8dff1 | ||
|
|
781c63e966 | ||
|
|
2da1883726 | ||
|
|
83ffac7cd2 | ||
|
|
6198903cdf | ||
|
|
bd98f8188c | ||
|
|
73ea402b1c | ||
|
|
4284684a3b | ||
|
|
b5d522410a | ||
|
|
284cb8e2a7 | ||
|
|
079aab2315 | ||
|
|
645ee382cf | ||
|
|
e947a772ce | ||
|
|
5d63adf7df | ||
|
|
f1a872f861 | ||
|
|
02b1d02f09 | ||
|
|
a3479b3503 | ||
|
|
740535a8f2 | ||
|
|
19ed684a52 | ||
|
|
bd72949fa7 | ||
|
|
a277cd5b0c | ||
|
|
fd6e7e94df | ||
|
|
2f6403478d | ||
|
|
a4372ffc61 | ||
|
|
d6ce92811e | ||
|
|
e5aecdf315 | ||
|
|
6d1c457a75 | ||
|
|
6e16aec6d3 | ||
|
|
f899d7bb04 | ||
|
|
e36646ce7f | ||
|
|
f3d36a74c9 | ||
|
|
4e11c9c36e | ||
|
|
0a7ac36584 | ||
|
|
fc4850f354 | ||
|
|
6e9a8d2074 | ||
|
|
c712d7da07 | ||
|
|
5183181d1c | ||
|
|
b024f89ba8 | ||
|
|
fbbe516b9a | ||
|
|
d48a3fd948 | ||
|
|
86f0c53bd3 | ||
|
|
7f6cc2048b | ||
|
|
b2829f6384 | ||
|
|
67c5041860 | ||
|
|
33df9e3132 | ||
|
|
602bc28a45 | ||
|
|
5a7a494701 | ||
|
|
9fa82cedbd | ||
|
|
b0408284b8 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a defect with the software
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/question.md
vendored
2
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask for information or support
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
labels: question
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
29
.github/workflows/node.js.yml
vendored
Normal file
29
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x]
|
||||
|
||||
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 --if-present
|
||||
- run: npm test
|
||||
36
.github/workflows/npm-publish.yml
vendored
Normal file
36
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Publishes module to registry when a new release is created.
|
||||
#
|
||||
# The following secrets need to be configured for this workflow:
|
||||
#
|
||||
# * NPM_TOKEN - Auth token from npmjs.com
|
||||
|
||||
name: Publish to NPM
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
|
||||
publish-npm:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- run: npm ci
|
||||
- run: npm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -25,4 +25,9 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Docusaurus
|
||||
www/build
|
||||
www/build
|
||||
|
||||
#VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
/.vs/slnx.sqlite
|
||||
/.vs
|
||||
|
||||
@@ -57,17 +57,18 @@ A quick and dirty guide on how to setup *next-auth* locally to work on it and te
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
3. Link React between the repo and the version installed in your project:
|
||||
|
||||
npm link ../your-application/node_modules/react
|
||||
|
||||
*This is an annoying step and not obvious, but is needed because of how React has been written (otherwise React crashes when you try to use the `useSession()` hook in your project).*
|
||||
|
||||
4. Finally link your project back to your local copy of next auth:
|
||||
3. Link your project back to your local copy of next auth:
|
||||
|
||||
cd ../your-application
|
||||
npm link ../next-auth
|
||||
|
||||
4. Finally link React between the repo and the version installed in your project:
|
||||
|
||||
cd ../next-auth
|
||||
npm link ../your-application/node_modules/react
|
||||
|
||||
*This is an annoying step and not obvious, but is needed because of how React has been written (otherwise React crashes when you try to use the `useSession()` hook in your project).*
|
||||
|
||||
That's it!
|
||||
|
||||
Notes: You may need to repeat both `npm link` steps if you install / update additional dependancies with `npm i`.
|
||||
|
||||
26
README.md
26
README.md
@@ -14,27 +14,38 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication
|
||||
### Flexible and easy to use
|
||||
|
||||
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
* Built-in support for [many popular OAuth sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
* Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
* Supports email / passwordless authentication
|
||||
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
* Supports both JSON Web Tokens and database sessions
|
||||
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
|
||||
### Own your own data
|
||||
|
||||
NextAuth.js can be used with or without a database.
|
||||
|
||||
* An open source solution that allows you to keep control of your data
|
||||
* Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
* Built-in support for for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/database)
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](https://next-auth.js.org/configuration/databases)
|
||||
* Works great with databases from popular hosting providers
|
||||
* Can also be used without a database (e.g. OAuth + JWT)
|
||||
* Can also be used *without a database* (e.g. OAuth + JWT)
|
||||
|
||||
### Secure by default
|
||||
|
||||
* Designed to be secure by default and promote best practice for safeguarding user data
|
||||
* Promotes the use of passwordless sign in mechanisms
|
||||
* Designed to be secure by default and encourage best practice for safeguarding user data
|
||||
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
|
||||
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
|
||||
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
|
||||
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
|
||||
* Auto-generates symmetric signing and encryption keys for developer convenience
|
||||
* Features tab/window syncing and keepalive messages to support short lived sessions
|
||||
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
|
||||
|
||||
Security focused features include CSRF protection, use of signed cookies, cookie prefixes, secure cookies, HTTP only, host only and secure only cookies and promoting passwordless sign-in.
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -45,7 +56,6 @@ import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
site: 'https://example.com'
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
@@ -97,7 +107,7 @@ export default () => {
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
[NextAuth.js 2.0 is possible thanks to its contributors.](https://next-auth.js.org/contributors)
|
||||
[NextAuth.js is possible thanks to its contributors.](https://next-auth.js.org/contributors)
|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
96
package-lock.json
generated
96
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "2.0.0",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -1041,6 +1041,11 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@panva/asn1.js": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz",
|
||||
"integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw=="
|
||||
},
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
@@ -1334,6 +1339,11 @@
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
},
|
||||
"base64url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
|
||||
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A=="
|
||||
},
|
||||
"bignumber.js": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
|
||||
@@ -1413,6 +1423,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"browserify-zlib": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
|
||||
"integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
|
||||
"requires": {
|
||||
"pako": "~1.0.5"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz",
|
||||
@@ -2253,6 +2271,11 @@
|
||||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
@@ -3573,6 +3596,11 @@
|
||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"futoin-hkdf": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.2.tgz",
|
||||
"integrity": "sha512-3EVi3ETTyJg5PSXlxLCaUVVn0pSbDf62L3Gwxne7Uq+d8adOSNWQAad4gg7WToHkcgnCJb3Wlb1P8r4Evj4GPw=="
|
||||
},
|
||||
"gensync": {
|
||||
"version": "1.0.0-beta.1",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz",
|
||||
@@ -4187,6 +4215,14 @@
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"jose": {
|
||||
"version": "1.27.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-1.27.2.tgz",
|
||||
"integrity": "sha512-zLIwnMa8dh5A2jFo56KvhiXCaW0hFjdNvG0I5GScL8Wro+/r/SnyIYTbnX3fYztPNSfgQp56sDMHUuS9c3e6bw==",
|
||||
"requires": {
|
||||
"@panva/asn1.js": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
|
||||
@@ -4360,10 +4396,9 @@
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.15",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
|
||||
"dev": true
|
||||
"version": "4.17.19",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
|
||||
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
@@ -4421,6 +4456,11 @@
|
||||
"chalk": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
|
||||
},
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -4642,6 +4682,28 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz",
|
||||
"integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q=="
|
||||
},
|
||||
"node-jose": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/node-jose/-/node-jose-1.1.4.tgz",
|
||||
"integrity": "sha512-L31IFwL3pWWcMHxxidCY51ezqrDXMkvlT/5pLTfNw5sXmmOLJuN6ug7txzF/iuZN55cRpyOmoJrotwBQIoo5Lw==",
|
||||
"requires": {
|
||||
"base64url": "^3.0.1",
|
||||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^5.5.0",
|
||||
"es6-promise": "^4.2.8",
|
||||
"lodash": "^4.17.15",
|
||||
"long": "^4.0.0",
|
||||
"node-forge": "^0.8.5",
|
||||
"process": "^0.11.10",
|
||||
"react-zlib-js": "^1.0.4",
|
||||
"uuid": "^3.3.3"
|
||||
}
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "1.1.53",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
|
||||
@@ -4896,6 +4958,11 @@
|
||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
|
||||
"dev": true
|
||||
},
|
||||
"pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
|
||||
},
|
||||
"parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -6061,6 +6128,11 @@
|
||||
"integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==",
|
||||
"dev": true
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -6107,6 +6179,11 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"dev": true
|
||||
},
|
||||
"react-zlib-js": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-zlib-js/-/react-zlib-js-1.0.4.tgz",
|
||||
"integrity": "sha512-ynXD9DFxpE7vtGoa3ZwBtPmZrkZYw2plzHGbanUjBOSN4RtuXdektSfABykHtTiWEHMh7WdYj45LHtp228ZF1A=="
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
@@ -6367,7 +6444,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
|
||||
"integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"resolve-from": "^2.0.0",
|
||||
"semver": "^5.1.0"
|
||||
@@ -6376,8 +6452,7 @@
|
||||
"resolve-from": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -7384,6 +7459,11 @@
|
||||
"object.getownpropertydescriptors": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
|
||||
|
||||
25
package.json
25
package.json
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "next-auth",
|
||||
"version": "2.0.0",
|
||||
"description": "An authentication library for Next.js",
|
||||
"repository": "https://github.com/iaincollins/next-auth.git",
|
||||
"version": "3.0.1",
|
||||
"description": "Authentication for Next.js",
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -12,13 +13,19 @@
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --watch src --out-dir dist",
|
||||
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
|
||||
"test": "npm run test:db",
|
||||
"test": "npm run lint",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"db:start": "docker-compose -f test/docker/docker-compose.yml up -d",
|
||||
"db:start:mongo": "docker-compose -f test/docker/mongo.yml up -d",
|
||||
"db:start:mysql": "docker-compose -f test/docker/mysql.yml up -d",
|
||||
"db:start:postgres": "docker-compose -f test/docker/postgres.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/docker-compose.yml down",
|
||||
"db:stop:mongo": "docker-compose -f test/docker/mongo.yml down",
|
||||
"db:stop:mysql": "docker-compose -f test/docker/mysql.yml down",
|
||||
"db:stop:postgres": "docker-compose -f test/docker/postgres.yml down",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
@@ -36,19 +43,29 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"node-jose": "^1.1.4",
|
||||
"nodemailer": "^6.4.6",
|
||||
"oauth": "^0.9.15",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.7",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
"mysql": "^2.18.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Serverless target in Next.js does work if you try to read in files at runtime
|
||||
// Serverless target in Next.js does not work if you try to read in files at runtime
|
||||
// that are not JavaScript or JSON (e.g. CSS files).
|
||||
// https://github.com/iaincollins/next-auth/issues/281
|
||||
//
|
||||
@@ -15,4 +15,4 @@ const css = fs.readFileSync(pathToCss, 'utf8')
|
||||
const cssWithEscapedQuotes = css.replace(/"/gm, '\\"')
|
||||
const js = `module.exports = function() { return "${cssWithEscapedQuotes}" }`
|
||||
|
||||
fs.writeFileSync(pathToCssJs, js)
|
||||
fs.writeFileSync(pathToCssJs, js)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM
|
||||
TypeORM,
|
||||
Prisma
|
||||
}
|
||||
|
||||
334
src/adapters/prisma/index.js
Normal file
334
src/adapters/prisma/index.js
Normal file
@@ -0,0 +1,334 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
prisma,
|
||||
modelMapping = {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verificationRequest'
|
||||
}
|
||||
} = config
|
||||
|
||||
const { User, Account, Session, VerificationRequest } = modelMapping
|
||||
|
||||
function getCompoundId (providerId, providerAccountId) {
|
||||
return createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
return prisma[User].create({
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified ? profile.emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
try {
|
||||
return prisma[User].findOne({ where: { id } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return prisma[User].findOne({ where: { email } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].findOne({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
try {
|
||||
const { id, name, email, image, emailVerified } = user
|
||||
return prisma[User].update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
emailVerified: emailVerified ? emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
try {
|
||||
return prisma[User].delete({ where: { id: userId } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
return prisma[Account].create({
|
||||
data: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
compoundId: getCompoundId(providerId, providerAccountId),
|
||||
providerAccountId: `${providerAccountId}`,
|
||||
providerId,
|
||||
providerType,
|
||||
accessTokenExpires,
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].delete({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
} catch (error) {
|
||||
logger.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
return prisma[Session].create({
|
||||
data: {
|
||||
expires,
|
||||
userId: user.id,
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await prisma[Session].findOne({ where: { sessionToken } })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await prisma[Session].delete({ where: { sessionToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires } })
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return prisma[Session].delete({ where: { sessionToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const verificationRequest = await prisma[VerificationRequest].create({
|
||||
data: {
|
||||
identifier,
|
||||
token: hashedToken,
|
||||
expires
|
||||
}
|
||||
})
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// 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].findOne({ where: { 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 } })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
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 } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createConnection, getConnection, getManager } from 'typeorm'
|
||||
import { createConnection, getConnection } from 'typeorm'
|
||||
import { createHash } from 'crypto'
|
||||
import require_optional from 'require_optional' // eslint-disable-line camelcase
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
@@ -28,7 +29,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// anything to do them). This function updates arguments by reference.
|
||||
adapterTransform(typeOrmConfigObject, models, options)
|
||||
|
||||
const config = adapterConfig.loadConfig(typeOrmConfigObject, { models, ...options })
|
||||
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
|
||||
|
||||
// Create objects from models that can be consumed by functions in the adapter
|
||||
const User = models.User.model
|
||||
@@ -67,12 +68,14 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
await _connect()
|
||||
}
|
||||
|
||||
// Get manager from connection object
|
||||
// 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 debugMessage (debugCode, ...args) {
|
||||
if (appOptions && appOptions.debug) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
@@ -86,7 +89,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
let ObjectId
|
||||
if (config.type === 'mongodb') {
|
||||
idKey = '_id'
|
||||
const mongodb = await import('mongodb')
|
||||
// Using a dynamic import causes problems for some compilers/bundlers
|
||||
// that don't handle dynamic imports. To try and work around this we are
|
||||
// using the same method mongodb uses to load Object ID type, which is to
|
||||
// use the require_optional loader.
|
||||
const mongodb = require_optional('mongodb')
|
||||
ObjectId = mongodb.ObjectId
|
||||
}
|
||||
|
||||
@@ -96,7 +103,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// Use a conditional to default to 30 day session age if not set - it should
|
||||
// always be set but a meaningful fallback is helpful to facilitate testing.
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debugMessage('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
@@ -107,11 +114,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debugMessage('CREATE_USER', profile)
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
// Create user account
|
||||
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
|
||||
return await getManager().save(user)
|
||||
return await manager.save(user)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
@@ -119,7 +126,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debugMessage('GET_USER', id)
|
||||
debug('GET_USER', id)
|
||||
|
||||
// In the very specific case of both using JWT for storing session data
|
||||
// and using MongoDB to store user data, the ID is a string rather than
|
||||
@@ -132,7 +139,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
try {
|
||||
return connection.getRepository(User).findOne({ [idKey]: id })
|
||||
return manager.findOne(User, { [idKey]: id })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
@@ -140,10 +147,10 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debugMessage('GET_USER_BY_EMAIL', email)
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return connection.getRepository(User).findOne({ email })
|
||||
return manager.findOne(User, { email })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
@@ -151,11 +158,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debugMessage('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await connection.getRepository(Account).findOne({ providerId, providerAccountId })
|
||||
const account = await manager.findOne(Account, { providerId, providerAccountId })
|
||||
if (!account) { return null }
|
||||
return connection.getRepository(User).findOne({ [idKey]: account.userId })
|
||||
return manager.findOne(User, { [idKey]: account.userId })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
@@ -163,22 +170,22 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debugMessage('UPDATE_USER', user)
|
||||
return getManager().save(user)
|
||||
debug('UPDATE_USER', user)
|
||||
return manager.save(User, user)
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debugMessage('DELETE_USER', userId)
|
||||
debug('DELETE_USER', userId)
|
||||
// @TODO Delete user from DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debugMessage('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
// Create provider account linked to user
|
||||
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return getManager().save(account)
|
||||
return manager.save(account)
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
@@ -186,7 +193,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debugMessage('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
// @TODO Get current user from DB
|
||||
// @TODO Delete [provider] object from user object
|
||||
// @TODO Save changes to user object in DB
|
||||
@@ -194,7 +201,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debugMessage('CREATE_SESSION', user)
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
@@ -205,7 +212,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
|
||||
const session = new Session(user.id, expires)
|
||||
|
||||
return getManager().save(session)
|
||||
return manager.save(session)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
@@ -213,9 +220,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debugMessage('GET_SESSION', sessionToken)
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await connection.getRepository(Session).findOne({ sessionToken })
|
||||
const session = await manager.findOne(Session, { sessionToken })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > new Date(session.expires)) {
|
||||
@@ -231,7 +238,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debugMessage('UPDATE_SESSION', session)
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
@@ -259,7 +266,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
return getManager().save(session)
|
||||
return manager.save(Session, session)
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
@@ -267,9 +274,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debugMessage('DELETE_SESSION', sessionToken)
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return await connection.getRepository(Session).delete({ sessionToken })
|
||||
return await manager.delete(Session, { sessionToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
@@ -277,9 +284,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debugMessage('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { site } = appOptions
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
@@ -296,11 +303,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
|
||||
// Save to database
|
||||
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
|
||||
const verificationRequest = await getManager().save(newVerificationRequest)
|
||||
const verificationRequest = await manager.save(newVerificationRequest)
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, site, provider })
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
@@ -310,16 +317,16 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debugMessage('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// 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 connection.getRepository(VerificationRequest).findOne({ identifier, token: hashedToken })
|
||||
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await connection.getRepository(VerificationRequest).delete({ token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -331,11 +338,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debugMessage('DELETE_VERIFICATION', identifier, token)
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await connection.getRepository(VerificationRequest).delete({ token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
|
||||
@@ -7,19 +7,30 @@ const parseConnectionString = (configString) => {
|
||||
// to make configuration easier (in most use cases).
|
||||
//
|
||||
// TypeORM accepts connection string as a 'url' option, but unfortunately
|
||||
// not for all databases (e.g. SQLite) or options, so we handle parsing it
|
||||
// in this function..
|
||||
// not for all databases (e.g. SQLite) or for all options, so we handle
|
||||
// parsing it in this function.
|
||||
try {
|
||||
const parsedUrl = new URL(configString)
|
||||
const config = {}
|
||||
|
||||
// Remove : and convert strings like 'mongodb+srv' into 'mongodb'
|
||||
config.type = parsedUrl.protocol.replace(/:$/, '').replace(/\+(.*)?$/, '')
|
||||
config.host = parsedUrl.hostname
|
||||
config.port = Number(parsedUrl.port)
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '')
|
||||
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
|
||||
// Special case handling is required for mongodb+srv with TypeORM
|
||||
config.type = 'mongodb'
|
||||
config.url = configString.replace(/\?(.*)$/, '')
|
||||
config.useNewUrlParser = true
|
||||
} else {
|
||||
config.type = parsedUrl.protocol.replace(/:$/, '')
|
||||
config.host = parsedUrl.hostname
|
||||
config.port = Number(parsedUrl.port)
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
|
||||
}
|
||||
|
||||
// This option is recommended by mongodb
|
||||
if (config.type === 'mongodb') {
|
||||
config.useUnifiedTopology = true
|
||||
}
|
||||
|
||||
if (parsedUrl.search) {
|
||||
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
|
||||
@@ -42,7 +53,7 @@ const parseConnectionString = (configString) => {
|
||||
|
||||
const loadConfig = (config, { models, namingStrategy }) => {
|
||||
const defaultConfig = {
|
||||
name: 'default',
|
||||
name: 'nextauth',
|
||||
autoLoadEntities: true,
|
||||
entities: [
|
||||
new EntitySchema(models.User.schema),
|
||||
|
||||
@@ -1,51 +1,45 @@
|
||||
// Perform transforms on SQL models so they can be used with other databases
|
||||
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
|
||||
|
||||
const postgres = (models, options) => {
|
||||
const postgresTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for Postgres databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// Only transforms models that are not custom models
|
||||
const { models: customModels = {} } = options
|
||||
|
||||
// For Postgres we need to use the `timestamp with time zone` type
|
||||
// aka `timestamptz` to store timestamps correctly in UTC.
|
||||
if (!customModels.User) {
|
||||
for (const column in models.User.schema.columns) {
|
||||
if (models.User.schema.columns[column].type === 'timestamp') {
|
||||
models.User.schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.Account) {
|
||||
for (const column in models.Account.schema.columns) {
|
||||
if (models.Account.schema.columns[column].type === 'timestamp') {
|
||||
models.Account.schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.Session) {
|
||||
for (const column in models.Session.schema.columns) {
|
||||
if (models.Session.schema.columns[column].type === 'timestamp') {
|
||||
models.Session.schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.VerificationRequest) {
|
||||
for (const column in models.VerificationRequest.schema.columns) {
|
||||
if (models.VerificationRequest.schema.columns[column].type === 'timestamp') {
|
||||
models.VerificationRequest.schema.columns[column].type = 'timestamptz'
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodb = (models, options) => {
|
||||
const mysqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for MySQL databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For MySQL we default milisecond precision of all timestamps to 6 digits.
|
||||
// This ensures all timestamp fields use the same precision (unless explictly
|
||||
// configured otherwise) and that values in MySQL match those Postgress.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
// If precision explictly set (including to null) don't change it
|
||||
if (typeof models[model].schema.columns[column].precision === 'undefined') {
|
||||
models[model].schema.columns[column].precision = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbTransform = (models, options) => {
|
||||
// A CamelCase naming strategy is used for all document databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new CamelCaseNamingStrategy()
|
||||
@@ -65,56 +59,37 @@ const mongodb = (models, options) => {
|
||||
// see the result of queries like find() is wrong. You will see the same
|
||||
// Object ID in every property of type Object ID in the result (but the
|
||||
// database will look fine); so use `type: 'objectId'` for them instead.
|
||||
|
||||
// Only transforms models that are not custom models
|
||||
const { models: customModels = {} } = options
|
||||
|
||||
if (!customModels.User) {
|
||||
delete models.User.schema.columns.id.type
|
||||
models.User.schema.columns.id.objectId = true
|
||||
|
||||
// The options `unique: true` and `nullable: true` don't work the same
|
||||
// with MongoDB as they do with SQL databases like MySQL and Postgres,
|
||||
// we also to add sparce to the index. This still doesn't allow multiple
|
||||
// *null* values, but does allow some records to omit the property.
|
||||
delete models.User.schema.columns.email.unique
|
||||
models.User.schema.indices = [
|
||||
{
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
}
|
||||
]
|
||||
for (const model in models) {
|
||||
delete models[model].schema.columns.id.type
|
||||
models[model].schema.columns.id.objectId = true
|
||||
}
|
||||
|
||||
if (!customModels.Account) {
|
||||
delete models.Account.schema.columns.id.type
|
||||
models.Account.schema.columns.id.objectId = true
|
||||
models.Account.schema.columns.userId.type = 'objectId'
|
||||
}
|
||||
// Ensure reference to User ID in other models are Object IDs
|
||||
// This needs to done for any properties that reference another entity by ID
|
||||
models.Account.schema.columns.userId.type = 'objectId'
|
||||
models.Session.schema.columns.userId.type = 'objectId'
|
||||
|
||||
if (!customModels.Session) {
|
||||
delete models.Session.schema.columns.id.type
|
||||
models.Session.schema.columns.id.objectId = true
|
||||
models.Session.schema.columns.userId.type = 'objectId'
|
||||
}
|
||||
|
||||
if (!customModels.VerificationRequest) {
|
||||
delete models.VerificationRequest.schema.columns.id.type
|
||||
models.VerificationRequest.schema.columns.id.objectId = true
|
||||
}
|
||||
// The options `unique: true` and `nullable: true` don't work the same
|
||||
// with MongoDB as they do with SQL databases like MySQL and Postgres,
|
||||
// we need to create a sparse index to only allow unique values, while
|
||||
// still allowing multiple entires to omit the email address.
|
||||
delete models.User.schema.columns.email.unique
|
||||
models.User.schema.indices = [
|
||||
{
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const sqlite = (models, options) => {
|
||||
const sqliteTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQLite databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// Only transforms models that are not custom models
|
||||
const { models: customModels = {} } = options
|
||||
|
||||
// SQLite does not support `timestamp` fields so we remap them to `datetime`
|
||||
// in all models.
|
||||
//
|
||||
@@ -123,51 +98,32 @@ const sqlite = (models, options) => {
|
||||
//
|
||||
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
|
||||
// specific to SQLite and so we ignore that behaviour.
|
||||
if (!customModels.User) {
|
||||
for (const column in models.User.schema.columns) {
|
||||
if (models.User.schema.columns[column].type === 'timestamp') {
|
||||
models.User.schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.Account) {
|
||||
for (const column in models.Account.schema.columns) {
|
||||
if (models.Account.schema.columns[column].type === 'timestamp') {
|
||||
models.Account.schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.Session) {
|
||||
for (const column in models.Session.schema.columns) {
|
||||
if (models.Session.schema.columns[column].type === 'timestamp') {
|
||||
models.Session.schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!customModels.VerificationRequest) {
|
||||
for (const column in models.VerificationRequest.schema.columns) {
|
||||
if (models.VerificationRequest.schema.columns[column].type === 'timestamp') {
|
||||
models.VerificationRequest.schema.columns[column].type = 'datetime'
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default (config, models, options) => {
|
||||
// @TODO Refactor into switch statement
|
||||
if ((config.type && config.type.startsWith('mongodb')) ||
|
||||
(config.url && config.url.startsWith('mongodb'))) {
|
||||
mongodb(models, options)
|
||||
mongodbTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('postgres')) ||
|
||||
(config.url && config.url.startsWith('postgres'))) {
|
||||
postgres(models, options)
|
||||
postgresTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mysql')) ||
|
||||
(config.url && config.url.startsWith('mysql'))) {
|
||||
mysqlTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('sqlite')) ||
|
||||
(config.url && config.url.startsWith('sqlite'))) {
|
||||
sqlite(models, options)
|
||||
sqliteTransform(models, options)
|
||||
} else {
|
||||
// Apply snake case naming strategy by default for SQL databases
|
||||
// For all other SQL databases (e.g. MySQL) apply snake case naming
|
||||
// strategy, but otherwise use the models and schemas as they are.
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
@@ -1,176 +1,289 @@
|
||||
// fetch() is built in to Next.js 9.4 (you can use a polyfill if using an older version)
|
||||
/// Note: fetch() is built in to Next.js 9.4
|
||||
//
|
||||
// Note about signIn() and signOut() methods:
|
||||
//
|
||||
// On signIn() and signOut() we pass 'json: true' to request a response in JSON
|
||||
// instead of HTTP as redirect URLs on other domains are not returned to
|
||||
// requests made using the fetch API in the browser, and we need to ask the API
|
||||
// to return the response as a JSON object (the end point still defaults to
|
||||
// returning an HTTP response with a redirect for non-JavaScript clients).
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
/* global fetch:false */
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import logger from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
|
||||
// Note: In calls to fetch() from universal methods, all cookies are passed
|
||||
// through from the browser, when the server makes the HTTP request, so that
|
||||
// it can authenticate as the browser.
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
// 1. An empty value is legitimate when the code is being invoked client side as
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
keepAlive: 0, // 0 == disabled (don't send); 60 == send every 60 seconds
|
||||
clientMaxAge: 0, // 0 == disabled (only use cache); 60 == sync if last checked > 60 seconds ago
|
||||
// Properties starting with _ are used for tracking internal app state
|
||||
_clientLastSync: 0, // used for timestamp since last sycned (in seconds)
|
||||
_clientSyncTimer: null, // stores timer for poll interval
|
||||
_eventListenersAdded: false, // tracks if event listeners have been added,
|
||||
_clientSession: undefined, // stores last session response from hook,
|
||||
// Generate a unique ID to make it possible to identify when a message
|
||||
// was sent from this tab/window so it can be ignored to avoid event loops.
|
||||
_clientId: Math.random().toString(36).substring(2) + Date.now().toString(36),
|
||||
// Used to store to function export by getSession() hook
|
||||
_getSession: () => {}
|
||||
}
|
||||
|
||||
// These can be overridden with NEXTAUTH_ env vars in next.config.js
|
||||
// e.g. process.env.NEXTAUTH_SITE
|
||||
const NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME = 'next-auth.base-url'
|
||||
const NEXTAUTH_DEFAULT_SITE = ''
|
||||
const NEXTAUTH_DEFAULT_BASE_PATH = '/api/auth'
|
||||
const NEXTAUTH_DEFAULT_CLIENT_MAXAGE = 0 // e.g. 0 == disabled, 60 == 60 seconds
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined') {
|
||||
if (__NEXTAUTH._eventListenersAdded === false) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
|
||||
let NEXTAUTH_EVENT_LISTENER_ADDED = false
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
window.addEventListener('storage', async (event) => {
|
||||
if (event.key === 'nextauth.message') {
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message.event && message.event === 'session' && message.data) {
|
||||
// Ignore storage events fired from the same window that created them
|
||||
if (__NEXTAUTH._clientId === message.clientId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch new session data but pass 'true' to it not to fire an event to
|
||||
// avoid an infinite loop.
|
||||
//
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for window focus/blur events
|
||||
window.addEventListener('focus', async (event) => __NEXTAUTH._getSession({ event: 'focus' }))
|
||||
window.addEventListener('blur', async (event) => __NEXTAUTH._getSession({ event: 'blur' }))
|
||||
}
|
||||
}
|
||||
|
||||
// Method to set options. The documented way is to use the provider, but this
|
||||
// method is being left in as an alternative, that will be helpful if/when we
|
||||
// expose a vanilla JavaScript version that doesn't depend on React.
|
||||
const setOptions = ({
|
||||
baseUrl,
|
||||
basePath,
|
||||
clientMaxAge,
|
||||
keepAlive
|
||||
} = {}) => {
|
||||
if (baseUrl) { __NEXTAUTH.baseUrl = baseUrl }
|
||||
if (basePath) { __NEXTAUTH.basePath = basePath }
|
||||
if (clientMaxAge) { __NEXTAUTH.clientMaxAge = clientMaxAge }
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
|
||||
if (typeof window !== 'undefined' && keepAlive > 0) {
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) { clearTimeout(__NEXTAUTH._clientSyncTimer) }
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (__NEXTAUTH._clientSession) {
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
}
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getSession = async ({ req } = {}) => {
|
||||
const baseUrl = _baseUrl({ req })
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const session = await _fetchData(`${baseUrl}/session`, options)
|
||||
_sendMessage({ event: 'session', data: { triggeredBy: 'getSession' } })
|
||||
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 }
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const session = await _fetchData(`${baseUrl}/session`, fetchOptions)
|
||||
if (triggerEvent) {
|
||||
_sendMessage({ event: 'session', data: { trigger: 'getSession' } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getProviders = async ({ req } = {}) => {
|
||||
const baseUrl = _baseUrl({ req })
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
return _fetchData(`${baseUrl}/providers`, options)
|
||||
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 }
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const data = await _fetchData(`${baseUrl}/csrf`, fetchOptions)
|
||||
return data && data.csrfToken ? data.csrfToken : null
|
||||
}
|
||||
|
||||
// Universal method (client + server)
|
||||
const getCsrfToken = async ({ req } = {}) => {
|
||||
const baseUrl = _baseUrl({ req })
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const data = await _fetchData(`${baseUrl}/csrf`, options)
|
||||
return data.csrfToken
|
||||
// Universal method (client + server); does not require request headers
|
||||
const getProviders = async () => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
return _fetchData(`${baseUrl}/providers`)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
const SessionContext = createContext()
|
||||
|
||||
// Client side method
|
||||
// Hook to access the session data stored in the context
|
||||
const useSession = (session) => {
|
||||
// Try to use context if we can
|
||||
const value = useContext(SessionContext)
|
||||
// If we have no Provider in the tree we call the actual hook for fetching the session
|
||||
|
||||
// If we have no Provider in the tree, call the actual hook
|
||||
if (value === undefined) {
|
||||
return useSessionData(session)
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Internal hook for getting session from the api.
|
||||
const useSessionData = (session) => {
|
||||
const clientMaxAge = (process.env.NEXTAUTH_CLIENT_MAXAGE || NEXTAUTH_DEFAULT_CLIENT_MAXAGE) * 1000
|
||||
|
||||
const _useSessionHook = (session) => {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const _getSession = async (sendEvent = true) => {
|
||||
const _getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
setData(await getSession())
|
||||
const triggredByEvent = (event !== null)
|
||||
const triggeredByStorageEvent = !!((event && event === 'storage'))
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = Math.floor(new Date().getTime() / 1000)
|
||||
const clientSession = __NEXTAUTH._clientSession
|
||||
|
||||
// Updates triggered by a storage event *always* trigger an update and we
|
||||
// always update if we don't have any value for the current session state.
|
||||
if (triggeredByStorageEvent === false && clientSession !== undefined) {
|
||||
if (clientMaxAge === 0 && triggredByEvent !== true) {
|
||||
// If there is no time defined for when a session should be considered
|
||||
// stale, then it's okay to use the value we have until an event is
|
||||
// triggered which updates it.
|
||||
return
|
||||
} else if (clientMaxAge > 0 && clientSession === null) {
|
||||
// If the client doesn't have a session then we don't need to call
|
||||
// the server to check if it does (if they have signed in via another
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
// If the session freshness is within clientMaxAge then don't request
|
||||
// it again on this call (avoids too many invokations).
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = Math.floor(new Date().getTime() / 1000)
|
||||
|
||||
// If this call was invoked via a storage event (i.e. another window) then
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const triggerEvent = (triggeredByStorageEvent === false)
|
||||
const newClientSessionData = await getSession({ triggerEvent })
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
// if a session exists at least once.
|
||||
__NEXTAUTH._clientSession = newClientSessionData
|
||||
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
|
||||
// Send event to trigger other tabs to update (unless sendEvent is false)
|
||||
if (sendEvent) {
|
||||
_sendMessage({ event: 'session', data: { triggeredBy: 'useSessionData' } })
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && NEXTAUTH_EVENT_LISTENER_ADDED === false) {
|
||||
NEXTAUTH_EVENT_LISTENER_ADDED = true
|
||||
window.addEventListener('storage', async (event) => {
|
||||
if (event.key === 'nextauth.message') {
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message.event && message.event === 'session' && message.data) {
|
||||
// Fetch new session data but tell it not to fire an event to
|
||||
// avoid an infinate loop.
|
||||
//
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that causes problems depending on
|
||||
// how the session object is being used and may expose session
|
||||
// data to 3rd party scripts, it's safer to update the session
|
||||
// this way.
|
||||
await _getSession(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If CLIENT_MAXAGE is greater than zero, trigger auto re-fetching session
|
||||
if (clientMaxAge > 0) {
|
||||
setTimeout(async (session) => {
|
||||
await _getSession()
|
||||
}, clientMaxAge)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
}
|
||||
}
|
||||
useEffect(() => { _getSession() }, [])
|
||||
|
||||
__NEXTAUTH._getSession = _getSession
|
||||
|
||||
useEffect(() => {
|
||||
_getSession()
|
||||
})
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
// Client side method
|
||||
const signin = async (provider, args) => {
|
||||
const signIn = async (provider, args = {}) => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
|
||||
if (!provider) {
|
||||
// Redirect to sign in page if no provider specified
|
||||
const baseUrl = _baseUrl()
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
}
|
||||
|
||||
const providers = await getProviders()
|
||||
if (!providers[provider]) {
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!provider || !providers[provider]) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
const baseUrl = _baseUrl()
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
} else if (providers[provider].type === 'oauth') {
|
||||
// If is an OAuth provider, redirect to providers[provider].signinUrl
|
||||
window.location = `${providers[provider].signinUrl}?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
} else {
|
||||
// If is any other provider type, POST to providers[provider].signinUrl (with CSRF Token)
|
||||
const options = {
|
||||
const signInUrl = (providers[provider].type === 'credentials')
|
||||
? `${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: _encodedForm({
|
||||
...args,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
...args
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(providers[provider].signinUrl, options)
|
||||
window.location = res.url ? res.url : callbackUrl
|
||||
const res = await fetch(signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
}
|
||||
}
|
||||
|
||||
// Client side method
|
||||
const signout = async (args) => {
|
||||
const signOut = async (args = {}) => {
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
|
||||
const baseUrl = _baseUrl()
|
||||
const options = {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl
|
||||
callbackUrl: callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, options)
|
||||
|
||||
_sendMessage({ event: 'session', data: { triggeredBy: 'signout' } })
|
||||
|
||||
window.location = res.url ? res.url : callbackUrl
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
}
|
||||
|
||||
// Provider to wrap the app in to make session data available globally
|
||||
const Provider = ({ children, session }) => {
|
||||
const value = useSession(session)
|
||||
return createElement(SessionContext.Provider, { value }, children)
|
||||
const Provider = ({ children, session, options }) => {
|
||||
setOptions(options)
|
||||
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
|
||||
}
|
||||
|
||||
const _fetchData = async (url, options) => {
|
||||
const _fetchData = async (url, options = {}) => {
|
||||
try {
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
@@ -181,42 +294,16 @@ const _fetchData = async (url, options) => {
|
||||
}
|
||||
}
|
||||
|
||||
const _baseUrl = ({ req } = {}) => {
|
||||
if (req) {
|
||||
// Server Side
|
||||
// If we have a 'req' object are running sever side, so we should grab the
|
||||
// base URL from cookie that is set by the API route - which is how config
|
||||
// is shared automatically between the API route and the client.
|
||||
const cookies = req ? _parseCookies(req.headers.cookie) : null
|
||||
const baseUrlCookieName = process.env.NEXTAUTH_BASE_URL_COOKIE_NAME || NEXTAUTH_DEFAULT_BASE_URL_COOKIE_NAME
|
||||
const cookieValue = cookies[`__Secure-${baseUrlCookieName}`] || cookies[baseUrlCookieName]
|
||||
const [baseUrl] = cookieValue ? cookieValue.split('|') : [null]
|
||||
return baseUrl
|
||||
} else {
|
||||
// Client Side
|
||||
// Note: 'site' is empty by default; URL is normally relative.
|
||||
const site = process.env.NEXTAUTH_SITE || NEXTAUTH_DEFAULT_SITE
|
||||
const basePath = process.env.NEXTAUTH_BASE_PATH || NEXTAUTH_DEFAULT_BASE_PATH
|
||||
return `${site}${basePath}`
|
||||
}
|
||||
}
|
||||
const _apiBaseUrl = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
|
||||
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }
|
||||
|
||||
// Adapted from https://github.com/felixfong227/simple-cookie-parser/blob/master/index.js
|
||||
const _parseCookies = (string) => {
|
||||
if (!string) { return {} }
|
||||
try {
|
||||
const object = {}
|
||||
const a = string.split(';')
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const b = a[i].split('=')
|
||||
if (b[0].length > 1 && b[1]) {
|
||||
object[b[0].trim()] = decodeURIComponent(b[1])
|
||||
}
|
||||
}
|
||||
return object
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_COOKIE_PARSE_ERROR', error)
|
||||
return {}
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
|
||||
} else {
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,23 +315,29 @@ const _encodedForm = (formData) => {
|
||||
|
||||
const _sendMessage = (message) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('nextauth.message', JSON.stringify(message)) // eslint-disable-line
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000)
|
||||
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
// Some methods are exported with more than one name. This provides
|
||||
// flexibility over how they can be invoked and compatibility with earlier
|
||||
// releases (going back to v1 and earlier v2 beta releases).
|
||||
// e.g. NextAuth.session() or const { getSession } from 'next-auth/client'
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
getProviders,
|
||||
useSession,
|
||||
signIn,
|
||||
signOut,
|
||||
Provider,
|
||||
/* Deprecated / unsupported features below this line */
|
||||
// Use setOptions() set options globally in the app.
|
||||
setOptions,
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases.
|
||||
options: setOptions,
|
||||
session: getSession,
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
getSession,
|
||||
getProviders,
|
||||
getCsrfToken,
|
||||
useSession,
|
||||
Provider,
|
||||
signin,
|
||||
signout
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
:root {
|
||||
--color-background: #fff;
|
||||
--color-primary: #444;
|
||||
--color-control-border: #bbb;
|
||||
--color-button-hover-background: #f9f9f9;
|
||||
--color-button-active-background: #f5f5f5;
|
||||
--color-button-active-background: #f9f9f9;
|
||||
--color-button-active-border: #aaa;
|
||||
--border-width: 1px;
|
||||
--border-radius: .3rem;
|
||||
--color-error: #c94b4b;
|
||||
--color-info: #157efb;
|
||||
--color-seperator: #ccc;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
@@ -39,7 +43,7 @@ input[type] {
|
||||
width: 100%;
|
||||
padding: .5rem 1rem;
|
||||
border: var(--border-width) solid var(--color-control-border);
|
||||
background: #fff;
|
||||
background: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
|
||||
@@ -61,7 +65,7 @@ a.button {
|
||||
line-height: 1rem;
|
||||
&:link,
|
||||
&:visited {
|
||||
background-color: #fff;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
@@ -72,21 +76,20 @@ a.button {
|
||||
padding: .75rem 1rem;
|
||||
border: var(--border-width) solid var(--color-control-border);
|
||||
color: var(--color-primary);
|
||||
background-color: #fff;
|
||||
background-color: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
transition: all .1s ease-in-out;
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem #fff, inset 0 -.1rem .1rem rgba(0,0,0,.05);
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.05);
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-hover-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:active {
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem #fff, inset 0 -.1rem .1rem rgba(0,0,0,.1);
|
||||
box-shadow: 0 0.15rem 0.3rem rgba(0,0,0,.15), inset 0 .1rem .2rem var(--color-background), inset 0 -.1rem .1rem rgba(0,0,0,.1);
|
||||
background-color: var(--color-button-active-background);
|
||||
border-color: var(--color-button-active-border);
|
||||
cursor: pointer;
|
||||
@@ -143,19 +146,34 @@ a.site {
|
||||
hr {
|
||||
display: block;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
border-top: 1px solid var(--color-seperator);
|
||||
margin: 1.5em auto 0 auto;
|
||||
overflow: visible;
|
||||
|
||||
&::before {
|
||||
content: "or";
|
||||
background: #fff;
|
||||
background: var(--color-background);
|
||||
color: #888;
|
||||
padding: 0 .4rem;
|
||||
position: relative;
|
||||
top: -.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #f5f5f5;
|
||||
font-weight: 500;
|
||||
border-radius: 0.3rem;
|
||||
background: var(--color-info);
|
||||
color: #fff;
|
||||
p {
|
||||
text-align: left;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
> div,
|
||||
form {
|
||||
display: block;
|
||||
|
||||
@@ -7,4 +7,4 @@ import path from 'path'
|
||||
const pathToCss = path.join(__dirname, '/index.css')
|
||||
const css = fs.readFileSync(pathToCss, 'utf8')
|
||||
|
||||
export default () => css
|
||||
export default () => css
|
||||
|
||||
161
src/lib/jwt.js
161
src/lib/jwt.js
@@ -1,44 +1,157 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import jose from 'jose'
|
||||
import hkdf from 'futoin-hkdf'
|
||||
import logger from './logger'
|
||||
|
||||
const encode = async ({ secret, key = secret, token = {}, maxAge }) => {
|
||||
// If maxAge is set remove any existing created/expiry dates and replace them
|
||||
if (maxAge) {
|
||||
if (token.iat) { delete token.iat }
|
||||
if (token.exp) { delete token.exp }
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
|
||||
|
||||
// Set default algorithm for auto-generated symmetric encryption key
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
|
||||
|
||||
// Use encryption or not by default
|
||||
const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
const encode = async ({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
signingKey,
|
||||
signingOptions = {
|
||||
expiresIn: `${maxAge}s`
|
||||
},
|
||||
encryptionKey,
|
||||
encryptionOptions = {
|
||||
alg: 'dir',
|
||||
enc: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
zip: 'DEF'
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) => {
|
||||
// Signing Key
|
||||
const _signingKey = (signingKey)
|
||||
? jose.JWK.asKey(JSON.parse(signingKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
// Sign token
|
||||
const signedToken = jose.JWT.sign(token, _signingKey, signingOptions)
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = (encryptionKey)
|
||||
? jose.JWK.asKey(JSON.parse(encryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Encrypt token
|
||||
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
|
||||
} else {
|
||||
return signedToken
|
||||
}
|
||||
const signedToken = jwt.sign(token, secret, { expiresIn: maxAge })
|
||||
const encryptedToken = CryptoJS.AES.encrypt(signedToken, key).toString()
|
||||
return encryptedToken
|
||||
}
|
||||
|
||||
const decode = async ({ secret, key = secret, token, maxAge }) => {
|
||||
const decode = async ({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
signingKey,
|
||||
verificationKey = signingKey, // Optional (defaults to encryptionKey)
|
||||
verificationOptions = {
|
||||
maxTokenAge: `${maxAge}s`,
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
|
||||
},
|
||||
encryptionKey,
|
||||
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
|
||||
decryptionOptions = {
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) => {
|
||||
if (!token) return null
|
||||
const decryptedBytes = CryptoJS.AES.decrypt(token, key)
|
||||
const decryptedToken = decryptedBytes.toString(CryptoJS.enc.Utf8)
|
||||
const verifiedToken = jwt.verify(decryptedToken, secret, { maxAge })
|
||||
return verifiedToken
|
||||
|
||||
let tokenToVerify = token
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = (decryptionKey)
|
||||
? jose.JWK.asKey(JSON.parse(decryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Decrypt token
|
||||
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
|
||||
tokenToVerify = decryptedToken.toString('utf8')
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
const _signingKey = (verificationKey)
|
||||
? jose.JWK.asKey(JSON.parse(verificationKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
// Verify token
|
||||
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
|
||||
}
|
||||
|
||||
// This is a simple helper method to make it easier to use JWT from an API route
|
||||
const getJwt = async ({ req, secret, cookieName }) => {
|
||||
if (!req || !secret) throw new Error('Must pass { req, secret } to getJWT()')
|
||||
const getToken = async (args) => {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
raw = false
|
||||
} = args
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
|
||||
const secureCookieName = '__Secure-next-auth.session-token'
|
||||
const insecureCookieName = 'next-auth.session-token'
|
||||
const cookieValue = cookieName ? req.cookies[cookieName] : req.cookies[secureCookieName] || req.cookies[insecureCookieName]
|
||||
// Try to get token from cookie
|
||||
let token = req.cookies[cookieName]
|
||||
|
||||
if (!cookieValue) { return null }
|
||||
// If cookie not found in cookie look for bearer token in authorization header.
|
||||
// This allows clients that pass through tokens in headers rather than as
|
||||
// cookies to use this helper function.
|
||||
if (!token && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
||||
const urlEncodedToken = req.headers.authorization.split(' ')[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
return token
|
||||
}
|
||||
|
||||
try {
|
||||
return await decode({ secret, token: cookieValue })
|
||||
return await decode({ token, ...args })
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Generate warning (but only once at startup) when auto-generated keys are used
|
||||
let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
const getDerivedSigningKey = (secret) => {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, 64, { info: 'NextAuth.js Generated Signing Key', hash: 'SHA-256' })
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
const getDerivedEncryptionKey = (secret) => {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, 32, { info: 'NextAuth.js Generated Encryption Key', hash: 'SHA-256' })
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getJwt
|
||||
getToken
|
||||
}
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
const logger = {
|
||||
error: (errorCode, ...text) => {
|
||||
if (console) {
|
||||
!text
|
||||
? console.error(errorCode)
|
||||
: console.error(
|
||||
`[next-auth][error][${errorCode}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
|
||||
)
|
||||
}
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
console.error(
|
||||
`[next-auth][error][${errorCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
warn: (warnCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
console.warn(
|
||||
`[next-auth][warn][${warnCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/warning#${warnCode.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
debug: (debugCode, ...text) => {
|
||||
if (process && process.env && process.env._NEXT_AUTH_DEBUG) {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
if (process && process.env && process.env._NEXTAUTH_DEBUG) {
|
||||
console.log(
|
||||
`[next-auth][debug][${debugCode}]`,
|
||||
`[next-auth][debug][${debugCode.toLowerCase()}]`,
|
||||
text
|
||||
)
|
||||
}
|
||||
|
||||
27
src/lib/parse-url.js
Normal file
27
src/lib/parse-url.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// Simple universal (client/server) function to split host and path
|
||||
// We use this rather than a library because we need to use the same logic both
|
||||
// client and server side and we only need to parse out the host and path, while
|
||||
// supporting a default value, so a simple split is sufficent.
|
||||
export default (url) => {
|
||||
// Default values
|
||||
const defaultHost = 'http://localhost:3000'
|
||||
const defaultPath = '/api/auth'
|
||||
|
||||
if (!url) { url = `${defaultHost}${defaultPath}` }
|
||||
|
||||
// Default to HTTPS if no protocol explictly specified
|
||||
const protocol = url.match(/^http?:\/\//) ? 'http' : 'https'
|
||||
|
||||
// Normalize URLs by stripping protocol and no trailing slash
|
||||
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
|
||||
// Simple split based on first /
|
||||
const [_host, ..._path] = url.split('/')
|
||||
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
||||
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
basePath
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ export default (options) => {
|
||||
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
|
||||
profileUrl: null,
|
||||
idToken: true,
|
||||
state: false, // Apple doesn't support state verfication
|
||||
profile: (profile) => {
|
||||
// The name of the user will only return on first login
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name == null ? profile.sub : profile.name,
|
||||
name: profile.user != null ? profile.user.name.firstName + ' ' + profile.user.name.lastName : null,
|
||||
email: profile.email
|
||||
}
|
||||
},
|
||||
@@ -35,7 +37,9 @@ export default (options) => {
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: appleId
|
||||
},
|
||||
privateKey,
|
||||
// Automatically convert \\n into \n if found in private key. If the key
|
||||
// is passed in an environment variable \n can get escaped as \\n
|
||||
privateKey.replace(/\\n/g, '\n'),
|
||||
{
|
||||
algorithm: 'ES256',
|
||||
keyid: keyId
|
||||
|
||||
@@ -4,11 +4,11 @@ export default (options) => {
|
||||
name: 'Auth0',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code', response_type: 'code' },
|
||||
params: { grant_type: 'authorization_code' },
|
||||
scope: 'openid email profile',
|
||||
accessTokenUrl: `https://${options.subdomain}.auth0/oauth/token`,
|
||||
authorizationUrl: `https://${options.subdomain}.auth0.com/authorize?`,
|
||||
profileUrl: `http://${options.subdomain}.auth0.com/userinfo`,
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/userinfo`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.sub,
|
||||
|
||||
29
src/providers/battlenet.js
Normal file
29
src/providers/battlenet.js
Normal file
@@ -0,0 +1,29 @@
|
||||
export default (options) => {
|
||||
const { region } = options
|
||||
return {
|
||||
id: 'battlenet',
|
||||
name: 'Battle.net',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'openid',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl:
|
||||
region === 'CN'
|
||||
? 'https://www.battlenet.com.cn/oauth/token'
|
||||
: `https://${region}.battle.net/oauth/token`,
|
||||
authorizationUrl:
|
||||
region === 'CN'
|
||||
? 'https://www.battlenet.com.cn/oauth/authorize'
|
||||
: `https://${region}.battle.net/oauth/authorize`,
|
||||
profileUrl: 'https://us.battle.net/oauth/userinfo',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.battletag,
|
||||
email: null,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
23
src/providers/cognito.js
Normal file
23
src/providers/cognito.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default (options) => {
|
||||
const { domain } = options
|
||||
return {
|
||||
id: 'cognito',
|
||||
name: 'Cognito',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'openid profile email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: `https://${domain}/oauth2/token`,
|
||||
authorizationUrl: `https://${domain}/oauth2/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/oauth2/userInfo`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.username,
|
||||
email: profile.email,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -22,22 +22,23 @@ export default (options) => {
|
||||
}
|
||||
}
|
||||
|
||||
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
|
||||
const sendVerificationRequest = ({ identifier: email, url, baseUrl, provider }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { server, from } = provider
|
||||
const siteName = site.replace(/^https?:\/\//, '')
|
||||
// Strip protocol from URL and use domain as site name
|
||||
const site = baseUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
nodemailer
|
||||
.createTransport(server)
|
||||
.sendMail({
|
||||
to: emailAddress,
|
||||
to: email,
|
||||
from,
|
||||
subject: `Sign in to ${siteName}`,
|
||||
text: text({ url, siteName }),
|
||||
html: html({ url, siteName })
|
||||
subject: `Sign in to ${site}`,
|
||||
text: text({ url, site, email }),
|
||||
html: html({ url, site, email })
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
logger.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
|
||||
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
|
||||
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
|
||||
}
|
||||
return resolve()
|
||||
@@ -46,28 +47,55 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
const html = ({ url, siteName }) => {
|
||||
const buttonBackgroundColor = '#444444'
|
||||
const html = ({ url, site, email }) => {
|
||||
// Insert invisible space into domains and email address to prevent both the
|
||||
// email address and the domain from being turned into a hyperlink by email
|
||||
// clients like Outlook and Apple mail, as this is confusing because it seems
|
||||
// like they are supposed to click on their email address to sign in.
|
||||
const escapedEmail = `${email.replace(/\./g, '​.')}`
|
||||
const escapedSite = `${site.replace(/\./g, '​.')}`
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = '#f9f9f9'
|
||||
const textColor = '#444444'
|
||||
const mainBackgroundColor = '#ffffff'
|
||||
const buttonBackgroundColor = '#346df1'
|
||||
const buttonBorderColor = '#346df1'
|
||||
const buttonTextColor = '#ffffff'
|
||||
|
||||
return `
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 8px 0; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #888888;">
|
||||
${siteName}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 16px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 3px; padding: 12px 18px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedSite}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
Sign in as <strong>${escapedEmail}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
If you did not request this email you can safely ignore it.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
`
|
||||
}
|
||||
|
||||
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
|
||||
const text = ({ url, siteName }) => `Sign in to ${siteName}\n${url}\n\n`
|
||||
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (options) => {
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
name: profile.name || profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url
|
||||
}
|
||||
|
||||
@@ -2,16 +2,20 @@ import Auth0 from './auth0'
|
||||
import Apple from './apple'
|
||||
import Box from './box'
|
||||
import Credentials from './credentials'
|
||||
import BattleNet from './battlenet'
|
||||
import Cognito from './cognito'
|
||||
import Discord from './discord'
|
||||
import Email from './email'
|
||||
import Facebook from './facebook' // @TODO
|
||||
import Facebook from './facebook'
|
||||
import GitHub from './github'
|
||||
import GitLab from './gitlab'
|
||||
import Google from './google'
|
||||
import IdentityServer4 from './identity-server4'
|
||||
import LinkedIn from './linkedin'
|
||||
import Mixer from './mixer'
|
||||
import Okta from './okta'
|
||||
import Slack from './slack'
|
||||
import Spotify from './spotify'
|
||||
import Twitch from './twitch'
|
||||
import Twitter from './twitter'
|
||||
import Yandex from './yandex'
|
||||
@@ -21,6 +25,8 @@ export default {
|
||||
Apple,
|
||||
Box,
|
||||
Credentials,
|
||||
BattleNet,
|
||||
Cognito,
|
||||
Discord,
|
||||
Email,
|
||||
Facebook,
|
||||
@@ -28,9 +34,11 @@ export default {
|
||||
GitLab,
|
||||
Google,
|
||||
IdentityServer4,
|
||||
LinkedIn,
|
||||
Mixer,
|
||||
Okta,
|
||||
Slack,
|
||||
Spotify,
|
||||
Twitter,
|
||||
Twitch,
|
||||
Yandex
|
||||
|
||||
26
src/providers/linkedin.js
Normal file
26
src/providers/linkedin.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'linkedin',
|
||||
name: 'LinkedIn',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'r_liteprofile',
|
||||
params: {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: 'https://www.linkedin.com/oauth/v2/accessToken',
|
||||
authorizationUrl: 'https://www.linkedin.com/oauth/v2/authorization?response_type=code',
|
||||
profileUrl: 'https://api.linkedin.com/v2/me?projection=(id,localizedFirstName,localizedLastName)',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.localizedFirstName + ' ' + profile.localizedLastName,
|
||||
email: null,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
23
src/providers/spotify.js
Normal file
23
src/providers/spotify.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'spotify',
|
||||
name: 'Spotify',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'user-read-email',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://accounts.spotify.com/api/token',
|
||||
authorizationUrl:
|
||||
'https://accounts.spotify.com/authorize?response_type=code',
|
||||
profileUrl: 'https://api.spotify.com/v1/me',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.display_name,
|
||||
email: profile.email,
|
||||
image: profile.images[0].url
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,11 @@ export default (options) => {
|
||||
accessTokenUrl: 'https://api.twitter.com/oauth/access_token',
|
||||
requestTokenUrl: 'https://api.twitter.com/oauth/request_token',
|
||||
authorizationUrl: 'https://api.twitter.com/oauth/authenticate',
|
||||
profileUrl: 'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
|
||||
profileUrl:
|
||||
'https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, '.jpg')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import cookie from './lib/cookie'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import parseProviders from './lib/providers'
|
||||
@@ -12,9 +13,13 @@ import callback from './routes/callback'
|
||||
import session from './routes/session'
|
||||
import pages from './pages'
|
||||
import adapters from '../adapters'
|
||||
import logger from '../lib/logger'
|
||||
|
||||
const DEFAULT_SITE = 'http://localhost:3000'
|
||||
const DEFAULT_BASE_PATH = '/api/auth'
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
export default async (req, res, userSuppliedOptions) => {
|
||||
// To the best of my knowledge, we need to return a promise here
|
||||
@@ -32,17 +37,17 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
provider = nextauth[1],
|
||||
error
|
||||
error = nextauth[1]
|
||||
} = query
|
||||
|
||||
const {
|
||||
csrfToken: csrfTokenFromPost
|
||||
} = body
|
||||
|
||||
// Allow site name, path prefix to be overriden
|
||||
const site = userSuppliedOptions.site || DEFAULT_SITE
|
||||
const basePath = userSuppliedOptions.basePath || DEFAULT_BASE_PATH
|
||||
const baseUrl = `${site}${basePath}`
|
||||
// @todo refactor all existing references to site, baseUrl and basePath
|
||||
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||
const baseUrl = parsedUrl.baseUrl
|
||||
const basePath = parsedUrl.basePath
|
||||
|
||||
// Parse database / adapter
|
||||
let adapter
|
||||
@@ -58,7 +63,7 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
// If no secret option is specified then it creates one on the fly
|
||||
// based on options passed here. A options contains unique data, such as
|
||||
// oAuth provider secrets and database credentials it should be sufficent.
|
||||
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify(userSuppliedOptions)).digest('hex')
|
||||
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
|
||||
|
||||
// Use secure cookies if the site uses HTTPS
|
||||
// This being conditional allows cookies to work non-HTTPS development URLs
|
||||
@@ -89,15 +94,6 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
baseUrl: {
|
||||
name: `${cookiePrefix}next-auth.base-url`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
csrfToken: {
|
||||
// Default to __Host- for CSRF token for additional protection if using useSecureCookies
|
||||
// NB: The `__Host-` prefix is stricter than the `__Secure-` prefix.
|
||||
@@ -114,7 +110,7 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
}
|
||||
|
||||
// Session options
|
||||
const sessionOption = {
|
||||
const sessionOptions = {
|
||||
jwt: false,
|
||||
maxAge: 30 * 24 * 60 * 60, // Sessions expire after 30 days of being idle
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
@@ -123,8 +119,8 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
|
||||
// JWT options
|
||||
const jwtOptions = {
|
||||
secret,
|
||||
key: secret,
|
||||
secret, // Use application secret if no keys specified
|
||||
maxAge: sessionOptions.maxAge, // maxAge is dereived from session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userSuppliedOptions.jwt
|
||||
@@ -132,17 +128,17 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
|
||||
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
if (!adapter) {
|
||||
sessionOption.jwt = true
|
||||
sessionOptions.jwt = true
|
||||
}
|
||||
|
||||
// Event messages
|
||||
const eventsOption = {
|
||||
const eventsOptions = {
|
||||
...events,
|
||||
...userSuppliedOptions.events
|
||||
}
|
||||
|
||||
// Callback functions
|
||||
const callbacksOption = {
|
||||
const callbacksOptions = {
|
||||
...callbacks,
|
||||
...userSuppliedOptions.callbacks
|
||||
}
|
||||
@@ -180,22 +176,18 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
|
||||
}
|
||||
|
||||
// Set canonical site name + API route in a cookie to facilitate passing configuration
|
||||
// to the NextAuth client. There are potential security considerations around this
|
||||
// relating to trying to prevent attackers from exploiting this by setting this cookie
|
||||
// on the client first if they can get control of a sub domain or exploit a XSS
|
||||
// vulnerability, but this approach attempts to mitgate that by always verifying
|
||||
// the cookie and updating it if fails the verification check.
|
||||
let setUrlPrefixCookie = true
|
||||
if (req.cookies[cookies.baseUrl.name]) {
|
||||
const [baseUrlValue, baseUrlHash] = req.cookies[cookies.baseUrl.name].split('|')
|
||||
// If the hash on the cookie is verified, then we must have set the cookie and don't need to update it
|
||||
if (baseUrlValue === baseUrl && baseUrlHash === createHash('sha256').update(`${baseUrlValue}${secret}`).digest('hex')) { setUrlPrefixCookie = false }
|
||||
}
|
||||
// If the cookie is not set already (or if it is set, but failed verification) set header to update the cookie
|
||||
if (setUrlPrefixCookie) {
|
||||
const newUrlPrefixCookie = `${baseUrl}|${createHash('sha256').update(`${baseUrl}${secret}`).digest('hex')}`
|
||||
cookie.set(res, cookies.baseUrl.name, newUrlPrefixCookie, cookies.baseUrl.options)
|
||||
// Helper method for handling redirects, this is passed to all routes
|
||||
// @TODO Refactor into a lib instead of passing as an option
|
||||
// e.g. and call as redirect(req, res, url)
|
||||
const redirect = (redirectUrl) => {
|
||||
const reponseAsJson = !!((req.body && req.body.json === 'true'))
|
||||
if (reponseAsJson) {
|
||||
res.json({ url: redirectUrl })
|
||||
} else {
|
||||
res.status(302).setHeader('Location', redirectUrl)
|
||||
res.end()
|
||||
}
|
||||
return done()
|
||||
}
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
@@ -209,35 +201,28 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
// These computed settings can values in userSuppliedOptions but override them
|
||||
// and are request-specific.
|
||||
adapter,
|
||||
site,
|
||||
basePath,
|
||||
baseUrl,
|
||||
basePath,
|
||||
action,
|
||||
provider,
|
||||
cookies,
|
||||
secret,
|
||||
csrfToken,
|
||||
csrfTokenVerified,
|
||||
providers: parseProviders(userSuppliedOptions.providers, baseUrl),
|
||||
session: sessionOption,
|
||||
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
|
||||
session: sessionOptions,
|
||||
jwt: jwtOptions,
|
||||
events: eventsOption,
|
||||
callbacks: callbacksOption,
|
||||
callbackUrl: site
|
||||
events: eventsOptions,
|
||||
callbacks: callbacksOptions,
|
||||
callbackUrl: baseUrl,
|
||||
redirect
|
||||
}
|
||||
|
||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||
if (options.debug === true) { process.env._NEXT_AUTH_DEBUG = true }
|
||||
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }
|
||||
|
||||
// Get / Set callback URL based on query param / cookie + validation
|
||||
options.callbackUrl = await callbackUrlHandler(req, res, options)
|
||||
|
||||
const redirect = (redirectUrl) => {
|
||||
res.status(302).setHeader('Location', redirectUrl)
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
switch (action) {
|
||||
case 'providers':
|
||||
@@ -250,18 +235,18 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
res.json({ csrfToken })
|
||||
return done()
|
||||
case 'signin':
|
||||
if (provider && options.providers[provider]) {
|
||||
signin(req, res, options, done)
|
||||
} else {
|
||||
if (options.pages.signin) { return redirect(`${options.pages.signin}${options.pages.signin.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
|
||||
|
||||
pages.render(req, res, 'signin', { site, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
|
||||
if (options.pages.signIn) {
|
||||
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`
|
||||
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
|
||||
return redirect(redirectUrl)
|
||||
}
|
||||
|
||||
pages.render(req, res, 'signin', { baseUrl, basePath, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
|
||||
break
|
||||
case 'signout':
|
||||
if (options.pages.signout) { return redirect(`${options.pages.signout}${options.pages.signout.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`) }
|
||||
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }
|
||||
|
||||
pages.render(req, res, 'signout', { site, baseUrl, csrfToken, callbackUrl: options.callbackUrl }, done)
|
||||
pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
|
||||
break
|
||||
case 'callback':
|
||||
if (provider && options.providers[provider]) {
|
||||
@@ -274,12 +259,12 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
case 'verify-request':
|
||||
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
|
||||
|
||||
pages.render(req, res, 'verify-request', { site }, done)
|
||||
pages.render(req, res, 'verify-request', { baseUrl }, done)
|
||||
break
|
||||
case 'error':
|
||||
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
|
||||
|
||||
pages.render(req, res, 'error', { site, error, baseUrl }, done)
|
||||
pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
|
||||
break
|
||||
default:
|
||||
res.status(404).end()
|
||||
@@ -288,17 +273,30 @@ export default async (req, res, userSuppliedOptions) => {
|
||||
} else if (req.method === 'POST') {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
// Signin POST requests are used for email sign in
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (!csrfTokenVerified) {
|
||||
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
if (provider && options.providers[provider]) {
|
||||
signin(req, res, options, done)
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'signout':
|
||||
// Verified CSRF Token required for signout
|
||||
if (!csrfTokenVerified) {
|
||||
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
}
|
||||
|
||||
signout(req, res, options, done)
|
||||
break
|
||||
case 'callback':
|
||||
if (provider && options.providers[provider]) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
|
||||
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
callback(req, res, options, done)
|
||||
} else {
|
||||
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
|
||||
|
||||
@@ -20,7 +20,6 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
||||
const { adapter, jwt, events } = options
|
||||
|
||||
const useJwtSession = options.session.jwt
|
||||
const sessionMaxAge = options.session.maxAge
|
||||
|
||||
// If no adapter is configured then we don't have a database and cannot
|
||||
// persist data; in this mode we just return a dummy session object.
|
||||
@@ -52,7 +51,7 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
||||
if (sessionToken) {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
session = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
|
||||
session = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
if (session && session.user) {
|
||||
user = await getUser(session.user.id)
|
||||
isSignedIn = !!user
|
||||
@@ -86,12 +85,12 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
||||
|
||||
// Update emailVerified property on the user object
|
||||
const currentDate = new Date()
|
||||
userByEmail.emailVerified = currentDate
|
||||
user = await updateUser(userByEmail)
|
||||
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
|
||||
await dispatchEvent(events.updateUser, user)
|
||||
} else {
|
||||
// Create user account if there isn't one for the email address already
|
||||
user = await createUser({ ...profile, emailVerified: true })
|
||||
const currentDate = new Date()
|
||||
user = await createUser({ ...profile, emailVerified: currentDate })
|
||||
await dispatchEvent(events.createUser, user)
|
||||
isNewUser = true
|
||||
}
|
||||
|
||||
@@ -3,21 +3,21 @@ import cookie from '../lib/cookie'
|
||||
export default async (req, res, options) => {
|
||||
const { query } = req
|
||||
const { body } = req
|
||||
const { cookies, site, defaultCallbackUrl, callbacks } = options
|
||||
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = options
|
||||
|
||||
// Handle preserving and validating callback URLs
|
||||
// If no defaultCallbackUrl option specified, default to the homepage for the site
|
||||
let callbackUrl = defaultCallbackUrl || site
|
||||
let callbackUrl = defaultCallbackUrl || baseUrl
|
||||
|
||||
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
|
||||
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
|
||||
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
|
||||
if (callbackUrlParamValue) {
|
||||
// If callbackUrl form field or query parameter is passed try to use it if allowed
|
||||
callbackUrl = await callbacks.redirect(callbackUrlParamValue, site)
|
||||
callbackUrl = await callbacks.redirect(callbackUrlParamValue, baseUrl)
|
||||
} else if (callbackUrlCookieValue) {
|
||||
// If no callbackUrl specified, try using the value from the cookie if allowed
|
||||
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, site)
|
||||
callbackUrl = await callbacks.redirect(callbackUrlCookieValue, baseUrl)
|
||||
}
|
||||
|
||||
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Use the signin callback to control if a user is allowed to sign in or not.
|
||||
* Use the signIn callback to control if a user is allowed to sign in or not.
|
||||
*
|
||||
* This is triggered before sign in flow completes, so the user profile may be
|
||||
* a user object (with an ID) or it may be just their name and email address,
|
||||
@@ -15,7 +15,7 @@
|
||||
* @return {boolean|object} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
const signin = async (profile, account, metadata) => {
|
||||
const signIn = async (profile, account, metadata) => {
|
||||
const isAllowedToSignIn = true
|
||||
if (isAllowedToSignIn) {
|
||||
return Promise.resolve(true)
|
||||
@@ -68,7 +68,7 @@ const jwt = async (token, oAuthProfile) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
signin,
|
||||
signIn,
|
||||
redirect,
|
||||
session,
|
||||
jwt
|
||||
|
||||
@@ -15,7 +15,9 @@ const set = (res, name, value, options = {}) => {
|
||||
}
|
||||
|
||||
// Preserve any existing cookies that have already been set in the same session
|
||||
const setCookieHeader = res.getHeader('Set-Cookie') || []
|
||||
let setCookieHeader = res.getHeader('Set-Cookie') || []
|
||||
// If not an array (i.e. a string with a single cookie) convert it into an array
|
||||
if (!Array.isArray(setCookieHeader)) { setCookieHeader = [setCookieHeader] }
|
||||
setCookieHeader.push(_serialize(name, String(stringValue), options))
|
||||
res.setHeader('Set-Cookie', setCookieHeader)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const signin = async (message) => {
|
||||
const signIn = async (message) => {
|
||||
// Event triggered on successful sign in
|
||||
}
|
||||
|
||||
const signout = async (message) => {
|
||||
// Event triggered on signout
|
||||
const signOut = async (message) => {
|
||||
// Event triggered on sign out
|
||||
}
|
||||
|
||||
const createUser = async (message) => {
|
||||
@@ -28,8 +28,8 @@ const error = async (message) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
signin,
|
||||
signout,
|
||||
signIn,
|
||||
signOut,
|
||||
createUser,
|
||||
updateUser,
|
||||
linkAccount,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import oAuthClient from './client'
|
||||
|
||||
import { createHash } from 'crypto'
|
||||
import querystring from 'querystring'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
import oAuthClient from './client'
|
||||
import logger from '../../../lib/logger'
|
||||
|
||||
// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
|
||||
@@ -9,15 +11,38 @@ import logger from '../../../lib/logger'
|
||||
// appropriate credit) to make it easier to maintain and address issues as they
|
||||
// come up, as the node-oauth package does not seem to be actively maintained.
|
||||
|
||||
export default async (req, provider, callback) => {
|
||||
let { oauth_token, oauth_verifier, code } = req.query // eslint-disable-line camelcase
|
||||
// @TODO Refactor to use promises and not callbacks
|
||||
// @TODO Refactor to use jsonwebtoken instead of jwt-decode & remove dependancy
|
||||
export default async (req, provider, csrfToken, callback) => {
|
||||
// The "user" object is specific to apple provider and is provided on first sign in
|
||||
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
|
||||
let { oauth_token, oauth_verifier, code, user, state } = req.query // eslint-disable-line camelcase
|
||||
const client = oAuthClient(provider)
|
||||
|
||||
if (provider.version && provider.version.startsWith('2.')) {
|
||||
// For OAuth 2.0 flows, check state returned and matches expected value
|
||||
// (a hash of the NextAuth.js CSRF token).
|
||||
//
|
||||
// This check can be disabled for providers that do not support it by
|
||||
// setting `state: false` as a option on the provider (defaults to true).
|
||||
if (!Object.prototype.hasOwnProperty.call(provider, 'state') || provider.state === true) {
|
||||
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
||||
if (state !== expectedState) {
|
||||
return callback(new Error('Invalid state returned from oAuth provider'))
|
||||
}
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
// Get the CODE from Body
|
||||
const body = JSON.parse(JSON.stringify(req.body))
|
||||
code = body.code
|
||||
try {
|
||||
const body = JSON.parse(JSON.stringify(req.body))
|
||||
if (body.error) { throw new Error(body.error) }
|
||||
|
||||
code = body.code
|
||||
user = body.user != null ? JSON.parse(body.user) : null
|
||||
} catch (e) {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', e, req.body, provider.id, code)
|
||||
return callback()
|
||||
}
|
||||
}
|
||||
|
||||
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
|
||||
@@ -34,19 +59,32 @@ export default async (req, provider, callback) => {
|
||||
code,
|
||||
provider,
|
||||
(error, accessToken, refreshToken, results) => {
|
||||
// @TODO Handle error
|
||||
if (error || results.error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, results, provider.id, code)
|
||||
return callback(error || results.error)
|
||||
}
|
||||
|
||||
if (provider.idToken) {
|
||||
// If we don't have an ID Token most likely the user hit a cancel
|
||||
// button when signing in (or the provider is misconfigured).
|
||||
//
|
||||
// Unfortunately, we can't tell which, so we can't treat it as an
|
||||
// error, so instead we just returning nothing, which will cause the
|
||||
// user to be redirected back to the sign in page.
|
||||
if (!results || !results.id_token) {
|
||||
return callback()
|
||||
}
|
||||
|
||||
// Support services that use OpenID ID Tokens to encode profile data
|
||||
_decodeToken(
|
||||
provider,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
results.id_token,
|
||||
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider, user)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Use custom get() method for oAuth2 flows
|
||||
@@ -55,7 +93,10 @@ export default async (req, provider, callback) => {
|
||||
client.get(
|
||||
provider,
|
||||
accessToken,
|
||||
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -76,14 +117,21 @@ export default async (req, provider, callback) => {
|
||||
provider.profileUrl,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
(error, profileData) => callback(error, _getProfile(error, profileData, accessToken, refreshToken, provider))
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function _getProfile (error, profileData, accessToken, refreshToken, provider) {
|
||||
/**
|
||||
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
|
||||
* Returns profile, raw profile and auth provider details
|
||||
*/
|
||||
async function _getProfile (error, profileData, accessToken, refreshToken, provider, userData) {
|
||||
// @TODO Handle error
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_PROFILE_ERROR', error)
|
||||
@@ -94,15 +142,32 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
|
||||
// Convert profileData into an object if it's a string
|
||||
if (typeof profileData === 'string' || profileData instanceof String) { profileData = JSON.parse(profileData) }
|
||||
|
||||
// If a user object is supplied (e.g. Apple provider) add it to the profile object
|
||||
if (userData != null) {
|
||||
profileData.user = userData
|
||||
}
|
||||
|
||||
logger.debug('PROFILE_DATA', profileData)
|
||||
|
||||
profile = await provider.profile(profileData)
|
||||
} catch (exception) {
|
||||
// @TODO Handle parsing error
|
||||
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception)
|
||||
throw new Error('Failed to get OAuth profile')
|
||||
// If we didn't get a response either there was a problem with the provider
|
||||
// response *or* the user cancelled the action with the provider.
|
||||
//
|
||||
// Unfortuately, we can't tell which - at least not in a way that works for
|
||||
// all providers, so we return an empty object; the user should then be
|
||||
// redirected back to the sign up page. We log the error to help developers
|
||||
// who might be trying to debug this when configuring a new provider.
|
||||
logger.error('OAUTH_PARSE_PROFILE_ERROR', exception, profileData)
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
OAuthProfile: profileData
|
||||
}
|
||||
}
|
||||
|
||||
// Return profile, raw profile and auth provider details
|
||||
return ({
|
||||
return {
|
||||
profile: {
|
||||
name: profile.name,
|
||||
email: profile.email ? profile.email.toLowerCase() : null,
|
||||
@@ -116,8 +181,8 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
|
||||
accessToken,
|
||||
accessTokenExpires: null
|
||||
},
|
||||
oAuthProfile: profileData
|
||||
})
|
||||
OAuthProfile: profileData
|
||||
}
|
||||
}
|
||||
|
||||
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export default (_providers, baseUrl) => {
|
||||
export default (_providers, baseUrl, basePath) => {
|
||||
const providers = {}
|
||||
|
||||
_providers.forEach(provider => {
|
||||
const providerId = provider.id
|
||||
providers[providerId] = {
|
||||
...provider,
|
||||
signinUrl: `${baseUrl}/signin/${providerId}`,
|
||||
callbackUrl: `${baseUrl}/callback/${providerId}`
|
||||
signinUrl: `${baseUrl}${basePath}/signin/${providerId}`,
|
||||
callbackUrl: `${baseUrl}${basePath}/callback/${providerId}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { randomBytes } from 'crypto'
|
||||
|
||||
export default async (email, provider, options) => {
|
||||
try {
|
||||
const { baseUrl, adapter } = options
|
||||
const { baseUrl, basePath, adapter } = options
|
||||
|
||||
const { createVerificationRequest } = await adapter.getAdapter(options)
|
||||
|
||||
@@ -13,7 +13,7 @@ export default async (email, provider, options) => {
|
||||
const token = randomBytes(32).toString('hex')
|
||||
|
||||
// Send email with link containing token (the unhashed version)
|
||||
const url = `${baseUrl}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
|
||||
// @TODO Create invite (send secret so can be hashed)
|
||||
await createVerificationRequest(email, url, token, secret, provider, options)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import oAuthClient from '../oauth/client'
|
||||
import crypto from 'crypto'
|
||||
import { createHash } from 'crypto'
|
||||
import logger from '../../../lib/logger'
|
||||
|
||||
export default (provider, callback) => {
|
||||
export default (provider, csrfToken, callback) => {
|
||||
const { callbackUrl } = provider
|
||||
const client = oAuthClient(provider)
|
||||
if (provider.version && provider.version.startsWith('2.')) {
|
||||
@@ -10,7 +10,8 @@ export default (provider, callback) => {
|
||||
let url = client.getAuthorizeUrl({
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope,
|
||||
state: crypto.randomBytes(64).toString('hex')
|
||||
// A hash of the NextAuth.js CSRF token is used as the state
|
||||
state: createHash('sha256').update(csrfToken).digest('hex')
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import render from 'preact-render-to-string'
|
||||
|
||||
export default ({ site, error, baseUrl }) => {
|
||||
const signinPageUrl = `${baseUrl}/signin` // @TODO Make sign in URL configurable
|
||||
export default ({ baseUrl, basePath, error, res }) => {
|
||||
const signinPageUrl = `${baseUrl}${basePath}/signin`
|
||||
|
||||
let statusCode = 200
|
||||
let heading = <h1>Error</h1>
|
||||
let message = <p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
|
||||
let message = <p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
|
||||
|
||||
switch (error) {
|
||||
case 'Signin':
|
||||
@@ -14,50 +15,15 @@ export default ({ site, error, baseUrl }) => {
|
||||
case 'OAuthCreateAccount':
|
||||
case 'EmailCreateAccount':
|
||||
case 'Callback':
|
||||
heading = <h1>Sign in failed</h1>
|
||||
message =
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>Try signing with a different account.</p>
|
||||
</div>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
break
|
||||
case 'OAuthAccountNotLinked':
|
||||
heading = <h1>Sign in failed</h1>
|
||||
message =
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>An account associated with your email address already exists.</p>
|
||||
<p>Sign in with the same account you used originally to confirm your identity.</p>
|
||||
</div>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
// @TODO Add this text when account linking is complete
|
||||
// <p>Once you are signed in, you can link your accounts.</p>
|
||||
// @TODO Display email sign in option if an email provider is configured
|
||||
break
|
||||
case 'EmailSignin':
|
||||
heading = <h1>Sign in failed</h1>
|
||||
message =
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>Unable to send email.</p>
|
||||
</div>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
break
|
||||
case 'CredentialsSignin':
|
||||
heading = <h1>Sign in failed</h1>
|
||||
message =
|
||||
<div>
|
||||
<div className='message'>
|
||||
<p>Check the details you provided are correct.</p>
|
||||
</div>
|
||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||
</div>
|
||||
break
|
||||
// These messages are displayed in line on the sign in page
|
||||
res.status(302).setHeader('Location', `${signinPageUrl}?error=${error}`)
|
||||
res.end()
|
||||
return false
|
||||
case 'Configuration':
|
||||
statusCode = 500
|
||||
heading = <h1>Server error</h1>
|
||||
message =
|
||||
<div>
|
||||
@@ -68,6 +34,7 @@ export default ({ site, error, baseUrl }) => {
|
||||
</div>
|
||||
break
|
||||
case 'AccessDenied':
|
||||
statusCode = 403
|
||||
heading = <h1>Access Denied</h1>
|
||||
message =
|
||||
<div>
|
||||
@@ -80,6 +47,7 @@ export default ({ site, error, baseUrl }) => {
|
||||
case 'Verification':
|
||||
// @TODO Check if user is signed in already with the same email address.
|
||||
// If they are, no need to display this message, can just direct to callbackUrl
|
||||
statusCode = 403
|
||||
heading = <h1>Unable to sign in</h1>
|
||||
message =
|
||||
<div>
|
||||
@@ -93,6 +61,8 @@ export default ({ site, error, baseUrl }) => {
|
||||
default:
|
||||
}
|
||||
|
||||
res.status(statusCode)
|
||||
|
||||
return render(
|
||||
<div className='error'>
|
||||
{heading}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import signin from './signin'
|
||||
import signout from './signout'
|
||||
import verifyRequest from './verify-request'
|
||||
@@ -19,7 +17,8 @@ function render (req, res, page, props, done) {
|
||||
html = verifyRequest(props)
|
||||
break
|
||||
case 'error':
|
||||
html = error(props)
|
||||
html = error({ ...props, res })
|
||||
if (html === false) return done()
|
||||
break
|
||||
default:
|
||||
html = error(props)
|
||||
|
||||
@@ -2,8 +2,7 @@ import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import render from 'preact-render-to-string'
|
||||
|
||||
export default ({ req, csrfToken, providers, callbackUrl }) => {
|
||||
const withCallbackUrl = callbackUrl ? `?callbackUrl=${callbackUrl}` : ''
|
||||
const { email } = req.query
|
||||
const { email, error } = req.query
|
||||
|
||||
// We only want to render providers
|
||||
const providersToRender = providers.filter(provider => {
|
||||
@@ -19,12 +18,46 @@ export default ({ req, csrfToken, providers, callbackUrl }) => {
|
||||
}
|
||||
})
|
||||
|
||||
let errorMessage
|
||||
if (error) {
|
||||
switch (error) {
|
||||
case 'Signin':
|
||||
case 'OAuthSignin':
|
||||
case 'OAuthCallback':
|
||||
case 'OAuthCreateAccount':
|
||||
case 'EmailCreateAccount':
|
||||
case 'Callback':
|
||||
errorMessage = <p>Try signing with a different account.</p>
|
||||
break
|
||||
case 'OAuthAccountNotLinked':
|
||||
errorMessage = <p>To confirm your identity, sign in with the same account you used originally.</p>
|
||||
break
|
||||
case 'EmailSignin':
|
||||
errorMessage = <p>Check your email address.</p>
|
||||
break
|
||||
case 'CredentialsSignin':
|
||||
errorMessage = <p>Sign in failed. Check the details you provided are correct.</p>
|
||||
break
|
||||
default:
|
||||
errorMessage = <p>Unable to sign in.</p>
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return render(
|
||||
<div className='signin'>
|
||||
{errorMessage &&
|
||||
<div className='error'>
|
||||
{errorMessage}
|
||||
</div>}
|
||||
{providersToRender.map((provider, i) =>
|
||||
<div key={provider.id} className='provider'>
|
||||
{provider.type === 'oauth' &&
|
||||
<a className='button' data-provider={provider.id} href={`${provider.signinUrl}${withCallbackUrl}`}>Sign in with {provider.name}</a>}
|
||||
<form action={provider.signinUrl} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
{callbackUrl && <input type='hidden' name='callbackUrl' value={callbackUrl} />}
|
||||
<button type='submit' className='button'>Sign in with {provider.name}</button>
|
||||
</form>}
|
||||
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
|
||||
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
|
||||
<hr />}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import render from 'preact-render-to-string'
|
||||
|
||||
export default ({ baseUrl, csrfToken }) => {
|
||||
export default ({ baseUrl, basePath, csrfToken }) => {
|
||||
return render(
|
||||
<div className='signout'>
|
||||
<h1>Are you sure you want to sign out?</h1>
|
||||
<form action={`${baseUrl}/signout`} method='POST'>
|
||||
<form action={`${baseUrl}${basePath}/signout`} method='POST'>
|
||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||
<button type='submit'>Sign out</button>
|
||||
</form>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||
import render from 'preact-render-to-string'
|
||||
|
||||
export default ({ site }) => {
|
||||
export default ({ baseUrl }) => {
|
||||
return render(
|
||||
<div className='verify-request'>
|
||||
<h1>Check your email</h1>
|
||||
<p>A sign in link has been sent to your email address.</p>
|
||||
<p><a className='site' href={site}>{site.replace(/^https?:\/\//, '')}</a></p>
|
||||
<p><a className='site' href={baseUrl}>{baseUrl.replace(/^https?:\/\//, '')}</a></p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,15 +10,17 @@ export default async (req, res, options, done) => {
|
||||
provider: providerName,
|
||||
providers,
|
||||
adapter,
|
||||
site,
|
||||
secret,
|
||||
baseUrl,
|
||||
basePath,
|
||||
secret,
|
||||
cookies,
|
||||
callbackUrl,
|
||||
pages,
|
||||
jwt,
|
||||
events,
|
||||
callbacks
|
||||
callbacks,
|
||||
csrfToken,
|
||||
redirect
|
||||
} = options
|
||||
const provider = providers[providerName]
|
||||
const { type } = provider
|
||||
@@ -29,86 +31,111 @@ export default async (req, res, options, done) => {
|
||||
const sessionToken = req.cookies ? req.cookies[cookies.sessionToken.name] : null
|
||||
|
||||
if (type === 'oauth') {
|
||||
oAuthCallback(req, provider, async (error, oauthAccount) => {
|
||||
if (error) {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=oAuthCallback`)
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
try {
|
||||
const { profile, account, oAuthProfile } = await oauthAccount
|
||||
try {
|
||||
oAuthCallback(req, provider, csrfToken, async (error, profile, account, OAuthProfile) => {
|
||||
try {
|
||||
if (error) {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
return redirect(`${baseUrl}${basePath}/error?error=oAuthCallback`)
|
||||
}
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
const signinCallbackResponse = await callbacks.signin(profile, account, oAuthProfile)
|
||||
// Make it easier to debug when adding a new provider
|
||||
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
||||
|
||||
if (signinCallbackResponse === false) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
|
||||
res.end()
|
||||
return done()
|
||||
// If we don't have a profile object then either something went wrong
|
||||
// or the user cancelled signin in. We don't know which, so we just
|
||||
// direct the user to the signup page for now. We could do something
|
||||
// else in future.
|
||||
//
|
||||
// Note: In oAuthCallback an error is logged with debug info, so it
|
||||
// should at least be visible to developers what happened if it is an
|
||||
// error with the provider.
|
||||
if (!profile) {
|
||||
return redirect(`${baseUrl}${basePath}/signin`)
|
||||
}
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
// Attempt to get Profile from OAuth provider details before invoking
|
||||
// signIn callback - but if no user object is returned, that is fine
|
||||
// (that just means it's a new user signing in for the first time).
|
||||
let userOrProfile = profile
|
||||
if (adapter) {
|
||||
const { getUserByProviderAccountId } = await adapter.getAdapter(options)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
||||
if (userFromProviderAccountId) {
|
||||
userOrProfile = userFromProviderAccountId
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
||||
if (signInCallbackResponse === false) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
} else {
|
||||
return redirect(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
|
||||
// Handle first logins on new accounts
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return redirect(pages.newUser)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return redirect(callbackUrl || baseUrl)
|
||||
} catch (error) {
|
||||
if (error.name === 'AccountNotLinkedError') {
|
||||
// If the email on the account is already linked, but nto with this oAuth account
|
||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
|
||||
} else if (error.name === 'CreateUserError') {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
|
||||
} else {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = { user, account, isNewUser }
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, oAuthProfile)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signin, { user, account, isNewUser })
|
||||
|
||||
// Handle first logins on new accounts
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
res.status(302).setHeader('Location', pages.newUser)
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'AccountNotLinkedError') {
|
||||
// If the email on the account is already linked, but nto with this oAuth account
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthAccountNotLinked`)
|
||||
} else if (error.name === 'CreateUserError') {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=OAuthCreateAccount`)
|
||||
} else {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
|
||||
}
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
if (callbackUrl) {
|
||||
res.status(302).setHeader('Location', callbackUrl)
|
||||
res.end()
|
||||
} else {
|
||||
res.status(302).setHeader('Location', site)
|
||||
res.end()
|
||||
}
|
||||
return done()
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_CALLBACK_ERROR', error)
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (type === 'email') {
|
||||
try {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(options)
|
||||
@@ -118,9 +145,7 @@ export default async (req, res, options, done) => {
|
||||
// Verify email and verification token exist in database
|
||||
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
|
||||
if (!invite) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Verification`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
||||
}
|
||||
|
||||
// If verification token is valid, delete verification request token from
|
||||
@@ -132,23 +157,32 @@ export default async (req, res, options, done) => {
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
const signinCallbackResponse = await callbacks.signin(profile, account, null)
|
||||
|
||||
if (signinCallbackResponse === false) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
|
||||
res.end()
|
||||
return done()
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||
if (signInCallbackResponse === false) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
} else {
|
||||
return redirect(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = { user, account, isNewUser }
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload)
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
@@ -156,92 +190,85 @@ export default async (req, res, options, done) => {
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
} else {
|
||||
console.log('debug.cookies', cookies)
|
||||
console.log('debug.session', session)
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signin, { user, account, isNewUser })
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
|
||||
// Handle first logins on new accounts
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
res.status(302).setHeader('Location', pages.newUser)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(pages.newUser)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
if (callbackUrl) {
|
||||
res.status(302).setHeader('Location', callbackUrl)
|
||||
res.end()
|
||||
return redirect(callbackUrl)
|
||||
} else {
|
||||
res.status(302).setHeader('Location', site)
|
||||
res.end()
|
||||
return redirect(baseUrl)
|
||||
}
|
||||
return done()
|
||||
} catch (error) {
|
||||
if (error.name === 'CreateUserError') {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=EmailCreateAccount`)
|
||||
return redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
||||
} else {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Callback`)
|
||||
logger.error('CALLBACK_EMAIL_ERROR', error)
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
} else if (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')
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
|
||||
res.end()
|
||||
return done()
|
||||
return 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')
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const credentials = req.body
|
||||
|
||||
// If promise is rejected / throws error then display Configuration error
|
||||
let userObjectReturnedFromAuthorizeHandler
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=Configuration`)
|
||||
res.end()
|
||||
return done()
|
||||
if (error instanceof Error) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
} else {
|
||||
return redirect(error)
|
||||
}
|
||||
}
|
||||
|
||||
const user = userObjectReturnedFromAuthorizeHandler
|
||||
const account = { id: provider.id, type: 'credentials' }
|
||||
|
||||
// If no user is returned, credentials are not valid
|
||||
if (!user) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
res.end()
|
||||
return done()
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||
if (signInCallbackResponse === false) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
} else {
|
||||
return redirect(error)
|
||||
}
|
||||
}
|
||||
|
||||
const signinCallbackResponse = await callbacks.signin(user, account, credentials)
|
||||
|
||||
if (signinCallbackResponse === false) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
|
||||
res.end()
|
||||
return done()
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image
|
||||
}
|
||||
|
||||
const defaultJwtPayload = { user, account }
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload)
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
@@ -249,17 +276,9 @@ export default async (req, res, options, done) => {
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
|
||||
await dispatchEvent(events.signin, { user, account })
|
||||
await dispatchEvent(events.signIn, { user, account })
|
||||
|
||||
if (callbackUrl) {
|
||||
res.status(302).setHeader('Location', callbackUrl)
|
||||
res.end()
|
||||
} else {
|
||||
res.status(302).setHeader('Location', site)
|
||||
res.end()
|
||||
}
|
||||
|
||||
return done()
|
||||
return redirect(callbackUrl || baseUrl)
|
||||
} else {
|
||||
res.status(500).end(`Error: Callback for provider type ${type} not supported`)
|
||||
return done()
|
||||
|
||||
@@ -19,7 +19,7 @@ export default async (req, res, options, done) => {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
// Decrypt and verify token
|
||||
const decodedJwt = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
|
||||
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
|
||||
// Generate new session expiry date
|
||||
const sessionExpiresDate = new Date()
|
||||
@@ -30,9 +30,9 @@ export default async (req, res, options, done) => {
|
||||
// as needed for presentation purposes (e.g. "you are logged in as…").
|
||||
const defaultSessionPayload = {
|
||||
user: {
|
||||
name: decodedJwt.user && decodedJwt.user.name ? decodedJwt.user.name : null,
|
||||
email: decodedJwt.user && decodedJwt.user.email ? decodedJwt.user.email : null,
|
||||
image: decodedJwt.user && decodedJwt.user.image ? decodedJwt.user.image : null
|
||||
name: decodedJwt.name || null,
|
||||
email: decodedJwt.email || null,
|
||||
image: decodedJwt.picture || null
|
||||
},
|
||||
expires: sessionExpires
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default async (req, res, options, done) => {
|
||||
response = sessionPayload
|
||||
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newEncodedJwt = await jwt.encode({ secret: jwt.secret, token: jwtPayload, maxAge: sessionMaxAge })
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
|
||||
@@ -79,7 +79,7 @@ export default async (req, res, options, done) => {
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload)
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
|
||||
@@ -8,9 +8,11 @@ export default async (req, res, options, done) => {
|
||||
provider: providerName,
|
||||
providers,
|
||||
baseUrl,
|
||||
csrfTokenVerified,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks
|
||||
callbacks,
|
||||
csrfToken,
|
||||
redirect
|
||||
} = options
|
||||
const provider = providers[providerName]
|
||||
const { type } = provider
|
||||
@@ -20,29 +22,19 @@ export default async (req, res, options, done) => {
|
||||
return done()
|
||||
}
|
||||
|
||||
if (type === 'oauth') {
|
||||
oAuthSignin(provider, (error, oAuthSigninUrl) => {
|
||||
if (type === 'oauth' && req.method === 'POST') {
|
||||
oAuthSignin(provider, csrfToken, (error, oAuthSigninUrl) => {
|
||||
if (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
res
|
||||
.status(302)
|
||||
.setHeader('Location', `${baseUrl}/error?error=oAuthSignin`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=oAuthSignin`)
|
||||
}
|
||||
|
||||
res.status(302).setHeader('Location', oAuthSigninUrl)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(oAuthSigninUrl)
|
||||
})
|
||||
} else if (type === 'email' && req.method === 'POST') {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
res
|
||||
.status(302)
|
||||
.setHeader('Location', `${baseUrl}/error?error=Configuration`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
const { getUserByEmail } = await adapter.getAdapter(options)
|
||||
|
||||
@@ -58,54 +50,30 @@ export default async (req, res, options, done) => {
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
const signinCallbackResponse = await callbacks.signin(profile, account)
|
||||
|
||||
if (signinCallbackResponse === false) {
|
||||
res.status(302).setHeader('Location', `${baseUrl}/error?error=AccessDenied`)
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
|
||||
// If CSRF token not verified, send the user to sign in page, which will
|
||||
// display a new form with a valid token so that submitting it should work.
|
||||
//
|
||||
// Note: Adds ?csrf=true query string param to URL for debugging/tracking
|
||||
if (!csrfTokenVerified) {
|
||||
res
|
||||
.status(302)
|
||||
.setHeader(
|
||||
'Location',
|
||||
`${baseUrl}/signin?email=${encodeURIComponent(email)}&csrf=true`
|
||||
)
|
||||
res.end()
|
||||
return done()
|
||||
try {
|
||||
const signinCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
||||
if (signinCallbackResponse === false) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
} else {
|
||||
return redirect(error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSignin(email, provider, options)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_EMAIL_ERROR', error)
|
||||
res
|
||||
.status(302)
|
||||
.setHeader('Location', `${baseUrl}/error?error=EmailSignin`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
||||
}
|
||||
|
||||
res
|
||||
.status(302)
|
||||
.setHeader(
|
||||
'Location',
|
||||
`${baseUrl}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`
|
||||
)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`)
|
||||
} else {
|
||||
// If provider not supported, redirect to sign in page
|
||||
res.status(302).setHeader('Location', `${baseUrl}/signin`)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(`${baseUrl}${basePath}/signin`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,15 @@ import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
|
||||
export default async (req, res, options, done) => {
|
||||
const { adapter, cookies, events, jwt, callbackUrl, csrfTokenVerified, baseUrl } = options
|
||||
const sessionMaxAge = options.session.maxAge
|
||||
const { adapter, cookies, events, jwt, callbackUrl, redirect } = options
|
||||
const useJwtSession = options.session.jwt
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
|
||||
if (!csrfTokenVerified) {
|
||||
// If a csrfToken was not verified with this request, send the user to
|
||||
// the signout page, as they should have a valid one now and clicking
|
||||
// the signout button should work.
|
||||
//
|
||||
// Note: Adds ?csrf=true query string param to URL for debugging/tracking.
|
||||
// @TODO Add support for custom signin URLs
|
||||
res.status(302).setHeader('Location', `${baseUrl}/signout?csrf=true`)
|
||||
res.end()
|
||||
return done()
|
||||
}
|
||||
|
||||
if (useJwtSession) {
|
||||
// Dispatch signout event
|
||||
try {
|
||||
const decodedJwt = await jwt.decode({ secret: jwt.secret, token: sessionToken, maxAge: sessionMaxAge })
|
||||
await dispatchEvent(events.signout, decodedJwt)
|
||||
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
await dispatchEvent(events.signOut, decodedJwt)
|
||||
} catch (error) {
|
||||
// Do nothing if decoding the JWT fails
|
||||
}
|
||||
@@ -36,7 +23,7 @@ export default async (req, res, options, done) => {
|
||||
try {
|
||||
// Dispatch signout event
|
||||
const session = await getSession(sessionToken)
|
||||
await dispatchEvent(events.signout, session)
|
||||
await dispatchEvent(events.signOut, session)
|
||||
} catch (error) {
|
||||
// Do nothing if looking up the session fails
|
||||
}
|
||||
@@ -56,7 +43,5 @@ export default async (req, res, options, done) => {
|
||||
maxAge: 0
|
||||
})
|
||||
|
||||
res.status(302).setHeader('Location', callbackUrl)
|
||||
res.end()
|
||||
return done()
|
||||
return redirect(callbackUrl)
|
||||
}
|
||||
|
||||
8
test/fixtures/schemas/mysql.json
vendored
8
test/fixtures/schemas/mysql.json
vendored
@@ -15,7 +15,7 @@
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp",
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
@@ -69,7 +69,7 @@
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp",
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
@@ -92,7 +92,7 @@
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp",
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
@@ -126,7 +126,7 @@
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp",
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
|
||||
74
test/fixtures/sql/mysql.sql
vendored
Normal file
74
test/fixtures/sql/mysql.sql
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
CREATE TABLE accountsshould be
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMP(6),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMP(6),
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
74
test/fixtures/sql/postgres.sql
vendored
Normal file
74
test/fixtures/sql/postgres.sql
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id SERIAL,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id SERIAL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id SERIAL,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id SERIAL,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
@@ -4,15 +4,17 @@ const Adapters = require('../adapters')
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// @FIXME Does not actally bail on connection error, adapter *should* throw exception
|
||||
// Before that is addressed, we should make sure all routes in the app that
|
||||
// call getAdapter() or rely on methods that use getAdapter() will handle
|
||||
// a connection error gracefully.
|
||||
const adapter1 = Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1:27017/nextauth?ssl=false&retryWrites=true')
|
||||
await adapter1.getAdapter()
|
||||
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
|
||||
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
|
||||
|
||||
const adapter2 = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
|
||||
await adapter2.getAdapter()
|
||||
// Connect to local MongoDB instance
|
||||
// Note: MongoDB doesn't thrown a connection error right away if is a
|
||||
// problem with the credentials or host configuration, but after a few
|
||||
// seconds it throws a Timeout error (which is caught by the adapter).
|
||||
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
// @TODO create objects in database, check format of objects returned
|
||||
|
||||
console.log('MongoDB loaded ok')
|
||||
process.exit()
|
||||
|
||||
@@ -7,113 +7,177 @@ Callbacks are asynchronous functions you can use to control what happens when an
|
||||
|
||||
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.
|
||||
|
||||
:::tip
|
||||
If you want to pass data such as an Access Token or User ID to the browser when using JSON Web Tokens, you can persist the data in the token when the `jwt` callback is called, then pass the data through to the browser in the `session` callback.
|
||||
:::
|
||||
|
||||
You can specify a handler for any of the callbacks below.
|
||||
|
||||
#### How to use the callback option
|
||||
|
||||
```js
|
||||
callbacks: {
|
||||
signin: async (profile, account, metadata) => { },
|
||||
redirect: async (url, baseUrl) => { },
|
||||
session: async (session, token) => { },
|
||||
jwt: async (token) => { }
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
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)
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The documentation below shows how to implement each callback and their default behaviour.
|
||||
The documentation below shows how to implement each callback, their default behaviour and an example of what the response for each callback should be. Note that configuration options and authentication providers you are using can impact the values passed to the callbacks.
|
||||
|
||||
## Signin
|
||||
## Sign in callback
|
||||
|
||||
Use the signin callback to control if a user is allowed to sign in or not.
|
||||
Use the `signIn()` callback to control if a user is allowed to sign in.
|
||||
|
||||
This is triggered before sign in flow completes, so the user profile may be a
|
||||
user object (with an ID) or it may be just their name and email address,
|
||||
depending on the sign in flow and if they have an account already.
|
||||
|
||||
When using email sign in, this method is triggered both when the user requests
|
||||
to sign in and again when they activate the link in the sign in email.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {object} profile User profile (e.g. user id, name, email)
|
||||
* @param {object} account Account used to sign in (e.g. OAuth account)
|
||||
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
|
||||
* @return {boolean|object} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
const signin = async (profile, account, metadata) => {
|
||||
const isAllowedToSignIn = true
|
||||
if (isAllowedToSignIn) {
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
return Promise.resolve(false)
|
||||
```js title="pages/api/auth/[...nextauth.js]"
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {object} user User object
|
||||
* @param {object} account Provider account
|
||||
* @param {object} profile Provider profile
|
||||
* @return {boolean} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
*/
|
||||
signIn: async (user, account, profile) => {
|
||||
const isAllowedToSignIn = true
|
||||
if (isAllowedToSignIn) {
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
// Return false to display a default error message
|
||||
return Promise.resolve(false)
|
||||
// You can also Reject this callback with an Error or with a URL:
|
||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Redirect
|
||||
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
|
||||
|
||||
The redirect callback is called anytime the user is redirected to a callback URL
|
||||
(e.g. on signin or signout).
|
||||
Email accounts do not have profiles in the same way OAuth accounts do. On the first call during email sign in the `profile` object will include an property `verificationRequest: true` to indicate it is being triggered in the verification request flow. When the callback is invoked *after* a user has clicked on a sign in link, this property will not be present.
|
||||
|
||||
You can check for the `verificationRequest` property to avoid sending emails to addresses or domains on a blocklist (or to only explicitly generate them for email address in an allow list).
|
||||
|
||||
By default, for security, only Callback URLs on the same URL as the site are
|
||||
allowed, you can use the redirect callback to customise that behaviour.
|
||||
* When using the **Credentials Provider** the `user` object is the response returned from the `authorization` callback and the `profile` object is the raw body of the `HTTP POST` submission.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {string} url URL provided as callback URL by the client
|
||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||
* @return {string} URL the client will be redirect to
|
||||
*/
|
||||
const redirect = async (url, baseUrl) => {
|
||||
return url.startsWith(baseUrl)
|
||||
? Promise.resolve(url)
|
||||
: Promise.resolve(baseUrl)
|
||||
:::note
|
||||
When using NextAuth.js with a database, the User object will be either a user object from the database (including the User ID) if the user has signed in before or a simpler prototype user object (i.e. name, email, image) for users who have not signed in before.
|
||||
|
||||
When using NextAuth.js without a database, the user object it will always be a prototype user object, with information extracted from the profile.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you only want to allow users who already have accounts in the database to sign in, you can check for the existance of a `user.id` property and reject any sign in attempts from accounts that do not have one.
|
||||
|
||||
If you are using NextAuth.js without database and want to control who can sign in, you can check their email address or profile against a hard coded list in the `signIn()` callback.
|
||||
:::
|
||||
|
||||
## Redirect callback
|
||||
|
||||
The redirect callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
|
||||
|
||||
By default only URLs on the same URL as the site are allowed, you can use the redirect callback to customise that behaviour.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth.js]"
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {string} url URL provided as callback URL by the client
|
||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||
* @return {string} URL the client will be redirect to
|
||||
*/
|
||||
redirect: async (url, baseUrl) => {
|
||||
return url.startsWith(baseUrl)
|
||||
? Promise.resolve(url)
|
||||
: Promise.resolve(baseUrl)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Session
|
||||
|
||||
:::note
|
||||
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` (etc)
|
||||
e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
If JSON Web Tokens are enabled, you can also access the decrypted token and use
|
||||
this method to pass information from the encoded token back to the client.
|
||||
* 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.
|
||||
|
||||
The JWT callback is invoked before session is called, so anything you add to the
|
||||
JWT will be immediately available in the session callback.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {object} session Session object
|
||||
* @param {object} token JSON Web Token (if enabled)
|
||||
* @return {object} Session that will be returned to the client
|
||||
*/
|
||||
const session = async (session, token) => {
|
||||
return Promise.resolve(session)
|
||||
```js title="pages/api/auth/[...nextauth.js]"
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {object} session Session object
|
||||
* @param {object} user User object (if using database sessions)
|
||||
* JSON Web Token (if not using database sessions)
|
||||
* @return {object} Session that will be returned to the client
|
||||
*/
|
||||
session: async (session, user, sessionToken) => {
|
||||
session.foo = 'bar' // Add property to session
|
||||
return Promise.resolve(session)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JWT
|
||||
:::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.
|
||||
:::
|
||||
|
||||
This JSON Web Token callback is called whenever a JSON Web Token is created or updated.
|
||||
:::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.
|
||||
|
||||
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session` (etc)
|
||||
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.
|
||||
|
||||
On initial sign in with an OAuth provider, the raw oAuthProfile returned by the
|
||||
provider is also passed as a parameter - it is not available on subsequent calls.
|
||||
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).
|
||||
:::
|
||||
|
||||
You can take advantage of this to persist additional data you need from their
|
||||
raw profile to the encoded JWT.
|
||||
## JWT callback
|
||||
|
||||
```js
|
||||
/**
|
||||
* @param {object} token Decrypted JSON Web Token
|
||||
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
||||
* @return {object} JSON Web Token that will be saved
|
||||
*/
|
||||
const jwt = async (token, oAuthProfile) => {
|
||||
return Promise.resolve(token)
|
||||
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
|
||||
in) or updated (i.e whenever a session is accesed in the client).
|
||||
|
||||
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
|
||||
|
||||
* As with database session expiry times, token expiry time is extended whenever a session is active.
|
||||
* The arguments *user*, *account*, *profile* and *isNewUser* are only passed the first time this callback is called on a new session, after the user signs in.
|
||||
|
||||
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth.js]"
|
||||
callbacks: {
|
||||
/**
|
||||
* @param {object} token Decrypted JSON Web Token
|
||||
* @param {object} user User object (only available on sign in)
|
||||
* @param {object} account Provider account (only available on sign in)
|
||||
* @param {object} profile Provider profile (only available on sign in)
|
||||
* @param {boolean} isNewUser True if new user (only available on sign in)
|
||||
* @return {object} JSON Web Token that will be saved
|
||||
*/
|
||||
jwt: async (token, user, account, profile, isNewUser) => {
|
||||
const isSignIn = (user) ? true : false
|
||||
// Add auth_time to token on signin in
|
||||
if (isSignIn) { token.auth_time = new Date().toISOString() }
|
||||
return Promise.resolve(token)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain commonly imposed by browsers.
|
||||
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
|
||||
:::
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
---
|
||||
id: database
|
||||
title: Database Configuration
|
||||
---
|
||||
|
||||
Specifying a database is optional if you don't need to persist user data or support email sign in.
|
||||
|
||||
If you want to do either of these things you will need to specify a database.
|
||||
|
||||
If you don't specify a database then JSON Web Tokens will be enabled and used to store session data. If you do specify a database then database sessions will be enabled, unless you explicitly enable JSON Web Tokens for sessions by passing the option `sessions { jwt: true }`.
|
||||
|
||||
## How to specify a database
|
||||
|
||||
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
|
||||
|
||||
The following approaches are exactly equivalent:
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
```js
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "nextauth",
|
||||
password: "password",
|
||||
database: "nextauth",
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
See the [TypeORM configuration documentation](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) for all the supported database options.
|
||||
:::
|
||||
|
||||
## Setting up a database
|
||||
|
||||
NextAuth.js will configure your database with tables / collections automatically if `synchronize: true` is set.
|
||||
|
||||
If you are having problems connecting to your database, try enabling debug message with the `debug: true` option when initializing NextAuth.js.
|
||||
|
||||
:::warning
|
||||
The option **?synchronize=true** automatically synchronizes the database schema with what NextAuth.js expects. It is useful to create the tables you need in the database on first run against a test database but it should not be enabled in production as it may result in data loss.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If you want to set a prefix for all table names you can use the TypeORM **entityPrefix** option.
|
||||
|
||||
*For example:*
|
||||
|
||||
```js
|
||||
'mysql://username:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
|
||||
```
|
||||
|
||||
*…or as a database configuration object:*
|
||||
|
||||
```js
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: "127.0.0.1",
|
||||
port: 3306,
|
||||
username: "nextauth",
|
||||
password: "password",
|
||||
database: "nextauth",
|
||||
synchronize: true,
|
||||
entityPrefix: 'nextauth_'
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Supported databases
|
||||
|
||||
NextAuth.js uses TypeORM as the default database adapter, but only some databases are supported.
|
||||
|
||||
:::tip
|
||||
When configuring your database you also need to install an appropriate **node module** for your database.
|
||||
:::
|
||||
|
||||
### SQLite
|
||||
|
||||
*SQLite is intended only for development / testing and not for production use.*
|
||||
|
||||
Install module:
|
||||
`npm i sqlite3`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'sqlite://localhost/:memory:?synchronize=true'
|
||||
```
|
||||
|
||||
### MySQL
|
||||
|
||||
Install module:
|
||||
`npm i mysql`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
|
||||
Install module:
|
||||
`npm i mariadb`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mariadb://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
### Postgres
|
||||
|
||||
Install module:
|
||||
`npm i pg`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
Install module:
|
||||
`npm i mongodb`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
|
||||
## Unsupported databases
|
||||
|
||||
The following additional databases are supported by TypeORM (which the default adapter uses) and *may* work with NextAuth.js but have not been tested:
|
||||
|
||||
* cordova
|
||||
* expo
|
||||
* mssql
|
||||
* oracle
|
||||
* sqljs
|
||||
* react-native
|
||||
|
||||
Any database that supports ANSI SQL *should* work out of the box.
|
||||
|
||||
:::tip
|
||||
You can customize, extend or replace the models, you can do this by using the 'adapters' option and passing passing additional options to **Adapters.Default()**. How to do this is not yet documented.
|
||||
:::
|
||||
|
||||
:::note
|
||||
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with any database.
|
||||
:::
|
||||
|
||||
178
www/docs/configuration/databases.md
Normal file
178
www/docs/configuration/databases.md
Normal file
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: databases
|
||||
title: Databases
|
||||
---
|
||||
|
||||
NextAuth.js comes with multiple ways of connecting to a database:
|
||||
|
||||
* **TypeORM** (default)<br/>
|
||||
_The TypeORM adapter supports MySQL, Postgres, SQLite and MongoDB databases._
|
||||
* **Prisma**<br/>
|
||||
_The Prisma 2 adapter supports MySQL, Postgres and SQLite databases._
|
||||
* **Custom Adapter**<br/>
|
||||
_A custom Adapter can be used to connect to any database._
|
||||
|
||||
**This document covers the default adapter (TypeORM).**
|
||||
|
||||
See the [documentation for adapters](/schemas/adapters) to learn more about using Prisma adapter or using a custom adapter.
|
||||
|
||||
To learn more about databases in NextAuth.js and how they are used, check out [databases in the FAQ](/faq#databases).
|
||||
|
||||
---
|
||||
|
||||
## How to use a database
|
||||
|
||||
You can specify database credentials as as a connection string or a [TypeORM configuration](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) object.
|
||||
|
||||
The following approaches are exactly equivalent:
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name'
|
||||
```
|
||||
|
||||
```js
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth'
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
You can pass in any valid [TypeORM configuration option](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md).
|
||||
|
||||
*e.g. To set a prefix for all table names you can use the **entityPrefix** option as connection string parameter:*
|
||||
|
||||
```js
|
||||
'mysql://username:password@127.0.0.1:3306/database_name?entityPrefix=nextauth_'
|
||||
```
|
||||
|
||||
*…or as a database configuration object:*
|
||||
|
||||
```js
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth'
|
||||
entityPrefix: 'nextauth_'
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Setting up a database
|
||||
|
||||
Using SQL to create tables and columns is the recommended way to set up an SQL database for NextAuth.js.
|
||||
|
||||
Check out the links below for SQL you can run to set up a database for NextAuth.js.
|
||||
|
||||
* [MySQL Schema](/schemas/mysql)
|
||||
* [Postgres Schema](/schemas/postgres)
|
||||
|
||||
_If you are running SQLite, MongoDB or a Document database you can skip this step._
|
||||
|
||||
Alternatively, you can also have your database configured automatically using the `synchronize: true` option:
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name?synchronize=true'
|
||||
```
|
||||
|
||||
```js
|
||||
database: {
|
||||
type: 'mysql',
|
||||
host: '127.0.0.1',
|
||||
port: 3306,
|
||||
username: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
synchronize: true
|
||||
}
|
||||
```
|
||||
|
||||
:::warning
|
||||
**The `synchronize` option should not be used against production databases.**
|
||||
|
||||
It is useful to create the tables you need when setting up a database for the first time, but it should not be enabled against production databases as it may result in data loss if there is a difference between the schema that found in the database and the schema that the version of NextAuth.js being used is expecting.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Supported databases
|
||||
|
||||
The default database adapter is TypeORM, but only some databases supported by TypeORM are supported by NextAuth.js as custom logic needs to be handled by NextAuth.js.
|
||||
|
||||
Databases compatible with MySQL, Postgres and MongoDB should work out of the box with NextAuth.js. When used with any other database, NextAuth.js will assume an ANSI SQL compatible database.
|
||||
|
||||
:::tip
|
||||
When configuring your database you also need to install an appropriate **node module** for your database.
|
||||
:::
|
||||
|
||||
### MySQL
|
||||
|
||||
Install module:
|
||||
`npm i mysql`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mysql://username:password@127.0.0.1:3306/database_name'
|
||||
```
|
||||
|
||||
### MariaDB
|
||||
|
||||
Install module:
|
||||
`npm i mariadb`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mariadb://username:password@127.0.0.1:3306/database_name'
|
||||
```
|
||||
|
||||
### Postgres
|
||||
|
||||
Install module:
|
||||
`npm i pg`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'postgres://username:password@127.0.0.1:3306/database_name'
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
Install module:
|
||||
`npm i mongodb`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
|
||||
```
|
||||
|
||||
### SQLite
|
||||
|
||||
*SQLite is intended only for development / testing and not for production use.*
|
||||
|
||||
Install module:
|
||||
`npm i sqlite3`
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
database: 'sqlite://localhost/:memory:'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Other databases
|
||||
|
||||
See the [documentation for adapters](/schemas/adapters) for more information on advanced configuration, including how to use NextAuth.js with other databases using a [custom adapter](/tutorials/creating-a-database-adapter).
|
||||
23
www/docs/configuration/events.md
Normal file
23
www/docs/configuration/events.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
id: events
|
||||
title: Events
|
||||
---
|
||||
|
||||
Events are asynchronous functions that do not return a response, they are useful for audit logs / reporting.
|
||||
|
||||
You can specify a handler for any of these events below, for debugging or for an audit log.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
events: {
|
||||
signIn: async (message) => { /* on successful sign in */ },
|
||||
signOut: async (message) => { /* on signout */ },
|
||||
createUser: async (message) => { /* user created */ },
|
||||
linkAccount: async (message) => { /* account linked to a user */ },
|
||||
session: async (message) => { /* session is active */ },
|
||||
error: async (message) => { /* error in authentication flow */ }
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc) but typically contains a user object and/or contents of the JSON Web Token and other information relevent to the event.
|
||||
@@ -1,28 +1,31 @@
|
||||
---
|
||||
id: options
|
||||
title: NextAuth.js Options
|
||||
title: Options
|
||||
---
|
||||
|
||||
Options are passed to NextAuth.js when initializing it in an API route.
|
||||
## Environment Variables
|
||||
|
||||
:::note
|
||||
The only *required* options are **site** and **providers**.
|
||||
### NEXTAUTH_URL
|
||||
|
||||
When deploying to production, set the `NEXTAUTH_URL` environment variable to the canonical URL of your site.
|
||||
|
||||
```
|
||||
NEXTAUTH_URL=https://example.com
|
||||
```
|
||||
|
||||
If your Next.js application uses a custom base path, specify the route to the API endpoint in full.
|
||||
|
||||
_e.g. `NEXTAUTH_URL=https://example.com/custom-route/api/auth`_
|
||||
|
||||
:::tip
|
||||
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Options
|
||||
|
||||
### site
|
||||
|
||||
* **Default value**: `empty string`
|
||||
* **Required**: *Yes*
|
||||
|
||||
#### Description
|
||||
|
||||
The fully qualified URL for the root of your site.
|
||||
|
||||
e.g. `http://localhost:3000` or `https://www.example.com`
|
||||
|
||||
---
|
||||
Options are passed to NextAuth.js when initializing it in an API route.
|
||||
|
||||
### providers
|
||||
|
||||
@@ -40,36 +43,26 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
### database
|
||||
|
||||
* **Default value**: `null`
|
||||
* **Required**: *No* (Except by Email provider)
|
||||
* **Required**: *No (unless using email provider)*
|
||||
|
||||
#### Description
|
||||
|
||||
A database connection string or [TypeORM](https://github.com/typeorm/typeorm/blob/master/docs/using-ormconfig.md) configuration object.
|
||||
|
||||
NextAuth.js has built in support for MySQL, MariaDB, Postgress, MongoDB and SQLite databases.
|
||||
|
||||
The default database provider is also compatible with other ANSI SQL compatible databases.
|
||||
|
||||
NextAuth.js can can be used with any database by specifying a custom `adapter` option.
|
||||
|
||||
:::tip
|
||||
The Email provider currently requires a database to be configured.
|
||||
:::
|
||||
[A database connection string or configuration object.](/configuration/databases)
|
||||
|
||||
---
|
||||
|
||||
### secret
|
||||
|
||||
* **Default value**: `string` (*SHA hash of the "options" object*)
|
||||
* **Required**: *No* (but strongly recommended)
|
||||
* **Required**: *No - but strongly recommended!*
|
||||
|
||||
#### Description
|
||||
|
||||
A random string used to hash tokens and sign cookies (e.g. SHA hash).
|
||||
A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
|
||||
If not provided will be auto-generated based on hash of all your provided options.
|
||||
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
|
||||
The default behaviour is secure, but volatile, and it is strongly recommended you explicitly specify a value for secret to avoid invalidating any tokens when the automatically generated hash changes.
|
||||
The default behaviour is volatile, and it is strongly recommended you explicitly specify a value to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
|
||||
---
|
||||
|
||||
@@ -110,73 +103,87 @@ session: {
|
||||
|
||||
#### Description
|
||||
|
||||
JSON Web Tokens are only used if JWT sessions are enabled with `session: { jwt: true }` (see `session` documentation).
|
||||
JSON Web Tokens are can be used for session tokens if enabled with `session: { jwt: true }` option. JSON Web Tokens enabled by default if you have not specified a database.
|
||||
|
||||
The `jwt` object and all properties on it are optional.
|
||||
By default JSON Web Tokens tokens are signed (JWS) but not encrypted (JWE), as JWT encryption adds additional overhead and comes with some caveats. You can enable encryption by setting `encryption: true`.
|
||||
|
||||
When enabled, JSON Web Tokens is signed with `HMAC SHA256` and encrypted with symmetric `AES`.
|
||||
|
||||
Using JWT to store sessions is often faster, cheaper and more scaleable relying on a database.
|
||||
|
||||
Default values for this option are shown below:
|
||||
#### JSON Web Token Options
|
||||
|
||||
```js
|
||||
jwt: {
|
||||
// secret: 'my-secret-123', // Secret auto-generated if not specified.
|
||||
// A secret to use for key generation - you should set this explicitly
|
||||
// Defaults to NextAuth.js secret if not explicitly specified.
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// By default the JSON Web Token is signed with SHA256 and encrypted with AES.
|
||||
//
|
||||
// You can define your own encode/decode functions for signing + encryption if
|
||||
// you want to override the default behaviour (or to add/remove information
|
||||
// from the JWT when it is encoded).
|
||||
// encode: async ({ secret, key, token, maxAge }) => {},
|
||||
// decode: async ({ secret, key, token, maxAge }) => {},
|
||||
// Set to true to use encryption. Defaults to false (signing only).
|
||||
// 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 }) => {},
|
||||
}
|
||||
```
|
||||
|
||||
An example JSON WebToken contains an encrypted payload like this:
|
||||
An example JSON Web Token contains a payload like this:
|
||||
|
||||
```js
|
||||
{
|
||||
user: {
|
||||
name: 'Iain Collins',
|
||||
email: 'me@iaincollins.com',
|
||||
image: 'https://example.com/image.jpg',
|
||||
id: 1 // User ID will note be specified if used without a database
|
||||
},
|
||||
// The account object stores details for the authentication provider account
|
||||
// that was used to sign in. It only contains exactly one account, even the
|
||||
// user is linked to multiple provider accounts in a database.
|
||||
account: {
|
||||
provider: 'google',
|
||||
type: 'oauth',
|
||||
id: 3218529,
|
||||
refreshToken: 'cc0d32d79145091cd6cd8979f0a6d6b67d490899',
|
||||
accessToken: '931400799b4a980715bb55af1bb8e01d92316956',
|
||||
accessTokenExpires: null
|
||||
},
|
||||
isNewUser: true, // Is set to true if is first sign in
|
||||
iat: 1591150735, // Issued at
|
||||
exp: 4183150735 // Expires in
|
||||
name: 'Iain Collins',
|
||||
email: 'me@iaincollins.com',
|
||||
picture: 'https://example.com/image.jpg',
|
||||
"iat": 1594601838,
|
||||
"exp": 1597193838
|
||||
}
|
||||
```
|
||||
|
||||
You can use the built-in `getJwt()` helper method to read the token, like this:
|
||||
#### JWT Helper
|
||||
|
||||
You can use the built-in `getToken()` helper method to verify and decrypt the token, like this:
|
||||
|
||||
```js
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.JWT_SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
// Automatically decrypts and verifies JWT
|
||||
const token = await jwt.getJwt({ req, secret })
|
||||
res.end(JSON.stringify(token, null, 2))
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
console.log('JSON Web Token', token)
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
_For convenience, this helper function is also able to read and decode tokens passed in an HTTP Bearer header._
|
||||
|
||||
**Required**
|
||||
|
||||
The getToken() helper requires the following options:
|
||||
|
||||
* `req` - (object) Request object
|
||||
* `secret` - (string) JWT Secret
|
||||
|
||||
You must also pass *any options configured on the `jwt` option* to the helper.
|
||||
|
||||
e.g. Including custom session `maxAge` and custom signing and/or encryption keys or options
|
||||
|
||||
**Optional**
|
||||
|
||||
It also supports the following options:
|
||||
|
||||
* `secureCookie` - (boolean) Use secure prefixed cookie name
|
||||
|
||||
By default, the helper function will attempt to determine if it should use the secure prefixed cookie (e.g. `true` in production and `false` in development, unless NEXTAUTH_URL contains an HTTPS URL).
|
||||
|
||||
* `cookieName` - (string) Session token cookie name
|
||||
|
||||
The `secureCookie` option is ignored if `cookieName` is explcitly specified.
|
||||
|
||||
* `raw` - (boolean) Get raw token (not decoded)
|
||||
|
||||
If set to `true` returns the raw token without decrypting or verifying it.
|
||||
|
||||
:::note
|
||||
The JWT is stored in the Session Token cookie – the same cookie used for database sessions.
|
||||
The JWT is stored in the Session Token cookie, the same cookie used for tokens with database sessions.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -196,8 +203,8 @@ Pages specified will override the corresponding built-in page.
|
||||
|
||||
```js
|
||||
pages: {
|
||||
signin: '/auth/signin',
|
||||
signout: '/auth/signout',
|
||||
signIn: '/auth/signin',
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error', // Error code passed in query string as ?error=
|
||||
verifyRequest: '/auth/verify-request', // (used for check email message)
|
||||
newUser: null // If set, new users will be directed here on first sign in
|
||||
@@ -223,10 +230,10 @@ You can specify a handler for any of the callbacks below.
|
||||
|
||||
```js
|
||||
callbacks: {
|
||||
signin: async (profile, account, metadata) => { },
|
||||
signIn: async (profile, account, metadata) => { },
|
||||
redirect: async (url, baseUrl) => { },
|
||||
session: async (session, token) => { },
|
||||
jwt: async (token) => => { }
|
||||
jwt: async (token, profile) => => { }
|
||||
}
|
||||
```
|
||||
|
||||
@@ -249,8 +256,8 @@ The content of the message object varies depending on the flow (e.g. OAuth or Em
|
||||
|
||||
```js
|
||||
events: {
|
||||
signin: async (message) => { /* on successful sign in */ },
|
||||
signout: async (message) => { /* on signout */ },
|
||||
signIn: async (message) => { /* on successful sign in */ },
|
||||
signOut: async (message) => { /* on signout */ },
|
||||
createUser: async (message) => { /* user created */ },
|
||||
linkAccount: async (message) => { /* account linked to a user */ },
|
||||
session: async (message) => { /* session is active */ },
|
||||
@@ -260,6 +267,25 @@ events: {
|
||||
|
||||
---
|
||||
|
||||
### adapter
|
||||
|
||||
* **Default value**: *Adapater.Default()*
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases. An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
|
||||
|
||||
You can use the `adapter` option to use the Prisma adapter - or pass in your own adapter if you want to use a database that is not supported by one of the built-in adapters.
|
||||
|
||||
See the [adapter documentation](/schemas/adapters) for more information.
|
||||
|
||||
:::note
|
||||
If the `adapter` option is specified it overrides the `database` option, only specify one or the other.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### debug
|
||||
|
||||
* **Default value**: `false`
|
||||
@@ -273,52 +299,7 @@ Set debug to `true` to enable debug messages for authentication and database ope
|
||||
|
||||
## Advanced Options
|
||||
|
||||
|
||||
:::warning
|
||||
Advanced options are passed the same way as basic options, but may have complex implications or side effects. You should try to avoid using advanced options unless you are very comfortable using them.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### basePath
|
||||
|
||||
* **Default value**: `/api/auth`
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
This option allows you to specify a different base path if you don't want to use `/api/auth` for some reason.
|
||||
|
||||
If you set this option you **must** also specify the same value in the `NEXTAUTH_BASE_PATH` environment variable in `next.config.js` so that the client knows how to contact the server:
|
||||
|
||||
```js title="next.config.js"
|
||||
module.exports = {
|
||||
env: {
|
||||
NEXTAUTH_BASE_PATH: '/api/my-custom-auth-route',
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
This is required because the NextAuth.js API route is a separate codepath to the NextAuth.js Client.
|
||||
|
||||
As long as you also specify this option in an environment variable, the client will be able to pick up any subsequent configuration from the server, but if you do not set in both it the NextAuth.js Client will not work.
|
||||
|
||||
---
|
||||
|
||||
### adapter
|
||||
|
||||
* **Default value**: *Adapater.Default()*
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
A custom provider is an advanced option intended for use only you need to use NextAuth.js with a database configuration that is not supported by the default `database` adapter.
|
||||
|
||||
See the [adapter documentation](/schemas/adapters) for more information.
|
||||
|
||||
:::note
|
||||
If the `adapter` option is specified it overrides the `database` option.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
@@ -340,7 +321,7 @@ Properties on any custom `cookies` that are specified override this option.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Setting this option to *false* in production is a security risk and may allow sessions to hijacked.
|
||||
Setting this option to *false* in production is a security risk and may allow sessions to hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
|
||||
:::
|
||||
|
||||
---
|
||||
@@ -356,9 +337,15 @@ You can override the default cookie names and options for any of the cookies use
|
||||
|
||||
This is an advanced option and using it is not recommended as you may break authentication or introduce security flaws into your application.
|
||||
|
||||
You can specify one or more cookies with custom properties, but if you specify custom options for a cookie you must provided all the options for it. You will also likely want to create conditional behaviour to support local development (e.g. setting `secure: false` and not using cookie prefixes on localhost URLs).
|
||||
You can specify one or more cookies with custom properties, but if you specify custom options for a cookie you must provided all the options for that cookie.
|
||||
|
||||
**For example:**
|
||||
If you use this feature, you will likely want to create conditional behaviour to support setting different cookies policies in development and production builds, as you will be opting out of the built-in dynamic policy.
|
||||
|
||||
:::tip
|
||||
An example of a use case for this option is to support sharing session tokens across subdomains.
|
||||
:::
|
||||
|
||||
#### Example
|
||||
|
||||
```js
|
||||
cookies: {
|
||||
@@ -379,15 +366,6 @@ cookies: {
|
||||
secure: true
|
||||
}
|
||||
},
|
||||
baseUrl: {
|
||||
name: `__Secure-next-auth.base-url`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: true
|
||||
}
|
||||
},
|
||||
csrfToken: {
|
||||
name: `__Host-next-auth.csrf-token`,
|
||||
options: {
|
||||
@@ -401,40 +379,5 @@ cookies: {
|
||||
```
|
||||
|
||||
:::warning
|
||||
Changing the cookie options may introduce security flaws into your application and may break NextAuth.js integration now or in a future update. Using this option is not recommended.
|
||||
Using a custom cookie policy may introduce security flaws into your application and is intended as an option for advanced users who understand the implications. Using this option is not recommended.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### Client Max Age
|
||||
|
||||
* **Default value**: `0`
|
||||
* **Required**: *No*
|
||||
|
||||
#### Description
|
||||
|
||||
By default the NextAuth.js client will use whatever cached session object it has and will not not re-check the current session if using the `useSession()` hook.
|
||||
|
||||
You can change this behaviour and force it to periodically sync the session state by setting a `NEXTAUTH_CLIENT_MAXAGE` environment variable.
|
||||
|
||||
```js title="next.config.js"
|
||||
module.exports = {
|
||||
env: {
|
||||
NEXTAUTH_CLIENT_MAXAGE: 60, // Will re-check session every 60 seconds
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If set to `0` (the default) sessions are not re-checked automatically, only when a new window or tab is opened or when `getSession()` is called.
|
||||
|
||||
If set to any other value, specifies how many seconds the window or tab should poll the server to update the session data.
|
||||
|
||||
When a session is checked this way (or using `getSession()`) it is active and extends the life of the current session.
|
||||
|
||||
It can be useful to use this option to prevent sessions from timing out if your application has a short session expiry time.
|
||||
|
||||
This option usually has cost implications as checking session status triggers a call to a server side route and/or a database.
|
||||
|
||||
:::note
|
||||
In NextAuth.js session state is automatically synchronized across all open windows and tabs in the same browser. If you have session expiry times of 30 days or more (the default) you probably don't need to use this option, or can set it to a high value (e.g. every 24 hours).
|
||||
:::
|
||||
@@ -1,89 +1,66 @@
|
||||
---
|
||||
id: pages
|
||||
title: Custom Pages
|
||||
title: Pages
|
||||
---
|
||||
|
||||
NextAuth.js automatically creates simple, unbranded authentication pages for handling Sign in, Sign out, Email Verification and displaying error messages.
|
||||
|
||||
The options displayed on the sign up page are automatically generated based on the providers specified in the options passed to NextAuth.js.
|
||||
|
||||
## Using custom pages
|
||||
To add a custom login page, for example. You can use the `pages` option:
|
||||
|
||||
To add a custom login page, for example. You can us the `pages` option:
|
||||
|
||||
```javascript title="/pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
pages: {
|
||||
signin: '/auth/signin',
|
||||
signout: '/auth/signout',
|
||||
signIn: '/auth/signin',
|
||||
signOut: '/auth/signout',
|
||||
error: '/auth/error', // Error code passed in query string as ?error=
|
||||
verifyRequest: '/auth/verify-request', // (used for check email message)
|
||||
newUser: null // If set, new users will be directed here on first sign in
|
||||
}
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
## Sign in
|
||||
## Examples
|
||||
|
||||
### OAuth sign in page
|
||||
### OAuth Sign in
|
||||
|
||||
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
|
||||
|
||||
```jsx title="/pages/auth/signin"
|
||||
```jsx title="pages/auth/signin"
|
||||
import React from 'react'
|
||||
import { providers, signin } from 'next-auth/client'
|
||||
import { providers, signIn } from 'next-auth/client'
|
||||
|
||||
export default ({ providers }) => {
|
||||
return (
|
||||
<>
|
||||
{Object.values(providers).map(provider => (
|
||||
<p key={provider.name}>
|
||||
<a href={provider.signinUrl} onClick={(e) => e.preventDefault()}>
|
||||
<button onClick={() => signin(provider.id)}>Sign in with {provider.name}</button>
|
||||
</a>
|
||||
</p>
|
||||
<div key={provider.name}>
|
||||
<button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
export async function getInitalProps(context) {
|
||||
return {
|
||||
props: {
|
||||
providers: await providers(context)
|
||||
}
|
||||
providers: await providers(context)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
The **signin()** method automatically sets the callback URL to the current page. Using a link as a fallback means it sign in can work even without client side JavaScript.
|
||||
:::
|
||||
|
||||
### Email sign in page
|
||||
### Email Sign in
|
||||
|
||||
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
|
||||
|
||||
This is easier of if you use the build in `signin()` function, as it sets the CSRF automatically.
|
||||
|
||||
:::tip
|
||||
To create a sign in page that works on clients with and without client side JavaScript, you can use both the **signin()** method and the **csrfToken()** method
|
||||
:::
|
||||
|
||||
```jsx title="/pages/auth/email-signin"
|
||||
```jsx title="pages/auth/email-signin"
|
||||
import React from 'react'
|
||||
import { csrfToken, signin } from 'next-auth/client'
|
||||
import { csrfToken } from 'next-auth/client'
|
||||
|
||||
export default ({ csrfToken }) => {
|
||||
return (
|
||||
<form
|
||||
method='post'
|
||||
action='/api/auth/signin/email'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
signin('email', { email: document.getElementById('email').value })
|
||||
}}
|
||||
>
|
||||
<form method='post' action='/api/auth/signin/email'>
|
||||
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
|
||||
<label>
|
||||
Email address
|
||||
@@ -94,11 +71,53 @@ export default ({ csrfToken }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
export async function getInitalProps(context) {
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await csrfToken(context)
|
||||
}
|
||||
csrfToken: await csrfToken(context)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
|
||||
|
||||
```js
|
||||
signIn('email', { email: 'jsmith@example.com' })
|
||||
```
|
||||
|
||||
### Credentials Sign in
|
||||
|
||||
If you create a sign in form for credentials based authenticaiton, you will needt to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
|
||||
|
||||
```jsx title="pages/auth/credentials-signin"
|
||||
import React from 'react'
|
||||
import { csrfToken } from 'next-auth/client'
|
||||
|
||||
export default ({ csrfToken }) => {
|
||||
return (
|
||||
<form method='post' action='/api/auth/callback/credentials'>
|
||||
<input name='csrfToken' type='hidden' defaultValue={csrfToken}/>
|
||||
<label>
|
||||
Username
|
||||
<input name='username' type='text'/>
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input name='password' type='text'/>
|
||||
</label>
|
||||
<button type='submit'>Sign in</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getInitalProps(context) {
|
||||
return {
|
||||
csrfToken: await csrfToken(context)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also use the `signIn()` function which will handle obtaining the CSRF token for you:
|
||||
|
||||
```js
|
||||
signIn('credentials', { username: 'jsmith', password: '1234' })
|
||||
```
|
||||
@@ -1,19 +1,9 @@
|
||||
---
|
||||
id: providers
|
||||
title: Authentication Providers
|
||||
title: Providers
|
||||
---
|
||||
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
Authentication Providers in NextAuth.js are how you define services can be used to sign in.
|
||||
|
||||
NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0 and has built-in support for many popular OAuth sign-in services. It also supports email / passwordless authentication.
|
||||
|
||||
@@ -23,16 +13,20 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
||||
|
||||
* [Apple](/providers/apple)
|
||||
* [Auth0](/providers/auth0)
|
||||
* [Battle.net](/providers/battlenet)
|
||||
* [Box](/providers/box)
|
||||
* [Amazon Cognito](/providers/cognito)
|
||||
* [Discord](/providers/discord)
|
||||
* [Facebook](/providers/facebook)
|
||||
* [GitHub](/providers/github)
|
||||
* [GitLab](/providers/gitlab)
|
||||
* [Google](/providers/google)
|
||||
* [IdentityServer4](/providers/identity-server4)
|
||||
* [LinkedIn](/providers/LinkedIn)
|
||||
* [Mixer](/providers/Mixer)
|
||||
* [Okta](/providers/Okta)
|
||||
* [Slack](/providers/slack)
|
||||
* [Spotify](/providers/spotify)
|
||||
* [Twitch](/providers/Twitch)
|
||||
* [Twitter](/providers/twitter)
|
||||
* [Yandex](/providers/yandex)
|
||||
@@ -56,9 +50,9 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
||||
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
```
|
||||
|
||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
@@ -108,7 +102,7 @@ As an example of what this looks like, this is the the provider object returned
|
||||
```
|
||||
You can replace all the options in this JSON object with the ones from your custom provider – be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
@@ -127,15 +121,15 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
#### Options
|
||||
### OAuth provider options
|
||||
|
||||
| Name | Description | Required |
|
||||
| :--------------: | :-------------------------------------------------: | :------: |
|
||||
| id | An unique ID for your custom provider | Yes |
|
||||
| name | An unique name for your custom provider | Yes |
|
||||
| id | Unique ID for the provider | Yes |
|
||||
| name | Descriptive name for the provider | Yes |
|
||||
| type | Type of provider, in this case it should be `oauth` | Yes |
|
||||
| version | OAuth version. | Yes |
|
||||
| scope | OAuth access scopes | No |
|
||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | Yes |
|
||||
| scope | OAuth access scopes (expects array or string) | No |
|
||||
| params | Additional authorization URL parameters | No |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | Yes |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | No |
|
||||
@@ -145,6 +139,7 @@ providers: [
|
||||
| clientId | Client ID of the OAuth provider | Yes |
|
||||
| clientSecret | Client Secret of the OAuth provider | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | No |
|
||||
| state | Set to `false` for services that do not support `state` verfication | No |
|
||||
|
||||
:::note
|
||||
Feel free to open a PR for your custom configuration if you've created one for a provider that others may be interested in so we can add it to the list of built-in OAuth providers!
|
||||
@@ -158,10 +153,10 @@ Adding support for signing in via email in addition to one or more OAuth service
|
||||
|
||||
Configuration is similar to other providers, but the options are different:
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
// maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
|
||||
}),
|
||||
@@ -181,7 +176,7 @@ The Credentials provider allows you to handle signing in with arbitrary credenti
|
||||
|
||||
It is intended to support use cases where you have an existing system you need to authenticate users against.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
@@ -220,3 +215,16 @@ See the [Credentials provider documentation](/providers/credentials) for more in
|
||||
:::note
|
||||
The Credentials provider can only be used if JSON Web Tokens are enabled for sessions. Users authenticated with the Credentials provider are not persisted in the database.
|
||||
:::
|
||||
|
||||
<!-- React Image Component -->
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,43 +3,26 @@ id: contributors
|
||||
title: Contributors
|
||||
---
|
||||
|
||||
NextAuth.js was initially created by <a href="https://github.com/iaincollins">Iain Collins</a> in 2016.
|
||||
|
||||
NextAuth.js 2.0 has been possible thanks to the work of many individual contributors.
|
||||
|
||||
Special thanks to Lori Karikari for creating most of the OAuth providers and to Nico Domino for creating this site.
|
||||
|
||||
### Collaborators (Core Team)
|
||||
## 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>
|
||||
|
||||
### Contributors
|
||||
* <a href="https://github.com/lluia">Lluis Agusti</a>
|
||||
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
|
||||
|
||||
*Displayed in alphabetical order (by username).*
|
||||
_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._
|
||||
|
||||
* <a href="https://github.com/ajaymathur">Ajay Narain Mathur</a>
|
||||
* <a href="https://github.com/aol-nnov">Andrey (aol-nnov)</a>
|
||||
* <a href="https://github.com/cross19xx">Kenneth Kwakye-Gyamfi</a>
|
||||
* <a href="https://github.com/c100k">Chafik (c100k)</a>
|
||||
* <a href="https://github.com/dmitriyK1">Dmitriy (dmitriyK1)</a>
|
||||
* <a href="https://github.com/drudv">Dmitry Druganov</a>
|
||||
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
|
||||
* <a href="https://github.com/jenssogaard">Jens Soegaard</a>
|
||||
* <a href="https://github.com/joshuar500">Josh (joshuar500)</a>
|
||||
* <a href="https://github.com/khuezy">khuezy</a>
|
||||
* <a href="https://github.com/langovoi">Mark Langovoi</a>
|
||||
* <a href="https://github.com/lifehome">lifehome</a>
|
||||
* <a href="https://github.com/mebrein">Merijn (mebrein)</a>
|
||||
* <a href="https://github.com/NickBolles">Nick Bolles</a>
|
||||
* <a href="https://github.com/rahls7">Rahul (rahls7)</a>
|
||||
* <a href="https://github.com/rmcalvert">Ryan Calvert</a>
|
||||
* <a href="https://github.com/rxl881">Richard Lewis</a>
|
||||
* <a href="https://github.com/sponte">Stanislaw Wozniak</a>
|
||||
* <a href="https://github.com/9oelM">Joel M (9oelM)</a>
|
||||
## Other Contributors
|
||||
|
||||
*Last updated: 29 May 2020.*
|
||||
NextAuth.js as it exists today has been possible thanks to the work of many individual contributors.
|
||||
|
||||
Thank you to the [dozens of individual contributors](https://github.com/iaincollins/next-auth/graphs/contributors) who have help shaped NextAuth.js.
|
||||
|
||||
## History
|
||||
|
||||
NextAuth.js was originally developed by <a href="https://github.com/iaincollins">Iain Collins</a> in 2016.
|
||||
|
||||
In 2020, NextAuth.js was rebuilt from the ground up to support Serverless, with support for MySQL, Postgres and MongoDB, JSON Web Tokens and built in support for over a dozen authentication providers.
|
||||
|
||||
@@ -17,9 +17,11 @@ These errors are returned from the client. As the client is [Universal JavaScrip
|
||||
|
||||
#### CLIENT_USE_SESSION_ERROR
|
||||
|
||||
This error occurs when the `useSession()` React Hook has a problem fetching session data.
|
||||
|
||||
#### CLIENT_FETCH_ERROR
|
||||
|
||||
#### CLIENT_COOKIE_PARSE_ERROR
|
||||
If you see `CLIENT_FETCH_ERROR` make sure you have configured the `NEXTAUTH_URL` envionment variable.
|
||||
|
||||
---
|
||||
|
||||
|
||||
213
www/docs/faq.md
Normal file
213
www/docs/faq.md
Normal file
@@ -0,0 +1,213 @@
|
||||
---
|
||||
id: faq
|
||||
title: Frequently Asked Questions
|
||||
---
|
||||
|
||||
## About NextAuth.js
|
||||
|
||||
### Is NextAuth.js commercial software?
|
||||
|
||||
NextAuth.js is an open source project built by individual contributors.
|
||||
|
||||
It is not commercial software and is not associated with a commercial organization.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility
|
||||
|
||||
### What databases does NextAuth.js support?
|
||||
|
||||
You can use NextAuth.js with MySQL, MariaDB, Postgres, MongoDB and SQLite or without a database.
|
||||
|
||||
You can use also NextAuth.js with any database using a custom database adapter, or by using a custom credentials authentication provider - e.g. to support signing in with a username and password stored in an existing database.
|
||||
|
||||
### What authentication services does NextAuth.js support?
|
||||
|
||||
NextAuth.js includes built-in support for signing in with Apple, Auth0, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Okta, Slack, Spotify, Twitch, Twitter and Yandex.
|
||||
|
||||
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
|
||||
|
||||
You can also use a custom based provider to support signing in with a username and password stored in an external database and/or using two factor authentication.
|
||||
|
||||
### Does NextAuth.js support signing in with a username and password?
|
||||
|
||||
NextAuth.js is designed to avoid the need to store passwords for user accounts.
|
||||
|
||||
If you have an existing database of usernames and passwords, you can use a custom credentials provider to allow signing in with a username and password stored in an existing database.
|
||||
|
||||
_If you use a custom credentials provider user accounts will not be persisted in a database by NextAuth.js (even if one is configured). The option to use JSON Web Tokens for session tokens (which allow sign in without using a session database) must be enabled to use a custom credentials provider._
|
||||
|
||||
### Can I use NextAuth.js with a website that does not use Next.js?
|
||||
|
||||
NextAuth.js is designed for use with Next.js and Serverless.
|
||||
|
||||
You can create a website that handles sign in with Next.js and then access those sessions on a website that does not use Next.js as long as the websites are on the same domain.
|
||||
|
||||
If they are on a different subdomain you may need to set a custom cookie policy. NextAuth.js does not supporting signing into sites on different domains using the same service.
|
||||
|
||||
### Can I use NextAuth.js with React Native?
|
||||
|
||||
NextAuth.js is designed to handle sign in a Next.js web application.
|
||||
|
||||
It is designed as secure, confidental OAuth2 client with server side authentication flow, which allows it to do things public clients (which store embedded secrets) and browser-only clients cannot do.
|
||||
|
||||
It is not intended to be used in native applications on desktop or mobile applications, which typically use public clients (e.g. with client / secrets embedded in the application).
|
||||
|
||||
---
|
||||
|
||||
## Databases
|
||||
|
||||
### What databases are supported by NextAuth.js?
|
||||
|
||||
NextAuth.js can be used with MySQL, Postgres, MongoDB, SQLite and compatible databases (e.g. MariaDB, Amazon Aurora, Amazon DocumentDB…) or with no database.
|
||||
|
||||
It also provides an Adapter API which allows you to connect it to any database.
|
||||
|
||||
### What does NextAuth.js use databases for?
|
||||
|
||||
Databases in NextAuth.js are used for persisting users, oauth accounts, email sign in tokens and sessions.
|
||||
|
||||
Specifying a database is optional if you don't need to persist user data or support email sign in. If you don't specify a database then JSON Web Tokens will be enabled for session storage and used to store session data.
|
||||
|
||||
If you are using a database with NextAuth.js, you can still explicitly enable JSON Web Tokens for sessions (instead of using database sessions).
|
||||
|
||||
### Should I use a database?
|
||||
|
||||
* Using NextAuth.js without a database works well for internal tools - where you need to control who is able to sign in, but when you do not need to create user accounts for them in your application.
|
||||
|
||||
* Using NextAuth.js with a database is usually a better approach for a consumer facing application where you need to persist accounts (e.g. for billing, to contact customers, etc).
|
||||
|
||||
### What database should I use?
|
||||
|
||||
Managed database solutions for MySQL, Postgres and MongoDB (and compatible databases) are well supported from cloud providers such as Amazon, Google, Microsoft and Atlas.
|
||||
|
||||
If you are deploying directly to a particular cloud platform you may also want to consider serverless database offerings they have (e.g. [Amazon Aurora Serverless on AWS](https://aws.amazon.com/rds/aurora/serverless/)).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
### I think I've found a security problem, what should I do?
|
||||
|
||||
Less serious or edge case issues (e.g. queries about compatibility with optional RFC specifications) can be raised as public issues on GitHub.
|
||||
|
||||
If you discover what you think may be a potentially serious security problem, please contact a core team member via a private channel (e.g. via email to me@iaincollins.com) or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details.
|
||||
|
||||
### What is the disclosure policy for NextAuth.js?
|
||||
|
||||
We practice responsible disclosure.
|
||||
|
||||
If you contact us regarding a potentially serious issue, we will endeavor to get back to you within 72 hours and to publish a fix within 30 days. We will responsibly disclose the issue (and credit you with your consent) once a fix to resolve the issue has been released - or after 90 days, which ever is sooner.
|
||||
|
||||
### How do I get Refresh Tokens and Access Tokens for an OAuth account?
|
||||
|
||||
_This is not currently supported, but is something we would like to have support for in future._
|
||||
|
||||
NextAuth.js provides a solution for authentication, session management and user account creation.
|
||||
|
||||
NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by the provider) and it will save them (along with the User ID, Provider and Provider Account ID) to either:
|
||||
|
||||
1. A database - if a database connection string is provided
|
||||
2. A JSON Web Token - if JWT sessions are enabled (e.g. if no database specified)
|
||||
|
||||
However, NextAuth.js does not also handle Access Token rotation for you. If this is something you need, currently you will need to write the logic to handle that yourself.
|
||||
|
||||
---
|
||||
|
||||
## Feature Requests
|
||||
|
||||
### Why doesn't NextAuth.js support [a particular feature]?
|
||||
|
||||
NextAuth.js is an open source project built by individual contributors who are volunteers who writing code and providing support to help folks out in their spare time.
|
||||
|
||||
If you would like NextAuth.js to support a particular feature the best way to help make that happen is to raise a feature request describing the feature and to offer to work with other contributors to develop and test it.
|
||||
|
||||
If you are not able to develop a feature yourself, you can offer sponsor someone to work on it.
|
||||
|
||||
### I disagree with a design decision, how can I change your mind?
|
||||
|
||||
Product design decisions on NextAuth.js are made by core team members.
|
||||
|
||||
You can raise suggestions as feature requests / requests for enhancement.
|
||||
|
||||
Requests that provide the detail requested in the template and follow the format requested may be more likely to be supported, as additional detail prompted in the templates often provides important context.
|
||||
|
||||
Ultimately if your request is not accepted or is not actively in development, you are always free to fork the project under the terms of the ISC License.
|
||||
|
||||
---
|
||||
|
||||
## JSON Web Tokens
|
||||
|
||||
### Does NextAuth.js use JSON Web Tokens?
|
||||
|
||||
NextAuth.js supports both database session tokens and JWT session tokens.
|
||||
|
||||
* If a database is specified, database session tokens will be used by default.
|
||||
* If no database is specified, JWT session tokens will be used by default.
|
||||
|
||||
You can also choose to use JSON Web Tokens as session tokens with using a database, by explictly setting the `session: { jwt: true }` option.
|
||||
|
||||
### What are the advantages of JSON Web Tokens?
|
||||
|
||||
JSON Web Tokens can be used for session tokens, but are also used for lots of other things, such as sending signed objects between services in authentication flows.
|
||||
|
||||
* Advantages of using a JWT as a session token include that they do not require a database to store sessions, this can be faster and cheaper to run and easier to scale.
|
||||
|
||||
* JSON Web Tokens in NextAuth.js are secured using cryptographic signing (JWS) by default and it is easy for services and API endpoints to verify tokens without having to contact a database to verify them.
|
||||
|
||||
* You can enable encryption (JWE) to store include information directly in a JWT session token that you wish to keep secret and use the token to pass information between services / APIs on the same domain.
|
||||
|
||||
* You can use JWT to securely store information you do not mind the client knowing even without encryption, as the JWT is stored in an server-readable-only-token so data in the JWT is not accessible to third party JavaScript running on your site.
|
||||
|
||||
### What are the disadvantages of JSON Web Tokens?
|
||||
|
||||
* You cannot as easily expire a JSON Web Token - doing so requires maintaining a server side blocklist of invalid tokens (at least until they expire) and checking every token against the list every time a token is presented.
|
||||
|
||||
Shorter session expiry times are used when using JSON Web Tokens as session tokens to allow sessions to be invalidated sooner and simplify this problem.
|
||||
|
||||
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expirying if tehre is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
|
||||
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
|
||||
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
|
||||
|
||||
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
|
||||
|
||||
Even if appropriately configured, information stored in an encrypted JWT should not be assumed to be impossible to decrypt at some point - e.g. due to the discovery of a defect or advances in technology.
|
||||
|
||||
Avoid storing any data in a token that might be problematic if it were to be decrypted in the future.
|
||||
|
||||
* If you do not explictly specify a secret for for NextAuth.js, existing sessions will be invalidated any time your NextAuth.js configuration changes, as NextAuth.js will default to an auto-generated secret.
|
||||
|
||||
If using JSON Web Token you should at least specify a secret and ideally configure public/private keys.
|
||||
|
||||
### Are JSON Web Tokens secure?
|
||||
|
||||
By default tokens are signed (JWS) but not encrypted (JWE), as encryption adds additional overhead and reduces the amount of space avalible to store data (total cookie size for a domain is limited to 4KB).
|
||||
|
||||
* JSON Web Tokens in NextAuth.js use JWS and are signed using HS512 with an auto-generated key.
|
||||
|
||||
* If encryption is enabled by setting `jwt: { encrypt: true }` option then the JWT will _also_ use JWE to encrypt the token, using A256GCM with an auto-generated key.
|
||||
|
||||
You can specify other valid algorithms - [as specified in RFC 7518](https://tools.ietf.org/html/rfc7517) - with either a secret (for symmetric encryption) or a public/private key pair (for a symmetric encryption).
|
||||
|
||||
NextAuth.js will generate keys for you, but this will generate a warning at start up.
|
||||
|
||||
Using explict public/private keys for signing is strongly recommended.
|
||||
|
||||
### What signing and encryption standards does NextAuth.js support?
|
||||
|
||||
NextAuth.js includes a largely complete implementation of JSON Object Signing and Encryption (JOSE):
|
||||
|
||||
* [RFC 7515 - JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515)
|
||||
* [RFC 7516 - JSON Web Encryption (JWE)](https://tools.ietf.org/html/rfc7516)
|
||||
* [RFC 7517 - JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517)
|
||||
* [RFC 7518 - JSON Web Algorithms (JWA)](https://tools.ietf.org/html/rfc7518)
|
||||
* [RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)
|
||||
|
||||
This incorporates support for:
|
||||
|
||||
* [RFC 7638 - JSON Web Key Thumbprint](https://tools.ietf.org/html/rfc7638)
|
||||
* [RFC 7787 - JSON JWS Unencoded Payload Option](https://tools.ietf.org/html/rfc7797)
|
||||
* [RFC 8037 - CFRG Elliptic Curve ECDH and Signatures](https://tools.ietf.org/html/rfc8037)
|
||||
@@ -5,46 +5,7 @@ title: Client API
|
||||
|
||||
The NextAuth.js client library makes it easy to interact with sessions from React applications.
|
||||
|
||||
Some of the methods can be called both client side and server side.
|
||||
|
||||
:::note
|
||||
When using any of the client API methods server side, [context](https://nextjs.org/docs/api-reference/data-fetching/getInitialProps#context-object) must be passed as an argument. The documentation for **getSession()** has an example.
|
||||
:::
|
||||
|
||||
## useSession()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
|
||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||
|
||||
It works best when used with NextAuth.js `<Provider>` is added to `pages/_app.js` (see [provider](#provider)).
|
||||
|
||||
```jsx
|
||||
import { useSession } from 'next-auth/client'
|
||||
|
||||
export default () => {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return <>
|
||||
{session && <p>Signed in as {session.user.email}</p>}
|
||||
{!session && <p><a href="/api/auth/signin">Sign in</p>}
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## getSession()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
|
||||
NextAuth.js also provides a `getSession()` method which can be called client or server side to return a session.
|
||||
|
||||
It calls `/api/auth/session` and returns a promise with a session object, or null if no session exists.
|
||||
|
||||
A session object looks like this:
|
||||
#### Example Session Object
|
||||
|
||||
```js
|
||||
{
|
||||
@@ -58,69 +19,108 @@ A session object looks like this:
|
||||
}
|
||||
```
|
||||
|
||||
You can call `getSession()` inside a function to check if a user is signed in, or use it for server side rendered pages that supporting signing in without requiring client side JavaScript.
|
||||
:::tip
|
||||
The session data returned to the client does not contain sensitive information such as the Session Token or OAuth tokens. It contains a minimal payload that includes enough data needed to display information on a page about the user who is signed in for presentation purposes (e.g name, email, image).
|
||||
|
||||
:::info
|
||||
Note that because it exposed to the client it does not contain sensitive information such as the Session Token or OAuth service related tokens. It includes enough information (e.g name, email) to display information on a page about the user who is signed in, and an Access Token that can be used to identify the session without exposing the Session Token itself.
|
||||
You can use the [session callback](/configuration/callbacks#session) to customize the session object returned to the client if you need to return additional data in the session object.
|
||||
:::
|
||||
|
||||
Because it is a Universal method, you can use `getSession()` in both client and server side functions, such as `getInitialProps()` in Next.js:
|
||||
---
|
||||
|
||||
```jsx title="/pages/index.js"
|
||||
import { getSession } from 'next-auth/client'
|
||||
## useSession()
|
||||
|
||||
const Page = ({ session }) => (<p>
|
||||
{!session && <>
|
||||
Not signed in <br/>
|
||||
<a href="/api/auth/signin">Sign in</a>
|
||||
</>}
|
||||
{session && <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<a href="/api/auth/signout">Sign out</a>
|
||||
</>}
|
||||
</p>)
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
|
||||
Page.getInitialProps = async (context) => {
|
||||
return {
|
||||
session: await getSession(context)
|
||||
}
|
||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||
|
||||
It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { useSession } from 'next-auth/client'
|
||||
|
||||
export default () => {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return <>
|
||||
{session && <p>Signed in as {session.user.email}</p>}
|
||||
{!session && <p><a href="/api/auth/signin">Sign in</a></p>}
|
||||
</>
|
||||
}
|
||||
|
||||
export default Page
|
||||
```
|
||||
|
||||
#### Using getSession() in API routes
|
||||
---
|
||||
|
||||
You can also get the session object in Next.js API routes:
|
||||
## getSession()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
|
||||
NextAuth.js provides a `getSession()` method which can be called client or server side to return a session.
|
||||
|
||||
It calls `/api/auth/session` and returns a promise with a session object, or null if no session exists.
|
||||
|
||||
#### Client Side Example
|
||||
|
||||
```js
|
||||
async function myFunction() {
|
||||
const session = await getSession()
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### Server Side Example
|
||||
|
||||
```js
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default (req, res) => {
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
// Signed in
|
||||
const { accessToken } = session.user
|
||||
|
||||
// Do something with accessToken (e.g. look up user in DB)
|
||||
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.end(JSON.stringify({ /* data */ }))
|
||||
} else {
|
||||
// Not signed in
|
||||
res.status(302).setHeader('Location', pages.newUser)
|
||||
res.end()
|
||||
}
|
||||
/* ... */
|
||||
res.end()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
:::note
|
||||
When calling `getSession()` server side, you must pass the request object - e.g. `getSession({req})` - or you can the pass entire `context` object as it contains the `req` object.
|
||||
When calling `getSession()` server side, you need to pass `{req}` or `context` object.
|
||||
:::
|
||||
|
||||
The tutorial [securing pages and API routes](/tutorials/securing-pages-and-api-routes) shows how to use `getSession()` in server side calls.
|
||||
|
||||
---
|
||||
|
||||
## getCsrfToken()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
|
||||
The `getCsrfToken()` method returns the current Cross Site Request Forgery Token (CSRF Token) required to make POST requests (e.g. for signing in and signing out).
|
||||
|
||||
You likely only need to use this if you are not using the built-in `signIn()` and `signOut()` methods.
|
||||
|
||||
#### Client Side Example
|
||||
|
||||
```js
|
||||
async function myFunction() {
|
||||
const csrfToken = await getCsrfToken()
|
||||
/* ... */
|
||||
}
|
||||
```
|
||||
|
||||
#### Server Side Example
|
||||
|
||||
```js
|
||||
import { getCsrfToken } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const csrfToken = await getCsrfToken({ req })
|
||||
/* ... */
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## getProviders()
|
||||
@@ -130,49 +130,52 @@ When calling `getSession()` server side, you must pass the request object - e.g.
|
||||
|
||||
The `getProviders()` method returns the list of providers currently configured for sign in.
|
||||
|
||||
It calls `/api/auth/providers` and returns a with a list of the currently configured authentication providers.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## getCsrfToken()
|
||||
#### API Route
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: **Yes**
|
||||
```jsx title="pages/api/example.js"
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
The `getCsrfToken()` method returns the current Cross Site Request Forgery (CSRF Token) required to make POST requests (e.g. for signing in and signing out). It calls `/api/auth/csrf`.
|
||||
|
||||
You likely only need to use this if you are not using the built-in `signin()` and `signout()` methods.
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
console.log('Session', session)
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## signin()
|
||||
## signIn()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
|
||||
Using the `signin()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF tokens for you automatically when signing in with email.
|
||||
Using the `signIn()` method ensures the user ends back on the page they started on after completing a sign in flow. It will also handle CSRF Tokens for you automatically when signing in with email.
|
||||
|
||||
The `signin()` method can be called from the client in different ways, as shown below.
|
||||
The `signIn()` method can be called from the client in different ways, as shown below.
|
||||
|
||||
#### Redirects to sign in page when clicked
|
||||
|
||||
```js
|
||||
import { signin } from 'next-auth/client'
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default () => (
|
||||
<button onClick={signin}>Sign in</button>
|
||||
<button onClick={signIn}>Sign in</button>
|
||||
)
|
||||
```
|
||||
|
||||
#### Starts Google OAuth sign-in flow when clicked
|
||||
|
||||
```js
|
||||
import { signin } from 'next-auth/client'
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default () => (
|
||||
<button onClick={() => signin('google')}>Sign in with Google</button>
|
||||
<button onClick={() => signIn('google')}>Sign in with Google</button>
|
||||
)
|
||||
```
|
||||
|
||||
@@ -181,57 +184,53 @@ export default () => (
|
||||
When using it with the email flow, pass the target `email` as an option.
|
||||
|
||||
```js
|
||||
import { signin } from 'next-auth/client'
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default ({ email }) => (
|
||||
<button onClick={() => signin('email', { email })}>Sign in with Email</button>
|
||||
<button onClick={() => signIn('email', { email })}>Sign in with Email</button>
|
||||
)
|
||||
```
|
||||
|
||||
#### Specifying a callbackUrl
|
||||
|
||||
By default, the URL of page the client is on when they sign in is used as the `callbackUrl` and that is the URL the client will be redirected to after signing in.
|
||||
The `callbackUrl` specifies to which URL the user will be redirected after signing in. It defaults to the current URL of a user.
|
||||
|
||||
You can specify a different URL as the `callbackUrl` parameter by passing it in the second argument to `signin()`. This works for all calls to `signin()`.
|
||||
You can specify a different `callbackUrl` by specifying it as the second argument of `signIn()`. This works for all providers.
|
||||
|
||||
e.g.
|
||||
|
||||
* `signin(null, { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signin('google', { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signin('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn(null, { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn('google', { callbackUrl: 'http://localhost:3000/foo' })`
|
||||
* `signIn('email', { email, callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/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.
|
||||
|
||||
:::tip
|
||||
To also support signing in from clients that do not have client side JavaScript, use a regular link, add an onClick handler to it and call **e.preventDefault()** before calling the **signin()** method.
|
||||
:::
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## signout()
|
||||
## signOut()
|
||||
|
||||
* Client Side: **Yes**
|
||||
* Server Side: No
|
||||
|
||||
Using the `signout()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
|
||||
Using the `signOut()` method ensures the user ends back on the page they started on after completing the sign out flow. It also handles CSRF tokens for you automatically.
|
||||
|
||||
It reloads the page in the browser when complete.
|
||||
|
||||
```js
|
||||
import { signout } from 'next-auth/client'
|
||||
import { signOut } from 'next-auth/client'
|
||||
|
||||
export default () => (
|
||||
<button onClick={signout}>Sign out</button>
|
||||
<button onClick={signOut}>Sign out</button>
|
||||
)
|
||||
```
|
||||
|
||||
#### Specifying a callbackUrl
|
||||
|
||||
As with the `signin()` function, you can specify a `callbackUrl` parameter by passing it as an option.
|
||||
As with the `signIn()` function, you can specify a `callbackUrl` parameter by passing it as an option.
|
||||
|
||||
e.g. `signout{ callbackUrl: 'http://localhost:3000/foo' })`
|
||||
e.g. `signOut({ callbackUrl: 'http://localhost:3000/foo' })`
|
||||
|
||||
The URL must be considered valid by the [redirect callback handler](http://localhost:3000/configuration/callbacks#redirect). By default this means it must be an absolute URL at the same hostname (or else it will default to the homepage); you can define your own custom redirect callback to allow other URLs, including supporting relative URLs.
|
||||
The URL must be considered valid by the [redirect callback handler](/configuration/callbacks#redirect). 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -239,17 +238,14 @@ The URL must be considered valid by the [redirect callback handler](http://local
|
||||
|
||||
Using the supplied React `<Provider>` allows instances of `useSession()` to share the session object across components, by using [React Context](https://reactjs.org/docs/context.html) under the hood.
|
||||
|
||||
This improves performance, reduces network calls and avoids page flicker when rendering.
|
||||
This improves performance, reduces network calls and avoids page flicker when rendering. It is highly recommended and can be easily added to all pages in Next.js apps by using `pages/_app.js`.
|
||||
|
||||
It is highly recommended and can be easily added to all pages in Next.js apps by using `/pages/_app.js`.
|
||||
|
||||
```jsx title="/pages/_app.js"
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default ({ Component, pageProps }) => {
|
||||
const { session } = pageProps
|
||||
export default function App ({ Component, pageProps }) => {
|
||||
return (
|
||||
<Provider session={session}>
|
||||
<Provider session={pageProps.session}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
@@ -258,6 +254,61 @@ export default ({ Component, pageProps }) => {
|
||||
|
||||
If you pass the `session` page prop to the `<Provider>` – as in the example above – you can avoid checking the session twice on pages that support both server and client side rendering.
|
||||
|
||||
### Options
|
||||
|
||||
The session state is automatically synchronized across all open tabs/windows and they are all updated whenever they gain or lose focus or the state changes in any of them (e.g. a user signs in or out).
|
||||
|
||||
If you have session expiry times of 30 days (the default) or more then you probably don't need to change any of the default options in the Provider. If you need to, you can can trigger an update of the session object across all tabs/windows by calling `getSession()` from a client side function.
|
||||
|
||||
However, if you need to customise the session behaviour and/or are using short session expiry times, you can pass options to the provider to customise the behaviour of the `useSession()` hook.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default function App ({ Component, pageProps }) => {
|
||||
return (
|
||||
<Provider session={pageProps.session}
|
||||
options={{
|
||||
clientMaxAge: 60 // Re-fetch session if cache is older than 60 seconds
|
||||
keepAlive: 5 * 60 // Send keepAlive message every 5 minutes
|
||||
}}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
**These options have no effect on clients that are not signed in.**
|
||||
|
||||
Every tab/window maintains it's own copy of the local session state; the session it is not stored in shared storage like localStorage or sessionStorage. Any update in one tab/window triggers a message to other tabs/windows to update their own session state.
|
||||
|
||||
Using low values for `clientMaxAge` or `keepAlive` will increase network traffic and load on authenticated clients and may impact hosting costs and performance.
|
||||
:::
|
||||
|
||||
#### Client Max Age
|
||||
|
||||
The `clientMaxAge` option is the maximum age a session data can be on the client before it is considerd stale.
|
||||
|
||||
When `clientMaxAge` is set to `0` (the default) the cache will always be used when useSession is called and only explicit calls made to get the session status (i.e. `getSession()`) or event triggers, such as signing in or out in another tab/window, or a tab/window gaining or losing focus, will trigger an update of the session state.
|
||||
|
||||
If set to any value other than zero, it specifies in seconds the maxium age of session data on the client before the `useSession()` hook will call the server again to sync the session state.
|
||||
|
||||
Unless you have a short session expiry time (e.g. < 24 hours) you probably don't need to change this option. Setting this option to too short a value will increase load (and potentially hosting costs).
|
||||
|
||||
The value for `clientMaxAge` should always be lower than the value of the session `maxAge` option.
|
||||
|
||||
#### Keep Alive
|
||||
|
||||
The `keepAlive` option is how often the client should contact the server to avoid a session expirying.
|
||||
|
||||
When `keepAlive` is set to `0` (the default) it will not send a keep alive message.
|
||||
|
||||
If set to any value other than zero, it specifies in seconds how often the client should contact the server to update the session state. If the session state has expired when it is triggered, all open tabs/windows will be updated to reflect this.
|
||||
|
||||
The value for `keepAlive` should always be lower than the value of the session `maxAge` option.
|
||||
|
||||
:::note
|
||||
See [**the Next.js documentation**](https://nextjs.org/docs/advanced-features/custom-app) for more information on **_app.js** in Next.js applications.
|
||||
:::
|
||||
|
||||
@@ -3,33 +3,32 @@ id: example
|
||||
title: Example
|
||||
---
|
||||
|
||||
## Example with live demo
|
||||
### Check out the example project
|
||||
|
||||
The easiest way to get started is to clone the example Next.js application from the [next-auth-example](https://github.com/iaincollins/next-auth-example) repository and to the instructions in the [README](https://github.com/iaincollins/next-auth-example/blob/main/README.md).
|
||||
The easiest way to get started is to clone the [example application](https://github.com/iaincollins/next-auth-example) and follow the instructions in the [README](https://github.com/iaincollins/next-auth-example/blob/main/README.md).
|
||||
|
||||
You can find a live demo of the example project at [next-auth-example.now.sh](https://next-auth-example.now.sh)
|
||||
|
||||
## How to use NextAuth.js
|
||||
## Add to an existing project
|
||||
|
||||
*The examples below show how to add authentication with NextAuth.js to an existing Next.js project.*
|
||||
*The example code below shows how to add authentication to an existing Next.js project.*
|
||||
|
||||
### Add API route
|
||||
|
||||
To add NextAuth.js to a project, first create a file called `[...nextauth].js` in `pages/api/auth`.
|
||||
To add NextAuth.js to a project create a file called `[...nextauth].js` in `pages/api/auth`.
|
||||
|
||||
```javascript title="/pages/api/auth/[...nextauth].js"
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
const options = {
|
||||
site: process.env.SITE || 'http://localhost:3000',
|
||||
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
|
||||
// A database is optional, but required to persist accounts in a database
|
||||
@@ -49,28 +48,60 @@ See the [options documentation](/configuration/options) for how to configure pro
|
||||
|
||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||
|
||||
```jsx title="/pages/index.js"
|
||||
```jsx title="pages/index.js"
|
||||
import React from 'react'
|
||||
import { useSession } from 'next-auth/client'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
|
||||
export default () => {
|
||||
export default function Page() {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return <p>
|
||||
return <>
|
||||
{!session && <>
|
||||
Not signed in <br/>
|
||||
<a href="/api/auth/signin">Sign in</a>
|
||||
<button onClick={signIn}>Sign in</button>
|
||||
</>}
|
||||
{session && <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<a href="/api/auth/signout">Sign out</a>
|
||||
<button onClick={signOut}>Sign out</button>
|
||||
</>}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
```
|
||||
|
||||
*That's all the code you need to add authentication to a project!*
|
||||
***That's all the code you need to add authentication with NextAuth.js to a project!***
|
||||
|
||||
:::tip
|
||||
You can use the `useSession` hook from anywhere in your application (e.g. in a header component).
|
||||
:::
|
||||
|
||||
### Add to all pages
|
||||
|
||||
To allow session state to be shared between pages - which improves performance, reduces network traffic and avoids component state changes while rendering - you can use the NextAuth.js Provider in `pages/_app.js`.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default function App ({ Component, pageProps }) => {
|
||||
return (
|
||||
<Provider session={pageProps.session}>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
Check out the [client documentation](/getting-started/client) to see how you can improve the user experience and page performance by using the NextAuth.js client.
|
||||
:::
|
||||
|
||||
### Deploying
|
||||
|
||||
When deploying your site set the `NEXTAUTH_URL` environment variable to the canonical URL of the website.
|
||||
|
||||
```
|
||||
NEXTAUTH_URL=https://example.com
|
||||
```
|
||||
|
||||
:::tip
|
||||
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
|
||||
:::
|
||||
@@ -9,17 +9,16 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
|
||||
|
||||
It is designed from the ground up to support Next.js and Serverless.
|
||||
|
||||
[Follow the examples](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
|
||||
[Check out the example code](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
|
||||
|
||||
## Features
|
||||
|
||||
### Easy authentication
|
||||
### Flexible and easy to use
|
||||
|
||||
* Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
* Supports both JSON Web Tokens and database sessions
|
||||
* Built-in support for [many popular OAuth sign-in services](/configuration/providers)
|
||||
* Built-in support for [many popular sign-in services](/configuration/providers)
|
||||
* Supports email / passwordless authentication
|
||||
* Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
* Supports both JSON Web Tokens and database sessions
|
||||
* Designed for Serverless but runs anywhere (AWS Lambda, Docker, Heroku, etc…)
|
||||
|
||||
### Own your own data
|
||||
|
||||
@@ -27,31 +26,30 @@ NextAuth.js can be used with or without a database.
|
||||
|
||||
* An open source solution that allows you to keep control of your data
|
||||
* Supports Bring Your Own Database (BYOD) and can be used with any database
|
||||
* Built-in support for for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/database)
|
||||
* Built-in support for [MySQL, MariaDB, Postgres, MongoDB and SQLite](/configuration/databases)
|
||||
* Works great with databases from popular hosting providers
|
||||
* Can also be used *without a database* (e.g. OAuth + JWT)
|
||||
|
||||
*Note: Email sign in requires a database to store verification tokens - though if you are not too concered about when sign in emails expire, this can be an in-memory database like SQLite.*
|
||||
*Note: Email sign in requires a database to be configured to store single-use verification tokens.*
|
||||
|
||||
### Secure by default
|
||||
|
||||
Security focused features include CSRF protection, use of signed cookies, cookie prefixes, secure cookies, HTTP only, host only and secure only cookies, secure URL redirection handling, and promoting passwordless sign-in.
|
||||
|
||||
* Designed to be secure by default and promote best practice for safeguarding user data
|
||||
* Promotes the use of passwordless sign in mechanisms
|
||||
* Designed to be secure by default and encourage best practice for safeguarding user data
|
||||
* Uses Cross Site Request Forgery Tokens on POST routes (sign in, sign out)
|
||||
* Default cookie policy aims for the most restrictive policy appropriate for each cookie
|
||||
* JSON Web Tokens are signed and encrypted (HMAC+AES) and only accessible server side
|
||||
* When JSON Web Tokens are enabled, they are signed by default (JWS) with HS512
|
||||
* Use JWT encryption (JWE) by setting the option `encryption: true` (defaults to A256GCM)
|
||||
* Auto-generates symmetric signing and encryption keys for developer convenience
|
||||
* Features tab/window syncing and keepalive messages to support short lived sessions
|
||||
* Attempts to implement the latest guidance published by [Open Web Application Security Project](https://owasp.org/)
|
||||
|
||||
To keep your site secure while still making it easy to share data between the backend and front end securely [callback methods](/configuration/callbacks) are provided.
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
|
||||
The callbacks you send information to the client without having to handle session validation or JSON Web Token encryption /decryption yourself - just read and write JSON objects, and the rest is handled for you.
|
||||
## Credits
|
||||
|
||||
Advanced options allow you to define your own routines for signin and decoding JSON Web Tokens and to set custom cookie security policies and access controls, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
NextAuth.js is an open source project that is only possible [thanks to contributors](/contributors).
|
||||
|
||||
## Acknowledgement
|
||||
## Getting Started
|
||||
|
||||
[NextAuth.js 2.0 is possible thanks to its contributors.](/contributors)
|
||||
|
||||
## Getting started
|
||||
|
||||
[Follow the examples to get started.](/getting-started/example)
|
||||
[Check out the example code](/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
|
||||
|
||||
@@ -5,50 +5,58 @@ title: REST API
|
||||
|
||||
NextAuth.js exposes a REST API which is used by the NextAuth.js client.
|
||||
|
||||
:::note
|
||||
The default base path is `/api/auth` but it is configurable with the `basePath` option.
|
||||
:::
|
||||
|
||||
### GET /api/auth/signin
|
||||
#### GET /api/auth/signin
|
||||
|
||||
Displays the sign in page.
|
||||
|
||||
### GET /api/auth/signin/:provider
|
||||
#### POST /api/auth/signin/:provider
|
||||
|
||||
Starts an OAuth signin flow for the specified provider.
|
||||
|
||||
### POST /api/auth/signin/:provider
|
||||
|
||||
A POST submission is required for email sign in.
|
||||
|
||||
The POST submission requires CSRF token from `/api/auth/csrf`.
|
||||
|
||||
### GET /api/auth/callback/:provider
|
||||
#### GET /api/auth/callback/:provider
|
||||
|
||||
Handles retuning requests from OAuth services during sign in.
|
||||
|
||||
### GET /api/auth/signout
|
||||
For OAuth 2.0 providers that support the `state` option, the value of the `state` parameter is checked against the one that was generated when the sign in flow was started - this uses a hash of the CSRF token which MUST match for both the POST and GET calls during sign in.
|
||||
|
||||
#### GET /api/auth/signout
|
||||
|
||||
Displays the sign out page.
|
||||
|
||||
### POST /api/auth/signout
|
||||
#### POST /api/auth/signout
|
||||
|
||||
Handles signing out - this is a POST submission to prevent malicious links from triggering signing a user out without their consent.
|
||||
|
||||
The POST submission requires CSRF token from `/api/auth/csrf`.
|
||||
|
||||
### GET /api/auth/session
|
||||
#### GET /api/auth/session
|
||||
|
||||
Returns client-safe session object - or an empty object if there is no session.
|
||||
|
||||
### GET /api/auth/csrf
|
||||
The contents of the session object that is returned is configurable with the session callback.
|
||||
|
||||
#### GET /api/auth/csrf
|
||||
|
||||
Returns object containing CSRF token. In NextAuth.js, CSRF protection is present on all authentication routes. It uses the "double submit cookie method", which uses a signed HttpOnly, host-only cookie.
|
||||
|
||||
The CSRF token returned by this endpoint must be passed as form variable named `csrfToken` in all POST submissions to any API endpoint.
|
||||
|
||||
### GET /api/auth/providers
|
||||
#### GET /api/auth/providers
|
||||
|
||||
Returns a list of configured OAuth services and the configuration (e.g. sign in and callback URLs) for each service.
|
||||
Returns a list of configured OAuth services and details (e.g. sign in and callback URLs) for each service.
|
||||
|
||||
It can be used to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.
|
||||
It can be used to dynamically generate custom sign up pages and to check what callback URLs are configured for each OAuth provider that is configured.
|
||||
|
||||
---
|
||||
|
||||
:::note
|
||||
The default base path is `/api/auth` but it is configurable by specyfing a custom path in `NEXTAUTH_URL`
|
||||
|
||||
e.g.
|
||||
|
||||
`NEXTAUTH_URL=https://example.com/myapp/api/authentication`
|
||||
|
||||
`/api/auth/signin` -> `/myapp/api/authentication/signin`
|
||||
:::
|
||||
@@ -3,16 +3,15 @@ id: apple
|
||||
title: Apple
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developer.apple.com/sign-in-with-apple/get-started/
|
||||
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://developer.apple.com/account/resources/identifiers/list/serviceId
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
There are two ways you can use the Sign in with Apple provider.
|
||||
|
||||
@@ -39,16 +38,17 @@ providers: [
|
||||
|
||||
:::tip
|
||||
|
||||
Mac
|
||||
|
||||
Convert your apple key to a single line to use as a environment variable.
|
||||
```bash
|
||||
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
|
||||
```
|
||||
You can convert your Apple key to a single line to use it in a environment variable.
|
||||
|
||||
**Mac**
|
||||
|
||||
```bash
|
||||
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
|
||||
```
|
||||
|
||||
Windows
|
||||
**Windows**
|
||||
|
||||
```powershell
|
||||
```powershell
|
||||
$k8file = "AuthKey_ID.k8"
|
||||
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
|
||||
```
|
||||
@@ -127,7 +127,8 @@ openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||
```
|
||||
|
||||
:::tip
|
||||
On Windows?
|
||||
**Windows**
|
||||
|
||||
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
|
||||
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if it’s not already done.
|
||||
|
||||
@@ -170,7 +171,7 @@ app.prepare().then(() => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on https://localhost:3000')
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Example JWT code
|
||||
|
||||
@@ -3,15 +3,19 @@ id: auth0
|
||||
title: Auth0
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://auth0.com/docs/api/authentication#authorize-application
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://manage.auth0.com/dashboard
|
||||
|
||||
## Usage
|
||||
:::tip
|
||||
Configure your application in Auth0 as a 'Regular Web Application' (not a 'Single Page App').
|
||||
:::
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -20,8 +24,12 @@ providers: [
|
||||
Providers.Auth0({
|
||||
clientId: process.env.AUTH0_CLIENT_ID,
|
||||
clientSecret: process.env.AUTH0_CLIENT_SECRET,
|
||||
subdomain: process.env.AUTH0_SUBDOMAIN
|
||||
domain: process.env.AUTH0_DOMAIN
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
```
|
||||
|
||||
:::note
|
||||
`domain` should be the fully qualified domain – e.g. `dev-s6clz2lv.eu.auth0.com`
|
||||
:::
|
||||
27
www/docs/providers/battlenet.md
Normal file
27
www/docs/providers/battlenet.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
id: battle.net
|
||||
title: Battle.net
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://develop.battle.net/documentation/guides/using-oauth
|
||||
|
||||
## Configuration
|
||||
|
||||
https://develop.battle.net/access/clients
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.BattleNet({
|
||||
clientId: process.env.BATTLENET_CLIENT_ID,
|
||||
clientSecret: process.env.BATTLENET_CLIENT_SECRET,
|
||||
region: process.env.BATTLENET_REGION
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -3,15 +3,15 @@ id: box
|
||||
title: Box
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developer.box.com/reference/
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
35
www/docs/providers/cognito.md
Normal file
35
www/docs/providers/cognito.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
id: cognito
|
||||
title: Amazon Cognito
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html
|
||||
|
||||
## Configuration
|
||||
|
||||
https://console.aws.amazon.com/cognito/users/
|
||||
|
||||
You need to select your AWS region to go the the Cognito dashboard.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Cognito({
|
||||
clientId: process.env.COGNITO_CLIENT_ID,
|
||||
clientSecret: process.env.COGNITO_CLIENT_SECRET,
|
||||
domain: process.env.COGNITO_DOMAIN,
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::warning
|
||||
Make sure you select all the appropriate client settings or the OAuth flow will not work.
|
||||
:::
|
||||
|
||||

|
||||
@@ -3,39 +3,35 @@ id: credentials
|
||||
title: Credentials
|
||||
---
|
||||
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
|
||||
## Overview
|
||||
|
||||
The Credentials provider allows you to handle signing in with arbitrary credentials, such as a username and password, domain, or two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
|
||||
|
||||
It is intended to support use cases where you have an existing system you need to authenticate users against.
|
||||
|
||||
It comes with the constraint that users authenticated in this manner are not persisted in the database, and that the Credentials provider can only be used if JSON Web Tokens are enabled for sessions.
|
||||
It comes with the constraint that users authenticated in this manner are not persisted in the database, and consequently that the Credentials provider can only be used if JSON Web Tokens are enabled for sessions.
|
||||
|
||||
:::note
|
||||
The functionality provided for credentials based authentication is intentionally limited to discourage use of passwords due to the inherent security risks associated with them and the additional complexity associated with supporting usernames and passwords.
|
||||
:::
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials input and returns either a `user` object or `false`.
|
||||
The Credentials provider is specified like other providers, except that you need to define a handler for `authorize()` that accepts credentials submitted via HTTP POST as input and returns either:
|
||||
|
||||
If you return an object it will be persisted to the JSON Web Token and the user will be signed in.
|
||||
1. A `user` object, which indicates the credentials are valid.
|
||||
|
||||
If you return `false` or `null` then an error will be displayed advising the user to check their details.
|
||||
If you return an object it will be persisted to the JSON Web Token and the user will be signed in, unless a custom `signIn()` callback is configured that subsequently rejects it.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
2. Either `false` or `null`, which indicates failure.
|
||||
|
||||
If you return `false` or `null` then an error will be displayed advising the user to check their details.
|
||||
|
||||
3. `Promise.Rejected()` with an Error or a URL.
|
||||
|
||||
If you reject the promise with an Error, the user will be sent to the error page with the error message as a query parameter. If you reject the promise with a URL (a string), the user will be redirected to the URL.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
@@ -50,54 +46,36 @@ providers: [
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// that is false/null if the credentials are invalid.
|
||||
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
return null
|
||||
}
|
||||
// Add logic here to look up the user from the credentials supplied
|
||||
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
|
||||
if (user) {
|
||||
// Any user object returned here will be saved in the JSON Web Token
|
||||
// Any object returned will be saved in `user` property of the JWT
|
||||
return Promise.resolve(user)
|
||||
} else {
|
||||
// If you return null or false then the credentials will be rejected
|
||||
return Promise.resolve(null)
|
||||
// You can also Reject this callback with an Error or with a URL:
|
||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
To use your new credentials provider, you will need to create a form that posts back to `/api/auth/callback/credentials`.
|
||||
See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
|
||||
|
||||
All form parameters submitted will be passed as `credentials` to your `authorize` callback.
|
||||
## Multiple providers
|
||||
|
||||
```js title="/pages/signin"
|
||||
import React from 'react'
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<form method='post' action='/api/auth/callback/credentials'>
|
||||
<input name='email' type='text' defaultValue='' />
|
||||
<input name='password' type='password' defaultValue='' />
|
||||
<button type='submit'>Sign in</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
As the JSON Web Token is encrypted, you can safely store user credentials in it and revalidate them whenever an action is performed. See the [callbacks documentation](/configuration/callbacks) for more information on how to interact with the token.
|
||||
|
||||
## With multiple providers
|
||||
|
||||
### Usage
|
||||
### Example code
|
||||
|
||||
You can specify more than one credentials provider by specifying a unique `id` for each one.
|
||||
|
||||
You can also use them in conjuction with other provider options.
|
||||
|
||||
As with all providers, the order you specify them in, is the order they are displayed on the sign in page.
|
||||
As with all providers, the order you specify them is the order they are displayed on the sign in page.
|
||||
|
||||
```js
|
||||
providers: [
|
||||
@@ -105,7 +83,7 @@ As with all providers, the order you specify them in, is the order they are disp
|
||||
id: 'domain-login',
|
||||
name: "Domain Account",
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => { /* add function to get user */ }
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
},
|
||||
credentials: {
|
||||
@@ -118,7 +96,7 @@ As with all providers, the order you specify them in, is the order they are disp
|
||||
id: 'intranet-credentials',
|
||||
name: "Two Factor Auth",
|
||||
authorize: async (credentials) => {
|
||||
const user = (credentials) => { /* add function to get user */ }
|
||||
const user = { /* add function to get user */ }
|
||||
return Promise.resolve(user)
|
||||
},
|
||||
credentials: {
|
||||
@@ -130,6 +108,22 @@ As with all providers, the order you specify them in, is the order they are disp
|
||||
]
|
||||
```
|
||||
|
||||
### Sign in
|
||||
### Example UI
|
||||
|
||||
This example below shows a complex configuration is rendered by the built in sign in page.
|
||||
|
||||
You can also [use a custom sign in page](/configuration/pages#credentials-sign-in) if you want to provide a custom user experience.
|
||||
|
||||
<Image src="/img/signin-complex.png"/>
|
||||
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,15 +3,15 @@ id: discord
|
||||
title: Discord
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://discord.com/developers/docs/topics/oauth2
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://discord.com/developers/applications
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
@@ -15,7 +15,7 @@ The Email provider can be used in conjunction with (or instead of) one or more O
|
||||
|
||||
On initial sign in, a **Verification Token** is sent to the email address provided. By default this token is valid for 24 hours. If the Verification Token is used with that time (i.e. by clicking on the link in the email) an account is created for the user and they are signed in.
|
||||
|
||||
If someone provides the email address of an *existing account* when signin in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
|
||||
If someone provides the email address of an *existing account* when signing in, an email is sent and they are signed into the account associated with that email address when they follow the link in the email.
|
||||
|
||||
|
||||
:::tip
|
||||
@@ -39,7 +39,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
|
||||
|
||||
Now you can add the email provider like this:
|
||||
|
||||
```js {3} title="/pages/api/auth/[...nextauth].js"
|
||||
```js {3} title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
@@ -61,7 +61,7 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
|
||||
```
|
||||
Now you can add the provider settings to the NextAuth options object in the Email Provider.
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: {
|
||||
@@ -84,26 +84,40 @@ The Email Provider can be used with both JSON Web Tokens and database sessions,
|
||||
|
||||
You can fully customise the sign in email that is sent by passing a custom function as the `sendVerificationRequest` option to `Providers.Email()`.
|
||||
|
||||
The following example shows the complete source for the built-in `sendVerificationRequest()` method.
|
||||
e.g.
|
||||
|
||||
```js {3} title="pages/api/auth/[...nextauth].js"
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
sendVerificationRequest: ({ identifier: email, url, token, site, provider }) => { /* your function */ }
|
||||
})
|
||||
]
|
||||
```
|
||||
|
||||
The following code shows the complete source for the built-in `sendVerificationRequest()` method:
|
||||
|
||||
```js
|
||||
import nodemailer from 'nodemailer'
|
||||
const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, provider }) => {
|
||||
|
||||
const sendVerificationRequest = ({ identifier: email, url, token, baseUrl, provider }) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { server, from } = provider
|
||||
const siteName = site.replace(/^https?:\/\//, '')
|
||||
// Strip protocol from URL and use domain as site name
|
||||
const site = baseUrl.replace(/^https?:\/\//, '')
|
||||
|
||||
nodemailer
|
||||
.createTransport(server)
|
||||
.sendMail({
|
||||
to: emailAddress,
|
||||
to: email,
|
||||
from,
|
||||
subject: `Sign in to ${siteName}`,
|
||||
text: text({ url, siteName }),
|
||||
html: html({ url, siteName })
|
||||
subject: `Sign in to ${site}`,
|
||||
text: text({ url, site, email }),
|
||||
html: html({ url, site, email })
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
console.error('SEND_VERIFICATION_EMAIL_ERROR', emailAddress, error)
|
||||
logger.error('SEND_VERIFICATION_EMAIL_ERROR', email, error)
|
||||
return reject(new Error('SEND_VERIFICATION_EMAIL_ERROR', error))
|
||||
}
|
||||
return resolve()
|
||||
@@ -112,33 +126,61 @@ const sendVerificationRequest = ({ identifier: emailAddress, url, token, site, p
|
||||
}
|
||||
|
||||
// Email HTML body
|
||||
const html = ({ url, siteName }) => {
|
||||
const buttonBackgroundColor = '#444444'
|
||||
const html = ({ url, site, email }) => {
|
||||
// Insert invisible space into domains and email address to prevent both the
|
||||
// email address and the domain from being turned into a hyperlink by email
|
||||
// clients like Outlook and Apple mail, as this is confusing because it seems
|
||||
// like they are supposed to click on their email address to sign in.
|
||||
const escapedEmail = `${email.replace(/\./g, '​.')}`
|
||||
const escapedSite = `${site.replace(/\./g, '​.')}`
|
||||
|
||||
// Some simple styling options
|
||||
const backgroundColor = '#f9f9f9'
|
||||
const textColor = '#444444'
|
||||
const mainBackgroundColor = '#ffffff'
|
||||
const buttonBackgroundColor = '#346df1'
|
||||
const buttonBorderColor = '#346df1'
|
||||
const buttonTextColor = '#ffffff'
|
||||
|
||||
// Uses tables for layout and inline CSS due to email client limitations
|
||||
return `
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 8px 0; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: #888888;">
|
||||
${siteName}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 16px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 3px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 3px; padding: 12px 18px; border: 1px solid ${buttonBackgroundColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<body style="background: ${backgroundColor};">
|
||||
<table width="100%" border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 20px 0px; font-size: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
<strong>${escapedSite}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table width="100%" border="0" cellspacing="20" cellpadding="0" style="background: ${mainBackgroundColor}; max-width: 600px; margin: auto; border-radius: 10px;">
|
||||
<tr>
|
||||
<td align="center" style="padding: 10px 0px 0px 0px; font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
Sign in as <strong>${escapedEmail}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<table border="0" cellspacing="0" cellpadding="0">
|
||||
<tr>
|
||||
<td align="center" style="border-radius: 5px;" bgcolor="${buttonBackgroundColor}"><a href="${url}" target="_blank" style="font-size: 18px; font-family: Helvetica, Arial, sans-serif; color: ${buttonTextColor}; text-decoration: none; text-decoration: none;border-radius: 5px; padding: 10px 20px; border: 1px solid ${buttonBorderColor}; display: inline-block; font-weight: bold;">Sign in</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 0px 0px 10px 0px; font-size: 16px; line-height: 22px; font-family: Helvetica, Arial, sans-serif; color: ${textColor};">
|
||||
If you did not request this email you can safely ignore it.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
`
|
||||
}
|
||||
|
||||
// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)
|
||||
const text = ({ url, siteName }) => `Sign in to ${siteName}\n${url}\n\n`
|
||||
// Email text body – fallback for email clients that don't render HTML
|
||||
const text = ({ url, site }) => `Sign in to ${site}\n${url}\n\n`
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you want to generate email-client compatible HTML from React, check out https://mjml.io
|
||||
If you want to generate great looking email client compatible HTML with React, check out https://mjml.io
|
||||
:::
|
||||
|
||||
@@ -3,15 +3,15 @@ id: facebook
|
||||
title: Facebook
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://developers.facebook.com/apps/
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -27,4 +27,8 @@ providers: [
|
||||
|
||||
:::tip
|
||||
Production applications cannot use localhost URLs to sign in with Facebook. You need to use a dedicated development application in Facebook to use **localhost** callback URLs.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Email address may not be returned for accounts created on mobile.
|
||||
:::
|
||||
@@ -3,15 +3,15 @@ id: github
|
||||
title: GitHub
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://github.com/settings/apps
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -23,7 +23,12 @@ providers: [
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::warning
|
||||
Only allows one callback URL. May not return email address if privacy enabled.
|
||||
Only allows one callback URL per Client ID / Client Secret.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Email address is not returned if privacy settings are enabled.
|
||||
:::
|
||||
@@ -3,15 +3,15 @@ id: gitlab
|
||||
title: GitLab
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://docs.gitlab.com/ee/api/oauth2.html
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://gitlab.com/profile/applications
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -24,6 +24,7 @@ providers: [
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
Enable the *"read_user"* option in scope if you want to save the users email address on sign up.
|
||||
:::
|
||||
@@ -3,15 +3,15 @@ id: google
|
||||
title: Google
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developers.google.com/identity/protocols/oauth2
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://console.developers.google.com/apis/credentials
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -21,5 +21,14 @@ providers: [
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
]
|
||||
...
|
||||
```
|
||||
|
||||
:::warning
|
||||
Unlike most other providers, Google only Provide the Refresh Token on first sign in.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Google also return an `email_verified` boolean property in the OAuth profile.
|
||||
:::
|
||||
@@ -3,15 +3,12 @@ id: identity-server4
|
||||
title: IdentityServer4
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://identityserver4.readthedocs.io/en/latest/
|
||||
|
||||
|
||||
## App Configuration
|
||||
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -29,12 +26,13 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
## Demo IdentityServer
|
||||
|
||||
## Example using Demo IdentityServer
|
||||
https://demo.identityserver.io/
|
||||
The configuration below is for the demo server at https://demo.identityserver.io/
|
||||
|
||||
**The demo below is using a live server! You can copy and paste the values below and it will work out of the box.
|
||||
This intended as an example only. (use either <b>bob/bob</b>, <b>alice/alice</b>)
|
||||
If you want to try it out, you can copy and paste the configuration below.
|
||||
|
||||
You can sign in to the demo service with either <b>bob/bob</b> or <b>alice/alice</b>.
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
25
www/docs/providers/linkedin.md
Normal file
25
www/docs/providers/linkedin.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
id: linkedin
|
||||
title: LinkedIn
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://www.linkedin.com/developers/apps/
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.LinkedIn({
|
||||
clientId: process.env.LINKEDIN_CLIENT_ID,
|
||||
clientSecret: process.env.LINKEDIN_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
@@ -3,15 +3,15 @@ id: mixer
|
||||
title: Mixer
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://dev.mixer.com/reference/oauth
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://mixer.com/lab/oauth
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
@@ -3,15 +3,11 @@ id: okta
|
||||
title: Okta
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developer.okta.com/docs/reference/api/oidc
|
||||
|
||||
## App Configuration
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
@@ -3,15 +3,15 @@ id: slack
|
||||
title: Slack
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://api.slack.com
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://api.slack.com/apps
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
26
www/docs/providers/spotify.md
Normal file
26
www/docs/providers/spotify.md
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
id: spotify
|
||||
title: Spotify
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://developer.spotify.com/documentation
|
||||
|
||||
## Configuration
|
||||
|
||||
https://developer.spotify.com/dashboard/applications
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Spotify({
|
||||
clientId: process.env.SPOTIFY_CLIENT_ID,
|
||||
clientSecret: process.env.SPOTIFY_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
...
|
||||
```
|
||||
@@ -3,15 +3,15 @@ id: twitch
|
||||
title: Twitch
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://dev.twitch.tv/docs/authentication
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://dev.twitch.tv/console/apps
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
@@ -3,15 +3,15 @@ id: twitter
|
||||
title: Twitter
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://developer.twitter.com
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://developer.twitter.com/en/apps
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -26,7 +26,7 @@ providers: [
|
||||
```
|
||||
|
||||
:::tip
|
||||
Enable the *"Request email address from users"* option in your app permissions if you want to save the users email address on sign up.
|
||||
You must enable the *"Request email address from users"* option in your app permissions if you want to obtain the users email address.
|
||||
:::
|
||||
|
||||

|
||||
@@ -3,15 +3,15 @@ id: yandex
|
||||
title: Yandex
|
||||
---
|
||||
|
||||
## API Documentation
|
||||
## Documentation
|
||||
|
||||
https://tech.yandex.com/oauth/doc/dg/concepts/about-docpage/
|
||||
|
||||
## App Configuration
|
||||
## Configuration
|
||||
|
||||
https://oauth.yandex.com/client/new
|
||||
|
||||
## Usage
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
|
||||
@@ -3,23 +3,20 @@ id: adapters
|
||||
title: Database Adapters
|
||||
---
|
||||
|
||||
An "*Adapter*" in NextAuth.js is the thing that connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
An **Adapter** in NextAuth.js connects your application to whatever database or backend system you want to use to store data for user accounts, sessions, etc.
|
||||
|
||||
You do not need to specify an adapter explicltly unless you want to use advanced options such as custom models or schemas, or if you are creating a custom adapter to connect to a database that is not one of the supported datatabases.
|
||||
You do not need to specify an adapter explicitly unless you want to use advanced options such as custom models or schemas, if you want to use the Prisma adapter instead of the default TypeORM adapter, or if you are creating a custom adapter to connect to a database that is not one of the supported databases.
|
||||
|
||||
:::tip
|
||||
*The **adapter** option is currently considered advanced usage intended for use by NextAuth.js contributors.*
|
||||
:::
|
||||
### Database Schemas
|
||||
|
||||
## TypeORM (default adapter)
|
||||
Configure your database by creating the tables and columns to match the schema expected by NextAuth.js.
|
||||
|
||||
NextAuth.js comes with a default adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the database driver you want to use to your project and tell NextAuth.js to use it.
|
||||
* [MySQL Schema](/schemas/mysql)
|
||||
* [Postgres Schema](/schemas/postgres)
|
||||
|
||||
The default adapter comes with predefined models for **Users**, **Sessions**, **Account Linking** and **Verification Emails**. You can extend or replace the default models and schemas, or even provide your adapter to handle reading/writing from the database (or from multiple databases).
|
||||
## TypeORM Adapter
|
||||
|
||||
If you have an existing database / user management system or want to use a database that isn't supported out of the box you can create and pass your own adapter to handle actions like `createAccount`, `deleteAccount`, (etc) and do not have to use the built in models.
|
||||
|
||||
If you are using a database that is not supported out of the box, or if you want to use NextAuth.js with an existing database, or have a more complex setup with accounts and sessions spread across different systems then you can pass your own methods to be called for user and session creation / deletion (etc).
|
||||
NextAuth.js comes with a default adapter that uses [TypeORM](https://typeorm.io/) so that it can be used with many different databases without any further configuration, you simply add the node module for the database driver you want to use to your project and pass a database connection string to NextAuth.js.
|
||||
|
||||
The default adapter is the TypeORM adapter, the following configuration options are exactly equivalent.
|
||||
|
||||
@@ -47,178 +44,188 @@ adapter: Adapters.TypeORM.Adapter({
|
||||
})
|
||||
```
|
||||
|
||||
## Custom adapters
|
||||
The tutorial [Custom models with TypeORM](/tutorials/typeorm-custom-models) explains how to extend the built in models and schemas used by the TypeORM adapter. You can use these models in your own code.
|
||||
|
||||
Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
:::tip
|
||||
The `synchronize` option in TypeORM will generate SQL that exactly matches the documented schemas for MySQL and Postgres.
|
||||
|
||||
Creating a custom adapter is considerable undertaking and will require some trial and error and some reverse engineering as it is not currently well documented. The hope and expectation is to grow both the number of included (and third party) adapters over time.
|
||||
However, it should not be enabled against production databases as may cause dataloss if the configured schema does not match the expected schema!
|
||||
:::
|
||||
|
||||
An adapter in NextAuth.js is a function which returns an async `getAdapter()` method, which in turn returns a Promise with a list of functions used to handle operations such as creating user, linking a user and an OAuth account or handling reading and writing sessions.
|
||||
## Prisma Adapter
|
||||
|
||||
It uses this approach to allow database connection logic to live in the `getAdapter()` method. By calling the function just before an action needs to happen, it is possible to check database connection status and handle connecting / reconnecting to a database as required.
|
||||
You can also use NextAuth.js with [Prisma](https://www.prisma.io/docs/).
|
||||
|
||||
### Required methods
|
||||
To use this adapter, you need to install Prisma Client and Prisma CLI:
|
||||
|
||||
These methods are required for all sign in flows:
|
||||
```
|
||||
npm i @prisma/client
|
||||
npm add -D @prisma/cli
|
||||
```
|
||||
|
||||
* createUser
|
||||
* getUser
|
||||
* getUserByEmail
|
||||
* getUserByProviderAccountId
|
||||
* linkAccount
|
||||
* createSession
|
||||
* getSession
|
||||
* updateSession
|
||||
* deleteSession
|
||||
Configure your NextAuth.js to use the Prisma adapter:
|
||||
|
||||
These methods are required to support email / passwordless sign in:
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import Adapters from 'next-auth/adapters'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
* createVerificationRequest
|
||||
* getVerificationRequest
|
||||
* deleteVerificationRequest
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
### Unimplemented methods
|
||||
|
||||
These methods will be required in a future release, but are not yet invoked:
|
||||
|
||||
* getUserByCredentials
|
||||
* updateUser
|
||||
* deleteUser
|
||||
* unlinkAccount
|
||||
|
||||
### Example code
|
||||
|
||||
An example of adapter structure is shown below:
|
||||
|
||||
```js
|
||||
const Adapter = (config, options = {}) => {
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
|
||||
async function createUser (profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByCredentials (credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
getUserByCredentials,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
const options = {
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
],
|
||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
```
|
||||
|
||||
:::tip
|
||||
While Prisma includes an experimental feature in the migration command that is able to generate SQL from a schema, creating tables and columns using the provided SQL is currently recommended instead as SQL schemas automatically generated by Prisma may differ from the recommended schemas.
|
||||
:::
|
||||
|
||||
### Prisma Schema
|
||||
|
||||
Create a `schema.prisma` file similar to this one:
|
||||
|
||||
```json title="schema.prisma"
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./dev.db"
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @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")
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
Set the `datasource` option appropriately for your database:
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
|
||||
```json title="schema.prisma"
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
### Generate Client
|
||||
|
||||
Once you have saved your schema, you can run the Prisma CLI to generate the Prisma Client:
|
||||
|
||||
```
|
||||
npx @prisma/cli generate
|
||||
```
|
||||
|
||||
### Custom Models
|
||||
|
||||
You can add properties to the schema and map them to any database colum names you wish, but you should not change the base properties or types defined in the example schema.
|
||||
|
||||
The model names themselves can be changed with a configuration option, and the datasource can be changed to anything supported by Prisma.
|
||||
|
||||
You can use custom model names by using the `modelMapping` option (shown here with default values).
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
adapter: Adapters.Prisma.Adapter({
|
||||
prisma,
|
||||
modelMapping: {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verificationRequest'
|
||||
}
|
||||
})
|
||||
...
|
||||
```
|
||||
|
||||
:::tip
|
||||
If you experience issues with Prisma opening too many database connections opening in local development mode (e.g. due to Hot Module Reloading) you can use an approach like this when initalising the Prisma Client:
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
let prisma
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
if (!global.prisma) {
|
||||
global.prisma = new PrismaClient()
|
||||
}
|
||||
prisma = global.prisma
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
|
||||
## Custom Adapter
|
||||
|
||||
See the tutorial for [creating a database adapter](/tutorials/creating-a-database-adapter) for more information on how to create a custom adapter.
|
||||
|
||||
@@ -3,19 +3,18 @@ id: models
|
||||
title: Models
|
||||
---
|
||||
|
||||
## Overview
|
||||
Models in NextAuth.js are built for ANSI SQL but are polymorphic and are transformed to adapt to the database being used; there is some variance in specific data types (e.g. for datetime, text fields, etc) but they are functionally the same with as much parity in behaviour as possible.
|
||||
|
||||
This a description of the models and data structure used by NextAuth.js default database adapter.
|
||||
All table/collection names in the built in models are are plural, and all table names and column names use `snake_case` when used with an SQL database and `camelCase` when used with Document database.
|
||||
|
||||
You are free to define your own models and schemas if you want to use a custom database adapter.
|
||||
|
||||
In NextAuth.js all table/collection names are plural, and all table names and column names use snake_case when used with an SQL database and camelCase when used with Document database. Indexes are declared on properties where appropriate.
|
||||
|
||||
:::tip
|
||||
Models in NextAuth.js are built for ANSI SQL but are polymorphic and are transformed to adapt to the database being used; there is some variance in specific data types (e.g. for datetime, text fields, etc).
|
||||
:::note
|
||||
You can [extend the built in models](/tutorials/typeorm-custom-models) and even [create your own database adapter](/tutorials/creating-a-database-adapter) if you want to use NextAuth.js with a database that is not supported out of the box.
|
||||
:::
|
||||
|
||||
### User
|
||||
|
||||
---
|
||||
|
||||
## User
|
||||
|
||||
Table: `users`
|
||||
|
||||
@@ -31,7 +30,7 @@ If a user first signs in with OAuth then their email address is automatically po
|
||||
This provides a way to contact users and for users to maintain access to their account and sign in using email in the event they are unable to sign in with the OAuth provider in future (if email sign in is configured).
|
||||
:::
|
||||
|
||||
### Account
|
||||
## Account
|
||||
|
||||
Table: `accounts`
|
||||
|
||||
@@ -41,7 +40,7 @@ The Account model is for information about OAuth accounts associated with a User
|
||||
|
||||
A single User can have multiple Accounts, each Account can only have one User.
|
||||
|
||||
### Session
|
||||
## Session
|
||||
|
||||
Table: `sessions`
|
||||
|
||||
@@ -51,7 +50,7 @@ The Session model is used for database sessions. It is not used if JSON Web Toke
|
||||
|
||||
A single User can have multiple Sessions, each Session can only have one User.
|
||||
|
||||
### Verification Request
|
||||
## Verification Request
|
||||
|
||||
Table: `verification_requests`
|
||||
|
||||
@@ -61,29 +60,4 @@ The Verification Request model is used to store tokens for passwordless sign in
|
||||
|
||||
A single User can have multiple open Verification Requests (e.g. to sign in to different devices).
|
||||
|
||||
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
|
||||
|
||||
:::note
|
||||
See `src/adapters/typeorm/models` for the source for the current models and schemas.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Schemas
|
||||
|
||||
### MySQL
|
||||
|
||||
* See [MySQL schema documentation](/schemas/mysql) for details.
|
||||
|
||||
### Postgres
|
||||
|
||||
* See [Postgres schema documentation](/schemas/postgres) for details.
|
||||
|
||||
### MongoDB
|
||||
|
||||
MongoDB does not use schemas in the same way as most RDBMS databases, but the objects stored in MongoDB use similar datatypes to SQL, with some differences:
|
||||
|
||||
* ID fields are of type `ObjectID` rather than `int`
|
||||
* By convention all collection names and object properties are `camelCase` rather than `snake_case`
|
||||
* A sparse index is used on the User `email` property to allow it to not be specified, while enforcing uniqueness if it is - this ensures it is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property
|
||||
* All timestamps are stored as `ISODate()` in MongoDB, all timestamps on all models are stored in UTC (aka Zulu time)
|
||||
It has been designed to be extendable for other verification purposes in future (e.g. 2FA / short codes).
|
||||
22
www/docs/schemas/mongodb.md
Normal file
22
www/docs/schemas/mongodb.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
id: mongodb
|
||||
title: MongoDB
|
||||
---
|
||||
|
||||
MongoDB is a document database and does not use schemas in the same way as most RDBMS databases.
|
||||
|
||||
**In MongoDB as collections and indexes are created automatically.**
|
||||
|
||||
## Objects in MonogDB
|
||||
|
||||
Objects stored in MongoDB use similar datatypes to SQL, with some differences:
|
||||
|
||||
1. ID fields are of type `ObjectID` rather than type `int`.
|
||||
|
||||
2. All collection names and property names use `camelCase` rather than `snake_case`.
|
||||
|
||||
3. All timestamps are stored as `ISODate()` in MongoDB and all date/time values are stored in UTC.
|
||||
|
||||
4. A sparse index is used on the User `email` property to allow it to be optional, while still enforcing uniqueness if it is specified.
|
||||
|
||||
This is functionally equivalent to the ANSI SQL behaviour for a `unique` but `nullable` property.
|
||||
@@ -1,169 +1,87 @@
|
||||
---
|
||||
id: mysql
|
||||
title: MySQL Schema
|
||||
title: MySQL
|
||||
---
|
||||
|
||||
The schema generated for a MySQL database when using the built-in models.
|
||||
Schema for a MySQL database.
|
||||
|
||||
:::note
|
||||
When using MySQL the timezone is set to `Z` (aka Zulu time / UTC / GMT) in the adapter and all timestamps on all models are stored in UTC.
|
||||
When using a MySQL database with the default adapter (TypeORM) all timestamp columns use 6 digits of precision (unless another value for `precision` is specified in the schema) and the timezone is set to `Z` (aka Zulu Time / UTC) and all timestamps are stored in UTC.
|
||||
:::
|
||||
|
||||
## User
|
||||
```sql
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMP(6),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
```json
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
## Account
|
||||
CREATE TABLE users
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMP(6),
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
```json
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
## Session
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
```json
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
## Verification Request
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
```json
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
```
|
||||
@@ -1,169 +1,90 @@
|
||||
---
|
||||
id: postgres
|
||||
title: Postgres Schema
|
||||
title: Postgres
|
||||
---
|
||||
|
||||
The schema generated for a Postgres database when using the built-in models.
|
||||
Schema for a Postgres database.
|
||||
|
||||
:::note
|
||||
When using Postgres all timestamps on all models use the type `timestamp with time zone` (aka `timestamptz`) to store timestamps in UTC.
|
||||
When using a Postgres database with the default adapter (TypeORM) all properties of type `timestamp` are transformed to `timestamp with time zone`/`timestamptz` and all timestamps are stored in UTC.
|
||||
|
||||
This transform is also applied to any properties of type `timestamp` when using custom models.
|
||||
:::
|
||||
|
||||
## User
|
||||
```sql
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id SERIAL,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
```json
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id SERIAL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
## Account
|
||||
CREATE TABLE users
|
||||
(
|
||||
id SERIAL,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
```json
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id SERIAL,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
## Session
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
```json
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
## Verification Request
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
```json
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
```
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
|
||||
```
|
||||
23
www/docs/tutorials.md
Normal file
23
www/docs/tutorials.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
id: tutorials
|
||||
title: Tutorials and Explainers
|
||||
---
|
||||
|
||||
### How to restrict access
|
||||
|
||||
* [Securing pages and API routes](tutorials/securing-pages-and-api-routes)
|
||||
|
||||
### How to use custom models
|
||||
|
||||
* [Custom models with TypeORM](tutorials/typeorm-custom-models)
|
||||
|
||||
### How to use custom adapters
|
||||
|
||||
* [Creating a database adapter](tutorials/creating-a-database-adapter)
|
||||
|
||||
### How to write tests
|
||||
|
||||
* [Testing with Cypress](tutorials/testing-with-cypress)
|
||||
|
||||
|
||||
|
||||
180
www/docs/tutorials/creating-a-database-adapter.md
Normal file
180
www/docs/tutorials/creating-a-database-adapter.md
Normal file
@@ -0,0 +1,180 @@
|
||||
---
|
||||
id: creating-a-database-adapter
|
||||
title: Creating a database adapter
|
||||
---
|
||||
|
||||
Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
|
||||
Creating a custom adapter can be considerable undertaking and will require some trial and error and some reverse engineering using the built-in adapters for reference.
|
||||
|
||||
## How to create an adapter
|
||||
|
||||
From an implementation perspective, an adapter in NextAuth.js is a function which returns an async `getAdapter()` method, which in turn returns a Promise with a list of functions used to handle operations such as creating user, linking a user and an OAuth account or handling reading and writing sessions.
|
||||
|
||||
It uses this approach to allow database connection logic to live in the `getAdapter()` method. By calling the function just before an action needs to happen, it is possible to check database connection status and handle connecting / reconnecting to a database as required.
|
||||
|
||||
_See the code below for practical example._
|
||||
|
||||
### Required methods
|
||||
|
||||
These methods are required for all sign in flows:
|
||||
|
||||
* createUser
|
||||
* getUser
|
||||
* getUserByEmail
|
||||
* getUserByProviderAccountId
|
||||
* linkAccount
|
||||
* createSession
|
||||
* getSession
|
||||
* updateSession
|
||||
* deleteSession
|
||||
* updateUser
|
||||
|
||||
These methods are required to support email / passwordless sign in:
|
||||
|
||||
* createVerificationRequest
|
||||
* getVerificationRequest
|
||||
* deleteVerificationRequest
|
||||
|
||||
### Unimplemented methods
|
||||
|
||||
These methods will be required in a future release, but are not yet invoked:
|
||||
|
||||
* getUserByCredentials
|
||||
* deleteUser
|
||||
* unlinkAccount
|
||||
|
||||
### Example code
|
||||
|
||||
```js
|
||||
const Adapter = (config, options = {}) => {
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
|
||||
async function createUser (profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByCredentials (credentials) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (
|
||||
userId,
|
||||
providerId,
|
||||
providerAccountId
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (
|
||||
session,
|
||||
force
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (
|
||||
identifier,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (
|
||||
identifier,
|
||||
token,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
getUserByCredentials,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
```
|
||||
138
www/docs/tutorials/securing-pages-and-api-routes.md
Normal file
138
www/docs/tutorials/securing-pages-and-api-routes.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
id: securing-pages-and-api-routes
|
||||
title: Securing pages and API routes
|
||||
---
|
||||
|
||||
You can easily protect client and server side side rendered pages and API routes with NextAuth.js.
|
||||
|
||||
_You can find working examples of the approaches shown below in the [example project](https://github.com/iaincollins/next-auth-example/)._
|
||||
|
||||
:::tip
|
||||
The methods `getSession()` and `getToken()` both return an `object` if a session is valid and `null` if a session is invalid or has expired.
|
||||
:::
|
||||
|
||||
## Securing Pages
|
||||
|
||||
### Client Side
|
||||
|
||||
If data on a page is fetched using calls to secure API routes - i.e. routes which use `getSession()` or `getToken()` to access the session - you can use the `useSession` React Hook to secure pages.
|
||||
|
||||
```js title="pages/client-side-example.js"
|
||||
import { useSession, getSession } from 'next-auth/client'
|
||||
|
||||
export default function Page() {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
if (loading) return null
|
||||
|
||||
if (!loading && !session) return <p>Access Denied</p>
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Protected Page</h1>
|
||||
<p>You can view this page because you are signed in.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Server Side
|
||||
|
||||
You can protect server side rendered pages using the `getSession()` method.
|
||||
|
||||
```js title="pages/server-side-example.js"
|
||||
import { useSession, getSession } from 'next-auth/client'
|
||||
|
||||
export default function Page() {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
if (!session) return <p>Access Denied</p>
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Protected Page</h1>
|
||||
<p>You can view this page because you are signed in.</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
const session = await getSession(context)
|
||||
return {
|
||||
props: { session }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately avalible on page load to `useSession`.
|
||||
|
||||
```js title="pages/_app.js"
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default ({ Component, pageProps }) => {
|
||||
return (
|
||||
<Provider session={pageProps.session} >
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## Securing API Routes
|
||||
|
||||
### Using getSession()
|
||||
|
||||
You can protect API routes using the `getSession()` method.
|
||||
|
||||
```js title="pages/api/get-session-example.js"
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
if (session) {
|
||||
// Signed in
|
||||
console.log('Session', JSON.stringify(session, null, 2))
|
||||
} else {
|
||||
// Not Signed in
|
||||
res.status(401)
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
### Using getToken()
|
||||
|
||||
If you are using JSON Web Tokens you can use the `getToken()` helper to access the contents of the JWT without having to handle JWT decryption / verification yourself. This method can only be used server side.
|
||||
|
||||
```js title="pages/api/get-token-example.js"
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
if (token) {
|
||||
// Signed in
|
||||
console.log('JSON Web Token', JSON.stringify(token, null, 2))
|
||||
} else {
|
||||
// Not Signed in
|
||||
res.status(401)
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
|
||||
:::tip
|
||||
You can use the `getToken()` helper function in any application as long as you set the `NEXTAUTH_URL` environment variable and the application is able to read the JWT cookie (e.g. is on the same domain).
|
||||
:::
|
||||
|
||||
:::note
|
||||
Pass `getToken` the same value for `secret` as specified in `pages/api/auth/[...nextauth].js`.
|
||||
|
||||
See [the documentation for the JWT option](/configuration/options#jwt) for more information.
|
||||
:::
|
||||
124
www/docs/tutorials/testing-with-cypress.md
Normal file
124
www/docs/tutorials/testing-with-cypress.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
id: testing-with-cypress
|
||||
title: Testing with Cypress
|
||||
---
|
||||
|
||||
To test an implementation of NextAuth.js, we encourage you to use [Cypress](https://cypress.io).
|
||||
|
||||
## Setting up Cypress
|
||||
|
||||
To get started, install the dependencies:
|
||||
|
||||
`npm install --save-dev cypress cypress-social-login @testing-library/cypress`
|
||||
|
||||
:::note
|
||||
If you are using username/password based login, you will not need the `cypress-social-login` dependency.
|
||||
:::
|
||||
|
||||
Cypress will install and initialize the folder structure with example integration tests, a folder for plugins, etc.
|
||||
|
||||
Next you will have to create some configuration files for Cypress.
|
||||
|
||||
First, the primary cypress config:
|
||||
|
||||
```js title="cypress.json"
|
||||
{
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"chromeWebSecurity": false
|
||||
}
|
||||
```
|
||||
|
||||
This initial Cypress config will tell Cypress where to find your site on initial launch as well as allow it to open up URLs at domains that aren't your page, for example to be able to login to a social provider.
|
||||
|
||||
Second, a cypress file for environment variables. These can be defined in `cypress.json` under the key `env` as well, however since we're storing username / passwords in here we should keep those in a separate file and only commit `cypress.json` to version control, not `cypress.env.json`.
|
||||
|
||||
```js title="cypress.env.json"
|
||||
{
|
||||
"GOOGLE_USER": "username@company.com",
|
||||
"GOOGLE_PW": "password",
|
||||
"COOKIE_NAME": "__Secure-next-auth.session-token",
|
||||
"SITE_NAME": "http://localhost:3000"
|
||||
}
|
||||
```
|
||||
|
||||
You must change the login credentials you want to use, but you can also redefine the name of the `GOOGLE_*` variables if you're using a different provider. `COOKIE_NAME`, however, must be set to that value for NextAuth.js.
|
||||
|
||||
Third, if you're using the `cypress-social-login` plugin, you must add this to your `/cypress/plugins/index.js` file like so:
|
||||
|
||||
```js title="cypress/plugins/index.js"
|
||||
const { GoogleSocialLogin } = require('cypress-social-logins').plugins
|
||||
|
||||
module.exports = (on, config) => {
|
||||
on('task', {
|
||||
GoogleSocialLogin: GoogleSocialLogin,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
Finally, you can also add the following npm scripts to your `package.json`:
|
||||
|
||||
```json
|
||||
"test:e2e:open": "cypress open",
|
||||
"test:e2e:run": "cypress run"
|
||||
```
|
||||
|
||||
|
||||
## Writing a test
|
||||
|
||||
Once we've got all that configuration out of the way, we can begin writing tests to login using NextAuth.js.
|
||||
|
||||
The basic login test looks like this:
|
||||
|
||||
```js title="cypress/integration/login.js"
|
||||
describe('Login page', () => {
|
||||
before(() => {
|
||||
cy.log(`Visiting https://company.tld`)
|
||||
cy.visit('/')
|
||||
})
|
||||
it('Login with Google', () => {
|
||||
const username = Cypress.env('GOOGLE_USER')
|
||||
const password = Cypress.env('GOOGLE_PW')
|
||||
const loginUrl = Cypress.env('SITE_NAME')
|
||||
const cookieName = Cypress.env('COOKIE_NAME')
|
||||
const socialLoginOptions = {
|
||||
username,
|
||||
password,
|
||||
loginUrl,
|
||||
headless: true,
|
||||
logs: false,
|
||||
isPopup: true,
|
||||
loginSelector: `a[href="${Cypress.env(
|
||||
'SITE_NAME'
|
||||
)}/api/auth/signin/google"]`,
|
||||
postLoginSelector: '.unread-count',
|
||||
}
|
||||
|
||||
return cy
|
||||
.task('GoogleSocialLogin', socialLoginOptions)
|
||||
.then(({ cookies }) => {
|
||||
cy.clearCookies()
|
||||
|
||||
const cookie = cookies
|
||||
.filter(cookie => cookie.name === cookieName)
|
||||
.pop()
|
||||
if (cookie) {
|
||||
cy.setCookie(cookie.name, cookie.value, {
|
||||
domain: cookie.domain,
|
||||
expiry: cookie.expires,
|
||||
httpOnly: cookie.httpOnly,
|
||||
path: cookie.path,
|
||||
secure: cookie.secure,
|
||||
})
|
||||
|
||||
Cypress.Cookies.defaults({
|
||||
whitelist: cookieName,
|
||||
})
|
||||
cy.visit('/api/auth/signout')
|
||||
cy.get('form').submit()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
Things to note here include, that you must adjust the CSS selector defined under `postLoginSelector` to match a selector found on your page after the user is logged in. This is how Cypress knows whether it succeeded or not. Also, if you're using another provider, you will have to adjust the `loginSelector` URL.
|
||||
85
www/docs/tutorials/typeorm-custom-models.md
Normal file
85
www/docs/tutorials/typeorm-custom-models.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
id: typeorm-custom-models
|
||||
title: Custom models with TypeORM
|
||||
---
|
||||
|
||||
NextAuth.js provides a set of [models and schemas](/schemas/models) for the built-in TypeORM adapter that you can easily extend.
|
||||
|
||||
You can use these models with MySQL, MariaDB, Postgres, MongoDB and SQLite.
|
||||
|
||||
## Creating custom models
|
||||
|
||||
```js title="models/User.js"
|
||||
import Adapters from "next-auth/adapters"
|
||||
|
||||
// Extend the built-in models using class inheritance
|
||||
export default class User extends Adapters.TypeORM.Models.User.model {
|
||||
// You can extend the options in a model but you should not remove the base
|
||||
// properties or change the order of the built-in options on the constructor
|
||||
constructor(name, email, image, emailVerified) {
|
||||
super(name, email, image, emailVerified)
|
||||
}
|
||||
}
|
||||
|
||||
export const UserSchema = {
|
||||
name: "User",
|
||||
target: User,
|
||||
columns: {
|
||||
...Adapters.TypeORM.Models.User.schema.columns,
|
||||
// Adds a phoneNumber to the User schema
|
||||
phoneNumber: {
|
||||
type: "varchar",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```js title="models/index.js"
|
||||
// To make importing them easier, you can export all models from single file
|
||||
import User, { UserSchema } from "./User"
|
||||
|
||||
export default {
|
||||
User: {
|
||||
model: User,
|
||||
schema: UserSchema,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
[View source for built-in TypeORM models and schemas](https://github.com/iaincollins/next-auth/tree/main/src/adapters/typeorm/models)
|
||||
:::
|
||||
|
||||
## Using custom models
|
||||
|
||||
You can use custom models by specifying the TypeORM adapter explicitly and passing them as an option.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import Adapters from "next-auth/adapters"
|
||||
|
||||
import Models from "../../../models"
|
||||
|
||||
const options = {
|
||||
providers: [
|
||||
// Your providers
|
||||
],
|
||||
|
||||
adapter: Adapters.TypeORM.Adapter(
|
||||
// The first argument should be a database connection string or TypeORM config object
|
||||
"mysql://username:password@127.0.0.1:3306/database_name",
|
||||
// The second argument can be used to pass custom models and schemas
|
||||
{
|
||||
models: {
|
||||
User: Models.User,
|
||||
},
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
```
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user