mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
44 Commits
v3.13.2
...
v3.2.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c31cbbcd30 | ||
|
|
1728f50952 | ||
|
|
2eb17cba1a | ||
|
|
15196ee3d1 | ||
|
|
aa4439e182 | ||
|
|
66ec439b4d | ||
|
|
a49068643c | ||
|
|
1a315fe5ac | ||
|
|
652ac7de35 | ||
|
|
28ce71d99e | ||
|
|
28e2afbd3a | ||
|
|
eb828d42f8 | ||
|
|
d03504c6ef | ||
|
|
8827950f12 | ||
|
|
4f89d74d78 | ||
|
|
be159b1b18 | ||
|
|
19f2664a78 | ||
|
|
bd86e7c7c7 | ||
|
|
7ce37c71d7 | ||
|
|
3c3a4d2c4f | ||
|
|
5fcf80ce81 | ||
|
|
7a4534a6b1 | ||
|
|
ddaa830e10 | ||
|
|
9dbd372f08 | ||
|
|
dde908b54a | ||
|
|
831c59dd5c | ||
|
|
3abb0c8223 | ||
|
|
8c56e13577 | ||
|
|
12d7856640 | ||
|
|
4635113133 | ||
|
|
1aea187d5e | ||
|
|
47b8788249 | ||
|
|
06a160aa0c | ||
|
|
93f4dc0622 | ||
|
|
6088a05204 | ||
|
|
d242d72106 | ||
|
|
766874dbd8 | ||
|
|
0b7343702f | ||
|
|
0327b9049a | ||
|
|
2ee460de00 | ||
|
|
c8de34d003 | ||
|
|
d15572074f | ||
|
|
7b6fd818a5 | ||
|
|
e031591468 |
@@ -1,5 +1,3 @@
|
||||
# Rename file to .env and populate values
|
||||
# to be able to run tests
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_TWITTER_ID=
|
||||
NEXTAUTH_TWITTER_SECRET=
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# Rename file to .env.local (or .env) and populate values
|
||||
# to be able to run the dev app
|
||||
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_DOMAIN=
|
||||
AUTH0_SECRET=
|
||||
|
||||
GITHUB_ID=
|
||||
GITHUB_SECRET=
|
||||
|
||||
TWITTER_ID=
|
||||
TWITTER_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
# You can use any of these as the "DATABASE_URL" for
|
||||
# databases started with Docker using `npm run db:start`.
|
||||
# Note: If using with Prisma adapter, you need to use a `.env`
|
||||
# file rather than a `.env.local` file to configure env vars.
|
||||
# Postgres: DATABASE_URL=postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true
|
||||
# MySQL: DATABASE_URL=mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true
|
||||
# MongoDB: DATABASE_URL=mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true
|
||||
DATABASE_URL=
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -9,7 +9,7 @@ assignees: ''
|
||||
A clear and concise description of the feature being proposed.
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description of why this feature is necessary and what problems it solves.
|
||||
A clear and concise description description of why this feature is necessary and what problems it solves.
|
||||
|
||||
**Detail about proposed feature**
|
||||
A detailed description of how the proposal might work (if you have one).
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,8 +7,10 @@ this project (found in the CODE_OF_CONDUCT.md file).
|
||||
Also, please make sure you're familiar with and follow the instructions in the
|
||||
contributing guidelines (found in the CONTRIBUTING.md file).
|
||||
|
||||
Note before creating the Pull Request. Even though the CONTRIBUTONG.md tells otherwise, we ask you to use the `canary` branch as base for your PR. We are tranistioning to a new structure, and the CONTRIBUTONG.md file has not been updated yet. Thank you!
|
||||
|
||||
If you're new to contributing to open source projects, you might find this free
|
||||
video course helpful: https://kcd.im/pull-request
|
||||
video course helpful: http://kcd.im/pull-request
|
||||
|
||||
Please fill out the information below to expedite the review and (hopefully)
|
||||
merge of your pull request!
|
||||
|
||||
35
.github/labeler.yml
vendored
35
.github/labeler.yml
vendored
@@ -1,35 +0,0 @@
|
||||
test:
|
||||
- test/**/*
|
||||
|
||||
documentation:
|
||||
- www/**/*
|
||||
- ./**/*.md
|
||||
|
||||
providers:
|
||||
- src/providers/**/*
|
||||
- www/docs/configuration/providers.md
|
||||
- test/integration/**/*
|
||||
|
||||
adapters:
|
||||
- src/adapters/**/*
|
||||
- www/docs/schemas/adapters.md
|
||||
|
||||
databases:
|
||||
- www/docs/schemas/*.md
|
||||
- test/docker/databases/**/*
|
||||
- www/docs/configuration/databases.md
|
||||
- test/fixtures/**/*
|
||||
|
||||
core:
|
||||
- src/**/*
|
||||
|
||||
style:
|
||||
- src/css/**/*
|
||||
|
||||
client:
|
||||
- src/client/**/*
|
||||
- www/docs/getting-started/client.md
|
||||
|
||||
pages:
|
||||
- src/server/pages/**/*
|
||||
- www/docs/configuration/pages.md
|
||||
1
.github/stale.yml
vendored
1
.github/stale.yml
vendored
@@ -7,7 +7,6 @@ exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- priority
|
||||
- bug
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -1,30 +1,31 @@
|
||||
# Simple check that the build is valid and no linting errors.
|
||||
# Currently is run as a seperate workflow as it's fast to fail.
|
||||
name: Lint/Build
|
||||
name: Build Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
- canary
|
||||
pull_request:
|
||||
branches:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
- canary
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
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 }}
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -1,67 +0,0 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, next ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '43 17 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
11
.github/workflows/integration.yml
vendored
11
.github/workflows/integration.yml
vendored
@@ -2,10 +2,9 @@ name: Integration Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- next
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -29,7 +28,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
node-version: [12.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -38,8 +37,8 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
# Install dependencies
|
||||
- run: npm ci
|
||||
|
||||
# Run tests (build library, build + start test app in Docker, run tests)
|
||||
- run: npm test
|
||||
|
||||
11
.github/workflows/labeler.yml
vendored
11
.github/workflows/labeler.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
28
.github/workflows/release.yml
vendored
28
.github/workflows/release.yml
vendored
@@ -2,25 +2,29 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'next'
|
||||
- '3.x'
|
||||
pull_request:
|
||||
- main
|
||||
- canary
|
||||
jobs:
|
||||
release:
|
||||
name: 'Release'
|
||||
runs-on: ubuntu-latest
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 12
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run build
|
||||
- run: npx semantic-release@17
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
NPM_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npx semantic-release
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -11,8 +11,6 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
yarn.lock
|
||||
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
@@ -26,7 +24,6 @@ node_modules
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.next
|
||||
www/providers.json
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
@@ -36,7 +33,4 @@ www/providers.json
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
/_work
|
||||
39
.releaserc.json
Normal file
39
.releaserc.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"branches": [
|
||||
"main",
|
||||
{ "name": "canary", "prerelease": true }
|
||||
],
|
||||
"plugins": [
|
||||
["@semantic-release/commit-analyzer", {
|
||||
"preset": "conventionalcommits",
|
||||
"releaseRules": [
|
||||
{ "breaking": true, "release": "major" },
|
||||
{ "revert": true, "release": "patch" },
|
||||
{ "type": "feat", "release": "minor" },
|
||||
{ "type": "fix", "release": "patch" },
|
||||
{ "type": "perf", "release": "patch" },
|
||||
{ "type": "docs", "release": "patch" }
|
||||
]
|
||||
}],
|
||||
["@semantic-release/release-notes-generator", {
|
||||
"preset": "conventionalcommits",
|
||||
"presetConfig": {
|
||||
"types": [
|
||||
{ "type": "feat", "section": "Features", "hidden": false },
|
||||
{ "type": "fix", "section": "Bug Fixes", "hidden": false },
|
||||
{ "type": "perf", "section": "Performance Improvements", "hidden": false },
|
||||
{ "type": "revert", "section": "Reverts", "hidden": false },
|
||||
{ "type": "docs", "section": "Documentation", "hidden": false },
|
||||
{ "type": "style", "section": "Styles", "hidden": false },
|
||||
{ "type": "chore", "section": "Miscellaneous Chores", "hidden": false },
|
||||
{ "type": "refactor", "section": "Code Refactoring", "hidden": false },
|
||||
{ "type": "test", "section": "Tests", "hidden": false },
|
||||
{ "type": "build", "section": "Build System", "hidden": false },
|
||||
{ "type": "ci", "section": "Continuous Integration", "hidden": false }
|
||||
]
|
||||
}
|
||||
}],
|
||||
"@semantic-release/github",
|
||||
"@semantic-release/npm"
|
||||
]
|
||||
}
|
||||
155
CONTRIBUTING.md
155
CONTRIBUTING.md
@@ -8,70 +8,115 @@ Please see the [Code of Conduct](CODE_OF_CONDUCT.md) and follow any templates co
|
||||
|
||||
Please raise any significant new functionality or breaking change an issue for discussion before raising a Pull Request for it.
|
||||
|
||||
## For contributors
|
||||
## Pull Requests
|
||||
|
||||
Anyone can be a contributor. Either you found a typo, or you have an awesome feature request you could implement, we encourage you to create a Pull Request.
|
||||
### Pull Requests
|
||||
* The latest changes are always in `main`
|
||||
* Pull Requests should be raised for larger changes
|
||||
* Pull Requests do not need approval before merging for those with contributor access (it's just helpful to have them to track changes)
|
||||
* Rebasing in Pull Requests is preferred to keep a clean commit history (see below)
|
||||
* Running `npm run lint:fix` before committing can make resolving conflicts easier, but is not required
|
||||
* Merge commits (and pushing merge commits to `main`) are disabled in this repo, but commits in PR can be squashed so this is not a blocker
|
||||
* Pushing directly to main should ideally be reserved for minor updates (e.g. correcting typos) or small single-commit fixes
|
||||
|
||||
* The latest changes are always in `main`, so please make your Pull Request against that branch.
|
||||
* Pull Requests should be raised for any change
|
||||
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||
* Run `npm run lint:fix` before committing to make resolving conflicts easier (VSCode users, check out [this extension](https://marketplace.visualstudio.com/items?itemName=chenxsan.vscode-standardjs) to fix lint issues in development)
|
||||
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||
* If you add new functionality, please provide the corresponding documentation as well and make it part of the Pull Request
|
||||
## Rebasing
|
||||
|
||||
### Setting up local environment
|
||||
*If you don't rebase and end up with merge commits in a PR then it's not a blocker, we can always squash the commits when merging!*
|
||||
|
||||
A quick guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
If you create a branch and there are conflicting updates in the `main` branch, you can resolve them by rebasing from a check out of your branch:
|
||||
|
||||
git fetch
|
||||
git rebase origin/main
|
||||
|
||||
If there are any conflicts, you can resolve them and stage the files, then run:
|
||||
|
||||
git rebase --continue
|
||||
|
||||
*If there are a lot of changes you may be prompted to step more than once.*
|
||||
|
||||
When the rebase is complete (i.e. there are no more conflicts) you should push your changes to your branch before doing anything else:
|
||||
|
||||
git push --force-with-lease
|
||||
|
||||
You should see that any conflicts in your PR are now resolved. You can review changes to make sure it contains changes you intended to make.
|
||||
|
||||
*If you accidentally sync before pushing, it will trigger a merge. You can use `git merge --abort` to undo the merge.*
|
||||
|
||||
You can use `npm run lint:fix` to automatically apply Standard JS rules to resolve formatting differences (tabs vs spaces, line endings, etc).
|
||||
|
||||
## Setting up local environment
|
||||
|
||||
A quick and dirty guide on how to setup *next-auth* locally to work on it and test out any changes:
|
||||
|
||||
1. Clone the repo:
|
||||
```sh
|
||||
git clone git@github.com:nextauthjs/next-auth.git
|
||||
cd next-auth
|
||||
```
|
||||
|
||||
2. Install packages:
|
||||
```sh
|
||||
npm i
|
||||
```
|
||||
git clone git@github.com:iaincollins/next-auth.git
|
||||
cd next-auth/
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `.env.local.example` to `.env.local`, and add your env variables for each provider you want to test.
|
||||
2. Install packages and run the build command:
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
1. Start the dev application/server and CSS watching:
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
3. Link your project back to your local copy of next auth:
|
||||
|
||||
Your dev application will be available on ```http://localhost:3000```
|
||||
cd ../your-application
|
||||
npm link ../next-auth
|
||||
|
||||
That's it! 🎉
|
||||
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 dependencies with `npm i`.
|
||||
|
||||
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
|
||||
|
||||
#### Hot reloading
|
||||
### Hot reloading
|
||||
|
||||
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
|
||||
You might find it helpful to use the `npm run watch` command in the next-auth project, which will automatically (and silently) rebuild JS and CSS files as you edit them.
|
||||
|
||||
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
|
||||
cd next-auth/
|
||||
npm run watch
|
||||
|
||||
#### Databases
|
||||
If you are working on `next-auth/src/client/index.js` hot reloading will work as normal in your Next.js app.
|
||||
|
||||
However, if you are working on anything else (e.g. `next-auth/src/server/*` etc) then you will need to *stop and start* your app for changes to apply as **Next.js will not hot reload those changes by default**. To facilitate this, you can try [this webpack plugin](https://www.npmjs.com/package/webpack-clear-require-cache-plugin). Note that the `next.config.js` syntax in the plugin README may be out of date. It should look like this:
|
||||
|
||||
```
|
||||
const clearRequireCachePlugin = require('webpack-clear-require-cache-plugin')
|
||||
|
||||
module.exports = {
|
||||
webpack: (config, {
|
||||
buildId, dev, isServer, defaultLoaders, webpack,
|
||||
}) => {
|
||||
config.plugins.push(clearRequireCachePlugin([
|
||||
/\.next\/server\/static\/development\/pages/,
|
||||
/\.next\/server\/ssr-module-cache.js/,
|
||||
/next-auth/,
|
||||
]))
|
||||
|
||||
return config
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Databases
|
||||
|
||||
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.
|
||||
|
||||
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||
It will use port 3306, 5432, and 27017 on localhost respectively; it will not work if are running existing databases on localhost.
|
||||
|
||||
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
||||
|
||||
You will need Docker and Docker Compose installed to be able to start / stop the databases.
|
||||
You will need Docker installed to be able to start / stop the databases.
|
||||
|
||||
When stopping the databases, it will reset their contents.
|
||||
|
||||
#### Testing
|
||||
### Testing
|
||||
|
||||
Tests can be run with `npm run test`.
|
||||
|
||||
@@ -80,39 +125,3 @@ Automated tests are currently crude and limited in functionality, but improvemen
|
||||
Currently, to run tests you need to first have started local test databases (e.g. using `npm run db:start`).
|
||||
|
||||
The databases can take a few seconds to start up, so you might need to give it a minute before running the tests.
|
||||
|
||||
## For maintainers
|
||||
|
||||
We use [semantic-release](https://github.com/semantic-release/semantic-release) together with [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0) to automate releases. This makes the maintainenance process easier and less error-prone to human error. Please study the "Conventional Commits" site to understand how to write a good commit message.
|
||||
|
||||
When accepting Pull Requests, make sure the following:
|
||||
|
||||
* Use "Squash and merge"
|
||||
* Make sure you merge contributor PRs into `main`
|
||||
* Rewrite the commit message to conform to the `Conventional Commits` style. Check the "Recommended Scopes" section for further advice.
|
||||
* Optionally link issues the PR will resolve (You can add "close" in front of the issue numbers to close the issues automatically, when the PR is merged. `semantic-release` will also comment back to connected issues and PRs, notifying the users that a feature is added/bug fixed, etc.)
|
||||
|
||||
### Recommended Scopes
|
||||
|
||||
A typical conventional commit looks like this:
|
||||
```
|
||||
type(scope): title
|
||||
|
||||
body
|
||||
```
|
||||
|
||||
Scope is the part that will help groupping the different commit types in the release notes.
|
||||
|
||||
Some recommened scopes are:
|
||||
|
||||
- **provider** - Provider related changes. (eg.: "feat(provider): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **adapter** - Adapter related changes. (eg.: "feat(adapter): add X provider", "docs(provider): fix typo in X documentation"
|
||||
- **db** - Database related changes. (eg.: "feat(db): add X database", "docs(db): fix typo in X documentation"
|
||||
- **deps** - Adding/removing/updating a dependency (eg.: "chore(deps): add X")
|
||||
|
||||
> NOTE: If you are not sure which scope to use, you can simply ignore it. (eg.: "feat: add something"). Adding the correct type already helps a lot when analyzing the commit messages.
|
||||
|
||||
|
||||
### Skipping a release
|
||||
|
||||
Every commit that contains [skip release] or [release skip] in their message will be excluded from the commit analysis and won't participate in the release type determination. This is useful, if the PR being merged should not trigger a new `npm` release.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# https://docs.github.com/en/github/administering-a-repository/displaying-a-sponsor-button-in-your-repository
|
||||
|
||||
github: [balazsorban44]
|
||||
@@ -1,6 +1,6 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018-2021, Iain Collins
|
||||
Copyright (c) 2018-2020, Iain Collins
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
69
README.md
69
README.md
@@ -7,25 +7,12 @@
|
||||
Open Source. Full Stack. Own Your Data.
|
||||
</p>
|
||||
<p align="center" style="align: center;">
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3ARelease">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Release/badge.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/actions?query=workflow%3A%22Integration+Test%22">
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/result?p=next-auth">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
</a>
|
||||
<a href="https://www.npmtrends.com/next-auth">
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
</a>
|
||||
<a href="https://github.com/nextauthjs/next-auth/stargazers">
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/next-auth">
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?label=latest" alt="Github Stable Release" />
|
||||
</a>
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases&label=prerelease&sort=semver" alt="Github Prelease" />
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
|
||||
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@@ -82,7 +69,7 @@ NextAuth.js can be used with or without a database.
|
||||
|
||||
Advanced options allow you to define your own routines to handle controlling what accounts are allowed to sign in, for encoding and decoding JSON Web Tokens and to set custom cookie security policies and session properties, so you can control who is able to sign in and how often sessions have to be re-validated.
|
||||
|
||||
### TypeScript
|
||||
### Typescript
|
||||
|
||||
You can install the appropriate types via the following command:
|
||||
|
||||
@@ -90,7 +77,9 @@ You can install the appropriate types via the following command:
|
||||
npm install --save-dev @types/next-auth
|
||||
```
|
||||
|
||||
As of now, TypeScript is a community effort. If you encounter any problems with the types package, please create an issue at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth). Alternatively, you can open a pull request directly with your fixes there. We welcome anyone to start a discussion on migrating this package to TypeScript, or how to improve the TypeScript experience in general.
|
||||
If you encounter any problems with the types package, please create an issue and add the `typescript` label to it.
|
||||
|
||||
Alternatively, you can open a pull request directly with your fixes on the [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth) repository, where you'll find a `next-auth` subfolder.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -125,22 +114,26 @@ export default NextAuth({
|
||||
### Add React Component
|
||||
|
||||
```javascript
|
||||
import {
|
||||
useSession, signIn, signOut
|
||||
import React from 'react'
|
||||
import {
|
||||
useSession,
|
||||
signin,
|
||||
signout
|
||||
} from 'next-auth/client'
|
||||
|
||||
export default function Component() {
|
||||
export default function myComponent() {
|
||||
const [ session, loading ] = useSession()
|
||||
if(session) {
|
||||
return <>
|
||||
|
||||
return <p>
|
||||
{!session && <>
|
||||
Not signed in <br/>
|
||||
<button onClick={signin}>Sign in</button>
|
||||
</>}
|
||||
{session && <>
|
||||
Signed in as {session.user.email} <br/>
|
||||
<button onClick={() => signOut()}>Sign out</button>
|
||||
</>
|
||||
}
|
||||
return <>
|
||||
Not signed in <br/>
|
||||
<button onClick={() => signIn()}>Sign in</button>
|
||||
</>
|
||||
<button onClick={signout}>Sign out</button>
|
||||
</>}
|
||||
</p>
|
||||
}
|
||||
```
|
||||
|
||||
@@ -151,18 +144,10 @@ export default function Component() {
|
||||
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
|
||||
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
|
||||
</a>
|
||||
<div>
|
||||
<a href="https://vercel.com?utm_source=nextauthjs&utm_campaign=oss">
|
||||
<img width="170px" src="https://raw.githubusercontent.com/nextauthjs/next-auth/canary/www/static/img/powered-by-vercel.svg" alt="Powered By Vercel" />
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p align="left">Thanks to Vercel sponsoring this project by allowing it to be deployed for free for the entire NextAuth.js Team</p>
|
||||
</div>
|
||||
|
||||
## Contributing
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/iaincollins/next-auth/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
12
babel.config.json
Normal file
12
babel.config.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } } ]
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": [ "./src/server/pages/**" ],
|
||||
"presets": [ "preact" ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { signIn } from 'next-auth/client'
|
||||
|
||||
export default function AccessDenied () {
|
||||
return (
|
||||
<>
|
||||
<h1>Access Denied</h1>
|
||||
<p>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>You must be signed in to view this page
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import styles from './footer.module.css'
|
||||
import { version } from 'package.json'
|
||||
|
||||
export default function Footer () {
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<hr />
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}><a href='https://next-auth.js.org'>Documentation</a></li>
|
||||
<li className={styles.navItem}><a href='https://www.npmjs.com/package/next-auth'>NPM</a></li>
|
||||
<li className={styles.navItem}><a href='https://github.com/nextauthjs/next-auth-example'>GitHub</a></li>
|
||||
<li className={styles.navItem}><Link href='/policy'><a>Policy</a></Link></li>
|
||||
<li className={styles.navItem}><em>{version}</em></li>
|
||||
</ul>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
.footer {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import styles from './header.module.css'
|
||||
|
||||
// The approach used in this component shows how to built a sign in and sign out
|
||||
// component that works on pages which support both client and server side
|
||||
// rendering, and avoids any flash incorrect content on initial page load.
|
||||
export default function Header () {
|
||||
const [session, loading] = useSession()
|
||||
|
||||
return (
|
||||
<header>
|
||||
<noscript>
|
||||
<style>{'.nojs-show { opacity: 1; top: 0; }'}</style>
|
||||
</noscript>
|
||||
<div className={styles.signedInStatus}>
|
||||
<p
|
||||
className={`nojs-show ${
|
||||
!session && loading ? styles.loading : styles.loaded
|
||||
}`}
|
||||
>
|
||||
{!session && (
|
||||
<>
|
||||
<span className={styles.notSignedInText}>
|
||||
You are not signed in
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signin'
|
||||
className={styles.buttonPrimary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signIn()
|
||||
}}
|
||||
>
|
||||
Sign in
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
{session && (
|
||||
<>
|
||||
{session.user.image && (
|
||||
<span
|
||||
style={{ backgroundImage: `url(${session.user.image})` }}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
<span className={styles.signedInText}>
|
||||
<small>Signed in as</small>
|
||||
<br />
|
||||
<strong>{session.user.email || session.user.name}</strong>
|
||||
</span>
|
||||
<a
|
||||
href='/api/auth/signout'
|
||||
className={styles.button}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
signOut()
|
||||
}}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
<ul className={styles.navItems}>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/'>
|
||||
<a>Home</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/client'>
|
||||
<a>Client</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/server'>
|
||||
<a>Server</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected'>
|
||||
<a>Protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/protected-ssr'>
|
||||
<a>Protected(SSR)</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/api-example'>
|
||||
<a>API</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/credentials'>
|
||||
<a>Credentials</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href='/email'>
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/* Set min-height to avoid page reflow while session loading */
|
||||
.signedInStatus {
|
||||
display: block;
|
||||
min-height: 4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.loaded {
|
||||
position: relative;
|
||||
top: 0;
|
||||
opacity: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 .6rem .6rem;
|
||||
padding: .6rem 1rem;
|
||||
margin: 0;
|
||||
background-color: rgba(0,0,0,.05);
|
||||
transition: all 0.2s ease-in;
|
||||
}
|
||||
|
||||
.loading {
|
||||
top: -2rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.signedInText,
|
||||
.notSignedInText {
|
||||
position: absolute;
|
||||
padding-top: .8rem;
|
||||
left: 1rem;
|
||||
right: 6.5rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: inherit;
|
||||
z-index: 1;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
|
||||
.signedInText {
|
||||
padding-top: 0rem;
|
||||
left: 4.6rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 2rem;
|
||||
float: left;
|
||||
height: 2.8rem;
|
||||
width: 2.8rem;
|
||||
background-color: white;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.button,
|
||||
.buttonPrimary {
|
||||
float: right;
|
||||
margin-right: -.4rem;
|
||||
font-weight: 500;
|
||||
border-radius: .3rem;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
padding: .7rem .8rem;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: transparent;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.buttonPrimary {
|
||||
background-color: #346df1;
|
||||
border-color: #346df1;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
padding: .7rem 1.4rem;
|
||||
}
|
||||
|
||||
.buttonPrimary:hover {
|
||||
box-shadow: inset 0 0 5rem rgba(0,0,0,0.2)
|
||||
}
|
||||
|
||||
.navItems {
|
||||
margin-bottom: 2rem;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.navItem {
|
||||
display: inline-block;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Header from 'components/header'
|
||||
import Footer from 'components/footer'
|
||||
|
||||
export default function Layout ({ children }) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main>
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
7471
package-lock.json
generated
7471
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
@@ -8,29 +8,29 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "babel --config-file ./config/babel.config.json src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev": "next | npm run watch:css",
|
||||
"build:js": "babel src --out-dir dist",
|
||||
"build:css": "postcss src/**/*.css --base src --dir dist && node scripts/wrap-css.js",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"watch:js": "babel --watch src --out-dir dist",
|
||||
"watch:css": "postcss --watch src/**/*.css --base src --dir dist",
|
||||
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
|
||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
||||
"test:app:stop": "docker-compose -f test/docker/app.yml down",
|
||||
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql && npm run test:db:fauna",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"test:db:mssql": "node test/mssql.js",
|
||||
"test:db:fauna": "node test/fauna.js",
|
||||
"test:integration": "mocha test/integration",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "ts-standard",
|
||||
"lint:fix": "ts-standard --fix"
|
||||
"lint": "standard",
|
||||
"lint:fix": "standard --fix"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -43,17 +43,18 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"faunadb": "^3.0.1",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"nodemailer": "^6.4.16",
|
||||
"oauth": "^0.9.15",
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"preact-render-to-string": "^5.1.7",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
"typeorm": "^0.2.24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
@@ -64,57 +65,36 @@
|
||||
"mysql": "^2.18.1",
|
||||
"mssql": "^6.2.1",
|
||||
"pg": "^8.2.1",
|
||||
"@prisma/client": "^2.16.1"
|
||||
"@prisma/client": "^2.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@types/react": "^17.0.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.19.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"ts-standard": "^10.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
"standard": "^16.0.3"
|
||||
},
|
||||
"ts-standard": {
|
||||
"project": "./tsconfig.json",
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"test/",
|
||||
"next-env.d.ts"
|
||||
],
|
||||
"globals": [
|
||||
"localStorage",
|
||||
"location",
|
||||
"fetch"
|
||||
"test/"
|
||||
]
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/balazsorban44"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { Provider } from 'next-auth/client'
|
||||
import './styles.css'
|
||||
|
||||
// Use the <Provider> to improve performance and allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
export default function App ({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
// Provider options are not required but can be useful in situations where
|
||||
// you have a short session maxAge time. Shown here with default values.
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>API Example</h1>
|
||||
<p>The examples below show responses from the example API endpoints.</p>
|
||||
<p><em>You must be signed in to see responses.</em></p>
|
||||
<h2>Session</h2>
|
||||
<p>/api/examples/session</p>
|
||||
<iframe src='/api/examples/session' />
|
||||
<h2>JSON Web Token</h2>
|
||||
<p>/api/examples/jwt</p>
|
||||
<iframe src='/api/examples/jwt' />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
// const prisma = new PrismaClient()
|
||||
|
||||
export default NextAuth({
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
protection: 'pkce'
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev --preview-feature
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
// 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 })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// This is an example of to protect an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
|
||||
} else {
|
||||
res.send({ error: 'You must be sign in to view the protected content on this page.' })
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// This is an example of how to access a session from an API route
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Client Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the <strong>useSession()</strong> React Hook in the <strong></Header></strong> component.
|
||||
</p>
|
||||
<p>
|
||||
The <strong>useSession()</strong> React Hook easy to use and allows pages to render very quickly.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of this approach is that session state is shared between pages by using the <strong>Provider</strong> in <strong>_app.js</strong> so
|
||||
that navigation between pages using <strong>useSession()</strong> is very fast.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of <strong>useSession()</strong> is that it requires client side JavaScript.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const handleLogin = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signIn('credentials', options)
|
||||
}
|
||||
const response = await signIn('credentials', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async () => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Credentials login</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogin({ redirect: true, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: 'password' })}>Login</button><br />
|
||||
<span className='spacing'>No redirect, wrong password:</span>
|
||||
<button onClick={handleLogin({ redirect: false, password: '' })}>Login</button>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
const [response, setResponse] = React.useState(null)
|
||||
const [email, setEmail] = React.useState('')
|
||||
|
||||
const handleChange = (event) => {
|
||||
setEmail(event.target.value)
|
||||
}
|
||||
|
||||
const handleLogin = (options) => async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (options.redirect) {
|
||||
return signIn('email', options)
|
||||
}
|
||||
const response = await signIn('email', options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const handleLogout = (options) => async (event) => {
|
||||
if (options.redirect) {
|
||||
return signOut(options)
|
||||
}
|
||||
const response = await signOut(options)
|
||||
setResponse(response)
|
||||
}
|
||||
|
||||
const [session] = useSession()
|
||||
|
||||
if (session) {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email logout</h1>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button onClick={handleLogout({ redirect: true })}>Logout</button><br />
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button onClick={handleLogout({ redirect: false })}>Logout</button><br />
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Test different flows for Email login</h1>
|
||||
<label className='spacing'>
|
||||
Email address:{' '}
|
||||
<input type='text' id='email' name='email' value={email} onChange={handleChange} />
|
||||
</label><br />
|
||||
<form onSubmit={handleLogin({ redirect: true, email })}>
|
||||
<span className='spacing'>Default:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<form onSubmit={handleLogin({ redirect: false, email })}>
|
||||
<span className='spacing'>No redirect:</span>
|
||||
<button type='submit'>Sign in with Email</button>
|
||||
</form>
|
||||
<p>Response:</p>
|
||||
<pre style={{ background: '#eee', padding: 16 }}>{JSON.stringify(response, null, 2)}</pre>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import Layout from 'components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>NextAuth.js Example</h1>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
return (
|
||||
<Layout>
|
||||
<p>
|
||||
This is an example site to demonstrate how to use <a href='https://next-auth.js.org'>NextAuth.js</a> for authentication.
|
||||
</p>
|
||||
<h2>Terms of Service</h2>
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</p>
|
||||
<h2>Privacy Policy</h2>
|
||||
<p>
|
||||
This site uses JSON Web Tokens and an in-memory database which resets every ~2 hours.
|
||||
</p>
|
||||
<p>
|
||||
Data provided to this site is exclusively used to support signing in
|
||||
and is not passed to any third party services, other than via SMTP or OAuth for the
|
||||
purposes of authentication.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// This is an example of how to protect content using server rendering
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page ({ content, session }) {
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getServerSideProps (context) {
|
||||
const session = await getSession(context)
|
||||
let content = null
|
||||
|
||||
if (session) {
|
||||
const hostname = process.env.NEXTAUTH_URL || 'http://localhost:3000'
|
||||
const options = { headers: { cookie: context.req.headers.cookie } }
|
||||
const res = await fetch(`${hostname}/api/examples/protected`, options)
|
||||
const json = await res.json()
|
||||
if (json.content) { content = json.content }
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
import AccessDenied from '../components/access-denied'
|
||||
|
||||
export default function Page () {
|
||||
const [session, loading] = useSession()
|
||||
const [content, setContent] = useState()
|
||||
|
||||
// Fetch content from protected route
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const res = await fetch('/api/examples/protected')
|
||||
const json = await res.json()
|
||||
if (json.content) { setContent(json.content) }
|
||||
}
|
||||
fetchData()
|
||||
}, [session])
|
||||
|
||||
// When rendering client side don't display anything until loading is complete
|
||||
if (typeof window !== 'undefined' && loading) return null
|
||||
|
||||
// If no session exists, display access denied message
|
||||
if (!session) { return <Layout><AccessDenied /></Layout> }
|
||||
|
||||
// If session exists, display content
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Protected Page</h1>
|
||||
<p><strong>{content}</strong></p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
import Layout from '../components/layout'
|
||||
|
||||
export default function Page () {
|
||||
// As this page uses Server Side Rendering, the `session` will be already
|
||||
// populated on render without needing to go through a loading stage.
|
||||
// This is possible because of the shared context configured in `_app.js` that
|
||||
// is used by `useSession()`.
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Server Side Rendering</h1>
|
||||
<p>
|
||||
This page uses the universal <strong>getSession()</strong> method in <strong>getServerSideProps()</strong>.
|
||||
</p>
|
||||
<p>
|
||||
Using <strong>getSession()</strong> in <strong>getServerSideProps()</strong> is the recommended approach if you need to
|
||||
support Server Side Rendering with authentication.
|
||||
</p>
|
||||
<p>
|
||||
The advantage of Server Side Rendering is this page does not require client side JavaScript.
|
||||
</p>
|
||||
<p>
|
||||
The disadvantage of Server Side Rendering is that this page is slower to render.
|
||||
</p>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
// Export the `session` prop to use sessions with Server Side Rendering
|
||||
export async function getServerSideProps (context) {
|
||||
return {
|
||||
props: {
|
||||
session: await getSession(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
body {
|
||||
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';
|
||||
padding: 0 1rem 1rem 1rem;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
iframe {
|
||||
background: #ccc;
|
||||
border: 1px solid #ccc;
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
border-radius: .5rem;
|
||||
filter: invert(1);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id Int @default(autoincrement()) @id
|
||||
compoundId String @unique @map(name: "compound_id")
|
||||
userId Int @map(name: "user_id")
|
||||
providerType String @map(name: "provider_type")
|
||||
providerId String @map(name: "provider_id")
|
||||
providerAccountId String @map(name: "provider_account_id")
|
||||
refreshToken String? @map(name: "refresh_token")
|
||||
accessToken String? @map(name: "access_token")
|
||||
accessTokenExpires DateTime? @map(name: "access_token_expires")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@index([providerAccountId], name: "providerAccountId")
|
||||
@@index([providerId], name: "providerId")
|
||||
@@index([userId], name: "userId")
|
||||
|
||||
@@map(name: "accounts")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id Int @default(autoincrement()) @id
|
||||
userId Int @map(name: "user_id")
|
||||
expires DateTime
|
||||
sessionToken String @unique @map(name: "session_token")
|
||||
accessToken String @unique @map(name: "access_token")
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "sessions")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @default(autoincrement()) @id
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime? @map(name: "email_verified")
|
||||
image String?
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
||||
model VerificationRequest {
|
||||
id Int @default(autoincrement()) @id
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||
|
||||
@@map(name: "verification_requests")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
module.exports = {
|
||||
branches: [
|
||||
'+([0-9])?(.{+([0-9]),x}).x',
|
||||
'main',
|
||||
{ name: 'next', prerelease: true }
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// 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/nextauthjs/next-auth/issues/281
|
||||
// https://github.com/iaincollins/next-auth/issues/281
|
||||
//
|
||||
// To work around this issue, this script is a manual step that wraps CSS in a
|
||||
// JavaScript file that has the compiled CSS embedded in it, and exports only
|
||||
@@ -1,83 +1,84 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
function _debug (...args) {
|
||||
if (appOptions.debug) {
|
||||
console.log('[next-auth][debug]', ...args)
|
||||
}
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('createUser', profile)
|
||||
_debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('getUser', id)
|
||||
_debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('getUserByEmail', email)
|
||||
_debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
_debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('updateUser', user)
|
||||
_debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('deleteUser', userId)
|
||||
_debug('deleteUser', userId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
_debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
_debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('createSession', user)
|
||||
_debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('getSession', sessionToken)
|
||||
_debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('updateSession', session)
|
||||
_debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('deleteSession', sessionToken)
|
||||
_debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('createVerificationRequest', identifier)
|
||||
_debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
_debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('deleteVerification', identifier, token)
|
||||
_debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
505
src/adapters/fauna/index.js
Normal file
505
src/adapters/fauna/index.js
Normal file
@@ -0,0 +1,505 @@
|
||||
import { query as q } from 'faunadb'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config, options = {}) => {
|
||||
const { faunaClient } = config
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
function _debug (debugCode, ...args) {
|
||||
logger.debug(`fauna_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
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('createUser', profile)
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Create(
|
||||
q.Collection('user'), {
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified
|
||||
? profile.emailVerified
|
||||
: false,
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const newUser = await faunaClient.query(FQL)
|
||||
newUser.data.id = newUser.ref.id
|
||||
|
||||
return newUser.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_USER', error)
|
||||
return Promise.reject(new Error('CREATE_USER'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
_debug('getUser', id)
|
||||
|
||||
const FQL = q.Get(
|
||||
q.Ref(q.Collection('user'), id)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER', error)
|
||||
return Promise.reject(new Error('GET_USER'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
_debug('getUserByEmail', email)
|
||||
|
||||
if (!email) {
|
||||
return null
|
||||
}
|
||||
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(q.Index('user_by_email'), email)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
q.Get(q.Var('ref')),
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
|
||||
if (user == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
user.data.id = user.ref.id
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER_BY_EMAIL', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
_debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(
|
||||
q.Index('account_by_provider_account_id'),
|
||||
[providerId, providerAccountId]
|
||||
)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
q.Get(
|
||||
q.Ref(
|
||||
q.Collection('user'),
|
||||
q.Select(['data', 'userId'],
|
||||
q.Get(q.Var('ref'))
|
||||
)
|
||||
)
|
||||
),
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
|
||||
if (user == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('GET_USER_BY_PROVIDER_ACCOUNT_ID', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID'))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
_debug('updateUser', user)
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Update(
|
||||
q.Ref(q.Collection('user'), user.id),
|
||||
{
|
||||
data: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
emailVerified: user.emailVerified ? user.emailVerified : false,
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const user = await faunaClient.query(FQL)
|
||||
user.data.id = user.ref.id
|
||||
|
||||
return user.data
|
||||
} catch (error) {
|
||||
console.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
_debug('deleteUser', userId)
|
||||
|
||||
const FQL = q.Delete(
|
||||
q.Ref(q.Collection('user'), userId)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
_debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
|
||||
try {
|
||||
const timestamp = new Date().toISOString()
|
||||
const account = await faunaClient.query(
|
||||
q.Create(q.Collection('account'), {
|
||||
data: {
|
||||
userId: userId,
|
||||
providerId: providerId,
|
||||
providerType: providerType,
|
||||
providerAccountId: providerAccountId,
|
||||
refreshToken: refreshToken,
|
||||
accessToken: accessToken,
|
||||
accessTokenExpires: accessTokenExpires,
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return account.data
|
||||
} catch (error) {
|
||||
console.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
_debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index('account_by_provider_account_id'),
|
||||
[providerId, providerAccountId]
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
_debug('createSession', user)
|
||||
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL =
|
||||
q.Create(q.Collection('session'), {
|
||||
data: {
|
||||
userId: user.id,
|
||||
expires: q.Time(expires),
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex'),
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const session = await faunaClient.query(FQL)
|
||||
|
||||
session.data.id = session.ref.id
|
||||
|
||||
return session.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
_debug('getSession', sessionToken)
|
||||
|
||||
try {
|
||||
var session = await faunaClient.query(
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index('session_by_token'),
|
||||
sessionToken
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await _deleteSession(sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
session.data.id = session.ref.id
|
||||
|
||||
return session.data
|
||||
} catch (error) {
|
||||
console.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
_debug('updateSession', session)
|
||||
|
||||
try {
|
||||
const shouldUpdate = sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires
|
||||
if (!shouldUpdate && !force) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 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
|
||||
const currentDate = new Date()
|
||||
if (currentDate < dateSessionIsDueToBeUpdated && !force) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
|
||||
const updatedSession = await faunaClient.query(
|
||||
q.Update(
|
||||
q.Ref(q.Collection('session'), session.id),
|
||||
{
|
||||
data: {
|
||||
expires: q.Time(newExpiryDate.toISOString()),
|
||||
updatedAt: q.Time(new Date().toISOString())
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
updatedSession.data.id = updatedSession.ref.id
|
||||
|
||||
return updatedSession.data
|
||||
} catch (error) {
|
||||
console.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function _deleteSession (sessionToken) {
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index('session_by_token'),
|
||||
sessionToken
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return faunaClient.query(FQL)
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
_debug('deleteSession', sessionToken)
|
||||
|
||||
try {
|
||||
return await _deleteSession(sessionToken)
|
||||
} catch (error) {
|
||||
console.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
_debug('createVerificationRequest', identifier)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString()
|
||||
const FQL = q.Create(
|
||||
q.Collection('verification_request'), {
|
||||
data: {
|
||||
identifier: identifier,
|
||||
token: hashedToken,
|
||||
expires: expires === null ? null : q.Time(expires),
|
||||
createdAt: q.Time(timestamp),
|
||||
updatedAt: q.Time(timestamp)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
const verificationRequest = await faunaClient.query(FQL)
|
||||
|
||||
// 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.data
|
||||
} catch (error) {
|
||||
console.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('getVerificationRequest', identifier, token)
|
||||
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const FQL = q.Let(
|
||||
{
|
||||
ref: q.Match(q.Index('vertification_request_by_token'), hashedToken)
|
||||
},
|
||||
q.If(
|
||||
q.Exists(q.Var('ref')),
|
||||
{
|
||||
ref: q.Var('ref'),
|
||||
request: q.Select('data', q.Get(q.Var('ref')))
|
||||
},
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
const { ref, request: verificationRequest } = await faunaClient.query(FQL)
|
||||
const nowDate = Date.now()
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && verificationRequest.expires < nowDate) {
|
||||
// Delete the expired request so it cannot be used
|
||||
await faunaClient.query(
|
||||
q.Delete(ref)
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
console.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR'))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
_debug('deleteVerification', identifier, token)
|
||||
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const FQL = q.Delete(
|
||||
q.Select('ref',
|
||||
q.Get(
|
||||
q.Match(
|
||||
q.Index('vertification_request_by_token'), hashedToken
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
try {
|
||||
await faunaClient.query(FQL)
|
||||
} catch (error) {
|
||||
console.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_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,8 +1,10 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
import Fauna from './fauna'
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
Prisma,
|
||||
Fauna
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
@@ -20,7 +21,6 @@ const Adapter = (config) => {
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
@@ -219,7 +219,7 @@ const Adapter = (config) => {
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires: expires.toISOString() } })
|
||||
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))
|
||||
@@ -280,15 +280,11 @@ const Adapter = (config) => {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
token: hashedToken
|
||||
}
|
||||
})
|
||||
const verificationRequest = await prisma[VerificationRequest].findUnique({ where: { token: hashedToken } })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
await prisma[VerificationRequest].delete({ where: { token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -304,7 +300,7 @@ const Adapter = (config) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
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))
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
import adapterTransform from './lib/transform'
|
||||
import Models from './models'
|
||||
|
||||
import logger from '../../lib/logger'
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
@@ -41,12 +41,6 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
let connection = null
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// Helper function to reuse / restablish connections
|
||||
// (useful if they drop when after being idle)
|
||||
async function _connect () {
|
||||
@@ -83,6 +77,12 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
|
||||
const { manager } = connection
|
||||
|
||||
// Display debug output if debug option enabled
|
||||
// @TODO Refactor logger so is passed in appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
// flexiblity is required in the adapter to support non-SQL databases such
|
||||
// as MongoDB which have different pragmas.
|
||||
@@ -144,7 +144,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
}
|
||||
|
||||
try {
|
||||
return manager.findOne(User, { [idKey]: id })
|
||||
return manager.findUnique(User, { [idKey]: id })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
@@ -155,7 +155,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return manager.findOne(User, { email })
|
||||
return manager.findUnique(User, { email })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
@@ -165,9 +165,9 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await manager.findOne(Account, { providerId, providerAccountId })
|
||||
const account = await manager.findUnique(Account, { providerId, providerAccountId })
|
||||
if (!account) { return null }
|
||||
return manager.findOne(User, { [idKey]: account.userId })
|
||||
return manager.findUnique(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))
|
||||
@@ -227,7 +227,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await manager.findOne(Session, { sessionToken })
|
||||
const session = await manager.findUnique(Session, { sessionToken })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > new Date(session.expires)) {
|
||||
@@ -327,11 +327,11 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// 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 manager.findOne(VerificationRequest, { identifier, token: hashedToken })
|
||||
const verificationRequest = await manager.findUnique(VerificationRequest, { identifier, token: hashedToken })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
await manager.delete(VerificationRequest, { token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ const Adapter = (typeOrmConfig, options = {}) => {
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { identifier, 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))
|
||||
|
||||
103
src/client/index.d.ts
vendored
103
src/client/index.d.ts
vendored
@@ -1,103 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { GetServerSidePropsContext } from 'next'
|
||||
|
||||
interface DefaultSession {
|
||||
user: {
|
||||
name: string | null
|
||||
email: string | null
|
||||
image: string | null
|
||||
}
|
||||
expires: Date | string
|
||||
}
|
||||
|
||||
interface BroadcastMessage {
|
||||
event?: 'session'
|
||||
data?: {
|
||||
trigger?: 'signout' | 'getSession'
|
||||
}
|
||||
clientId: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type GetSession<S extends Record<string, unknown> = DefaultSession> = (options: {
|
||||
ctx?: GetServerSidePropsContext
|
||||
req?: GetServerSidePropsContext['req']
|
||||
event?: 'storage' | 'timer' | 'hidden' | string
|
||||
triggerEvent?: boolean
|
||||
}) => Promise<S>
|
||||
|
||||
export interface NextAuthConfig {
|
||||
baseUrl: string
|
||||
basePath: string
|
||||
baseUrlServer: string
|
||||
basePathServer: string
|
||||
/** 0 means disabled (don't send); 60 means send every 60 seconds */
|
||||
keepAlive: number
|
||||
/** 0 means disabled (only use cache); 60 means sync if last checked > 60 seconds ago */
|
||||
clientMaxAge: number
|
||||
/** Used for timestamp since last sycned (in seconds) */
|
||||
_clientLastSync: number
|
||||
/** Stores timer for poll interval */
|
||||
_clientSyncTimer: ReturnType<typeof setTimeout>
|
||||
/** Tracks if event listeners have been added */
|
||||
_eventListenersAdded: boolean
|
||||
/** Stores last session response from hook */
|
||||
_clientSession: DefaultSession | null | undefined
|
||||
/** Used to store to function export by getSession() hook */
|
||||
_getSession: any
|
||||
}
|
||||
|
||||
export type GetCsrfToken = (
|
||||
ctxOrReq: GetServerSidePropsContext & GetServerSidePropsContext['req']
|
||||
) => Promise<string | null>
|
||||
|
||||
export interface SessionOptions {
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
clientMaxAge?: number
|
||||
keepAlive?: number
|
||||
}
|
||||
|
||||
export type Provider<S extends Record<string, unknown> = DefaultSession > = (options: {
|
||||
children: React.ReactNode
|
||||
session: S
|
||||
options: SessionOptions
|
||||
}) => React.ReactNode
|
||||
|
||||
export type SetOptions = (options: SessionOptions) => void
|
||||
|
||||
export type SessionContext = React.createContext<[DefaultSession | null, boolean]>
|
||||
|
||||
export type UseSession = () => [any, boolean]
|
||||
|
||||
export type GetProviders = () => Promise<any[]>
|
||||
|
||||
// Sign in types
|
||||
|
||||
export interface SignInOptions {
|
||||
/** Defaults to the current URL. */
|
||||
callbackUrl?: string
|
||||
redirect?: boolean
|
||||
}
|
||||
export interface SignInResponse {
|
||||
error: string | null
|
||||
status: number
|
||||
ok: boolean
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export type SignIn<AuthorizationParams = Record<string, string>> = (
|
||||
provider?: string,
|
||||
options?: SignInOptions,
|
||||
authorizationParams?: AuthorizationParams
|
||||
) => SignInResponse
|
||||
|
||||
// Sign out types
|
||||
|
||||
interface SignOutResponse<RedirectType extends boolean=true> {
|
||||
/** Defaults to the current URL. */
|
||||
callbackUrl?: string
|
||||
redirect?: RedirectType
|
||||
}
|
||||
|
||||
export type SignOut<RedirectType extends boolean = true> = (params: SignOutResponse<RedirectType>) => RedirectType extends true ? Promise<{url?: string} | undefined> : undefined
|
||||
@@ -1,3 +1,5 @@
|
||||
/// 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
|
||||
@@ -8,8 +10,9 @@
|
||||
//
|
||||
// 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, { proxyLogger } from '../lib/logger'
|
||||
import logger from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
@@ -18,81 +21,158 @@ import parseUrl from '../lib/parse-url'
|
||||
// relative URLs are valid in that context and so defaults to empty.
|
||||
// 2. When invoked server side the value is picked up from an environment
|
||||
// variable and defaults to 'http://localhost:3000'.
|
||||
/** @type {import(".").NextAuthConfig} */
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
|
||||
keepAlive: 0,
|
||||
clientMaxAge: 0,
|
||||
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,
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_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: () => {}
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
// Fetch new session data but tell it to not to fire another event to
|
||||
// avoid an infinite loop.
|
||||
// Note: We could pass session data through and do something like
|
||||
// `setData(message.data)` but that can cause problems depending
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
|
||||
if (typeof window !== 'undefined') {
|
||||
if (__NEXTAUTH._eventListenersAdded === false) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
|
||||
}, false)
|
||||
// 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, 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 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); does not require request headers
|
||||
const getProviders = async () => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
return _fetchData(`${baseUrl}/providers`)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
const SessionContext = createContext()
|
||||
|
||||
/**
|
||||
* React Hook that gives you access
|
||||
* to the logged in user's session data.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#usesession)
|
||||
* @type {import(".").UseSession}
|
||||
*/
|
||||
export function useSession (session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
// Client side method
|
||||
const useSession = (session) => {
|
||||
// Try to use context if we can
|
||||
const value = useContext(SessionContext)
|
||||
|
||||
// If we have no Provider in the tree, call the actual hook
|
||||
if (value === undefined) {
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
// Internal hook for getting session from the api.
|
||||
const _useSessionHook = (session) => {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
const _getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
const triggredByEvent = (event !== null)
|
||||
const triggeredByStorageEvent = !!((event && event === 'storage'))
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
const currentTime = _now()
|
||||
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 && clientSession !== undefined) {
|
||||
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
|
||||
@@ -116,14 +196,13 @@ function _useSessionHook (session) {
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
// waiting for a response.
|
||||
__NEXTAUTH._clientLastSync = _now()
|
||||
__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 newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
})
|
||||
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.
|
||||
@@ -133,280 +212,117 @@ function _useSessionHook (session) {
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
__NEXTAUTH._getSession()
|
||||
})
|
||||
__NEXTAUTH._getSession = _getSession
|
||||
|
||||
_getSession()
|
||||
})
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called client or server side to return a session asynchronously.
|
||||
* It calls `/api/auth/session` and returns a promise with a session object,
|
||||
* or null if no session exists.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getsession)
|
||||
* @type {import(".").GetSession}
|
||||
*/
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getcsrftoken)
|
||||
* @type {import(".").GetCsrfToken}
|
||||
*/
|
||||
async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
}
|
||||
|
||||
/**
|
||||
* It calls `/api/auth/providers` and returns
|
||||
* a list of the currently configured authentication providers.
|
||||
* It can be useful if you are creating a dynamic custom sign in page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#getproviders)
|
||||
* @type {import(".").GetProviders}
|
||||
*/
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side method to initiate a signin flow
|
||||
* or send the user to the signin page listing all possible providers.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signin)
|
||||
* @type {import(".").SignIn}
|
||||
*/
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
|
||||
// Client side method
|
||||
const signIn = async (provider, args = {}, authParams = {}) => {
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
if (!provider || !providers[provider]) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
} else {
|
||||
let signInUrl = (providers[provider].type === 'credentials')
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
: `${baseUrl}/signin/${provider}`
|
||||
if (authParams) {
|
||||
signInUrl += `?${new URLSearchParams(authParams).toString()}`
|
||||
}
|
||||
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: _encodedForm({
|
||||
...args,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl: callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the user out, by removing the session cookie.
|
||||
* Automatically adds the CSRF token to the request.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#signout)
|
||||
* @type {import(".").SignOut}
|
||||
*/
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
// Client side method
|
||||
const signOut = async (args = {}) => {
|
||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
body: _encodedForm({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
callbackUrl: callbackUrl,
|
||||
json: true
|
||||
})
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
|
||||
return data
|
||||
_sendMessage({ event: 'session', data: { trigger: 'signout' } })
|
||||
window.location = data.url ? data.url : callbackUrl
|
||||
}
|
||||
|
||||
// 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.
|
||||
/** @type {import(".").SetOptions} */
|
||||
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
clearTimeout(__NEXTAUTH._clientSyncTimer)
|
||||
}
|
||||
|
||||
// Set next timer to trigger in number of seconds
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider to wrap the app in to make session data available globally.
|
||||
* Can also be used to throttle the number of requests to the endpoint
|
||||
* `/api/auth/session`.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/getting-started/client#provider)
|
||||
* @type {import(".").Provider}
|
||||
*/
|
||||
export function Provider ({ children, session, options }) {
|
||||
// Provider to wrap the app in to make session data available globally
|
||||
const Provider = ({ children, session, options }) => {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
{ value: useSession(session) },
|
||||
children
|
||||
)
|
||||
return createElement(SessionContext.Provider, { value: useSession(session) }, children)
|
||||
}
|
||||
|
||||
/**
|
||||
* If passed 'appContext' via getInitialProps() in _app.js
|
||||
* then get the req object from ctx and use that for the
|
||||
* req value to allow _fetchData to
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
const _fetchData = async (url, options = {}) => {
|
||||
try {
|
||||
const baseUrl = await _apiBaseUrl()
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${baseUrl}/${path}`, options)
|
||||
const res = await fetch(url, options)
|
||||
const data = await res.json()
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
return Promise.resolve(Object.keys(data).length > 0 ? data : null) // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', path, error)
|
||||
return null
|
||||
logger.error('CLIENT_FETCH_ERROR', url, error)
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
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')
|
||||
}
|
||||
if (!process.env.NEXTAUTH_URL) { logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set') }
|
||||
|
||||
// Return absolute path when called server side
|
||||
return `${__NEXTAUTH.baseUrlServer}${__NEXTAUTH.basePathServer}`
|
||||
return `${__NEXTAUTH.baseUrl}${__NEXTAUTH.basePath}`
|
||||
} else {
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
// Return relative path when called client side
|
||||
return __NEXTAUTH.basePath
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
const _encodedForm = (formData) => {
|
||||
return Object.keys(formData).map((key) => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API)
|
||||
* Only not using it directly, because Safari does not support it.
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import(".").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive (onReceive) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('storage', async (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import(".").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== 'session' || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
}
|
||||
const _sendMessage = (message) => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const timestamp = Math.floor(new Date().getTime() / 1000)
|
||||
localStorage.setItem('nextauth.message', JSON.stringify({ ...message, clientId: __NEXTAUTH._clientId, timestamp })) // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,16 @@
|
||||
:root {
|
||||
--border-width: 1px;
|
||||
--border-radius: .3rem;
|
||||
--color-error: #c94b4b;
|
||||
--color-info: #157efb;
|
||||
--color-info-text: #fff;
|
||||
}
|
||||
|
||||
.__next-auth-theme-auto,
|
||||
.__next-auth-theme-light {
|
||||
--color-background: #fff;
|
||||
--color-text: #000;
|
||||
--color-primary: #444;
|
||||
--color-control-border: #bbb;
|
||||
--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;
|
||||
}
|
||||
|
||||
.__next-auth-theme-dark {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.__next-auth-theme-auto {
|
||||
--color-background: #000;
|
||||
--color-text: #fff;
|
||||
--color-primary: #ccc;
|
||||
--color-control-border: #555;
|
||||
--color-button-active-background: #060606;
|
||||
--color-button-active-border: #666;
|
||||
--color-seperator: #444;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
margin: 0;
|
||||
@@ -50,11 +22,6 @@ h1 {
|
||||
font-weight: 400;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text)
|
||||
}
|
||||
|
||||
form {
|
||||
@@ -79,8 +46,7 @@ input[type] {
|
||||
background: var(--color-background);
|
||||
font-size: 1rem;
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0, 0, 0, .2);
|
||||
color: var(--color-text);
|
||||
box-shadow: inset 0 .1rem .2rem rgba(0,0,0,.2);
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
@@ -97,7 +63,6 @@ p {
|
||||
a.button {
|
||||
text-decoration: none;
|
||||
line-height: 1rem;
|
||||
|
||||
&:link,
|
||||
&:visited {
|
||||
background-color: var(--color-background);
|
||||
@@ -114,17 +79,17 @@ a.button {
|
||||
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 var(--color-background), inset 0 -.1rem .1rem rgba(0, 0, 0, .05);
|
||||
transition: all .1s ease-in-out;
|
||||
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 {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
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);
|
||||
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;
|
||||
@@ -136,21 +101,20 @@ a.site {
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
line-height: 2rem;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.page {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: table;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
>div {
|
||||
> div {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
@@ -165,14 +129,12 @@ a.site {
|
||||
padding-right: 2rem;
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.signin {
|
||||
|
||||
button,
|
||||
a.button,
|
||||
input[type="text"] {
|
||||
@@ -203,29 +165,25 @@ a.site {
|
||||
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;
|
||||
color: var(--color-info-text);
|
||||
}
|
||||
}
|
||||
|
||||
>div,
|
||||
> div,
|
||||
form {
|
||||
display: block;
|
||||
margin: 0 auto 0.5rem auto;
|
||||
|
||||
input[type] {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const pathToCss = path.join(process.cwd(), '/dist/css/index.css')
|
||||
const pathToCss = path.join(__dirname, '/index.css')
|
||||
const css = fs.readFileSync(pathToCss, 'utf8')
|
||||
|
||||
export default function css () {
|
||||
return fs.readFileSync(pathToCss, 'utf8')
|
||||
}
|
||||
export default () => css
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export class UnknownError extends Error {
|
||||
class UnknownError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'UnknownError'
|
||||
this.message = message
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
@@ -15,25 +16,26 @@ export class UnknownError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
class CreateUserError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'CreateUserError'
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
// Thrown when an Email address is already associated with an account
|
||||
// but the user is trying an OAuth account that is not linked to it.
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
// but the user is trying an oAuth account that is not linked to it.
|
||||
class AccountNotLinkedError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AccountNotLinkedError'
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'OAuthCallbackError'
|
||||
}
|
||||
module.exports = {
|
||||
UnknownError,
|
||||
CreateUserError,
|
||||
AccountNotLinkedError
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
import jose from 'jose'
|
||||
import hkdf from 'futoin-hkdf'
|
||||
import logger from './logger'
|
||||
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
@@ -13,7 +13,7 @@ const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
async function encode ({
|
||||
const encode = async ({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
@@ -28,9 +28,9 @@ async function encode ({
|
||||
zip: 'DEF'
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) {
|
||||
} = {}) => {
|
||||
// Signing Key
|
||||
const _signingKey = signingKey
|
||||
const _signingKey = (signingKey)
|
||||
? jose.JWK.asKey(JSON.parse(signingKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
@@ -39,17 +39,18 @@ async function encode ({
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = encryptionKey
|
||||
const _encryptionKey = (encryptionKey)
|
||||
? jose.JWK.asKey(JSON.parse(encryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Encrypt token
|
||||
return jose.JWE.encrypt(signedToken, _encryptionKey, encryptionOptions)
|
||||
} else {
|
||||
return signedToken
|
||||
}
|
||||
return signedToken
|
||||
}
|
||||
|
||||
async function decode ({
|
||||
const decode = async ({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
@@ -65,14 +66,14 @@ async function decode ({
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
} = {}) {
|
||||
} = {}) => {
|
||||
if (!token) return null
|
||||
|
||||
let tokenToVerify = token
|
||||
|
||||
if (encryption) {
|
||||
// Encryption Key
|
||||
const _encryptionKey = decryptionKey
|
||||
const _encryptionKey = (decryptionKey)
|
||||
? jose.JWK.asKey(JSON.parse(decryptionKey))
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
@@ -82,7 +83,7 @@ async function decode ({
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
const _signingKey = verificationKey
|
||||
const _signingKey = (verificationKey)
|
||||
? jose.JWK.asKey(JSON.parse(verificationKey))
|
||||
: getDerivedSigningKey(secret)
|
||||
|
||||
@@ -90,16 +91,7 @@ async function decode ({
|
||||
return jose.JWT.verify(tokenToVerify, _signingKey, verificationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side method to retrieve the JWT from `req`.
|
||||
* @param {{
|
||||
* req: NextApiRequest
|
||||
* secureCookie?: boolean
|
||||
* cookieName?: string
|
||||
* raw?: boolean
|
||||
* }} params
|
||||
*/
|
||||
async function getToken (params) {
|
||||
const getToken = async (args) => {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
@@ -107,7 +99,7 @@ async function getToken (params) {
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
raw = false
|
||||
} = params
|
||||
} = args
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
|
||||
// Try to get token from cookie
|
||||
@@ -116,7 +108,7 @@ async function getToken (params) {
|
||||
// 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?.split(' ')[0] === 'Bearer') {
|
||||
if (!token && req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
|
||||
const urlEncodedToken = req.headers.authorization.split(' ')[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
@@ -126,8 +118,8 @@ async function getToken (params) {
|
||||
}
|
||||
|
||||
try {
|
||||
return decode({ token, ...params })
|
||||
} catch {
|
||||
return await decode({ token, ...args })
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -136,46 +128,24 @@ async function getToken (params) {
|
||||
let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
|
||||
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
|
||||
if (crypto.hkdfSync) {
|
||||
return Buffer.from(
|
||||
crypto.hkdfSync(
|
||||
digest,
|
||||
secret,
|
||||
Buffer.alloc(0),
|
||||
encryptionInfo,
|
||||
byteLength
|
||||
)
|
||||
)
|
||||
}
|
||||
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
|
||||
}
|
||||
|
||||
function getDerivedSigningKey (secret) {
|
||||
const getDerivedSigningKey = (secret) => {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 64,
|
||||
encryptionInfo: 'NextAuth.js Generated Signing Key'
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
function getDerivedEncryptionKey (secret) {
|
||||
const getDerivedEncryptionKey = (secret) => {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 32,
|
||||
encryptionInfo: 'NextAuth.js Generated Encryption Key'
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
10
src/lib/logger.d.ts
vendored
10
src/lib/logger.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
export interface LoggerInstance {
|
||||
warn: (code?: string, ...message: unknown[]) => void
|
||||
error: (code?: string, ...message: unknown[]) => void
|
||||
debug: (code?: string, ...message: unknown[]) => void
|
||||
}
|
||||
|
||||
export declare function proxyLogger (logger: LoggerInstance, basePath: string): LoggerInstance
|
||||
|
||||
const _logger: LoggerInstance
|
||||
export default _logger
|
||||
@@ -1,82 +1,32 @@
|
||||
/** @type {import("./logger").LoggerInstance} */
|
||||
const _logger = {
|
||||
error (code, ...message) {
|
||||
const logger = {
|
||||
error: (errorCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
console.error(
|
||||
`[next-auth][error][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
...message
|
||||
`[next-auth][error][${errorCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/errors#${errorCode.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
warn (code, ...message) {
|
||||
warn: (warnCode, ...text) => {
|
||||
if (!console) { return }
|
||||
if (text && text.length <= 1) { text = text[0] || '' }
|
||||
console.warn(
|
||||
`[next-auth][warn][${code.toLowerCase()}]`,
|
||||
`\nhttps://next-auth.js.org/warnings#${code.toLowerCase()}`,
|
||||
...message
|
||||
`[next-auth][warn][${warnCode.toLowerCase()}]`,
|
||||
text,
|
||||
`\nhttps://next-auth.js.org/warnings#${warnCode.toLowerCase()}`
|
||||
)
|
||||
},
|
||||
debug (code, ...message) {
|
||||
if (!process?.env?._NEXTAUTH_DEBUG) return
|
||||
console.log(
|
||||
`[next-auth][debug][${code.toLowerCase()}]`,
|
||||
...message
|
||||
)
|
||||
debug: (debugCode, ...text) => {
|
||||
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.toLowerCase()}]`,
|
||||
text
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the built-in logger.
|
||||
* Any `undefined` level will use the default logger.
|
||||
* @param {Partial<import("./logger").LoggerInstance>} newLogger
|
||||
*/
|
||||
export function setLogger (newLogger = {}) {
|
||||
if (newLogger.error) _logger.error = newLogger.error
|
||||
if (newLogger.warn) _logger.warn = newLogger.warn
|
||||
if (newLogger.debug) _logger.debug = newLogger.debug
|
||||
}
|
||||
|
||||
export default _logger
|
||||
|
||||
/**
|
||||
* Serializes client-side log messages and sends them to the server
|
||||
* @param {import("./logger").LoggerInstance} logger
|
||||
* @param {string} basePath
|
||||
* @return {import("./logger").LoggerInstance}
|
||||
*/
|
||||
export function proxyLogger (logger = _logger, basePath) {
|
||||
try {
|
||||
if (typeof window === 'undefined') {
|
||||
return logger
|
||||
}
|
||||
|
||||
const clientLogger = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, ...message) => {
|
||||
_logger[level](code, ...message) // Log on client as usual
|
||||
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({
|
||||
level,
|
||||
code,
|
||||
message: JSON.stringify(message.map(m => {
|
||||
if (m instanceof Error) {
|
||||
// Serializing errors: https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
return { name: m.name, message: m.message, stack: m.stack }
|
||||
}
|
||||
return m
|
||||
}))
|
||||
})
|
||||
if (navigator.sendBeacon) {
|
||||
return navigator.sendBeacon(url, body)
|
||||
}
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body
|
||||
})
|
||||
}
|
||||
}
|
||||
return clientLogger
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
}
|
||||
export default logger
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/**
|
||||
* 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.
|
||||
* @param {string} url
|
||||
*/
|
||||
export default function parseUrl (url) {
|
||||
// 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'
|
||||
@@ -13,7 +10,7 @@ export default function parseUrl (url) {
|
||||
if (!url) { url = `${defaultHost}${defaultPath}` }
|
||||
|
||||
// Default to HTTPS if no protocol explictly specified
|
||||
const protocol = url.startsWith('http:') ? 'http' : 'https'
|
||||
const protocol = url.match(/^http?:\/\//) ? 'http' : 'https'
|
||||
|
||||
// Normalize URLs by stripping protocol and no trailing slash
|
||||
url = url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||
@@ -23,5 +20,8 @@ export default function parseUrl (url) {
|
||||
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
||||
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
||||
|
||||
return { baseUrl, basePath }
|
||||
return {
|
||||
baseUrl,
|
||||
basePath
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'apple',
|
||||
@@ -10,6 +12,7 @@ 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 {
|
||||
@@ -20,11 +23,30 @@ export default (options) => {
|
||||
},
|
||||
clientId: null,
|
||||
clientSecret: {
|
||||
appleId: null,
|
||||
teamId: null,
|
||||
privateKey: null,
|
||||
keyId: null
|
||||
},
|
||||
protection: 'none', // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
clientSecretCallback: async ({ appleId, keyId, teamId, privateKey }) => {
|
||||
const response = jwt.sign(
|
||||
{
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: appleId
|
||||
},
|
||||
// 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
|
||||
}
|
||||
)
|
||||
return Promise.resolve(response)
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,22 @@ export default (options) => {
|
||||
requestTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
|
||||
authorizationUrl: 'https://www.bungie.net/en/OAuth/Authorize?response_type=code',
|
||||
profileUrl: 'https://www.bungie.net/platform/User/GetBungieAccount/{membershipId}/254/',
|
||||
prepareProfileRequest: ({ provider, url, headers, results }) => {
|
||||
if (!results.membership_id) {
|
||||
// internal error
|
||||
// @TODO: handle better
|
||||
throw new Error('Expected membership_id to be passed.')
|
||||
}
|
||||
|
||||
if (!provider.apiKey) {
|
||||
throw new Error('The Bungie provider requires the apiKey option to be present.')
|
||||
}
|
||||
|
||||
headers['X-API-Key'] = provider.apiKey
|
||||
url = url.replace('{membershipId}', results.membership_id)
|
||||
|
||||
return url
|
||||
},
|
||||
profile: (profile) => {
|
||||
const { bungieNetUser: user } = profile.Response
|
||||
|
||||
@@ -20,9 +36,7 @@ export default (options) => {
|
||||
email: null
|
||||
}
|
||||
},
|
||||
headers: {
|
||||
'X-API-Key': null
|
||||
},
|
||||
apiKey: null,
|
||||
clientId: null,
|
||||
clientSecret: null,
|
||||
...options
|
||||
|
||||
@@ -14,7 +14,7 @@ export default (options) => {
|
||||
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
|
||||
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
|
||||
} else {
|
||||
const format = profile.avatar.startsWith('a_') ? 'gif' : 'png'
|
||||
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
|
||||
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'eveonline',
|
||||
name: 'EVE Online',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.eveonline.com/oauth/token',
|
||||
authorizationUrl: 'https://login.eveonline.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://login.eveonline.com/oauth/verify',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
email: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'faceit',
|
||||
name: 'FACEIT',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(`${options.clientId}:${options.clientSecret}`).toString('base64')}`
|
||||
},
|
||||
accessTokenUrl: 'https://api.faceit.com/auth/v1/oauth/token',
|
||||
authorizationUrl: 'https://accounts.faceit.com/accounts?redirect_popup=true&response_type=code',
|
||||
profileUrl: 'https://api.faceit.com/auth/v1/resources/userinfo',
|
||||
profile (profile) {
|
||||
const { guid: id, nickname: name, email, picture: image } = profile
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
image
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export default (options) => {
|
||||
profile: (profile) => {
|
||||
return { ...profile, id: profile.sub }
|
||||
},
|
||||
setGetAccessTokenAuthHeader: false,
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,78 +6,56 @@ import Basecamp from './basecamp'
|
||||
import BattleNet from './battlenet'
|
||||
import Box from './box'
|
||||
import Bungie from './bungie'
|
||||
import Cognito from './cognito'
|
||||
import Credentials from './credentials'
|
||||
import Cognito from './cognito'
|
||||
import Discord from './discord'
|
||||
import Email from './email'
|
||||
import EVEOnline from './eveonline'
|
||||
import Facebook from './facebook'
|
||||
import FACEIT from './faceit'
|
||||
import Foursquare from './foursquare'
|
||||
import FusionAuth from './fusionauth'
|
||||
import GitHub from './github'
|
||||
import GitLab from './gitlab'
|
||||
import Google from './google'
|
||||
import IdentityServer4 from './identity-server4'
|
||||
import Instagram from './instagram'
|
||||
import Kakao from './kakao'
|
||||
import LINE from './line'
|
||||
import LinkedIn from './linkedin'
|
||||
import MailRu from './mailru'
|
||||
import Medium from './medium'
|
||||
import Mixer from './mixer'
|
||||
import Netlify from './netlify'
|
||||
import Okta from './okta'
|
||||
import Osso from './osso'
|
||||
import Reddit from './reddit'
|
||||
import Salesforce from './salesforce'
|
||||
import Slack from './slack'
|
||||
import Spotify from './spotify'
|
||||
import Strava from './strava'
|
||||
import Twitch from './twitch'
|
||||
import Twitter from './twitter'
|
||||
import VK from './vk'
|
||||
import Yandex from './yandex'
|
||||
import Zoho from './zoho'
|
||||
|
||||
export default {
|
||||
Apple,
|
||||
Atlassian,
|
||||
Auth0,
|
||||
Apple,
|
||||
AzureADB2C,
|
||||
Basecamp,
|
||||
BattleNet,
|
||||
Box,
|
||||
Bungie,
|
||||
Cognito,
|
||||
Credentials,
|
||||
Cognito,
|
||||
Discord,
|
||||
Email,
|
||||
EVEOnline,
|
||||
Facebook,
|
||||
FACEIT,
|
||||
Foursquare,
|
||||
FusionAuth,
|
||||
GitHub,
|
||||
GitLab,
|
||||
Google,
|
||||
IdentityServer4,
|
||||
Instagram,
|
||||
Kakao,
|
||||
LINE,
|
||||
LinkedIn,
|
||||
MailRu,
|
||||
Medium,
|
||||
Mixer,
|
||||
Netlify,
|
||||
Okta,
|
||||
Osso,
|
||||
Reddit,
|
||||
Salesforce,
|
||||
Slack,
|
||||
Spotify,
|
||||
Strava,
|
||||
Twitch,
|
||||
Twitter,
|
||||
VK,
|
||||
Yandex,
|
||||
Zoho
|
||||
Twitch,
|
||||
Yandex
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* @param {import("../server").Provider} options
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // pages/api/auth/[...nextauth].js
|
||||
* import Providers from `next-auth/providers`
|
||||
* ...
|
||||
* providers: [
|
||||
* Providers.Instagram({
|
||||
* clientId: process.env.INSTAGRAM_CLIENT_ID,
|
||||
* clientSecret: process.env.INSTAGRAM_CLIENT_SECRET
|
||||
* })
|
||||
* ]
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* ...
|
||||
* <button onClick={() => signIn("instagram")}>
|
||||
* Sign in
|
||||
* </button>
|
||||
* ...
|
||||
* ```
|
||||
* *Resources:*
|
||||
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/instagram)
|
||||
* - [Instagram Documentation](https://developers.facebook.com/docs/instagram-basic-display-api/getting-started)
|
||||
* - [Configuration](https://developers.facebook.com/apps)
|
||||
*/
|
||||
export default function Instagram (options) {
|
||||
return {
|
||||
id: 'instagram',
|
||||
name: 'Instagram',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'user_profile',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.instagram.com/oauth/access_token',
|
||||
authorizationUrl: 'https://api.instagram.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://graph.instagram.com/me?fields=id,username,account_type,name',
|
||||
async profile (profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
email: null,
|
||||
image: null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'kakao',
|
||||
name: 'Kakao',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://kauth.kakao.com/oauth/token',
|
||||
authorizationUrl: 'https://kauth.kakao.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://kapi.kakao.com/v2/user/me',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.kakao_account?.profile.nickname,
|
||||
email: profile.kakao_account?.email,
|
||||
image: profile.kakao_account?.profile.profile_image_url
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'line',
|
||||
name: 'LINE',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'profile openid',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.line.me/oauth2/v2.1/token',
|
||||
authorizationUrl: 'https://access.line.me/oauth2/v2.1/authorize?response_type=code',
|
||||
profileUrl: 'https://api.line.me/v2/profile',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.userId,
|
||||
name: profile.displayName,
|
||||
email: null,
|
||||
image: profile.pictureUrl
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'mailru',
|
||||
name: 'Mail.ru',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'userinfo',
|
||||
params: {
|
||||
grant_type: 'authorization_code'
|
||||
},
|
||||
accessTokenUrl: 'https://oauth.mail.ru/token',
|
||||
requestTokenUrl: 'https://oauth.mail.ru/token',
|
||||
authorizationUrl: 'https://oauth.mail.ru/login?response_type=code',
|
||||
profileUrl: 'https://oauth.mail.ru/userinfo',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'basicProfile',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.medium.com/v1/tokens',
|
||||
authorizationUrl: 'https://medium.com/m/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.medium.com/v1/me',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.name,
|
||||
email: null,
|
||||
image: profile.data.imageUrl
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
22
src/providers/mixer.js
Normal file
22
src/providers/mixer.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'mixer',
|
||||
name: 'Mixer',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'user:details:self',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://mixer.com/api/v1/oauth/token',
|
||||
authorizationUrl: 'https://mixer.com/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://mixer.com/api/v1/users/current',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.username,
|
||||
image: profile.avatarUrl,
|
||||
email: profile.email
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export default (options) => {
|
||||
profile: (profile) => {
|
||||
return { ...profile, id: profile.sub }
|
||||
},
|
||||
setGetAccessTokenAuthHeader: false,
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'osso',
|
||||
name: 'SAML SSO',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: `https://${options.domain}/oauth/token`,
|
||||
authorizationUrl: `https://${options.domain}/oauth/authorize?response_type=code`,
|
||||
profileUrl: `https://${options.domain}/oauth/me`,
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name || profile.email,
|
||||
email: profile.email
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// Logging in works but trying to retrieve the profile results in 401 unauthorized
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'reddit',
|
||||
@@ -11,12 +12,12 @@ export default (options) => {
|
||||
'https://www.reddit.com/api/v1/authorize?response_type=code',
|
||||
profileUrl: 'https://oauth.reddit.com/api/v1/me',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
image: null,
|
||||
email: null
|
||||
}
|
||||
// return {
|
||||
// id: profile.id,
|
||||
// name: profile.name,
|
||||
// image: null,
|
||||
// email: null,
|
||||
// };
|
||||
},
|
||||
...options
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'salesforce',
|
||||
name: 'Salesforce',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { display: 'page', grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.salesforce.com/services/oauth2/token',
|
||||
authorizationUrl: 'https://login.salesforce.com/services/oauth2/authorize?response_type=code',
|
||||
profileUrl: 'https://login.salesforce.com/services/oauth2/userinfo',
|
||||
protection: 'none', // REVIEW: Can we use "pkce" ?
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
id: profile.user_id,
|
||||
image: profile.picture
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ export default (options) => {
|
||||
scope: [],
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
|
||||
accessTokenGetter: (json) => json.authed_user.access_token,
|
||||
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
||||
authorizationParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
|
||||
additionalAuthorizeParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
|
||||
profileUrl: 'https://slack.com/api/users.identity',
|
||||
profile: (profile) => {
|
||||
const { user } = profile
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
export default (options) => {
|
||||
const apiVersion = '5.126' // https://vk.com/dev/versions
|
||||
|
||||
return {
|
||||
id: 'vk',
|
||||
name: 'VK',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'email',
|
||||
params: {
|
||||
grant_type: 'authorization_code'
|
||||
},
|
||||
accessTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
|
||||
requestTokenUrl: `https://oauth.vk.com/access_token?v=${apiVersion}`,
|
||||
authorizationUrl:
|
||||
`https://oauth.vk.com/authorize?response_type=code&v=${apiVersion}`,
|
||||
profileUrl: `https://api.vk.com/method/users.get?fields=photo_100&v=${apiVersion}`,
|
||||
profile: (result) => {
|
||||
const profile = result.response?.[0] ?? {}
|
||||
|
||||
return {
|
||||
id: profile.id,
|
||||
name: [profile.first_name, profile.last_name].filter(Boolean).join(' '),
|
||||
email: profile.email,
|
||||
image: profile.photo_100
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export default (options) => {
|
||||
return {
|
||||
id: 'zoho',
|
||||
name: 'Zoho',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'AaaServer.profile.Read',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://accounts.zoho.com/oauth/v2/token',
|
||||
authorizationUrl: 'https://accounts.zoho.com/oauth/v2/auth?response_type=code',
|
||||
profileUrl: 'https://accounts.zoho.com/oauth/user/info',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.ZUID,
|
||||
name: `${profile.First_Name} ${profile.Last_Name}`,
|
||||
email: profile.Email,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
94
src/server/index.d.ts
vendored
94
src/server/index.d.ts
vendored
@@ -1,94 +0,0 @@
|
||||
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
|
||||
import { LoggerInstance } from 'src/lib/logger'
|
||||
import { CallbacksOptions } from './lib/callbacks'
|
||||
import { CookiesOptions } from './lib/cookie'
|
||||
import { EventsOptions } from './lib/events'
|
||||
|
||||
export interface Provider {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
params: Record<string, unknown>
|
||||
scope: string
|
||||
accessTokenUrl: string
|
||||
authorizationUrl: string
|
||||
profileUrl?: string
|
||||
grant_type?: string
|
||||
profile?: (profile: any) => Promise<any>
|
||||
}
|
||||
|
||||
/** @docs https://next-auth.js.org/configuration/options */
|
||||
export interface NextAuthOptions {
|
||||
/** @docs https://next-auth.js.org/configuration/options#theme */
|
||||
theme?: 'auto' | 'dark' | 'light'
|
||||
/** @docs https://next-auth.js.org/configuration/options#providers */
|
||||
providers: Provider[]
|
||||
/** @docs https://next-auth.js.org/configuration/options#database */
|
||||
database?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#secret */
|
||||
secret?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#session */
|
||||
session?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#jwt */
|
||||
jwt?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#pages */
|
||||
pages?: {
|
||||
signIn?: string
|
||||
signOut?: string
|
||||
/** Error code passed in query string as ?error= */
|
||||
error?: string
|
||||
verifyRequest?: string
|
||||
/** If set, new users will be directed here on first sign in */
|
||||
newUser?: string
|
||||
}
|
||||
/**
|
||||
* Callbacks are asynchronous functions you can use to control what happens when an action is performed.
|
||||
* Callbacks are extremely powerful, especially in scenarios involving JSON Web Tokens as
|
||||
* they allow you to implement access controls without a database and
|
||||
* to integrate with external databases or APIs.
|
||||
* @docs https://next-auth.js.org/configuration/options#callbacks
|
||||
*/
|
||||
callbacks?: CallbacksOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#events */
|
||||
events?: EventsOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#adapter */
|
||||
adapter?: any
|
||||
/** @docs https://next-auth.js.org/configuration/options#debug */
|
||||
debug?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#usesecurecookies */
|
||||
useSecureCookies?: boolean
|
||||
/** @docs https://next-auth.js.org/configuration/options#cookies */
|
||||
cookies?: CookiesOptions
|
||||
/** @docs https://next-auth.js.org/configuration/options#logger */
|
||||
logger: LoggerInstance
|
||||
}
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
export type NextAuthSharedOptions = 'pages' | 'jwt' | 'events' | 'callbacks' | 'cookies' | 'secret' | 'adapter' | 'theme' | 'debug' | 'logger'
|
||||
|
||||
export interface NextAuthInternalOptions extends Pick<NextAuthOptions, NextAuthSharedOptions> {
|
||||
pkce?: {
|
||||
code_verifier?: string
|
||||
/**
|
||||
* Could be `"plain"`, but not recommended.
|
||||
* We ignore it for now.
|
||||
* @spec https://tools.ietf.org/html/rfc7636#section-4.2.
|
||||
*/
|
||||
code_challenge_method?: 'S256'
|
||||
}
|
||||
provider?: Provider
|
||||
baseUrl?: string
|
||||
basePath?: string
|
||||
action?: string
|
||||
csrfToken?: string
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
options: NextAuthInternalOptions
|
||||
}
|
||||
|
||||
export interface NextAuthResponse extends NextApiResponse {}
|
||||
|
||||
export declare function NextAuthHandler (req: NextAuthRequest, res: NextAuthResponse, options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
export declare function NextAuthHandler (options: NextAuthOptions): ReturnType<NextApiHandler>
|
||||
@@ -1,19 +1,19 @@
|
||||
import adapters from '../adapters'
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import logger, { setLogger } from '../lib/logger'
|
||||
import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
import parseProviders from './lib/providers'
|
||||
import cookie from './lib/cookie'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-req'
|
||||
import * as routes from './routes'
|
||||
import renderPage from './pages'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import createSecret from './lib/create-secret'
|
||||
import * as pkce from './lib/oauth/pkce-handler'
|
||||
import * as state from './lib/oauth/state-handler'
|
||||
import parseProviders from './lib/providers'
|
||||
import events from './lib/events'
|
||||
import callbacks from './lib/callbacks'
|
||||
import providers from './routes/providers'
|
||||
import signin from './routes/signin'
|
||||
import signout from './routes/signout'
|
||||
import callback from './routes/callback'
|
||||
import session from './routes/session'
|
||||
import pages from './pages'
|
||||
import adapters from '../adapters'
|
||||
import logger from '../lib/logger'
|
||||
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
@@ -21,80 +21,196 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("next").NextApiRequest} req
|
||||
* @param {import("next").NextApiResponse} res
|
||||
* @param {import(".").NextAuthOptions} userOptions
|
||||
*/
|
||||
async function NextAuthHandler (req, res, userOptions) {
|
||||
if (userOptions.logger) {
|
||||
setLogger(userOptions.logger)
|
||||
}
|
||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||
if (userOptions.debug) {
|
||||
process.env._NEXTAUTH_DEBUG = true
|
||||
}
|
||||
|
||||
async function NextAuth (req, res, userSuppliedOptions) {
|
||||
// To the best of my knowledge, we need to return a promise here
|
||||
// to avoid early termination of calls to the serverless function
|
||||
// (and then return that promise when we are done) - eslint
|
||||
// complains but I'm not sure there is another way to do this.
|
||||
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
|
||||
extendRes(req, res, resolve)
|
||||
// This is passed to all methods that handle responses, and must be called
|
||||
// when they are complete so that the serverless function knows when it is
|
||||
// safe to return and that no more data will be sent.
|
||||
const done = resolve
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
|
||||
|
||||
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
|
||||
return res.status(500).end(`Error: ${error}`)
|
||||
res
|
||||
.status(500)
|
||||
.end(
|
||||
`Error: ${error}`
|
||||
)
|
||||
return done()
|
||||
}
|
||||
|
||||
const { url, query, body } = req
|
||||
const {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
providerId = nextauth[1],
|
||||
provider = nextauth[1],
|
||||
error = nextauth[1]
|
||||
} = req.query
|
||||
} = query
|
||||
|
||||
// @todo refactor all existing references to baseUrl and basePath
|
||||
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||
const {
|
||||
csrfToken: csrfTokenFromPost
|
||||
} = body
|
||||
|
||||
const cookies = {
|
||||
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userOptions.cookies
|
||||
}
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const { csrfToken, csrfTokenVerified } = csrfTokenHandler(req, res, cookies, secret)
|
||||
|
||||
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
if (provider &&
|
||||
provider.type === 'oauth' && provider.version?.startsWith('2') &&
|
||||
(!provider.protection && provider.state !== false)
|
||||
) {
|
||||
provider.protection = 'state' // Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
}
|
||||
|
||||
const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle
|
||||
// @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
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
|
||||
let adapter
|
||||
if (userSuppliedOptions.adapter) {
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
adapter = userSuppliedOptions.adapter
|
||||
} else if (userSuppliedOptions.database) {
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
adapter = adapters.Default(userSuppliedOptions.database)
|
||||
}
|
||||
|
||||
// Secret used salt cookies and tokens (e.g. for CSRF protection).
|
||||
// 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({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
|
||||
|
||||
// Use secure cookies if the site uses HTTPS
|
||||
// This being conditional allows cookies to work non-HTTPS development URLs
|
||||
// Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
|
||||
// prefix, but enable them by default if the site URL is HTTPS; but not for
|
||||
// non-HTTPS URLs like http://localhost which are used in development).
|
||||
// For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
|
||||
const useSecureCookies = userSuppliedOptions.useSecureCookies || baseUrl.startsWith('https://')
|
||||
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
||||
|
||||
// @TODO Review cookie settings (names, options)
|
||||
const cookies = {
|
||||
// default cookie options
|
||||
sessionToken: {
|
||||
name: `${cookiePrefix}next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
callbackUrl: {
|
||||
name: `${cookiePrefix}next-auth.callback-url`,
|
||||
options: {
|
||||
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.
|
||||
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userSuppliedOptions.cookies
|
||||
}
|
||||
|
||||
// Session options
|
||||
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)
|
||||
...userSuppliedOptions.session
|
||||
}
|
||||
|
||||
// JWT options
|
||||
const jwtOptions = {
|
||||
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
|
||||
}
|
||||
|
||||
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
if (!adapter) {
|
||||
sessionOptions.jwt = true
|
||||
}
|
||||
|
||||
// Event messages
|
||||
const eventsOptions = {
|
||||
...events,
|
||||
...userSuppliedOptions.events
|
||||
}
|
||||
|
||||
// Callback functions
|
||||
const callbacksOptions = {
|
||||
...callbacks,
|
||||
...userSuppliedOptions.callbacks
|
||||
}
|
||||
|
||||
// Ensure CSRF Token cookie is set for any subsequent requests.
|
||||
// Used as part of the strateigy for mitigation for CSRF tokens.
|
||||
//
|
||||
// Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
|
||||
// where 'token' is the CSRF token and 'hash' is a hash made of the token and
|
||||
// the secret, and the two values are joined by a pipe '|'. By storing the
|
||||
// value and the hash of the value (with the secret used as a salt) we can
|
||||
// verify the cookie was set by the server and not by a malicous attacker.
|
||||
//
|
||||
// For more details, see the following OWASP links:
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
// https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
|
||||
let csrfToken
|
||||
let csrfTokenVerified = false
|
||||
if (req.cookies[cookies.csrfToken.name]) {
|
||||
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
|
||||
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
|
||||
// If hash matches then we trust the CSRF token value
|
||||
csrfToken = csrfTokenValue
|
||||
|
||||
// If this is a POST request and the CSRF Token in the Post request matches
|
||||
// the cookie we have already verified is one we have set, then token is verified!
|
||||
if (req.method === 'POST' && csrfToken === csrfTokenFromPost) { csrfTokenVerified = true }
|
||||
}
|
||||
}
|
||||
if (!csrfToken) {
|
||||
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
|
||||
// (e.g. because it's been modifed or because the secret has changed) create a new token.
|
||||
csrfToken = randomBytes(32).toString('hex')
|
||||
const newCsrfTokenCookie = `${csrfToken}|${createHash('sha256').update(`${csrfToken}${secret}`).digest('hex')}`
|
||||
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.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,
|
||||
// except for the options with special handling above
|
||||
req.options = {
|
||||
debug: false,
|
||||
pages: {},
|
||||
theme: 'auto',
|
||||
const options = {
|
||||
// Defaults options can be overidden
|
||||
debug: false, // Enable debug messages to be displayed
|
||||
pages: {}, // Custom pages (e.g. sign in, sign out, errors)
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
...userSuppliedOptions,
|
||||
// These computed settings can values in userSuppliedOptions but override them
|
||||
// and are request-specific.
|
||||
adapter,
|
||||
baseUrl,
|
||||
@@ -104,154 +220,116 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
cookies,
|
||||
secret,
|
||||
csrfToken,
|
||||
providers,
|
||||
// Session options
|
||||
session: {
|
||||
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
...userOptions.session
|
||||
},
|
||||
// JWT options
|
||||
jwt: {
|
||||
secret, // Use application secret if no keys specified
|
||||
maxAge, // same as session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userOptions.jwt
|
||||
},
|
||||
// Event messages
|
||||
events: {
|
||||
...defaultEvents,
|
||||
...userOptions.events
|
||||
},
|
||||
// Callback functions
|
||||
callbacks: {
|
||||
...defaultCallbacks,
|
||||
...userOptions.callbacks
|
||||
},
|
||||
pkce: {},
|
||||
logger
|
||||
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
|
||||
session: sessionOptions,
|
||||
jwt: jwtOptions,
|
||||
events: eventsOptions,
|
||||
callbacks: callbacksOptions,
|
||||
callbackUrl: baseUrl,
|
||||
redirect
|
||||
}
|
||||
|
||||
await callbackUrlHandler(req, res)
|
||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }
|
||||
|
||||
const render = renderPage(req, res)
|
||||
const { pages } = req.options
|
||||
// Get / Set callback URL based on query param / cookie + validation
|
||||
options.callbackUrl = await callbackUrlHandler(req, res, options)
|
||||
|
||||
if (req.method === 'GET') {
|
||||
switch (action) {
|
||||
case 'providers':
|
||||
return routes.providers(req, res)
|
||||
providers(req, res, options, done)
|
||||
break
|
||||
case 'session':
|
||||
return routes.session(req, res)
|
||||
session(req, res, options, done)
|
||||
break
|
||||
case 'csrf':
|
||||
return res.json({ csrfToken })
|
||||
res.json({ csrfToken })
|
||||
return done()
|
||||
case 'signin':
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) { signinUrl = `${signinUrl}&error=${error}` }
|
||||
return res.redirect(signinUrl)
|
||||
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)
|
||||
}
|
||||
|
||||
return render.signin()
|
||||
pages.render(req, res, 'signin', { baseUrl, basePath, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
|
||||
break
|
||||
case 'signout':
|
||||
if (pages.signOut) {
|
||||
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
|
||||
}
|
||||
return render.signout()
|
||||
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }
|
||||
|
||||
pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
|
||||
break
|
||||
case 'callback':
|
||||
if (provider) {
|
||||
if (await pkce.handleCallback(req, res)) return
|
||||
if (await state.handleCallback(req, res)) return
|
||||
return routes.callback(req, res)
|
||||
if (provider && options.providers[provider]) {
|
||||
callback(req, res, options, done)
|
||||
} else {
|
||||
res.status(400).end(`Error: HTTP GET is not supported for ${url}`)
|
||||
return done()
|
||||
}
|
||||
break
|
||||
case 'verify-request':
|
||||
if (pages.verifyRequest) {
|
||||
return res.redirect(pages.verifyRequest)
|
||||
}
|
||||
return render.verifyRequest()
|
||||
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
|
||||
|
||||
pages.render(req, res, 'verify-request', { baseUrl }, done)
|
||||
break
|
||||
case 'error':
|
||||
if (pages.error) {
|
||||
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
|
||||
}
|
||||
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
|
||||
|
||||
// These error messages are displayed in line on the sign in page
|
||||
if ([
|
||||
'Signin',
|
||||
'OAuthSignin',
|
||||
'OAuthCallback',
|
||||
'OAuthCreateAccount',
|
||||
'EmailCreateAccount',
|
||||
'Callback',
|
||||
'OAuthAccountNotLinked',
|
||||
'EmailSignin',
|
||||
'CredentialsSignin'
|
||||
].includes(error)) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
|
||||
break
|
||||
default:
|
||||
res.status(404).end()
|
||||
return done()
|
||||
}
|
||||
} else if (req.method === 'POST') {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (csrfTokenVerified && provider) {
|
||||
if (await pkce.handleSignin(req, res)) return
|
||||
if (await state.handleSignin(req, res)) return
|
||||
return routes.signin(req, res)
|
||||
if (!csrfTokenVerified) {
|
||||
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
case 'signout':
|
||||
// Verified CSRF Token required for signout
|
||||
if (csrfTokenVerified) {
|
||||
return routes.signout(req, res)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
case 'callback':
|
||||
if (provider) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (provider.type === 'credentials' && !csrfTokenVerified) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
if (await pkce.handleCallback(req, res)) return
|
||||
if (await state.handleCallback(req, res)) return
|
||||
return routes.callback(req, res)
|
||||
if (provider && options.providers[provider]) {
|
||||
signin(req, res, options, done)
|
||||
}
|
||||
break
|
||||
case '_log':
|
||||
if (userOptions.logger) {
|
||||
try {
|
||||
const {
|
||||
code = 'CLIENT_ERROR',
|
||||
level = 'error',
|
||||
message = '[]'
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error('LOGGER_ERROR', error)
|
||||
}
|
||||
case 'signout':
|
||||
// Verified CSRF Token required for signout
|
||||
if (!csrfTokenVerified) {
|
||||
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
}
|
||||
return res.end()
|
||||
|
||||
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}`)
|
||||
return done()
|
||||
}
|
||||
break
|
||||
default:
|
||||
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
|
||||
return done()
|
||||
}
|
||||
} else {
|
||||
res.status(400).end(`Error: HTTP ${req.method} is not supported for ${url}`)
|
||||
return done()
|
||||
}
|
||||
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
/** Tha main entry point to next-auth */
|
||||
export default function NextAuth (...args) {
|
||||
export default async (...args) => {
|
||||
if (args.length === 1) {
|
||||
return (req, res) => NextAuthHandler(req, res, args[0])
|
||||
return (req, res) => NextAuth(req, res, args[0])
|
||||
}
|
||||
|
||||
return NextAuthHandler(...args)
|
||||
return NextAuth(...args)
|
||||
}
|
||||
|
||||
@@ -1,221 +1,223 @@
|
||||
// This function handles the complex flow of signing users in, and either creating,
|
||||
// linking (or not linking) accounts depending on if the user is currently logged
|
||||
// in, if they have account already and the authentication mechanism they are using.
|
||||
//
|
||||
// It prevents insecure behaviour, such as linking oAuth accounts unless a user is
|
||||
// signed in and authenticated with an existing valid account.
|
||||
//
|
||||
// All verification (e.g. oAuth flows or email address verificaiton flows) are
|
||||
// done prior to this handler being called to avoid additonal complexity in this
|
||||
// handler.
|
||||
import { AccountNotLinkedError } from '../../lib/errors'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
|
||||
/**
|
||||
* This function handles the complex flow of signing users in, and either creating,
|
||||
* linking (or not linking) accounts depending on if the user is currently logged
|
||||
* in, if they have account already and the authentication mechanism they are using.
|
||||
*
|
||||
* It prevents insecure behaviour, such as linking OAuth accounts unless a user is
|
||||
* signed in and authenticated with an existing valid account.
|
||||
*
|
||||
* All verification (e.g. OAuth flows or email address verificaiton flows) are
|
||||
* done prior to this handler being called to avoid additonal complexity in this
|
||||
* handler.
|
||||
*/
|
||||
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
|
||||
// Input validation
|
||||
if (!profile) throw new Error('Missing profile')
|
||||
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
|
||||
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
|
||||
export default async (sessionToken, profile, providerAccount, options) => {
|
||||
try {
|
||||
// Input validation
|
||||
if (!profile) { throw new Error('Missing profile') }
|
||||
if (!providerAccount || !providerAccount.id || !providerAccount.type) { throw new Error('Missing or invalid provider account') }
|
||||
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: {
|
||||
jwt: useJwtSession
|
||||
const { adapter, jwt, events } = options
|
||||
|
||||
const useJwtSession = options.session.jwt
|
||||
|
||||
// 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.
|
||||
if (!adapter) {
|
||||
return {
|
||||
user: profile,
|
||||
account: providerAccount,
|
||||
session: {}
|
||||
}
|
||||
}
|
||||
} = options
|
||||
|
||||
// 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.
|
||||
if (!adapter) {
|
||||
return {
|
||||
user: profile,
|
||||
account: providerAccount,
|
||||
session: {}
|
||||
}
|
||||
}
|
||||
const {
|
||||
createUser,
|
||||
updateUser,
|
||||
getUser,
|
||||
getUserByProviderAccountId,
|
||||
getUserByEmail,
|
||||
linkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession
|
||||
} = await adapter.getAdapter(options)
|
||||
|
||||
const {
|
||||
createUser,
|
||||
updateUser,
|
||||
getUser,
|
||||
getUserByProviderAccountId,
|
||||
getUserByEmail,
|
||||
linkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession
|
||||
} = await adapter.getAdapter(options)
|
||||
let session = null
|
||||
let user = null
|
||||
let isSignedIn = null
|
||||
let isNewUser = false
|
||||
|
||||
let session = null
|
||||
let user = null
|
||||
let isSignedIn = null
|
||||
let isNewUser = false
|
||||
|
||||
if (sessionToken) {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
session = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
if (session?.sub) {
|
||||
user = await getUser(session.sub)
|
||||
if (sessionToken) {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
session = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
if (session && session.sub) {
|
||||
user = await getUser(session.sub)
|
||||
isSignedIn = !!user
|
||||
}
|
||||
} catch (e) {
|
||||
// If session can't be verified, treat as no session
|
||||
}
|
||||
} else {
|
||||
session = await getSession(sessionToken)
|
||||
if (session && session.userId) {
|
||||
user = await getUser(session.userId)
|
||||
isSignedIn = !!user
|
||||
}
|
||||
} catch {
|
||||
// If session can't be verified, treat as no session
|
||||
}
|
||||
}
|
||||
session = await getSession(sessionToken)
|
||||
if (session?.userId) {
|
||||
user = await getUser(session.userId)
|
||||
isSignedIn = !!user
|
||||
}
|
||||
}
|
||||
|
||||
if (providerAccount.type === 'email') {
|
||||
// If signing in with an email, check if an account with the same email address exists already
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
if (userByEmail) {
|
||||
// If they are not already signed in as the same user, this flow will
|
||||
// sign them out of the current session and sign them in as the new user
|
||||
if (isSignedIn) {
|
||||
if (user.id !== userByEmail.id && !useJwtSession) {
|
||||
// Delete existing session if they are currently signed in as another user.
|
||||
// This will switch user accounts for the session in cases where the user was
|
||||
// already logged in with a different account.
|
||||
await deleteSession(sessionToken)
|
||||
if (providerAccount.type === 'email') {
|
||||
// If signing in with an email, check if an account with the same email address exists already
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
if (userByEmail) {
|
||||
// If they are not already signed in as the same user, this flow will
|
||||
// sign them out of the current session and sign them in as the new user
|
||||
if (isSignedIn) {
|
||||
if (user.id !== userByEmail.id && !useJwtSession) {
|
||||
// Delete existing session if they are currently signed in as another user.
|
||||
// This will switch user accounts for the session in cases where the user was
|
||||
// already logged in with a different account.
|
||||
await deleteSession(sessionToken)
|
||||
}
|
||||
}
|
||||
|
||||
// Update emailVerified property on the user object
|
||||
const currentDate = new Date()
|
||||
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
|
||||
const currentDate = new Date()
|
||||
user = await createUser({ ...profile, emailVerified: currentDate })
|
||||
await dispatchEvent(events.createUser, user)
|
||||
isNewUser = true
|
||||
}
|
||||
|
||||
// Update emailVerified property on the user object
|
||||
const currentDate = new Date()
|
||||
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
|
||||
const currentDate = new Date()
|
||||
user = await createUser({ ...profile, emailVerified: currentDate })
|
||||
await dispatchEvent(events.createUser, user)
|
||||
isNewUser = true
|
||||
}
|
||||
// Create new session
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
|
||||
// Create new session
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
} else if (providerAccount.type === 'oauth') {
|
||||
// If signing in with oauth account, check to see if the account exists already
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
|
||||
if (userByProviderAccountId) {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in with this account, we don't need to do anything
|
||||
// Note: These are cast as strings here to ensure they match as in
|
||||
// some flows (e.g. JWT with a database) one of the values might be a
|
||||
// string and the other might be an ObjectID and would otherwise fail.
|
||||
if (`${userByProviderAccountId.id}` === `${user.id}`) {
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
} else {
|
||||
// If the user is currently signed in, but the new account they are signing in
|
||||
// with is already associated with another account, then we cannot link them
|
||||
// and need to return an error.
|
||||
throw new AccountNotLinkedError()
|
||||
}
|
||||
} else {
|
||||
// If there is no active session, but the account being signed in with is already
|
||||
// associated with a valid user then create session to sign the user in.
|
||||
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
|
||||
return {
|
||||
session,
|
||||
user: userByProviderAccountId,
|
||||
isNewUser
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in and the oAuth account isn't already associated
|
||||
// with another user account then we can go ahead and link the accounts safely.
|
||||
await linkAccount(
|
||||
user.id,
|
||||
providerAccount.provider,
|
||||
providerAccount.type,
|
||||
providerAccount.id,
|
||||
providerAccount.refreshToken,
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount })
|
||||
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
} else if (providerAccount.type === 'oauth') {
|
||||
// If signing in with oauth account, check to see if the account exists already
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
|
||||
if (userByProviderAccountId) {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in with this account, we don't need to do anything
|
||||
// Note: These are cast as strings here to ensure they match as in
|
||||
// some flows (e.g. JWT with a database) one of the values might be a
|
||||
// string and the other might be an ObjectID and would otherwise fail.
|
||||
if (`${userByProviderAccountId.id}` === `${user.id}`) {
|
||||
// As they are already signed in, we don't need to do anything after linking them
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
}
|
||||
// If the user is currently signed in, but the new account they are signing in
|
||||
// with is already associated with another account, then we cannot link them
|
||||
// and need to return an error.
|
||||
throw new AccountNotLinkedError()
|
||||
}
|
||||
// If there is no active session, but the account being signed in with is already
|
||||
// associated with a valid user then create session to sign the user in.
|
||||
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
|
||||
return {
|
||||
session,
|
||||
user: userByProviderAccountId,
|
||||
isNewUser
|
||||
}
|
||||
} else {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in and the OAuth account isn't already associated
|
||||
// with another user account then we can go ahead and link the accounts safely.
|
||||
await linkAccount(
|
||||
user.id,
|
||||
providerAccount.provider,
|
||||
providerAccount.type,
|
||||
providerAccount.id,
|
||||
providerAccount.refreshToken,
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
|
||||
// As they are already signed in, we don't need to do anything after linking them
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
// If the user is not signed in and it looks like a new oAuth account then we
|
||||
// check there also isn't an user account already associated with the same
|
||||
// email address as the one in the oAuth profile.
|
||||
//
|
||||
// This step is often overlooked in oAuth implementations, but covers the following cases:
|
||||
//
|
||||
// 1. It makes it harder for someone to accidentally create two accounts.
|
||||
// e.g. by signin in with email, then again with an oauth account connected to the same email.
|
||||
// 2. It makes it harder to hijack a user account using a 3rd party oAuth account.
|
||||
// e.g. by creating an oauth account then changing the email address associated with it.
|
||||
//
|
||||
// It's quite common for services to automatically link accounts in this case, but it's
|
||||
// better practice to require the user to sign in *then* link accounts to be sure
|
||||
// someone is not exploiting a problem with a third party oAuth service.
|
||||
//
|
||||
// oAuth providers should require email address verification to prevent this, but in
|
||||
// practice that is not always the case; this helps protect against that.
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
if (userByEmail) {
|
||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
||||
// we do already have an account with the same email address as the one in the
|
||||
// oAuth profile the user has just tried to sign in with.
|
||||
//
|
||||
// We don't want to have two accounts with the same email address, and we don't
|
||||
// want to link them in case it's not safe to do so, so instead we prompt the user
|
||||
// to sign in via email to verify their identity and then link the accounts.
|
||||
throw new AccountNotLinkedError()
|
||||
} else {
|
||||
// If the current user is not logged in and the profile isn't linked to any user
|
||||
// accounts (by email or provider account id)...
|
||||
//
|
||||
// If no account matching the same [provider].id or .email exists, we can
|
||||
// create a new account for the user, link it to the oAuth acccount and
|
||||
// create a new session for them so they are signed in with it.
|
||||
user = await createUser(profile)
|
||||
await dispatchEvent(events.createUser, user)
|
||||
|
||||
await linkAccount(
|
||||
user.id,
|
||||
providerAccount.provider,
|
||||
providerAccount.type,
|
||||
providerAccount.id,
|
||||
providerAccount.refreshToken,
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount })
|
||||
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
isNewUser = true
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is not signed in and it looks like a new OAuth account then we
|
||||
// check there also isn't an user account already associated with the same
|
||||
// email address as the one in the OAuth profile.
|
||||
//
|
||||
// This step is often overlooked in OAuth implementations, but covers the following cases:
|
||||
//
|
||||
// 1. It makes it harder for someone to accidentally create two accounts.
|
||||
// e.g. by signin in with email, then again with an oauth account connected to the same email.
|
||||
// 2. It makes it harder to hijack a user account using a 3rd party OAuth account.
|
||||
// e.g. by creating an oauth account then changing the email address associated with it.
|
||||
//
|
||||
// It's quite common for services to automatically link accounts in this case, but it's
|
||||
// better practice to require the user to sign in *then* link accounts to be sure
|
||||
// someone is not exploiting a problem with a third party OAuth service.
|
||||
//
|
||||
// OAuth providers should require email address verification to prevent this, but in
|
||||
// practice that is not always the case; this helps protect against that.
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
if (userByEmail) {
|
||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
||||
// we do already have an account with the same email address as the one in the
|
||||
// OAuth profile the user has just tried to sign in with.
|
||||
//
|
||||
// We don't want to have two accounts with the same email address, and we don't
|
||||
// want to link them in case it's not safe to do so, so instead we prompt the user
|
||||
// to sign in via email to verify their identity and then link the accounts.
|
||||
throw new AccountNotLinkedError()
|
||||
}
|
||||
// If the current user is not logged in and the profile isn't linked to any user
|
||||
// accounts (by email or provider account id)...
|
||||
//
|
||||
// If no account matching the same [provider].id or .email exists, we can
|
||||
// create a new account for the user, link it to the OAuth acccount and
|
||||
// create a new session for them so they are signed in with it.
|
||||
user = await createUser(profile)
|
||||
await dispatchEvent(events.createUser, user)
|
||||
|
||||
await linkAccount(
|
||||
user.id,
|
||||
providerAccount.provider,
|
||||
providerAccount.type,
|
||||
providerAccount.id,
|
||||
providerAccount.refreshToken,
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
isNewUser = true
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
}
|
||||
} else {
|
||||
return Promise.reject(new Error('Provider not supported'))
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import cookie from '../lib/cookie'
|
||||
|
||||
/**
|
||||
* Get callback URL based on query param / cookie + validation,
|
||||
* and add it to `req.options.callbackUrl`.
|
||||
* @note: `req.options` must already be defined when called.
|
||||
*/
|
||||
export default async function callbackUrlHandler (req, res) {
|
||||
export default async (req, res, options) => {
|
||||
const { query } = req
|
||||
const { body } = req
|
||||
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.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 || 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
|
||||
@@ -25,9 +21,7 @@ export default async function callbackUrlHandler (req, res) {
|
||||
}
|
||||
|
||||
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
|
||||
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) {
|
||||
cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options)
|
||||
}
|
||||
if (callbackUrl && (callbackUrl !== callbackUrlCookieValue)) { cookie.set(res, cookies.callbackUrl.name, callbackUrl, cookies.callbackUrl.options) }
|
||||
|
||||
req.options.callbackUrl = callbackUrl
|
||||
return Promise.resolve(callbackUrl)
|
||||
}
|
||||
|
||||
7
src/server/lib/callbacks.d.ts
vendored
7
src/server/lib/callbacks.d.ts
vendored
@@ -1,7 +0,0 @@
|
||||
|
||||
export interface CallbacksOptions {
|
||||
signIn?: (user: any, account: any, profile: any) => Promise<never | string>
|
||||
jwt?: (token: any, user: any, account: any, profile: any, isNewUser?: boolean) => Promise<any>
|
||||
session?: (session: any, userOrToken: any) => Promise<any>
|
||||
redirect?: (url: string, baseUrl: string) => Promise<string>
|
||||
}
|
||||
@@ -9,14 +9,19 @@
|
||||
* requests to sign in and again when they activate the link in the sign in
|
||||
* email.
|
||||
*
|
||||
* @param {object} profile User profile (e.g. user id, name, email)
|
||||
* @param {object} account Account used to sign in (e.g. OAuth account)
|
||||
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
|
||||
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
|
||||
* Return `false` to deny access
|
||||
* @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
|
||||
*/
|
||||
export async function signIn () {
|
||||
return true
|
||||
const signIn = async (profile, account, metadata) => {
|
||||
const isAllowedToSignIn = true
|
||||
if (isAllowedToSignIn) {
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,13 +31,12 @@ export async function signIn () {
|
||||
*
|
||||
* @param {string} url URL provided as callback URL by the client
|
||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||
* @return {Promise<string>} URL the client will be redirect to
|
||||
* @return {string} URL the client will be redirect to
|
||||
*/
|
||||
export async function redirect (url, baseUrl) {
|
||||
if (url.startsWith(baseUrl)) {
|
||||
return url
|
||||
}
|
||||
return baseUrl
|
||||
const redirect = async (url, baseUrl) => {
|
||||
return url.startsWith(baseUrl)
|
||||
? Promise.resolve(url)
|
||||
: Promise.resolve(baseUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,24 +45,31 @@ export async function redirect (url, baseUrl) {
|
||||
*
|
||||
* @param {object} session Session object
|
||||
* @param {object} token JSON Web Token (if enabled)
|
||||
* @return {Promise<object>} Session that will be returned to the client
|
||||
* @return {object} Session that will be returned to the client
|
||||
*/
|
||||
export async function session (session) {
|
||||
return session
|
||||
const session = async (session, token) => {
|
||||
return Promise.resolve(session)
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is called whenever a JSON Web Token is created / updated.
|
||||
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
|
||||
*
|
||||
* On initial sign in, the raw OAuthProfile is passed if the user is signing in
|
||||
* On initial sign in, the raw oAuthProfile is passed if the user is signing in
|
||||
* with an OAuth provider. It is not avalible on subsequent calls. You can
|
||||
* take advantage of this to persist additional data you need to in the JWT.
|
||||
*
|
||||
* @param {object} token Decrypted JSON Web Token
|
||||
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
||||
* @return {Promise<object>} JSON Web Token that will be saved
|
||||
* @return {object} JSON Web Token that will be saved
|
||||
*/
|
||||
export async function jwt (token) {
|
||||
return token
|
||||
const jwt = async (token, oAuthProfile) => {
|
||||
return Promise.resolve(token)
|
||||
}
|
||||
|
||||
export default {
|
||||
signIn,
|
||||
redirect,
|
||||
session,
|
||||
jwt
|
||||
}
|
||||
16
src/server/lib/cookie.d.ts
vendored
16
src/server/lib/cookie.d.ts
vendored
@@ -1,16 +0,0 @@
|
||||
export interface CookieOption {
|
||||
name: string
|
||||
options: {
|
||||
httpOnly: boolean
|
||||
sameSite: string
|
||||
path?: string
|
||||
secure: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface CookiesOptions {
|
||||
sessionToken: CookieOption
|
||||
callbackUrl: CookieOption
|
||||
csrfToken: CookieOption
|
||||
pkceCodeVerifier: CookieOption
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
/**
|
||||
* Function to set cookies server side
|
||||
*
|
||||
* Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
|
||||
* * https://github.com/jshttp/cookie/blob/master/index.js
|
||||
* * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
|
||||
*
|
||||
* As only partial functionlity is required, only the code we need has been incorporated here
|
||||
* (with fixes for specific issues) to keep dependancy size down.
|
||||
*/
|
||||
export function set (res, name, value, options = {}) {
|
||||
// Function to set cookies server side
|
||||
//
|
||||
// Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
|
||||
// * https://github.com/jshttp/cookie/blob/master/index.js
|
||||
// * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
|
||||
//
|
||||
// As only partial functionlity is required, only the code we need has been incorporated here
|
||||
// (with fixes for specific issues) to keep dependancy size down.
|
||||
const set = (res, name, value, options = {}) => {
|
||||
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
|
||||
|
||||
if ('maxAge' in options) {
|
||||
@@ -101,57 +99,6 @@ function _serialize (name, val, options) {
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Use secure cookies if the site uses HTTPS
|
||||
* This being conditional allows cookies to work non-HTTPS development URLs
|
||||
* Honour secure cookie option, which sets 'secure' and also adds '__Secure-'
|
||||
* prefix, but enable them by default if the site URL is HTTPS; but not for
|
||||
* non-HTTPS URLs like http://localhost which are used in development).
|
||||
* For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
|
||||
*
|
||||
* @TODO Review cookie settings (names, options)
|
||||
* @return {import("./cookie").CookiesOptions}
|
||||
*/
|
||||
export function defaultCookies (useSecureCookies) {
|
||||
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
||||
return {
|
||||
// default cookie options
|
||||
sessionToken: {
|
||||
name: `${cookiePrefix}next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
callbackUrl: {
|
||||
name: `${cookiePrefix}next-auth.callback-url`,
|
||||
options: {
|
||||
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.
|
||||
name: `${useSecureCookies ? '__Host-' : ''}next-auth.csrf-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
},
|
||||
pkceCodeVerifier: {
|
||||
name: `${cookiePrefix}next-auth.pkce.code_verifier`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
secure: useSecureCookies
|
||||
}
|
||||
}
|
||||
}
|
||||
export default {
|
||||
set
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
/**
|
||||
* Secret used salt cookies and tokens (e.g. for CSRF protection).
|
||||
* 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.
|
||||
*/
|
||||
export default function createSecret ({ userOptions, basePath, baseUrl }) {
|
||||
return userOptions.secret || createHash('sha256').update(JSON.stringify({
|
||||
baseUrl, basePath, ...userOptions
|
||||
})).digest('hex')
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
import * as cookie from './cookie'
|
||||
|
||||
/**
|
||||
* Ensure CSRF Token cookie is set for any subsequent requests.
|
||||
* Used as part of the strateigy for mitigation for CSRF tokens.
|
||||
*
|
||||
* Creates a cookie like 'next-auth.csrf-token' with the value 'token|hash',
|
||||
* where 'token' is the CSRF token and 'hash' is a hash made of the token and
|
||||
* the secret, and the two values are joined by a pipe '|'. By storing the
|
||||
* value and the hash of the value (with the secret used as a salt) we can
|
||||
* verify the cookie was set by the server and not by a malicous attacker.
|
||||
*
|
||||
* For more details, see the following OWASP links:
|
||||
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie
|
||||
* https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf
|
||||
*/
|
||||
export default function csrfTokenHandler (req, res, cookies, secret) {
|
||||
const { csrfToken: csrfTokenFromRequest } = req.body
|
||||
|
||||
let csrfTokenFromCookie
|
||||
let csrfTokenVerified = false
|
||||
if (req.cookies[cookies.csrfToken.name]) {
|
||||
const [csrfTokenValue, csrfTokenHash] = req.cookies[cookies.csrfToken.name].split('|')
|
||||
if (csrfTokenHash === createHash('sha256').update(`${csrfTokenValue}${secret}`).digest('hex')) {
|
||||
// If hash matches then we trust the CSRF token value
|
||||
csrfTokenFromCookie = csrfTokenValue
|
||||
|
||||
// If this is a POST request and the CSRF Token in the Post request matches
|
||||
// the cookie we have already verified is one we have set, then token is verified!
|
||||
if (req.method === 'POST' && csrfTokenFromCookie === csrfTokenFromRequest) { csrfTokenVerified = true }
|
||||
}
|
||||
}
|
||||
if (!csrfTokenFromCookie) {
|
||||
// If no csrfToken - because it's not been set yet, or because the hash doesn't match
|
||||
// (e.g. because it's been modifed or because the secret has changed) create a new token.
|
||||
csrfTokenFromCookie = randomBytes(32).toString('hex')
|
||||
const newCsrfTokenCookie = `${csrfTokenFromCookie}|${createHash('sha256').update(`${csrfTokenFromCookie}${secret}`).digest('hex')}`
|
||||
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
|
||||
}
|
||||
return { csrfToken: csrfTokenFromCookie, csrfTokenVerified }
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/** Event triggered on successful sign in */
|
||||
export async function signIn (message) {}
|
||||
|
||||
/** Event triggered on sign out */
|
||||
export async function signOut (message) {}
|
||||
|
||||
/** Event triggered on user creation */
|
||||
export async function createUser (message) {}
|
||||
|
||||
/** Event triggered when a user object is updated */
|
||||
export async function updateUser (message) {}
|
||||
|
||||
/** Event triggered when an account is linked to a user */
|
||||
export async function linkAccount (message) {}
|
||||
|
||||
/** Event triggered when a session is active */
|
||||
export async function session (message) {}
|
||||
|
||||
/**
|
||||
* @TODO Event triggered when something goes wrong in an authentication flow
|
||||
* This event may be fired multiple times when an error occurs
|
||||
*/
|
||||
export async function error (message) {}
|
||||
@@ -1,6 +1,6 @@
|
||||
import logger from '../../lib/logger'
|
||||
|
||||
export default async function dispatchEvent (event, message) {
|
||||
export default async (event, message) => {
|
||||
try {
|
||||
await event(message)
|
||||
} catch (e) {
|
||||
|
||||
12
src/server/lib/events.d.ts
vendored
12
src/server/lib/events.d.ts
vendored
@@ -1,12 +0,0 @@
|
||||
export type EventType=
|
||||
| 'signIn'
|
||||
| 'signOut'
|
||||
| 'createUser'
|
||||
| 'updateUser'
|
||||
| 'linkAccount'
|
||||
| 'session'
|
||||
| 'error'
|
||||
|
||||
export type EventCallback = (message: any) => Promise<void>
|
||||
|
||||
export type EventsOptions = Partial<Record<EventType, EventCallback>>
|
||||
38
src/server/lib/events.js
Normal file
38
src/server/lib/events.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const signIn = async (message) => {
|
||||
// Event triggered on successful sign in
|
||||
}
|
||||
|
||||
const signOut = async (message) => {
|
||||
// Event triggered on sign out
|
||||
}
|
||||
|
||||
const createUser = async (message) => {
|
||||
// Event triggered on user creation
|
||||
}
|
||||
|
||||
const updateUser = async (message) => {
|
||||
// Event triggered when a user object is updated
|
||||
}
|
||||
|
||||
const linkAccount = async (message) => {
|
||||
// Event triggered when an account is linked to a user
|
||||
}
|
||||
|
||||
const session = async (message) => {
|
||||
// Event triggered when a session is active
|
||||
}
|
||||
|
||||
const error = async (message) => {
|
||||
// @TODO Event triggered when something goes wrong in an authentication flow
|
||||
// This event may be fired multiple times when an error occurs
|
||||
}
|
||||
|
||||
export default {
|
||||
signIn,
|
||||
signOut,
|
||||
createUser,
|
||||
updateUser,
|
||||
linkAccount,
|
||||
session,
|
||||
error
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Extends res.{end,json,send} with `done()`,
|
||||
* and redirect to support sending url as json.
|
||||
*
|
||||
* When a response is complete, it will call the `done` method,
|
||||
* so that the serverless function knows when it is
|
||||
* safe to return and that no more data will be sent.
|
||||
*/
|
||||
export default function extendRes (req, res, done) {
|
||||
const originalResEnd = res.end.bind(res)
|
||||
res.end = (...args) => {
|
||||
done()
|
||||
return originalResEnd(...args)
|
||||
}
|
||||
|
||||
const originalResJson = res.json.bind(res)
|
||||
res.json = (...args) => {
|
||||
done()
|
||||
return originalResJson(...args)
|
||||
}
|
||||
|
||||
const originalResSend = res.send.bind(res)
|
||||
res.send = (...args) => {
|
||||
done()
|
||||
return originalResSend(...args)
|
||||
}
|
||||
|
||||
res.redirect = (url) => {
|
||||
if (req.body?.json === 'true') {
|
||||
return res.json({ url })
|
||||
}
|
||||
res.status(302).setHeader('Location', url)
|
||||
return res.end()
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,50 @@
|
||||
import { decode as jwtDecode } from 'jsonwebtoken'
|
||||
|
||||
import { createHash } from 'crypto'
|
||||
import querystring from 'querystring'
|
||||
import jwtDecode from 'jwt-decode'
|
||||
import oAuthClient from './client'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
|
||||
/** @param {import("../..").NextAuthRequest} req */
|
||||
export default async function oAuthCallback (req) {
|
||||
const { provider, pkce } = req.options
|
||||
// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
|
||||
// These methods have been forked from `node-oauth` to fix bugs; it may make
|
||||
// sense to migrate all the methods we need from node-oauth to nexth-auth (with
|
||||
// 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.
|
||||
|
||||
// @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?.startsWith('2.')) {
|
||||
// The "user" object is specific to the Apple provider and is provided on first sign in
|
||||
// e.g. {"name":{"firstName":"Johnny","lastName":"Appleseed"},"email":"johnny.appleseed@nextauth.com"}
|
||||
let { code, user } = req.query // eslint-disable-line camelcase
|
||||
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') {
|
||||
try {
|
||||
const body = JSON.parse(JSON.stringify(req.body))
|
||||
if (body.error) {
|
||||
throw new Error(body.error)
|
||||
}
|
||||
if (body.error) { throw new Error(body.error) }
|
||||
|
||||
code = body.code
|
||||
user = body.user != null ? JSON.parse(body.user) : null
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
|
||||
throw error
|
||||
} catch (e) {
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', e, req.body, provider.id, code)
|
||||
return callback()
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW: Is this used by any of the providers?
|
||||
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
|
||||
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
|
||||
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
|
||||
@@ -36,93 +52,105 @@ export default async function oAuthCallback (req) {
|
||||
client.useAuthorizationHeaderforGET(true)
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await client.getOAuthAccessToken(code, provider, pkce.code_verifier)
|
||||
let profileData
|
||||
if (provider.idToken) {
|
||||
if (!tokens?.id_token) {
|
||||
throw new OAuthCallbackError('Missing JWT ID Token')
|
||||
// Use custom getOAuthAccessToken() method for oAuth2 flows
|
||||
client.getOAuthAccessToken = _getOAuthAccessToken
|
||||
|
||||
await client.getOAuthAccessToken(
|
||||
code,
|
||||
provider,
|
||||
(error, accessToken, refreshToken, results) => {
|
||||
if (error || results.error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, results, provider.id, code)
|
||||
return callback(error || results.error)
|
||||
}
|
||||
|
||||
// Support services that use OpenID ID Tokens to encode profile data
|
||||
profileData = jwtDecode(tokens.id_token, { json: true })
|
||||
} else {
|
||||
profileData = await client.get(provider, tokens.accessToken, tokens)
|
||||
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,
|
||||
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
|
||||
client.get = _get
|
||||
|
||||
client.get(
|
||||
provider,
|
||||
accessToken,
|
||||
results,
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return getProfile({ profileData, provider, tokens, user })
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle OAuth v1.x
|
||||
const {
|
||||
oauth_token: oauthToken, oauth_verifier: oauthVerifier
|
||||
} = req.query
|
||||
const tokens = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
|
||||
const profileData = await client.get(
|
||||
provider.profileUrl,
|
||||
tokens.accessToken,
|
||||
tokens.refreshToken
|
||||
)
|
||||
} else {
|
||||
// Handle oAuth v1.x
|
||||
await client.getOAuthAccessToken(
|
||||
oauth_token,
|
||||
null,
|
||||
oauth_verifier,
|
||||
(error, accessToken, refreshToken, results) => {
|
||||
// @TODO Handle error
|
||||
if (error || results.error) {
|
||||
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error, results)
|
||||
}
|
||||
|
||||
return getProfile({ profileData, tokens, provider })
|
||||
} catch (error) {
|
||||
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
|
||||
throw error
|
||||
client.get(
|
||||
provider.profileUrl,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
async (error, profileData) => {
|
||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
||||
callback(error, profile, account, OAuthProfile)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
|
||||
* Returns profile, raw profile and auth provider details
|
||||
* @param {{
|
||||
* profileData: object | string
|
||||
* tokens: {
|
||||
* accessToken: string
|
||||
* idToken?: string
|
||||
* refreshToken?: string
|
||||
* access_token: string
|
||||
* expires_in?: string | Date | null
|
||||
* refresh_token?: string
|
||||
* id_token?: string
|
||||
* }
|
||||
* provider: import("../..").Provider
|
||||
* user?: object
|
||||
* }} profileParams
|
||||
*/
|
||||
async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
async function _getProfile (error, profileData, accessToken, refreshToken, provider, userData) {
|
||||
// @TODO Handle error
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_PROFILE_ERROR', error)
|
||||
}
|
||||
|
||||
let profile = {}
|
||||
try {
|
||||
// Convert profileData into an object if it's a string
|
||||
if (typeof profileData === 'string' || profileData instanceof String) {
|
||||
profileData = JSON.parse(profileData)
|
||||
}
|
||||
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 (user != null) {
|
||||
profileData.user = user
|
||||
if (userData != null) {
|
||||
profileData.user = userData
|
||||
}
|
||||
|
||||
logger.debug('PROFILE_DATA', profileData)
|
||||
|
||||
const profile = await provider.profile(profileData, tokens)
|
||||
// Return profile, raw profile and auth provider details
|
||||
return {
|
||||
profile: {
|
||||
...profile,
|
||||
email: profile.email?.toLowerCase() ?? null
|
||||
},
|
||||
account: {
|
||||
provider: provider.id,
|
||||
type: provider.type,
|
||||
id: profile.id,
|
||||
...tokens
|
||||
},
|
||||
OAuthProfile: profileData
|
||||
}
|
||||
profile = await provider.profile(profileData)
|
||||
} catch (exception) {
|
||||
// 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.
|
||||
@@ -138,4 +166,123 @@ async function getProfile ({ profileData, tokens, provider, user }) {
|
||||
OAuthProfile: profileData
|
||||
}
|
||||
}
|
||||
|
||||
// Return profile, raw profile and auth provider details
|
||||
return {
|
||||
profile: {
|
||||
name: profile.name,
|
||||
email: profile.email ? profile.email.toLowerCase() : null,
|
||||
image: profile.image
|
||||
},
|
||||
account: {
|
||||
provider: provider.id,
|
||||
type: provider.type,
|
||||
id: profile.id,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires: null
|
||||
},
|
||||
OAuthProfile: profileData
|
||||
}
|
||||
}
|
||||
|
||||
// Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
async function _getOAuthAccessToken (code, provider, callback) {
|
||||
const url = provider.accessTokenUrl
|
||||
const setGetAccessTokenAuthHeader = (provider.setGetAccessTokenAuthHeader !== null) ? provider.setGetAccessTokenAuthHeader : true
|
||||
const params = { ...provider.params } || {}
|
||||
const headers = { ...provider.headers } || {}
|
||||
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
|
||||
|
||||
if (!params[codeParam]) { params[codeParam] = code }
|
||||
|
||||
if (!params.client_id) { params.client_id = provider.clientId }
|
||||
|
||||
if (!params.client_secret) {
|
||||
// For some providers it useful to be able to generate the secret on the fly
|
||||
// e.g. For Sign in With Apple a JWT token using the properties in clientSecret
|
||||
if (provider.clientSecretCallback) {
|
||||
params.client_secret = await provider.clientSecretCallback(provider.clientSecret)
|
||||
} else {
|
||||
params.client_secret = provider.clientSecret
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
|
||||
|
||||
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
|
||||
// Added as a fix to accomodate change in Twitch oAuth API
|
||||
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
|
||||
// Added as a fix for Reddit Authentication
|
||||
if (provider.id === 'reddit') {
|
||||
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
|
||||
}
|
||||
// Okta errors when this is set. Maybe there are other Providers that also wont like this.
|
||||
if (setGetAccessTokenAuthHeader) {
|
||||
if (!headers.Authorization) { headers.Authorization = `Bearer ${code}` }
|
||||
}
|
||||
|
||||
const postData = querystring.stringify(params)
|
||||
|
||||
this._request(
|
||||
'POST',
|
||||
url,
|
||||
headers,
|
||||
postData,
|
||||
null,
|
||||
(error, data, response) => {
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
let results
|
||||
try {
|
||||
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
|
||||
// responses should be in JSON
|
||||
results = JSON.parse(data)
|
||||
} catch (e) {
|
||||
// However both Facebook + Github currently use rev05 of the spec and neither
|
||||
// seem to specify a content-type correctly in their response headers. :(
|
||||
// Clients of these services suffer a minor performance cost.
|
||||
results = querystring.parse(data)
|
||||
}
|
||||
const accessToken = provider.accessTokenGetter ? provider.accessTokenGetter(results) : results.access_token
|
||||
const refreshToken = results.refresh_token
|
||||
callback(null, accessToken, refreshToken, results)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
*
|
||||
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
|
||||
* e.g. see providers/bungie
|
||||
*/
|
||||
function _get (provider, accessToken, results, callback) {
|
||||
let url = provider.profileUrl
|
||||
const headers = provider.headers || {}
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
headers.Authorization = this.buildAuthHeader(accessToken)
|
||||
|
||||
// This line is required for Twitch
|
||||
headers['Client-ID'] = provider.clientId
|
||||
accessToken = null
|
||||
}
|
||||
|
||||
const prepareRequest = provider.prepareProfileRequest
|
||||
if (prepareRequest) {
|
||||
url = prepareRequest({ provider, url, headers, results }) || url
|
||||
}
|
||||
|
||||
this._request('GET', url, headers, null, accessToken, callback)
|
||||
}
|
||||
|
||||
function _decodeToken (provider, accessToken, refreshToken, idToken, callback) {
|
||||
if (!idToken) { throw new Error('Missing JWT ID Token', provider, idToken) }
|
||||
const decodedToken = jwtDecode(idToken)
|
||||
const profileData = JSON.stringify(decodedToken)
|
||||
callback(null, profileData, accessToken, refreshToken, provider)
|
||||
}
|
||||
|
||||
@@ -1,251 +1,31 @@
|
||||
// @TODO Refactor to remove dependancy on 'oauth' package
|
||||
// It is already quite monkey patched, we don't use all the features and and it
|
||||
// would be easier to maintain if all the code was native to next-auth.
|
||||
import { OAuth, OAuth2 } from 'oauth'
|
||||
import querystring from 'querystring'
|
||||
import logger from '../../../lib/logger'
|
||||
import { sign as jwtSign } from 'jsonwebtoken'
|
||||
|
||||
/**
|
||||
* @TODO Refactor to remove dependancy on 'oauth' package
|
||||
* It is already quite monkey patched, we don't use all the features and and it
|
||||
* would be easier to maintain if all the code was native to next-auth.
|
||||
* @param {import("../..").Provider} provider
|
||||
*/
|
||||
export default function oAuthClient (provider) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
// Handle OAuth v2.x
|
||||
const authorizationUrl = new URL(provider.authorizationUrl)
|
||||
const basePath = authorizationUrl.origin
|
||||
const authorizePath = authorizationUrl.pathname
|
||||
export default (provider) => {
|
||||
if (provider.version && provider.version.startsWith('2.')) {
|
||||
// Handle oAuth v2.x
|
||||
const basePath = new URL(provider.authorizationUrl).origin
|
||||
const authorizePath = new URL(provider.authorizationUrl).pathname
|
||||
const accessTokenPath = new URL(provider.accessTokenUrl).pathname
|
||||
const oauth2Client = new OAuth2(
|
||||
return new OAuth2(
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
basePath,
|
||||
authorizePath,
|
||||
accessTokenPath,
|
||||
provider.headers
|
||||
)
|
||||
oauth2Client.getOAuthAccessToken = getOAuth2AccessToken
|
||||
oauth2Client.get = getOAuth2
|
||||
return oauth2Client
|
||||
}
|
||||
// Handle OAuth v1.x
|
||||
const oauth1Client = new OAuth(
|
||||
provider.requestTokenUrl,
|
||||
provider.accessTokenUrl,
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
provider.version || '1.0',
|
||||
provider.callbackUrl,
|
||||
provider.encoding || 'HMAC-SHA1'
|
||||
)
|
||||
|
||||
// Promisify get() and getOAuth2AccessToken() for OAuth1
|
||||
const originalGet = oauth1Client.get.bind(oauth1Client)
|
||||
oauth1Client.get = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
originalGet(...args, (error, result) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthAccessToken = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
originalGetOAuth1AccessToken(...args, (error, accessToken, refreshToken, results) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve({ accessToken, refreshToken, results })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const originalGetOAuthRequestToken = oauth1Client.getOAuthRequestToken.bind(oauth1Client)
|
||||
oauth1Client.getOAuthRequestToken = (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
originalGetOAuthRequestToken(...args, (error, oauthToken) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(oauthToken)
|
||||
})
|
||||
})
|
||||
}
|
||||
return oauth1Client
|
||||
}
|
||||
|
||||
/**
|
||||
* @TODO Refactor monkey patching in OAuth2.getOAuthAccessToken() and OAuth2.get()
|
||||
* These methods have been forked from `node-oauth` to fix bugs; it may make
|
||||
* sense to migrate all the methods we need from node-oauth to nexth-auth (with
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
* @param {string} code
|
||||
* @param {import("../..").Provider} provider
|
||||
* @param {string | undefined} codeVerifier
|
||||
*/
|
||||
async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
const url = provider.accessTokenUrl
|
||||
const params = { ...provider.params }
|
||||
const headers = { ...provider.headers }
|
||||
const codeParam = (params.grant_type === 'refresh_token') ? 'refresh_token' : 'code'
|
||||
|
||||
if (!params[codeParam]) { params[codeParam] = code }
|
||||
|
||||
if (!params.client_id) { params.client_id = provider.clientId }
|
||||
|
||||
// For Apple the client secret must be generated on-the-fly.
|
||||
// Using the properties in clientSecret to create a JWT.
|
||||
if (provider.id === 'apple' && typeof provider.clientSecret === 'object') {
|
||||
const { keyId, teamId, privateKey } = provider.clientSecret
|
||||
const clientSecret = jwtSign({
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + (86400 * 180), // 6 months
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: provider.clientId
|
||||
},
|
||||
// Automatically convert \\n into \n if found in private key. If the key
|
||||
// is passed in an environment variable \n can get escaped as \\n
|
||||
privateKey.replace(/\\n/g, '\n'),
|
||||
{ algorithm: 'ES256', keyid: keyId }
|
||||
)
|
||||
params.client_secret = clientSecret
|
||||
provider.headers)
|
||||
} else {
|
||||
params.client_secret = provider.clientSecret
|
||||
}
|
||||
|
||||
if (!params.redirect_uri) { params.redirect_uri = provider.callbackUrl }
|
||||
|
||||
if (!headers['Content-Type']) { headers['Content-Type'] = 'application/x-www-form-urlencoded' }
|
||||
// Added as a fix to accomodate change in Twitch OAuth API
|
||||
if (!headers['Client-ID']) { headers['Client-ID'] = provider.clientId }
|
||||
// Added as a fix for Reddit Authentication
|
||||
if (provider.id === 'reddit') {
|
||||
headers.Authorization = 'Basic ' + Buffer.from((provider.clientId + ':' + provider.clientSecret)).toString('base64')
|
||||
}
|
||||
|
||||
if (provider.id === 'identity-server4' && !headers.Authorization) {
|
||||
headers.Authorization = `Bearer ${code}`
|
||||
}
|
||||
|
||||
if (provider.protection === 'pkce') {
|
||||
params.code_verifier = codeVerifier
|
||||
}
|
||||
|
||||
const postData = querystring.stringify(params)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request(
|
||||
'POST',
|
||||
url,
|
||||
headers,
|
||||
postData,
|
||||
null,
|
||||
(error, data, response) => {
|
||||
if (error) {
|
||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, data, response)
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
let raw
|
||||
try {
|
||||
// As of http://tools.ietf.org/html/draft-ietf-oauth-v2-07
|
||||
// responses should be in JSON
|
||||
raw = JSON.parse(data)
|
||||
} catch {
|
||||
// However both Facebook + Github currently use rev05 of the spec and neither
|
||||
// seem to specify a content-type correctly in their response headers. :(
|
||||
// Clients of these services suffer a minor performance cost.
|
||||
raw = querystring.parse(data)
|
||||
}
|
||||
|
||||
let accessToken
|
||||
if (provider.id === 'slack') {
|
||||
const { ok, error } = raw
|
||||
if (!ok) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
accessToken = raw.authed_user.access_token
|
||||
} else {
|
||||
accessToken = raw.access_token
|
||||
}
|
||||
|
||||
resolve({
|
||||
accessToken,
|
||||
accessTokenExpires: null,
|
||||
refreshToken: raw.refresh_token,
|
||||
idToken: raw.id_token,
|
||||
...raw
|
||||
})
|
||||
}
|
||||
// Handle oAuth v1.x
|
||||
return new OAuth(
|
||||
provider.requestTokenUrl,
|
||||
provider.accessTokenUrl,
|
||||
provider.clientId,
|
||||
provider.clientSecret,
|
||||
(provider.version || '1.0'),
|
||||
provider.callbackUrl,
|
||||
(provider.encoding || 'HMAC-SHA1')
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/ciaranj/node-oauth/blob/a7f8a1e21c362eb4ed2039431fb9ac2ae749f26a/lib/oauth2.js
|
||||
*
|
||||
* 18/08/2020 @robertcraigie added results parameter to pass data to an optional request preparer.
|
||||
* e.g. see providers/bungie
|
||||
* @param {import("../..").Provider} provider
|
||||
* @param {string} accessToken
|
||||
* @param {any} results
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
headers.Authorization = this.buildAuthHeader(accessToken)
|
||||
|
||||
// Mail.ru & vk.com require 'access_token' as URL request parameter
|
||||
if (['mailru', 'vk'].includes(provider.id)) {
|
||||
const safeAccessTokenURL = new URL(url)
|
||||
safeAccessTokenURL.searchParams.append('access_token', accessToken)
|
||||
url = safeAccessTokenURL.href
|
||||
}
|
||||
|
||||
// This line is required for Twitch
|
||||
if (provider.id === 'twitch') {
|
||||
headers['Client-ID'] = provider.clientId
|
||||
}
|
||||
accessToken = null
|
||||
}
|
||||
|
||||
if (provider.id === 'bungie') {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve(profileData)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** Bungie needs special handling */
|
||||
function prepareProfileUrl ({ provider, url, results }) {
|
||||
if (!results.membership_id) {
|
||||
// internal error
|
||||
// @TODO: handle better
|
||||
throw new Error('Expected membership_id to be passed.')
|
||||
}
|
||||
|
||||
if (!provider.headers?.['X-API-Key']) {
|
||||
throw new Error('The Bungie provider requires the X-API-Key option to be present in "headers".')
|
||||
}
|
||||
|
||||
return url.replace('{membershipId}', results.membership_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import pkceChallenge from 'pkce-challenge'
|
||||
import * as cookie from '../cookie'
|
||||
import jwt from '../../../lib/jwt'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
|
||||
const PKCE_LENGTH = 64
|
||||
const PKCE_CODE_CHALLENGE_METHOD = 'S256' // can be 'plain', not recommended https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/**
|
||||
* Adds `code_verifier` to `req.options.pkce`, and removes the corresponding cookie
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
if (!(cookies.pkceCodeVerifier.name in req.cookies)) {
|
||||
throw new OAuthCallbackError('The code_verifier cookie was not found.')
|
||||
}
|
||||
const pkce = await jwt.decode({
|
||||
...req.options.jwt,
|
||||
token: req.cookies[cookies.pkceCodeVerifier.name],
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
encryption: true
|
||||
})
|
||||
req.options.pkce = pkce
|
||||
logger.debug('OAUTH_CALLBACK_PROTECTION', 'Read PKCE verifier from cookie', {
|
||||
code_verifier: pkce.code_verifier,
|
||||
pkceLength: PKCE_LENGTH,
|
||||
method: PKCE_CODE_CHALLENGE_METHOD
|
||||
})
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, null, { maxAge: 0 }) // remove PKCE after it has been used
|
||||
} catch (error) {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `code_challenge` and `code_challenge_method` to `req.options.pkce`.
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
const { cookies, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'pkce') { // Provider does not support PKCE, nothing to do.
|
||||
return
|
||||
}
|
||||
// Started login flow, add generated pkce to req.options and (encrypted) code_verifier to a cookie
|
||||
const pkce = pkceChallenge(PKCE_LENGTH)
|
||||
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE challenge/verifier', {
|
||||
...pkce,
|
||||
pkceLength: PKCE_LENGTH,
|
||||
method: PKCE_CODE_CHALLENGE_METHOD
|
||||
})
|
||||
|
||||
provider.authorizationParams = {
|
||||
...provider.authorizationParams,
|
||||
code_challenge: pkce.code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD
|
||||
}
|
||||
|
||||
const encryptedCodeVerifier = await jwt.encode({
|
||||
...req.options.jwt,
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
token: { code_verifier: pkce.code_verifier },
|
||||
encryption: true
|
||||
})
|
||||
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (PKCE_MAX_AGE * 1000))
|
||||
cookie.set(res, cookies.pkceCodeVerifier.name, encryptedCodeVerifier, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.pkceCodeVerifier.options
|
||||
})
|
||||
logger.debug('OAUTH_SIGNIN_PROTECTION', 'Created PKCE code_verifier saved in cookie')
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
}
|
||||
|
||||
export default { handleSignin, handleCallback }
|
||||
@@ -1,70 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
import logger from '../../../lib/logger'
|
||||
import { OAuthCallbackError } from '../../../lib/errors'
|
||||
|
||||
/**
|
||||
* For OAuth 2.0 flows, if the provider supports state,
|
||||
* check if state matches the one sent on signin
|
||||
* (a hash of the NextAuth.js CSRF token).
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleCallback (req, res) {
|
||||
const { csrfToken, provider, baseUrl, basePath } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
const { state } = req.query
|
||||
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
||||
|
||||
logger.debug(
|
||||
'OAUTH_CALLBACK_PROTECTION',
|
||||
'Comparing received and expected state',
|
||||
{ state, expectedState }
|
||||
)
|
||||
if (state !== expectedState) {
|
||||
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('STATE_ERROR', error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds CSRF token to the authorizationParams.
|
||||
* @param {import("../..").NextAuthRequest} req
|
||||
* @param {import("../..").NextAuthResponse} res
|
||||
*/
|
||||
export async function handleSignin (req, res) {
|
||||
const { provider, baseUrl, basePath, csrfToken } = req.options
|
||||
try {
|
||||
if (provider.protection !== 'state') { // Provider does not support state, nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
if ('state' in provider) {
|
||||
logger.warn(
|
||||
'STATE_OPTION_DEPRECATION',
|
||||
'The `state` provider option is being replaced with `protection`. See the docs.'
|
||||
)
|
||||
}
|
||||
|
||||
// A hash of the NextAuth.js CSRF token is used as the state
|
||||
const state = createHash('sha256').update(csrfToken).digest('hex')
|
||||
|
||||
provider.authorizationParams = { ...provider.authorizationParams, state }
|
||||
logger.debug(
|
||||
'OAUTH_CALLBACK_PROTECTION',
|
||||
'Added state to authorization params',
|
||||
{ state }
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
}
|
||||
|
||||
export default { handleSignin, handleCallback }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user