mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
69 Commits
@auth/soli
...
v3.2.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb8ec8a469 | ||
|
|
65504d6917 | ||
|
|
3fcdd22656 | ||
|
|
7a1d712096 | ||
|
|
f7ff4c9219 | ||
|
|
20f40d027a | ||
|
|
b5384e7403 | ||
|
|
b5c4e91f17 | ||
|
|
f1f144951a | ||
|
|
0380edfae9 | ||
|
|
4d89b27784 | ||
|
|
e17acb6762 | ||
|
|
91e26ca475 | ||
|
|
c8e76b4b5d | ||
|
|
a8362ec380 | ||
|
|
f2ad69358f | ||
|
|
ca06976422 | ||
|
|
7fa4275340 | ||
|
|
c684336b32 | ||
|
|
82d16e6ac4 | ||
|
|
bf7efbc252 | ||
|
|
b9862b86b5 | ||
|
|
9b579b5fcb | ||
|
|
abcf845ebf | ||
|
|
ee398d1acd | ||
|
|
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
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -4,12 +4,16 @@ about: Ask a question about NextAuth.js or for help using it
|
|||||||
labels: question
|
labels: question
|
||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
<!-- NOTE: Questions will be converted to Discussions. You can find them at https://github.com/nextauthjs/next-auth/discussions! -->
|
||||||
|
|
||||||
**Your question**
|
**Your question**
|
||||||
A clear and concise question.
|
<!-- A clear and concise question. -->
|
||||||
|
|
||||||
**What are you trying to do**
|
**What are you trying to do**
|
||||||
A description of what you are trying to do, for context.
|
<!-- A description of what you are trying to do, for context. -->
|
||||||
|
|
||||||
|
**Reproduction**
|
||||||
|
<!-- If your question is code related, adding a reproduction to your use case can greatly reduce the time it takes us to figure out how to better help you. -->
|
||||||
|
|
||||||
**Feedback**
|
**Feedback**
|
||||||
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
*Documentation refers to searching through [online documentation](https://next-auth.js.org), code comments and issue history. The example project refers to [next-auth-example](https://github.com/iaincollins/next-auth-example).*
|
||||||
|
|||||||
41
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
41
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<!--
|
||||||
|
Thanks for your interest in the project. Bugs filed and PRs submitted are appreciated!
|
||||||
|
|
||||||
|
Please make sure that you are familiar with and follow the Code of Conduct for
|
||||||
|
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).
|
||||||
|
|
||||||
|
If you're new to contributing to open source projects, you might find this free
|
||||||
|
video course helpful: https://kcd.im/pull-request
|
||||||
|
|
||||||
|
Please fill out the information below to expedite the review and (hopefully)
|
||||||
|
merge of your pull request!
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||||
|
|
||||||
|
**What**:
|
||||||
|
|
||||||
|
<!-- Why are these changes necessary? -->
|
||||||
|
|
||||||
|
**Why**:
|
||||||
|
|
||||||
|
<!-- How were these changes implemented? -->
|
||||||
|
|
||||||
|
**How**:
|
||||||
|
|
||||||
|
<!-- Have you done all of these things? -->
|
||||||
|
|
||||||
|
**Checklist**:
|
||||||
|
|
||||||
|
<!-- add "N/A" to the end of each line that's irrelevant to your changes -->
|
||||||
|
<!-- to check an item, place an "x" in the box like so: "- [x] Documentation" -->
|
||||||
|
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Ready to be merged
|
||||||
|
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||||
|
|
||||||
|
<!-- feel free to add additional comments -->
|
||||||
21
.github/labeler.yml
vendored
Normal file
21
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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/**/*
|
||||||
8
.github/stale.yml
vendored
8
.github/stale.yml
vendored
@@ -8,15 +8,17 @@ exemptLabels:
|
|||||||
- security
|
- security
|
||||||
- priority
|
- priority
|
||||||
# Label to use when marking an issue as stale
|
# Label to use when marking an issue as stale
|
||||||
staleLabel: wontfix
|
staleLabel: stale
|
||||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
markComment: >
|
markComment: >
|
||||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||||
It will be closed if no further activity occurs. If you think your issue
|
It will be closed if no further activity occurs. If you think your issue
|
||||||
is still relevant, feel free to comment on it to keep ot open. Thanks!
|
is still relevant, feel free to comment on it to keep it open. (Read more at #912)
|
||||||
|
Thanks!
|
||||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
closeComment: >
|
closeComment: >
|
||||||
Hi there! It looks like this issue hasn't had any activity for a while.
|
Hi there! It looks like this issue hasn't had any activity for a while.
|
||||||
To keep things tidy, I am going to close this issue for now.
|
To keep things tidy, I am going to close this issue for now.
|
||||||
If you think your issue is still relevant, just leave a comment
|
If you think your issue is still relevant, just leave a comment
|
||||||
and I will reopen it. Thanks!
|
and I will reopen it. (Read more at #912)
|
||||||
|
Thanks!
|
||||||
|
|||||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -4,9 +4,13 @@ name: Build Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
4
.github/workflows/integration.yml
vendored
4
.github/workflows/integration.yml
vendored
@@ -2,9 +2,9 @@ name: Integration Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main, canary ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ main ]
|
branches: [ main, canary ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
12
.github/workflows/labeler.yml
vendored
Normal file
12
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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 }}"
|
||||||
|
sync-labels: true
|
||||||
34
.github/workflows/npm-publish.yml
vendored
34
.github/workflows/npm-publish.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
# Publishes module to registry when a new release is created.
|
|
||||||
# The following secrets need to be configured for this workflow:
|
|
||||||
# * NPM_TOKEN - Auth token from npmjs.com
|
|
||||||
name: Publish to NPM
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm run build
|
|
||||||
- run: npm run lint
|
|
||||||
|
|
||||||
publish-npm:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: 12
|
|
||||||
registry-url: https://registry.npmjs.org/
|
|
||||||
- run: npm ci
|
|
||||||
- run: npm publish
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
|
||||||
30
.github/workflows/release.yml
vendored
Normal file
30
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- canary
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
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: 12
|
||||||
|
- name: Install dependencies
|
||||||
|
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 }}
|
||||||
|
run: npx semantic-release
|
||||||
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
5
CHANGELOG.md
Normal file
5
CHANGELOG.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
The changelog is automatically updated using
|
||||||
|
[semantic-release](https://github.com/semantic-release/semantic-release). You
|
||||||
|
can see it on the [releases page](../../releases).
|
||||||
@@ -8,48 +8,26 @@ 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.
|
Please raise any significant new functionality or breaking change an issue for discussion before raising a Pull Request for it.
|
||||||
|
|
||||||
## Pull Requests
|
## For contributors
|
||||||
|
|
||||||
* The latest changes are always in `main`
|
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 should be raised for larger changes
|
### Pull Requests
|
||||||
* Pull Requests do not need approval before merging for those with contributor access (it's just helpful to have them to track changes)
|
|
||||||
|
* The latest changes are always in `canary`, so please make your Pull Request against that branch.
|
||||||
|
* Pull Requests should be raised for any change
|
||||||
|
* Pull Requests need approval of a [core contributor](https://next-auth.js.org/contributors#core-team) before merging
|
||||||
* Rebasing in Pull Requests is preferred to keep a clean commit history (see below)
|
* 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
|
* Running `npm run lint:fix` before committing can make resolving conflicts easier
|
||||||
* 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
|
* We encourage you to test your changes, and if you have the opportunity, please make those tests part of the Pull Request
|
||||||
* Pushing directly to main should ideally be reserved for minor updates (e.g. correcting typos) or small single-commit fixes
|
* 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!*
|
|
||||||
|
|
||||||
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:
|
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:
|
1. Clone the repo:
|
||||||
|
|
||||||
git clone git@github.com:iaincollins/next-auth.git
|
git clone git@github.com:nextauthjs/next-auth.git
|
||||||
cd next-auth/
|
cd next-auth/
|
||||||
|
|
||||||
2. Install packages and run the build command:
|
2. Install packages and run the build command:
|
||||||
@@ -75,7 +53,7 @@ Notes: You may need to repeat both `npm link` steps if you install / update addi
|
|||||||
|
|
||||||
If you need an example project to link to, you can use [next-auth-example](https://github.com/iaincollins/next-auth-example).
|
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
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@@ -104,11 +82,11 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Databases
|
#### Databases
|
||||||
|
|
||||||
Included is a Docker Compose file that starts up MySQL, Postgres, and MongoDB databases on localhost.
|
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; it will not work if are running existing databases on localhost.
|
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||||
|
|
||||||
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
You can start them with `npm run db:start` and stop them with `npm run db:stop`.
|
||||||
|
|
||||||
@@ -116,7 +94,7 @@ You will need Docker installed to be able to start / stop the databases.
|
|||||||
|
|
||||||
When stopping the databases, it will reset their contents.
|
When stopping the databases, it will reset their contents.
|
||||||
|
|
||||||
### Testing
|
#### Testing
|
||||||
|
|
||||||
Tests can be run with `npm run test`.
|
Tests can be run with `npm run test`.
|
||||||
|
|
||||||
@@ -125,3 +103,39 @@ 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`).
|
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.
|
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 `canary`
|
||||||
|
* 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,6 +1,6 @@
|
|||||||
ISC License
|
ISC License
|
||||||
|
|
||||||
Copyright (c) 2018-2020, Iain Collins
|
Copyright (c) 2018-2021, Iain Collins
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
|||||||
95
README.md
95
README.md
@@ -1,7 +1,20 @@
|
|||||||
# NextAuth.js
|
<p align="center">
|
||||||
|
<br/>
|
||||||

|
<a href="https://next-auth.js.org" target="_blank"><img width="150px" src="https://next-auth.js.org/img/logo/logo-sm.png" /></a>
|
||||||

|
<h3 align="center">NextAuth.js</h3>
|
||||||
|
<p align="center">Authentication for Next.js</p>
|
||||||
|
<p align="center">
|
||||||
|
Open Source. Full Stack. Own Your Data.
|
||||||
|
</p>
|
||||||
|
<p align="center" style="align: center;">
|
||||||
|
<img src="https://github.com/nextauthjs/next-auth/workflows/Build%20Test/badge.svg" alt="Build Test" />
|
||||||
|
<img src="https://github.com/nextauthjs/next-auth/workflows/Integration%20Test/badge.svg" alt="Integration Test" />
|
||||||
|
<img src="https://img.shields.io/bundlephobia/minzip/next-auth" alt="Bundle Size"/>
|
||||||
|
<img src="https://img.shields.io/npm/dm/next-auth" alt="Downloads" />
|
||||||
|
<img src="https://img.shields.io/github/stars/nextauthjs/next-auth" alt="Github Stars" />
|
||||||
|
<img src="https://img.shields.io/github/v/release/nextauthjs/next-auth?include_prereleases" alt="Github Release" />
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
@@ -9,9 +22,15 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
|
|||||||
|
|
||||||
It is designed from the ground up to support Next.js and Serverless.
|
It is designed from the ground up to support Next.js and Serverless.
|
||||||
|
|
||||||
[Follow the examples](https://next-auth.js.org/getting-started/example) to see how easy it is to use NextAuth.js for authentication.
|
## Getting Started
|
||||||
|
|
||||||
Install: `npm i next-auth`
|
```
|
||||||
|
npm install --save next-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
The easiest way to continue getting started, is to follow the [getting started](https://next-auth.js.org/getting-started/example) section in our docs.
|
||||||
|
|
||||||
|
We also have a section of [tutorials](https://next-auth.js.org/tutorials) for those looking for more specific examples.
|
||||||
|
|
||||||
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
|
See [next-auth.js.org](https://next-auth.js.org) for more information and documentation.
|
||||||
|
|
||||||
@@ -52,13 +71,15 @@ Advanced options allow you to define your own routines to handle controlling wha
|
|||||||
|
|
||||||
### Typescript
|
### Typescript
|
||||||
|
|
||||||
This library gained Typescript support recently. You can install the types in the following way:
|
You can install the appropriate types via the following command:
|
||||||
```
|
|
||||||
$ npm i -D @types/next-auth
|
|
||||||
```
|
|
||||||
In you encounter any issue with them, please raise an issue and add the "typescript" label to it, we'll try to help you with it as soon as possible.
|
|
||||||
|
|
||||||
Alternatively you can raise a PR directly with your fixes on [**DefinitelyTyped**](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/next-auth).
|
```
|
||||||
|
npm install --save-dev @types/next-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
## Example
|
||||||
|
|
||||||
@@ -68,7 +89,7 @@ Alternatively you can raise a PR directly with your fixes on [**DefinitelyTyped*
|
|||||||
import NextAuth from 'next-auth'
|
import NextAuth from 'next-auth'
|
||||||
import Providers from 'next-auth/providers'
|
import Providers from 'next-auth/providers'
|
||||||
|
|
||||||
const options = {
|
export default NextAuth({
|
||||||
providers: [
|
providers: [
|
||||||
// OAuth authentication providers
|
// OAuth authentication providers
|
||||||
Providers.Apple({
|
Providers.Apple({
|
||||||
@@ -87,45 +108,43 @@ const options = {
|
|||||||
],
|
],
|
||||||
// SQL or MongoDB database (or leave empty)
|
// SQL or MongoDB database (or leave empty)
|
||||||
database: process.env.DATABASE_URL
|
database: process.env.DATABASE_URL
|
||||||
}
|
})
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Add React Component
|
### Add React Component
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import React from 'react'
|
import {
|
||||||
import {
|
useSession, signIn, signOut
|
||||||
useSession,
|
|
||||||
signin,
|
|
||||||
signout
|
|
||||||
} from 'next-auth/client'
|
} from 'next-auth/client'
|
||||||
|
|
||||||
export default function myComponent() {
|
export default function Component() {
|
||||||
const [ session, loading ] = useSession()
|
const [ session, loading ] = useSession()
|
||||||
|
if(session) {
|
||||||
return <p>
|
return <>
|
||||||
{!session && <>
|
|
||||||
Not signed in <br/>
|
|
||||||
<button onClick={signin}>Sign in</button>
|
|
||||||
</>}
|
|
||||||
{session && <>
|
|
||||||
Signed in as {session.user.email} <br/>
|
Signed in as {session.user.email} <br/>
|
||||||
<button onClick={signout}>Sign out</button>
|
<button onClick={() => signOut()}>Sign out</button>
|
||||||
</>}
|
</>
|
||||||
</p>
|
}
|
||||||
|
return <>
|
||||||
|
Not signed in <br/>
|
||||||
|
<button onClick={() => signIn()}>Sign in</button>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Acknowledgement
|
## Acknowledgements
|
||||||
|
|
||||||
[NextAuth.js is possible thanks to its contributors.](https://next-auth.js.org/contributors)
|
[NextAuth.js is made possible thanks to all of its contributors.](https://next-auth.js.org/contributors)
|
||||||
|
|
||||||
## Getting started
|
<a href="https://github.com/nextauthjs/next-auth/graphs/contributors">
|
||||||
|
<img width="500px" src="https://contrib.rocks/image?repo=nextauthjs/next-auth" />
|
||||||
[Follow the examples to get started.](https://next-auth.js.org/getting-started/example)
|
</a>
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you'd like to contribute to you can find useful information in our [Contributing Guide](https://github.com/iaincollins/next-auth/blob/main/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/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
ISC
|
||||||
|
|||||||
6203
package-lock.json
generated
6203
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next-auth",
|
"name": "next-auth",
|
||||||
"version": "3.1.0",
|
"version": "0.0.0-semantically-released",
|
||||||
"description": "Authentication for Next.js",
|
"description": "Authentication for Next.js",
|
||||||
"homepage": "https://next-auth.js.org",
|
"homepage": "https://next-auth.js.org",
|
||||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||||
@@ -17,11 +17,12 @@
|
|||||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
"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: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": "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:mysql": "node test/mysql.js",
|
||||||
"test:db:postgres": "node test/postgres.js",
|
"test:db:postgres": "node test/postgres.js",
|
||||||
"test:db:mongodb": "node test/mongodb.js",
|
"test:db:mongodb": "node test/mongodb.js",
|
||||||
"test:db:mssql": "node test/mssql.js",
|
"test:db:mssql": "node test/mssql.js",
|
||||||
|
"test:db:fauna": "node test/fauna.js",
|
||||||
"test:integration": "mocha test/integration",
|
"test:integration": "mocha test/integration",
|
||||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||||
@@ -42,11 +43,11 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.0.0",
|
"crypto-js": "^4.0.0",
|
||||||
|
"faunadb": "^3.0.1",
|
||||||
"futoin-hkdf": "^1.3.2",
|
"futoin-hkdf": "^1.3.2",
|
||||||
"jose": "^1.27.2",
|
"jose": "^1.27.2",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jwt-decode": "^2.2.0",
|
"nodemailer": "^6.4.16",
|
||||||
"nodemailer": "^6.4.6",
|
|
||||||
"oauth": "^0.9.15",
|
"oauth": "^0.9.15",
|
||||||
"preact": "^10.4.1",
|
"preact": "^10.4.1",
|
||||||
"preact-render-to-string": "^5.1.7",
|
"preact-render-to-string": "^5.1.7",
|
||||||
@@ -55,22 +56,27 @@
|
|||||||
"typeorm": "^0.2.24"
|
"typeorm": "^0.2.24"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1 || ^17",
|
||||||
"react-dom": "^16.13.1"
|
"react-dom": "^16.13.1 || ^17"
|
||||||
},
|
},
|
||||||
"peerOptionalDependencies": {
|
"peerOptionalDependencies": {
|
||||||
"mongodb": "^3.5.9",
|
"mongodb": "^3.5.9",
|
||||||
"mysql": "^2.18.1",
|
"mysql": "^2.18.1",
|
||||||
"mssql": "^6.2.1",
|
"mssql": "^6.2.1",
|
||||||
"pg": "^8.2.1",
|
"pg": "^8.2.1",
|
||||||
"@prisma/client": "^2.3.0"
|
"@prisma/client": "^2.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.8.4",
|
"@babel/cli": "^7.8.4",
|
||||||
"@babel/core": "^7.9.6",
|
"@babel/core": "^7.9.6",
|
||||||
"@babel/preset-env": "^7.9.6",
|
"@babel/preset-env": "^7.9.6",
|
||||||
|
"@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",
|
||||||
"autoprefixer": "^9.7.6",
|
"autoprefixer": "^9.7.6",
|
||||||
"babel-preset-preact": "^2.0.0",
|
"babel-preset-preact": "^2.0.0",
|
||||||
|
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"mocha": "^8.1.3",
|
"mocha": "^8.1.3",
|
||||||
@@ -83,7 +89,7 @@
|
|||||||
"puppeteer": "^5.2.1",
|
"puppeteer": "^5.2.1",
|
||||||
"puppeteer-extra": "^3.1.15",
|
"puppeteer-extra": "^3.1.15",
|
||||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||||
"standard": "^14.3.3"
|
"standard": "^16.0.3"
|
||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Serverless target in Next.js does not work if you try to read in files at runtime
|
// Serverless target in Next.js does not work if you try to read in files at runtime
|
||||||
// that are not JavaScript or JSON (e.g. CSS files).
|
// that are not JavaScript or JSON (e.g. CSS files).
|
||||||
// https://github.com/iaincollins/next-auth/issues/281
|
// https://github.com/nextauthjs/next-auth/issues/281
|
||||||
//
|
//
|
||||||
// To work around this issue, this script is a manual step that wraps CSS in a
|
// 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
|
// JavaScript file that has the compiled CSS embedded in it, and exports only
|
||||||
|
|||||||
519
src/adapters/fauna/index.js
Normal file
519
src/adapters/fauna/index.js
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
import { query as q } from 'faunadb'
|
||||||
|
import { createHash, randomBytes } from 'crypto'
|
||||||
|
import logger from '../../lib/logger'
|
||||||
|
|
||||||
|
const Adapter = (config, options = {}) => {
|
||||||
|
const {
|
||||||
|
faunaClient,
|
||||||
|
collections = {
|
||||||
|
User: 'user',
|
||||||
|
Account: 'account',
|
||||||
|
Session: 'session',
|
||||||
|
VerificationRequest: 'verification_request'
|
||||||
|
},
|
||||||
|
indexes = {
|
||||||
|
Account: 'account_by_provider_account_id',
|
||||||
|
User: 'user_by_email',
|
||||||
|
Session: 'session_by_token',
|
||||||
|
VerificationRequest: 'verification_request_by_token'
|
||||||
|
}
|
||||||
|
} = 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(collections.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(collections.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(indexes.User), 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(indexes.Account),
|
||||||
|
[providerId, providerAccountId]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
q.If(
|
||||||
|
q.Exists(q.Var('ref')),
|
||||||
|
q.Get(
|
||||||
|
q.Ref(
|
||||||
|
q.Collection(collections.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(collections.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(collections.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(collections.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(indexes.Account),
|
||||||
|
[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(collections.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 {
|
||||||
|
const session = await faunaClient.query(
|
||||||
|
q.Get(
|
||||||
|
q.Match(
|
||||||
|
q.Index(indexes.Session),
|
||||||
|
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(collections.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(indexes.Session),
|
||||||
|
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(collections.VerificationRequest), {
|
||||||
|
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(indexes.VerificationRequest), 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(indexes.VerificationRequest), 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 TypeORM from './typeorm'
|
||||||
import Prisma from './prisma'
|
import Prisma from './prisma'
|
||||||
|
import Fauna from './fauna'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Default: TypeORM.Adapter,
|
Default: TypeORM.Adapter,
|
||||||
TypeORM,
|
TypeORM,
|
||||||
Prisma
|
Prisma,
|
||||||
|
Fauna
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const Adapter = (config) => {
|
|||||||
async function getUser (id) {
|
async function getUser (id) {
|
||||||
debug('GET_USER', id)
|
debug('GET_USER', id)
|
||||||
try {
|
try {
|
||||||
return prisma[User].findOne({ where: { id } })
|
return prisma[User].findUnique({ where: { id } })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||||
@@ -68,7 +68,7 @@ const Adapter = (config) => {
|
|||||||
debug('GET_USER_BY_EMAIL', email)
|
debug('GET_USER_BY_EMAIL', email)
|
||||||
try {
|
try {
|
||||||
if (!email) { return Promise.resolve(null) }
|
if (!email) { return Promise.resolve(null) }
|
||||||
return prisma[User].findOne({ where: { email } })
|
return prisma[User].findUnique({ where: { email } })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||||
@@ -78,9 +78,9 @@ const Adapter = (config) => {
|
|||||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||||
try {
|
try {
|
||||||
const account = await prisma[Account].findOne({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
const account = await prisma[Account].findUnique({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||||
if (!account) { return null }
|
if (!account) { return null }
|
||||||
return prisma[User].findOne({ where: { id: account.userId } })
|
return prisma[User].findUnique({ where: { id: account.userId } })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||||
@@ -174,7 +174,7 @@ const Adapter = (config) => {
|
|||||||
async function getSession (sessionToken) {
|
async function getSession (sessionToken) {
|
||||||
debug('GET_SESSION', sessionToken)
|
debug('GET_SESSION', sessionToken)
|
||||||
try {
|
try {
|
||||||
const session = await prisma[Session].findOne({ where: { sessionToken } })
|
const session = await prisma[Session].findUnique({ where: { sessionToken } })
|
||||||
|
|
||||||
// Check session has not expired (do not return it if it has)
|
// Check session has not expired (do not return it if it has)
|
||||||
if (session && session.expires && new Date() > session.expires) {
|
if (session && session.expires && new Date() > session.expires) {
|
||||||
@@ -280,7 +280,7 @@ const Adapter = (config) => {
|
|||||||
// Hash token provided with secret before trying to match it with database
|
// Hash token provided with secret before trying to match it with database
|
||||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||||
const verificationRequest = await prisma[VerificationRequest].findOne({ where: { token: hashedToken } })
|
const verificationRequest = await prisma[VerificationRequest].findUnique({ where: { token: hashedToken } })
|
||||||
|
|
||||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||||
// Delete verification entry so it cannot be used again
|
// Delete verification entry so it cannot be used again
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ const _useSessionHook = (session) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Client side method
|
// Client side method
|
||||||
const signIn = async (provider, args = {}, authParams = {}) => {
|
const signIn = async (provider, args = {}) => {
|
||||||
const baseUrl = _apiBaseUrl()
|
const baseUrl = _apiBaseUrl()
|
||||||
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
const callbackUrl = (args && args.callbackUrl) ? args.callbackUrl : window.location
|
||||||
const providers = await getProviders()
|
const providers = await getProviders()
|
||||||
@@ -233,14 +233,10 @@ const signIn = async (provider, args = {}, authParams = {}) => {
|
|||||||
// If Provider not recognized, redirect to sign in page
|
// If Provider not recognized, redirect to sign in page
|
||||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||||
} else {
|
} else {
|
||||||
let signInUrl = (providers[provider].type === 'credentials')
|
const signInUrl = (providers[provider].type === 'credentials')
|
||||||
? `${baseUrl}/callback/${provider}`
|
? `${baseUrl}/callback/${provider}`
|
||||||
: `${baseUrl}/signin/${provider}`
|
: `${baseUrl}/signin/${provider}`
|
||||||
|
|
||||||
if (authParams) {
|
|
||||||
signInUrl += `?${new URLSearchParams(authParams).toString()}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||||
// callback URL and any other parameters supplied.
|
// callback URL and any other parameters supplied.
|
||||||
const fetchOptions = {
|
const fetchOptions = {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class CreateUserError extends UnknownError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Thrown when an Email address is already associated with an account
|
// 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.
|
// but the user is trying an OAuth account that is not linked to it.
|
||||||
class AccountNotLinkedError extends UnknownError {
|
class AccountNotLinkedError extends UnknownError {
|
||||||
constructor (message) {
|
constructor (message) {
|
||||||
super(message)
|
super(message)
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
// 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
|
* Simple universal (client/server) function to split host and path
|
||||||
// client and server side and we only need to parse out the host and path, while
|
* We use this rather than a library because we need to use the same logic both
|
||||||
// supporting a default value, so a simple split is sufficent.
|
* client and server side and we only need to parse out the host and path, while
|
||||||
export default (url) => {
|
* supporting a default value, so a simple split is sufficent.
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
export default function parseUrl (url) {
|
||||||
// Default values
|
// Default values
|
||||||
const defaultHost = 'http://localhost:3000'
|
const defaultHost = 'http://localhost:3000'
|
||||||
const defaultPath = '/api/auth'
|
const defaultPath = '/api/auth'
|
||||||
@@ -20,8 +23,5 @@ export default (url) => {
|
|||||||
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
const baseUrl = _host ? `${protocol}://${_host}` : defaultHost
|
||||||
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
const basePath = _path.length > 0 ? `/${_path.join('/')}` : defaultPath
|
||||||
|
|
||||||
return {
|
return { baseUrl, basePath }
|
||||||
baseUrl,
|
|
||||||
basePath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
|
||||||
|
|
||||||
export default (options) => {
|
export default (options) => {
|
||||||
return {
|
return {
|
||||||
id: 'apple',
|
id: 'apple',
|
||||||
@@ -12,7 +10,6 @@ export default (options) => {
|
|||||||
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
|
authorizationUrl: 'https://appleid.apple.com/auth/authorize?response_type=code&id_token&response_mode=form_post',
|
||||||
profileUrl: null,
|
profileUrl: null,
|
||||||
idToken: true,
|
idToken: true,
|
||||||
state: false, // Apple doesn't support state verfication
|
|
||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
// The name of the user will only return on first login
|
// The name of the user will only return on first login
|
||||||
return {
|
return {
|
||||||
@@ -23,30 +20,10 @@ export default (options) => {
|
|||||||
},
|
},
|
||||||
clientId: null,
|
clientId: null,
|
||||||
clientSecret: {
|
clientSecret: {
|
||||||
appleId: null,
|
|
||||||
teamId: null,
|
teamId: null,
|
||||||
privateKey: null,
|
privateKey: null,
|
||||||
keyId: null
|
keyId: null
|
||||||
},
|
},
|
||||||
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
|
...options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
src/providers/azure-ad-b2c.js
Normal file
24
src/providers/azure-ad-b2c.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default (options) => {
|
||||||
|
const tenant = options.tenantId ? options.tenantId : 'common'
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'azure-ad-b2c',
|
||||||
|
name: 'Azure Active Directory B2C',
|
||||||
|
type: 'oauth',
|
||||||
|
version: '2.0',
|
||||||
|
params: {
|
||||||
|
grant_type: 'authorization_code'
|
||||||
|
},
|
||||||
|
accessTokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`,
|
||||||
|
authorizationUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query`,
|
||||||
|
profileUrl: 'https://graph.microsoft.com/v1.0/me/',
|
||||||
|
profile: (profile) => {
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
name: profile.displayName,
|
||||||
|
email: profile.userPrincipalName
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/providers/bungie.js
Normal file
30
src/providers/bungie.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export default (options) => {
|
||||||
|
return {
|
||||||
|
id: 'bungie',
|
||||||
|
name: 'Bungie',
|
||||||
|
type: 'oauth',
|
||||||
|
version: '2.0',
|
||||||
|
scope: '',
|
||||||
|
params: { reauth: 'true', grant_type: 'authorization_code' },
|
||||||
|
accessTokenUrl: 'https://www.bungie.net/platform/app/oauth/token/',
|
||||||
|
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/',
|
||||||
|
profile: (profile) => {
|
||||||
|
const { bungieNetUser: user } = profile.Response
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.membershipId,
|
||||||
|
name: user.displayName,
|
||||||
|
image: `https://www.bungie.net${user.profilePicturePath.startsWith('/') ? '' : '/'}${user.profilePicturePath}`,
|
||||||
|
email: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': null
|
||||||
|
},
|
||||||
|
clientId: null,
|
||||||
|
clientSecret: null,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,20 @@ export default (options) => {
|
|||||||
scope: 'identify email',
|
scope: 'identify email',
|
||||||
params: { grant_type: 'authorization_code' },
|
params: { grant_type: 'authorization_code' },
|
||||||
accessTokenUrl: 'https://discord.com/api/oauth2/token',
|
accessTokenUrl: 'https://discord.com/api/oauth2/token',
|
||||||
authorizationUrl:
|
authorizationUrl: 'https://discord.com/api/oauth2/authorize?response_type=code&prompt=none',
|
||||||
'https://discord.com/api/oauth2/authorize?response_type=code&prompt=none',
|
|
||||||
profileUrl: 'https://discord.com/api/users/@me',
|
profileUrl: 'https://discord.com/api/users/@me',
|
||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
|
if (profile.avatar === null) {
|
||||||
|
const defaultAvatarNumber = parseInt(profile.discriminator) % 5
|
||||||
|
profile.image_url = `https://cdn.discordapp.com/embed/avatars/${defaultAvatarNumber}.png`
|
||||||
|
} else {
|
||||||
|
const format = profile.premium_type === 1 || profile.premium_type === 2 ? 'gif' : 'png'
|
||||||
|
profile.image_url = `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.${format}`
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
name: profile.username,
|
name: profile.username,
|
||||||
image: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`,
|
image: profile.image_url,
|
||||||
email: profile.email
|
email: profile.email
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
22
src/providers/foursquare.js
Normal file
22
src/providers/foursquare.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default ({ apiVersion, ...options }) => {
|
||||||
|
return {
|
||||||
|
id: 'foursquare',
|
||||||
|
name: 'Foursquare',
|
||||||
|
type: 'oauth',
|
||||||
|
version: '2.0',
|
||||||
|
params: { grant_type: 'authorization_code' },
|
||||||
|
accessTokenUrl: 'https://foursquare.com/oauth2/access_token',
|
||||||
|
authorizationUrl:
|
||||||
|
'https://foursquare.com/oauth2/authenticate?response_type=code',
|
||||||
|
profileUrl: `https://api.foursquare.com/v2/users/self?v=${apiVersion}`,
|
||||||
|
profile: (profile) => {
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
name: `${profile.firstName} ${profile.lastName}`,
|
||||||
|
image: `${profile.prefix}original${profile.suffix}`,
|
||||||
|
email: profile.contact.email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ export default (options) => {
|
|||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
return { ...profile, id: profile.sub }
|
return { ...profile, id: profile.sub }
|
||||||
},
|
},
|
||||||
setGetAccessTokenAuthHeader: false,
|
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,65 @@
|
|||||||
import Apple from './apple'
|
import Apple from './apple'
|
||||||
import Atlassian from './atlassian'
|
import Atlassian from './atlassian'
|
||||||
import Auth0 from './auth0'
|
import Auth0 from './auth0'
|
||||||
|
import AzureADB2C from './azure-ad-b2c'
|
||||||
import Basecamp from './basecamp'
|
import Basecamp from './basecamp'
|
||||||
import BattleNet from './battlenet'
|
import BattleNet from './battlenet'
|
||||||
import Box from './box'
|
import Box from './box'
|
||||||
import Credentials from './credentials'
|
import Bungie from './bungie'
|
||||||
import Cognito from './cognito'
|
import Cognito from './cognito'
|
||||||
|
import Credentials from './credentials'
|
||||||
import Discord from './discord'
|
import Discord from './discord'
|
||||||
import Email from './email'
|
import Email from './email'
|
||||||
import Facebook from './facebook'
|
import Facebook from './facebook'
|
||||||
|
import Foursquare from './foursquare'
|
||||||
import FusionAuth from './fusionauth'
|
import FusionAuth from './fusionauth'
|
||||||
import GitHub from './github'
|
import GitHub from './github'
|
||||||
import GitLab from './gitlab'
|
import GitLab from './gitlab'
|
||||||
import Google from './google'
|
import Google from './google'
|
||||||
import IdentityServer4 from './identity-server4'
|
import IdentityServer4 from './identity-server4'
|
||||||
import LinkedIn from './linkedin'
|
import LinkedIn from './linkedin'
|
||||||
|
import MailRu from './mailru'
|
||||||
import Mixer from './mixer'
|
import Mixer from './mixer'
|
||||||
|
import Netlify from './netlify'
|
||||||
import Okta from './okta'
|
import Okta from './okta'
|
||||||
import Slack from './slack'
|
import Slack from './slack'
|
||||||
import Spotify from './spotify'
|
import Spotify from './spotify'
|
||||||
|
import Strava from './strava'
|
||||||
import Twitch from './twitch'
|
import Twitch from './twitch'
|
||||||
import Twitter from './twitter'
|
import Twitter from './twitter'
|
||||||
|
import VK from './vk'
|
||||||
import Yandex from './yandex'
|
import Yandex from './yandex'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
Apple,
|
||||||
Atlassian,
|
Atlassian,
|
||||||
Auth0,
|
Auth0,
|
||||||
Apple,
|
AzureADB2C,
|
||||||
Basecamp,
|
Basecamp,
|
||||||
BattleNet,
|
BattleNet,
|
||||||
Box,
|
Box,
|
||||||
Credentials,
|
Bungie,
|
||||||
Cognito,
|
Cognito,
|
||||||
|
Credentials,
|
||||||
Discord,
|
Discord,
|
||||||
Email,
|
Email,
|
||||||
Facebook,
|
Facebook,
|
||||||
|
Foursquare,
|
||||||
FusionAuth,
|
FusionAuth,
|
||||||
GitHub,
|
GitHub,
|
||||||
GitLab,
|
GitLab,
|
||||||
Google,
|
Google,
|
||||||
IdentityServer4,
|
IdentityServer4,
|
||||||
LinkedIn,
|
LinkedIn,
|
||||||
|
MailRu,
|
||||||
Mixer,
|
Mixer,
|
||||||
|
Netlify,
|
||||||
Okta,
|
Okta,
|
||||||
Slack,
|
Slack,
|
||||||
Spotify,
|
Spotify,
|
||||||
Twitter,
|
Strava,
|
||||||
Twitch,
|
Twitch,
|
||||||
|
Twitter,
|
||||||
|
VK,
|
||||||
Yandex
|
Yandex
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/providers/mailru.js
Normal file
25
src/providers/mailru.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/providers/netlify.js
Normal file
21
src/providers/netlify.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default (options) => {
|
||||||
|
return {
|
||||||
|
id: 'netlify',
|
||||||
|
name: 'Netlify',
|
||||||
|
type: 'oauth',
|
||||||
|
version: '2.0',
|
||||||
|
params: { grant_type: 'authorization_code' },
|
||||||
|
accessTokenUrl: 'https://api.netlify.com/oauth/token',
|
||||||
|
authorizationUrl: 'https://app.netlify.com/authorize?response_type=code',
|
||||||
|
profileUrl: 'https://api.netlify.com/api/v1/user',
|
||||||
|
profile: (profile) => {
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
name: profile.full_name,
|
||||||
|
email: profile.email,
|
||||||
|
image: profile.avatar_url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,12 @@ export default (options) => {
|
|||||||
client_secret: options.clientSecret
|
client_secret: options.clientSecret
|
||||||
},
|
},
|
||||||
// These will be different depending on the Org.
|
// These will be different depending on the Org.
|
||||||
accessTokenUrl: `https://${options.domain}/oauth2/v1/token`,
|
accessTokenUrl: `https://${options.domain}/v1/token`,
|
||||||
authorizationUrl: `https://${options.domain}/oauth2/v1/authorize/?response_type=code`,
|
authorizationUrl: `https://${options.domain}/v1/authorize/?response_type=code`,
|
||||||
profileUrl: `https://${options.domain}/oauth2/v1/userinfo/`,
|
profileUrl: `https://${options.domain}/v1/userinfo/`,
|
||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
return { ...profile, id: profile.sub }
|
return { ...profile, id: profile.sub }
|
||||||
},
|
},
|
||||||
setGetAccessTokenAuthHeader: false,
|
|
||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ export default (options) => {
|
|||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
type: 'oauth',
|
type: 'oauth',
|
||||||
version: '2.0',
|
version: '2.0',
|
||||||
scope: 'identity.basic identity.email identity.avatar',
|
scope: [],
|
||||||
params: { grant_type: 'authorization_code' },
|
params: { grant_type: 'authorization_code' },
|
||||||
accessTokenUrl: 'https://slack.com/api/oauth.access',
|
accessTokenUrl: 'https://slack.com/api/oauth.v2.access',
|
||||||
authorizationUrl: 'https://slack.com/oauth/authorize?response_type=code',
|
authorizationUrl: 'https://slack.com/oauth/v2/authorize',
|
||||||
|
authorizationParams: { user_scope: 'identity.basic,identity.email,identity.avatar' },
|
||||||
profileUrl: 'https://slack.com/api/users.identity',
|
profileUrl: 'https://slack.com/api/users.identity',
|
||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
const { user } = profile
|
const { user } = profile
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default (options) => {
|
|||||||
id: profile.id,
|
id: profile.id,
|
||||||
name: profile.display_name,
|
name: profile.display_name,
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
image: profile.images.length > 0 ? profile.images[0].url : undefined
|
image: profile.images?.[0]?.url
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
|
|||||||
22
src/providers/strava.js
Normal file
22
src/providers/strava.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default (options) => {
|
||||||
|
return {
|
||||||
|
id: 'strava',
|
||||||
|
name: 'Strava',
|
||||||
|
type: 'oauth',
|
||||||
|
version: '2.0',
|
||||||
|
scope: 'read',
|
||||||
|
params: { grant_type: 'authorization_code' },
|
||||||
|
accessTokenUrl: 'https://www.strava.com/api/v3/oauth/token',
|
||||||
|
authorizationUrl:
|
||||||
|
'https://www.strava.com/api/v3/oauth/authorize?response_type=code',
|
||||||
|
profileUrl: 'https://www.strava.com/api/v3/athlete',
|
||||||
|
profile: (profile) => {
|
||||||
|
return {
|
||||||
|
id: profile.id,
|
||||||
|
name: profile.firstname,
|
||||||
|
image: profile.profile
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/providers/vk.js
Normal file
30
src/providers/vk.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export default (options) => {
|
||||||
|
const apiVersion = '5.126' // https://vk.com/dev/versions
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: 'vk',
|
||||||
|
name: 'vk.com',
|
||||||
|
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,19 +1,20 @@
|
|||||||
import { createHash, randomBytes } from 'crypto'
|
import { createHash, randomBytes } from 'crypto'
|
||||||
import jwt from '../lib/jwt'
|
import jwt from '../lib/jwt'
|
||||||
import parseUrl from '../lib/parse-url'
|
import parseUrl from '../lib/parse-url'
|
||||||
import cookie from './lib/cookie'
|
import * as cookie from './lib/cookie'
|
||||||
import callbackUrlHandler from './lib/callback-url-handler'
|
import callbackUrlHandler from './lib/callback-url-handler'
|
||||||
import parseProviders from './lib/providers'
|
import parseProviders from './lib/providers'
|
||||||
import events from './lib/events'
|
import * as events from './lib/events'
|
||||||
import callbacks from './lib/callbacks'
|
import * as defaultCallbacks from './lib/defaultCallbacks'
|
||||||
import providers from './routes/providers'
|
import providers from './routes/providers'
|
||||||
import signin from './routes/signin'
|
import signin from './routes/signin'
|
||||||
import signout from './routes/signout'
|
import signout from './routes/signout'
|
||||||
import callback from './routes/callback'
|
import callback from './routes/callback'
|
||||||
import session from './routes/session'
|
import session from './routes/session'
|
||||||
import pages from './pages'
|
import renderPage from './pages'
|
||||||
import adapters from '../adapters'
|
import adapters from '../adapters'
|
||||||
import logger from '../lib/logger'
|
import logger from '../lib/logger'
|
||||||
|
import redirect from './lib/redirect'
|
||||||
|
|
||||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||||
// environment variable must be set.
|
// environment variable must be set.
|
||||||
@@ -21,7 +22,7 @@ if (!process.env.NEXTAUTH_URL) {
|
|||||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (req, res, userSuppliedOptions) => {
|
async function NextAuthHandler (req, res, userOptions) {
|
||||||
// To the best of my knowledge, we need to return a promise here
|
// To the best of my knowledge, we need to return a promise here
|
||||||
// to avoid early termination of calls to the serverless function
|
// to avoid early termination of calls to the serverless function
|
||||||
// (and then return that promise when we are done) - eslint
|
// (and then return that promise when we are done) - eslint
|
||||||
@@ -30,7 +31,21 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
// This is passed to all methods that handle responses, and must be called
|
// 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
|
// when they are complete so that the serverless function knows when it is
|
||||||
// safe to return and that no more data will be sent.
|
// safe to return and that no more data will be sent.
|
||||||
const done = resolve
|
|
||||||
|
const originalResEnd = res.end.bind(res)
|
||||||
|
res.end = (...args) => {
|
||||||
|
resolve()
|
||||||
|
return originalResEnd(...args)
|
||||||
|
}
|
||||||
|
res.redirect = redirect(req, res)
|
||||||
|
|
||||||
|
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)
|
||||||
|
res.status(500)
|
||||||
|
return res.end(`Error: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
const { url, query, body } = req
|
const { url, query, body } = req
|
||||||
const {
|
const {
|
||||||
@@ -44,26 +59,26 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
csrfToken: csrfTokenFromPost
|
csrfToken: csrfTokenFromPost
|
||||||
} = body
|
} = body
|
||||||
|
|
||||||
// @todo refactor all existing references to site, baseUrl and basePath
|
// @todo refactor all existing references to baseUrl and basePath
|
||||||
const parsedUrl = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||||
const baseUrl = parsedUrl.baseUrl
|
|
||||||
const basePath = parsedUrl.basePath
|
|
||||||
|
|
||||||
// Parse database / adapter
|
// Parse database / adapter
|
||||||
let adapter
|
let adapter
|
||||||
if (userSuppliedOptions.adapter) {
|
if (userOptions.adapter) {
|
||||||
// If adapter is provided, use it (advanced usage, overrides database)
|
// If adapter is provided, use it (advanced usage, overrides database)
|
||||||
adapter = userSuppliedOptions.adapter
|
adapter = userOptions.adapter
|
||||||
} else if (userSuppliedOptions.database) {
|
} else if (userOptions.database) {
|
||||||
// If database URI or config object is provided, use it (simple usage)
|
// If database URI or config object is provided, use it (simple usage)
|
||||||
adapter = adapters.Default(userSuppliedOptions.database)
|
adapter = adapters.Default(userOptions.database)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secret used salt cookies and tokens (e.g. for CSRF protection).
|
// Secret used salt cookies and tokens (e.g. for CSRF protection).
|
||||||
// If no secret option is specified then it creates one on the fly
|
// 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
|
// based on options passed here. A options contains unique data, such as
|
||||||
// oAuth provider secrets and database credentials it should be sufficent.
|
// OAuth provider secrets and database credentials it should be sufficent.
|
||||||
const secret = userSuppliedOptions.secret || createHash('sha256').update(JSON.stringify({ baseUrl, basePath, ...userSuppliedOptions })).digest('hex')
|
const secret = userOptions.secret || createHash('sha256').update(JSON.stringify({
|
||||||
|
baseUrl, basePath, ...userOptions
|
||||||
|
})).digest('hex')
|
||||||
|
|
||||||
// Use secure cookies if the site uses HTTPS
|
// Use secure cookies if the site uses HTTPS
|
||||||
// This being conditional allows cookies to work non-HTTPS development URLs
|
// This being conditional allows cookies to work non-HTTPS development URLs
|
||||||
@@ -71,7 +86,7 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
// prefix, but enable them by default if the site URL is HTTPS; but not for
|
// 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).
|
// non-HTTPS URLs like http://localhost which are used in development).
|
||||||
// For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
|
// For more on prefixes see https://googlechrome.github.io/samples/cookie-prefixes/
|
||||||
const useSecureCookies = userSuppliedOptions.useSecureCookies || baseUrl.startsWith('https://')
|
const useSecureCookies = userOptions.useSecureCookies || baseUrl.startsWith('https://')
|
||||||
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
const cookiePrefix = useSecureCookies ? '__Secure-' : ''
|
||||||
|
|
||||||
// @TODO Review cookie settings (names, options)
|
// @TODO Review cookie settings (names, options)
|
||||||
@@ -106,7 +121,7 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Allow user cookie options to override any cookie settings above
|
// Allow user cookie options to override any cookie settings above
|
||||||
...userSuppliedOptions.cookies
|
...userOptions.cookies
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session options
|
// Session options
|
||||||
@@ -114,7 +129,7 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
jwt: false,
|
jwt: false,
|
||||||
maxAge: 30 * 24 * 60 * 60, // Sessions expire after 30 days of being idle
|
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)
|
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||||
...userSuppliedOptions.session
|
...userOptions.session
|
||||||
}
|
}
|
||||||
|
|
||||||
// JWT options
|
// JWT options
|
||||||
@@ -123,7 +138,7 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
maxAge: sessionOptions.maxAge, // maxAge is dereived from session maxAge,
|
maxAge: sessionOptions.maxAge, // maxAge is dereived from session maxAge,
|
||||||
encode: jwt.encode,
|
encode: jwt.encode,
|
||||||
decode: jwt.decode,
|
decode: jwt.decode,
|
||||||
...userSuppliedOptions.jwt
|
...userOptions.jwt
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||||
@@ -134,13 +149,13 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
// Event messages
|
// Event messages
|
||||||
const eventsOptions = {
|
const eventsOptions = {
|
||||||
...events,
|
...events,
|
||||||
...userSuppliedOptions.events
|
...userOptions.events
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback functions
|
// Callback functions
|
||||||
const callbacksOptions = {
|
const callbacksOptions = {
|
||||||
...callbacks,
|
...defaultCallbacks,
|
||||||
...userSuppliedOptions.callbacks
|
...userOptions.callbacks
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure CSRF Token cookie is set for any subsequent requests.
|
// Ensure CSRF Token cookie is set for any subsequent requests.
|
||||||
@@ -176,28 +191,13 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
cookie.set(res, cookies.csrfToken.name, newCsrfTokenCookie, cookies.csrfToken.options)
|
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,
|
// User provided options are overriden by other options,
|
||||||
// except for the options with special handling above
|
// except for the options with special handling above
|
||||||
const options = {
|
const options = {
|
||||||
// Defaults options can be overidden
|
debug: false,
|
||||||
debug: false, // Enable debug messages to be displayed
|
pages: {},
|
||||||
pages: {}, // Custom pages (e.g. sign in, sign out, errors)
|
|
||||||
// Custom options override defaults
|
// Custom options override defaults
|
||||||
...userSuppliedOptions,
|
...userOptions,
|
||||||
// These computed settings can values in userSuppliedOptions but override them
|
// These computed settings can values in userSuppliedOptions but override them
|
||||||
// and are request-specific.
|
// and are request-specific.
|
||||||
adapter,
|
adapter,
|
||||||
@@ -208,108 +208,120 @@ export default async (req, res, userSuppliedOptions) => {
|
|||||||
cookies,
|
cookies,
|
||||||
secret,
|
secret,
|
||||||
csrfToken,
|
csrfToken,
|
||||||
providers: parseProviders(userSuppliedOptions.providers, baseUrl, basePath),
|
providers: parseProviders({ providers: userOptions.providers, baseUrl, basePath }),
|
||||||
session: sessionOptions,
|
session: sessionOptions,
|
||||||
jwt: jwtOptions,
|
jwt: jwtOptions,
|
||||||
events: eventsOptions,
|
events: eventsOptions,
|
||||||
callbacks: callbacksOptions,
|
callbacks: callbacksOptions
|
||||||
callbackUrl: baseUrl,
|
|
||||||
redirect
|
|
||||||
}
|
}
|
||||||
|
req.options = options
|
||||||
|
|
||||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||||
if (options.debug === true) { process.env._NEXTAUTH_DEBUG = true }
|
if (options.debug) {
|
||||||
|
process.env._NEXTAUTH_DEBUG = true
|
||||||
|
}
|
||||||
|
|
||||||
// Get / Set callback URL based on query param / cookie + validation
|
// Get / Set callback URL based on query param / cookie + validation
|
||||||
options.callbackUrl = await callbackUrlHandler(req, res, options)
|
const callbackUrl = await callbackUrlHandler(req, res)
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'providers':
|
case 'providers':
|
||||||
providers(req, res, options, done)
|
providers(req, res)
|
||||||
break
|
break
|
||||||
case 'session':
|
case 'session':
|
||||||
session(req, res, options, done)
|
session(req, res)
|
||||||
break
|
break
|
||||||
case 'csrf':
|
case 'csrf':
|
||||||
res.json({ csrfToken })
|
res.json({ csrfToken })
|
||||||
return done()
|
return res.end()
|
||||||
case 'signin':
|
case 'signin':
|
||||||
if (options.pages.signIn) {
|
if (options.pages.signIn) {
|
||||||
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${options.callbackUrl}`
|
let redirectUrl = `${options.pages.signIn}${options.pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${callbackUrl}`
|
||||||
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
|
if (req.query.error) { redirectUrl = `${redirectUrl}&error=${req.query.error}` }
|
||||||
return redirect(redirectUrl)
|
return res.redirect(redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
pages.render(req, res, 'signin', { baseUrl, basePath, providers: Object.values(options.providers), callbackUrl: options.callbackUrl, csrfToken }, done)
|
renderPage(req, res, 'signin', { providers: Object.values(options.providers), callbackUrl, csrfToken })
|
||||||
break
|
break
|
||||||
case 'signout':
|
case 'signout':
|
||||||
if (options.pages.signOut) { return redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`) }
|
if (options.pages.signOut) {
|
||||||
|
return res.redirect(`${options.pages.signOut}${options.pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
pages.render(req, res, 'signout', { baseUrl, basePath, csrfToken, callbackUrl: options.callbackUrl }, done)
|
renderPage(req, res, 'signout', { csrfToken, callbackUrl })
|
||||||
break
|
break
|
||||||
case 'callback':
|
case 'callback':
|
||||||
if (provider && options.providers[provider]) {
|
if (provider && options.providers[provider]) {
|
||||||
callback(req, res, options, done)
|
callback(req, res)
|
||||||
} else {
|
} else {
|
||||||
res.status(400).end(`Error: HTTP GET is not supported for ${url}`)
|
res.status(400)
|
||||||
return done()
|
return res.end(`Error: HTTP GET is not supported for ${url}`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'verify-request':
|
case 'verify-request':
|
||||||
if (options.pages.verifyRequest) { return redirect(options.pages.verifyRequest) }
|
if (options.pages.verifyRequest) { return res.redirect(options.pages.verifyRequest) }
|
||||||
|
|
||||||
pages.render(req, res, 'verify-request', { baseUrl }, done)
|
renderPage(req, res, 'verify-request')
|
||||||
break
|
break
|
||||||
case 'error':
|
case 'error':
|
||||||
if (options.pages.error) { return redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
|
if (options.pages.error) { return res.redirect(`${options.pages.error}${options.pages.error.includes('?') ? '&' : '?'}error=${error}`) }
|
||||||
|
|
||||||
pages.render(req, res, 'error', { baseUrl, basePath, error }, done)
|
renderPage(req, res, 'error', { error })
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
res.status(404).end()
|
res.status(404)
|
||||||
return done()
|
return res.end()
|
||||||
}
|
}
|
||||||
} else if (req.method === 'POST') {
|
} else if (req.method === 'POST') {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'signin':
|
case 'signin':
|
||||||
// Verified CSRF Token required for all sign in routes
|
// Verified CSRF Token required for all sign in routes
|
||||||
if (!csrfTokenVerified) {
|
if (!csrfTokenVerified) {
|
||||||
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider && options.providers[provider]) {
|
if (provider && options.providers[provider]) {
|
||||||
signin(req, res, options, done)
|
signin(req, res)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'signout':
|
case 'signout':
|
||||||
// Verified CSRF Token required for signout
|
// Verified CSRF Token required for signout
|
||||||
if (!csrfTokenVerified) {
|
if (!csrfTokenVerified) {
|
||||||
return redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||||
}
|
}
|
||||||
|
|
||||||
signout(req, res, options, done)
|
signout(req, res)
|
||||||
break
|
break
|
||||||
case 'callback':
|
case 'callback':
|
||||||
if (provider && options.providers[provider]) {
|
if (provider && options.providers[provider]) {
|
||||||
// Verified CSRF Token required for credentials providers only
|
// Verified CSRF Token required for credentials providers only
|
||||||
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
|
if (options.providers[provider].type === 'credentials' && !csrfTokenVerified) {
|
||||||
return redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(req, res, options, done)
|
callback(req, res)
|
||||||
} else {
|
} else {
|
||||||
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
|
res.status(400)
|
||||||
return done()
|
return res.end(`Error: HTTP POST is not supported for ${url}`)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
res.status(400).end(`Error: HTTP POST is not supported for ${url}`)
|
res.status(400)
|
||||||
return done()
|
return res.end(`Error: HTTP POST is not supported for ${url}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(400).end(`Error: HTTP ${req.method} is not supported for ${url}`)
|
res.status(400)
|
||||||
return done()
|
return res.end(`Error: HTTP ${req.method} is not supported for ${url}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tha main entry point to next-auth */
|
||||||
|
export default function NextAuth (...args) {
|
||||||
|
if (args.length === 1) {
|
||||||
|
return (req, res) => NextAuthHandler(req, res, args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextAuthHandler(...args)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
// 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 { AccountNotLinkedError } from '../../lib/errors'
|
||||||
import dispatchEvent from '../lib/dispatch-event'
|
import dispatchEvent from '../lib/dispatch-event'
|
||||||
|
|
||||||
export default async (sessionToken, profile, providerAccount, options) => {
|
/**
|
||||||
|
* 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) {
|
||||||
try {
|
try {
|
||||||
// Input validation
|
// Input validation
|
||||||
if (!profile) { throw new Error('Missing profile') }
|
if (!profile) { throw new Error('Missing profile') }
|
||||||
@@ -52,8 +54,8 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
|||||||
if (useJwtSession) {
|
if (useJwtSession) {
|
||||||
try {
|
try {
|
||||||
session = await jwt.decode({ ...jwt, token: sessionToken })
|
session = await jwt.decode({ ...jwt, token: sessionToken })
|
||||||
if (session && session.user) {
|
if (session && session.sub) {
|
||||||
user = await getUser(session.user.id)
|
user = await getUser(session.sub)
|
||||||
isSignedIn = !!user
|
isSignedIn = !!user
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -136,7 +138,7 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (isSignedIn) {
|
if (isSignedIn) {
|
||||||
// If the user is already signed in and the oAuth account isn't already associated
|
// 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.
|
// with another user account then we can go ahead and link the accounts safely.
|
||||||
await linkAccount(
|
await linkAccount(
|
||||||
user.id,
|
user.id,
|
||||||
@@ -157,28 +159,28 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user is not signed in and it looks like a new oAuth account then we
|
// 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
|
// check there also isn't an user account already associated with the same
|
||||||
// email address as the one in the oAuth profile.
|
// email address as the one in the OAuth profile.
|
||||||
//
|
//
|
||||||
// This step is often overlooked in oAuth implementations, but covers the following cases:
|
// 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.
|
// 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.
|
// 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.
|
// 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.
|
// 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
|
// 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
|
// 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.
|
// someone is not exploiting a problem with a third party OAuth service.
|
||||||
//
|
//
|
||||||
// oAuth providers should require email address verification to prevent this, but in
|
// OAuth providers should require email address verification to prevent this, but in
|
||||||
// practice that is not always the case; this helps protect against that.
|
// practice that is not always the case; this helps protect against that.
|
||||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||||
if (userByEmail) {
|
if (userByEmail) {
|
||||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
// 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
|
// 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.
|
// 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
|
// 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
|
// want to link them in case it's not safe to do so, so instead we prompt the user
|
||||||
@@ -189,7 +191,7 @@ export default async (sessionToken, profile, providerAccount, options) => {
|
|||||||
// accounts (by email or provider account id)...
|
// accounts (by email or provider account id)...
|
||||||
//
|
//
|
||||||
// If no account matching the same [provider].id or .email exists, we can
|
// 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 account for the user, link it to the OAuth acccount and
|
||||||
// create a new session for them so they are signed in with it.
|
// create a new session for them so they are signed in with it.
|
||||||
user = await createUser(profile)
|
user = await createUser(profile)
|
||||||
await dispatchEvent(events.createUser, user)
|
await dispatchEvent(events.createUser, user)
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import cookie from '../lib/cookie'
|
import * as cookie from '../lib/cookie'
|
||||||
|
|
||||||
export default async (req, res, options) => {
|
export default async function callbackUrlHandler (req, res) {
|
||||||
const { query } = req
|
const { query } = req
|
||||||
const { body } = req
|
const { body } = req
|
||||||
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = options
|
const { cookies, baseUrl, defaultCallbackUrl, callbacks } = req.options
|
||||||
|
|
||||||
// Handle preserving and validating callback URLs
|
// Handle preserving and validating callback URLs
|
||||||
// If no defaultCallbackUrl option specified, default to the homepage for the site
|
// If no defaultCallbackUrl option specified, default to the homepage for the site
|
||||||
let callbackUrl = defaultCallbackUrl || baseUrl
|
let callbackUrl = defaultCallbackUrl || baseUrl
|
||||||
|
|
||||||
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
|
// Try reading callbackUrlParamValue from request body (form submission) then from query param (get request)
|
||||||
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
|
const callbackUrlParamValue = body.callbackUrl || query.callbackUrl || null
|
||||||
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
|
const callbackUrlCookieValue = req.cookies[cookies.callbackUrl.name] || null
|
||||||
@@ -21,7 +20,9 @@ export default async (req, res, options) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save callback URL in a cookie so that can be used for subsequent requests in signin/signout/callback flow
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(callbackUrl)
|
return callbackUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
// Function to set cookies server side
|
/**
|
||||||
//
|
* 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
|
* Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
|
||||||
// * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
|
* * 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.
|
* As only partial functionlity is required, only the code we need has been incorporated here
|
||||||
const set = (res, name, value, options = {}) => {
|
* (with fixes for specific issues) to keep dependancy size down.
|
||||||
|
*/
|
||||||
|
export function set (res, name, value, options = {}) {
|
||||||
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
|
const stringValue = typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value)
|
||||||
|
|
||||||
if ('maxAge' in options) {
|
if ('maxAge' in options) {
|
||||||
@@ -98,7 +100,3 @@ function _serialize (name, val, options) {
|
|||||||
|
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
|
||||||
set
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,19 +9,14 @@
|
|||||||
* requests to sign in and again when they activate the link in the sign in
|
* requests to sign in and again when they activate the link in the sign in
|
||||||
* email.
|
* email.
|
||||||
*
|
*
|
||||||
* @param {object} profile User profile (e.g. user id, name, 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} account Account used to sign in (e.g. OAuth account)
|
||||||
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
|
* @param {object} metadata Provider specific metadata (e.g. OAuth Profile)
|
||||||
* @return {boolean|object} Return `true` (or a modified JWT) to allow sign in
|
* @return {Promise<boolean|never>} Return `true` (or a modified JWT) to allow sign in
|
||||||
* Return `false` to deny access
|
* Return `false` to deny access
|
||||||
*/
|
*/
|
||||||
const signIn = async (profile, account, metadata) => {
|
export async function signIn () {
|
||||||
const isAllowedToSignIn = true
|
return true
|
||||||
if (isAllowedToSignIn) {
|
|
||||||
return Promise.resolve(true)
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,12 +26,13 @@ const signIn = async (profile, account, metadata) => {
|
|||||||
*
|
*
|
||||||
* @param {string} url URL provided as callback URL by the client
|
* @param {string} url URL provided as callback URL by the client
|
||||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||||
* @return {string} URL the client will be redirect to
|
* @return {Promise<string>} URL the client will be redirect to
|
||||||
*/
|
*/
|
||||||
const redirect = async (url, baseUrl) => {
|
export async function redirect (url, baseUrl) {
|
||||||
return url.startsWith(baseUrl)
|
if (url.startsWith(baseUrl)) {
|
||||||
? Promise.resolve(url)
|
return url
|
||||||
: Promise.resolve(baseUrl)
|
}
|
||||||
|
return baseUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,31 +41,24 @@ const redirect = async (url, baseUrl) => {
|
|||||||
*
|
*
|
||||||
* @param {object} session Session object
|
* @param {object} session Session object
|
||||||
* @param {object} token JSON Web Token (if enabled)
|
* @param {object} token JSON Web Token (if enabled)
|
||||||
* @return {object} Session that will be returned to the client
|
* @return {Promise<object>} Session that will be returned to the client
|
||||||
*/
|
*/
|
||||||
const session = async (session, token) => {
|
export async function session (session) {
|
||||||
return Promise.resolve(session)
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This callback is called whenever a JSON Web Token is created / updated.
|
* This callback is called whenever a JSON Web Token is created / updated.
|
||||||
* e.g. On sign in, `getSession()`, `useSession()`, `/api/auth/session` (etc)
|
* 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
|
* 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.
|
* take advantage of this to persist additional data you need to in the JWT.
|
||||||
*
|
*
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
* @param {object} oAuthProfile OAuth profile - only available on sign in
|
||||||
* @return {object} JSON Web Token that will be saved
|
* @return {Promise<object>} JSON Web Token that will be saved
|
||||||
*/
|
*/
|
||||||
const jwt = async (token, oAuthProfile) => {
|
export async function jwt (token) {
|
||||||
return Promise.resolve(token)
|
return token
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
signIn,
|
|
||||||
redirect,
|
|
||||||
session,
|
|
||||||
jwt
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import logger from '../../lib/logger'
|
import logger from '../../lib/logger'
|
||||||
|
|
||||||
export default async (event, message) => {
|
export default async function dispatchEvent (event, message) {
|
||||||
try {
|
try {
|
||||||
await event(message)
|
await event(message)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,38 +1,23 @@
|
|||||||
const signIn = async (message) => {
|
/** Event triggered on successful sign in */
|
||||||
// Event triggered on successful sign in
|
export async function signIn (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const signOut = async (message) => {
|
/** Event triggered on sign out */
|
||||||
// Event triggered on sign out
|
export async function signOut (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const createUser = async (message) => {
|
/** Event triggered on user creation */
|
||||||
// Event triggered on user creation
|
export async function createUser (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const updateUser = async (message) => {
|
/** Event triggered when a user object is updated */
|
||||||
// Event triggered when a user object is updated
|
export async function updateUser (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const linkAccount = async (message) => {
|
/** Event triggered when an account is linked to a user */
|
||||||
// Event triggered when an account is linked to a user
|
export async function linkAccount (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const session = async (message) => {
|
/** Event triggered when a session is active */
|
||||||
// Event triggered when a session is active
|
export async function session (message) {}
|
||||||
}
|
|
||||||
|
|
||||||
const error = async (message) => {
|
/**
|
||||||
// @TODO Event triggered when something goes wrong in an authentication flow
|
* @TODO Event triggered when something goes wrong in an authentication flow
|
||||||
// This event may be fired multiple times when an error occurs
|
* This event may be fired multiple times when an error occurs
|
||||||
}
|
*/
|
||||||
|
export async function error (message) {}
|
||||||
export default {
|
|
||||||
signIn,
|
|
||||||
signOut,
|
|
||||||
createUser,
|
|
||||||
updateUser,
|
|
||||||
linkAccount,
|
|
||||||
session,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
|
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import querystring from 'querystring'
|
import { decode as jwtDecode } from 'jsonwebtoken'
|
||||||
import jwtDecode from 'jwt-decode'
|
|
||||||
import oAuthClient from './client'
|
import oAuthClient from './client'
|
||||||
import logger from '../../../lib/logger'
|
import logger from '../../../lib/logger'
|
||||||
|
class OAuthCallbackError extends Error {
|
||||||
|
constructor (message) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'OAuthCallbackError'
|
||||||
|
this.message = message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @TODO Refactor monkey patching in _getOAuthAccessToken() and _get()
|
export default async function oAuthCallback (req, csrfToken) {
|
||||||
// These methods have been forked from `node-oauth` to fix bugs; it may make
|
// The "user" object is specific to the Apple provider and is provided on first sign in
|
||||||
// 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"}
|
// 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 provider = req.options.providers[req.options.provider]
|
||||||
const client = oAuthClient(provider)
|
const client = oAuthClient(provider)
|
||||||
|
|
||||||
if (provider.version && provider.version.startsWith('2.')) {
|
if (provider.version?.startsWith('2.')) {
|
||||||
|
let { code, user, state } = req.query // eslint-disable-line camelcase
|
||||||
// For OAuth 2.0 flows, check state returned and matches expected value
|
// For OAuth 2.0 flows, check state returned and matches expected value
|
||||||
// (a hash of the NextAuth.js CSRF token).
|
// (a hash of the NextAuth.js CSRF token).
|
||||||
//
|
//
|
||||||
// This check can be disabled for providers that do not support it by
|
// Apple does not support state verification.
|
||||||
// setting `state: false` as a option on the provider (defaults to true).
|
if (provider.id !== 'apple') {
|
||||||
if (!Object.prototype.hasOwnProperty.call(provider, 'state') || provider.state === true) {
|
|
||||||
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
const expectedState = createHash('sha256').update(csrfToken).digest('hex')
|
||||||
if (state !== expectedState) {
|
if (state !== expectedState) {
|
||||||
return callback(new Error('Invalid state returned from oAuth provider'))
|
throw new OAuthCallbackError('Invalid state returned from OAuth provider')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
try {
|
try {
|
||||||
const body = JSON.parse(JSON.stringify(req.body))
|
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
|
code = body.code
|
||||||
user = body.user != null ? JSON.parse(body.user) : null
|
user = body.user != null ? JSON.parse(body.user) : null
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', e, req.body, provider.id, code)
|
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error, req.body, provider.id, code)
|
||||||
return callback()
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REVIEW: Is this used by any of the providers?
|
||||||
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
|
// Pass authToken in header by default (unless 'useAuthTokenHeader: false' is set)
|
||||||
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
|
if (Object.prototype.hasOwnProperty.call(provider, 'useAuthTokenHeader')) {
|
||||||
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
|
client.useAuthorizationHeaderforGET(provider.useAuthTokenHeader)
|
||||||
@@ -52,78 +52,56 @@ export default async (req, provider, csrfToken, callback) => {
|
|||||||
client.useAuthorizationHeaderforGET(true)
|
client.useAuthorizationHeaderforGET(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use custom getOAuthAccessToken() method for oAuth2 flows
|
try {
|
||||||
client.getOAuthAccessToken = _getOAuthAccessToken
|
const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(code, provider)
|
||||||
|
const tokens = { accessToken, refreshToken, idToken: results.id_token }
|
||||||
await client.getOAuthAccessToken(
|
let profileData
|
||||||
code,
|
if (provider.idToken) {
|
||||||
provider,
|
// If we don't have an ID Token most likely the user hit a cancel
|
||||||
(error, accessToken, refreshToken, results) => {
|
// button when signing in (or the provider is misconfigured).
|
||||||
if (error || results.error) {
|
//
|
||||||
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, results, provider.id, code)
|
// Unfortunately, we can't tell which, so we can't treat it as an
|
||||||
return callback(error || results.error)
|
// error, so instead we just returning nothing, which will cause the
|
||||||
|
// user to be redirected back to the sign in page.
|
||||||
|
if (!results?.id_token) {
|
||||||
|
throw new OAuthCallbackError()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (provider.idToken) {
|
// Support services that use OpenID ID Tokens to encode profile data
|
||||||
// If we don't have an ID Token most likely the user hit a cancel
|
profileData = decodeIdToken(results.id_token)
|
||||||
// button when signing in (or the provider is misconfigured).
|
} else {
|
||||||
//
|
profileData = await client.get(provider, accessToken, results)
|
||||||
// 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,
|
|
||||||
async (error, profileData) => {
|
|
||||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
|
||||||
callback(error, profile, account, OAuthProfile)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
client.get(
|
return _getProfile({ profileData, provider, tokens, user })
|
||||||
provider.profileUrl,
|
} catch (error) {
|
||||||
accessToken,
|
logger.error('OAUTH_GET_ACCESS_TOKEN_ERROR', error, provider.id, code)
|
||||||
refreshToken,
|
throw error
|
||||||
async (error, profileData) => {
|
}
|
||||||
const { profile, account, OAuthProfile } = await _getProfile(error, profileData, accessToken, refreshToken, provider)
|
}
|
||||||
callback(error, profile, account, OAuthProfile)
|
|
||||||
}
|
try {
|
||||||
)
|
// Handle OAuth v1.x
|
||||||
}
|
const {
|
||||||
|
oauth_token: oauthToken, oauth_verifier: oauthVerifier
|
||||||
|
} = req.query
|
||||||
|
const { accessToken, refreshToken, results } = await client.getOAuthAccessToken(oauthToken, null, oauthVerifier)
|
||||||
|
const profileData = await client.get(
|
||||||
|
provider.profileUrl,
|
||||||
|
accessToken,
|
||||||
|
refreshToken
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const tokens = {
|
||||||
|
accessToken, refreshToken, idToken: results.id_token
|
||||||
|
}
|
||||||
|
|
||||||
|
return _getProfile({
|
||||||
|
profileData, tokens, provider
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('OAUTH_V1_GET_ACCESS_TOKEN_ERROR', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,25 +109,41 @@ export default async (req, provider, csrfToken, callback) => {
|
|||||||
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
|
* //6/30/2020 @geraldnolan added userData parameter to attach additional data to the profileData object
|
||||||
* Returns profile, raw profile and auth provider details
|
* Returns profile, raw profile and auth provider details
|
||||||
*/
|
*/
|
||||||
async function _getProfile (error, profileData, accessToken, refreshToken, provider, userData) {
|
async function _getProfile ({
|
||||||
// @TODO Handle error
|
profileData, tokens: { accessToken, refreshToken, idToken }, provider, user
|
||||||
if (error) {
|
}) {
|
||||||
logger.error('OAUTH_GET_PROFILE_ERROR', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
let profile = {}
|
|
||||||
try {
|
try {
|
||||||
// Convert profileData into an object if it's a string
|
// 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 a user object is supplied (e.g. Apple provider) add it to the profile object
|
||||||
if (userData != null) {
|
if (user != null) {
|
||||||
profileData.user = userData
|
profileData.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
profileData.idToken = idToken
|
||||||
|
|
||||||
logger.debug('PROFILE_DATA', profileData)
|
logger.debug('PROFILE_DATA', profileData)
|
||||||
|
|
||||||
profile = await provider.profile(profileData)
|
const profile = await provider.profile(profileData)
|
||||||
|
// 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,
|
||||||
|
refreshToken,
|
||||||
|
accessToken,
|
||||||
|
accessTokenExpires: null
|
||||||
|
},
|
||||||
|
OAuthProfile: profileData
|
||||||
|
}
|
||||||
} catch (exception) {
|
} catch (exception) {
|
||||||
// If we didn't get a response either there was a problem with the provider
|
// 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.
|
// response *or* the user cancelled the action with the provider.
|
||||||
@@ -165,111 +159,11 @@ async function _getProfile (error, profileData, accessToken, refreshToken, provi
|
|||||||
OAuthProfile: profileData
|
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
|
function decodeIdToken (idToken) {
|
||||||
async function _getOAuthAccessToken (code, provider, callback) {
|
if (!idToken) {
|
||||||
const url = provider.accessTokenUrl
|
throw new OAuthCallbackError('Missing JWT ID Token')
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return jwtDecode(idToken, { json: true })
|
||||||
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 }
|
|
||||||
|
|
||||||
// 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 = 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
|
|
||||||
function _get (provider, accessToken, callback) {
|
|
||||||
const 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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,31 +1,228 @@
|
|||||||
// @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 { OAuth, OAuth2 } from 'oauth'
|
||||||
|
import querystring from 'querystring'
|
||||||
|
import logger from '../../../lib/logger'
|
||||||
|
import { sign as jwtSign } from 'jsonwebtoken'
|
||||||
|
|
||||||
export default (provider) => {
|
/**
|
||||||
if (provider.version && provider.version.startsWith('2.')) {
|
* @TODO Refactor to remove dependancy on 'oauth' package
|
||||||
// Handle oAuth v2.x
|
* It is already quite monkey patched, we don't use all the features and and it
|
||||||
const basePath = new URL(provider.authorizationUrl).origin
|
* would be easier to maintain if all the code was native to next-auth.
|
||||||
const authorizePath = new URL(provider.authorizationUrl).pathname
|
*/
|
||||||
|
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
|
||||||
const accessTokenPath = new URL(provider.accessTokenUrl).pathname
|
const accessTokenPath = new URL(provider.accessTokenUrl).pathname
|
||||||
return new OAuth2(
|
const oauth2Client = new OAuth2(
|
||||||
provider.clientId,
|
provider.clientId,
|
||||||
provider.clientSecret,
|
provider.clientSecret,
|
||||||
basePath,
|
basePath,
|
||||||
authorizePath,
|
authorizePath,
|
||||||
accessTokenPath,
|
accessTokenPath,
|
||||||
provider.headers)
|
provider.headers
|
||||||
} else {
|
|
||||||
// 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')
|
|
||||||
)
|
)
|
||||||
|
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
|
||||||
|
oauth1Client.get = (...args) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
originalGet(...args, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const originalGetOAuth1AccessToken = oauth1Client.getOAuthAccessToken
|
||||||
|
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
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
async function getOAuth2AccessToken (code, provider) {
|
||||||
|
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
|
||||||
|
} 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 === 'okta' || provider.id === 'identity-server4') && !headers.Authorization) {
|
||||||
|
headers.Authorization = `Bearer ${code}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
let accessToken
|
||||||
|
if (provider.id === 'spotify') {
|
||||||
|
accessToken = results.authed_user.access_token
|
||||||
|
} else {
|
||||||
|
accessToken = results.access_token
|
||||||
|
}
|
||||||
|
const refreshToken = results.refresh_token
|
||||||
|
resolve({ 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
|
||||||
|
*/
|
||||||
|
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,14 +1,11 @@
|
|||||||
export default (_providers, baseUrl, basePath) => {
|
export default function parseProviders ({ providers, baseUrl, basePath }) {
|
||||||
const providers = {}
|
return providers.reduce((acc, provider) => {
|
||||||
|
|
||||||
_providers.forEach(provider => {
|
|
||||||
const providerId = provider.id
|
const providerId = provider.id
|
||||||
providers[providerId] = {
|
acc[providerId] = {
|
||||||
...provider,
|
...provider,
|
||||||
signinUrl: `${baseUrl}${basePath}/signin/${providerId}`,
|
signinUrl: `${baseUrl}${basePath}/signin/${providerId}`,
|
||||||
callbackUrl: `${baseUrl}${basePath}/callback/${providerId}`
|
callbackUrl: `${baseUrl}${basePath}/callback/${providerId}`
|
||||||
}
|
}
|
||||||
})
|
return acc
|
||||||
|
}, {})
|
||||||
return providers
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/server/lib/redirect.js
Normal file
12
src/server/lib/redirect.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default function redirect (req, res) {
|
||||||
|
// This is the one you will use. The wrapper is just to set it up in src/server/index.
|
||||||
|
return function redirect (url) {
|
||||||
|
const reponseAsJson = req.body?.json === 'true'
|
||||||
|
if (reponseAsJson) {
|
||||||
|
res.json({ url })
|
||||||
|
} else {
|
||||||
|
res.status(302).setHeader('Location', url)
|
||||||
|
}
|
||||||
|
return res.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
|
|
||||||
export default async (email, provider, options) => {
|
export default async function email (email, provider, options) {
|
||||||
try {
|
try {
|
||||||
const { baseUrl, basePath, adapter } = options
|
const { baseUrl, basePath, adapter } = options
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,17 @@ import oAuthClient from '../oauth/client'
|
|||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import logger from '../../../lib/logger'
|
import logger from '../../../lib/logger'
|
||||||
|
|
||||||
export default (provider, csrfToken, callback, authParams) => {
|
export default async function oauth (provider, csrfToken) {
|
||||||
const { callbackUrl } = provider
|
const { callbackUrl } = provider
|
||||||
const client = oAuthClient(provider)
|
const client = oAuthClient(provider)
|
||||||
if (provider.version && provider.version.startsWith('2.')) {
|
if (provider.version?.startsWith('2.')) {
|
||||||
// Handle oAuth v2.x
|
// Handle OAuth v2.x
|
||||||
let url = client.getAuthorizeUrl({
|
let url = client.getAuthorizeUrl({
|
||||||
...authParams,
|
redirect_uri: callbackUrl,
|
||||||
redirect_uri: provider.callbackUrl,
|
|
||||||
scope: provider.scope,
|
scope: provider.scope,
|
||||||
// A hash of the NextAuth.js CSRF token is used as the state
|
// A hash of the NextAuth.js CSRF token is used as the state
|
||||||
state: createHash('sha256').update(csrfToken).digest('hex')
|
state: createHash('sha256').update(csrfToken).digest('hex'),
|
||||||
|
...provider.authorizationParams
|
||||||
})
|
})
|
||||||
|
|
||||||
// If the authorizationUrl specified in the config has query parameters on it
|
// If the authorizationUrl specified in the config has query parameters on it
|
||||||
@@ -28,15 +28,14 @@ export default (provider, csrfToken, callback, authParams) => {
|
|||||||
url = url.replace(baseUrl, provider.authorizationUrl + '&')
|
url = url.replace(baseUrl, provider.authorizationUrl + '&')
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null, url)
|
return url
|
||||||
} else {
|
}
|
||||||
// Handle oAuth v1.x
|
|
||||||
client.getOAuthRequestToken((error, oAuthToken) => {
|
try {
|
||||||
if (error) {
|
const oAuthToken = await client.getOAuthRequestToken(callbackUrl)
|
||||||
logger.error('GET_AUTHORISATION_URL_ERROR', error)
|
return `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
|
||||||
}
|
} catch (error) {
|
||||||
const url = `${provider.authorizationUrl}?oauth_token=${oAuthToken}`
|
logger.error('GET_AUTHORISATION_URL_ERROR', error)
|
||||||
callback(error, url)
|
throw error
|
||||||
}, callbackUrl)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||||
import render from 'preact-render-to-string'
|
import render from 'preact-render-to-string'
|
||||||
|
|
||||||
export default ({ baseUrl, basePath, error, res }) => {
|
export default function error ({ baseUrl, basePath, error, res }) {
|
||||||
const signinPageUrl = `${baseUrl}${basePath}/signin`
|
const signinPageUrl = `${baseUrl}${basePath}/signin`
|
||||||
|
|
||||||
let statusCode = 200
|
let statusCode = 200
|
||||||
@@ -19,37 +19,38 @@ export default ({ baseUrl, basePath, error, res }) => {
|
|||||||
case 'EmailSignin':
|
case 'EmailSignin':
|
||||||
case 'CredentialsSignin':
|
case 'CredentialsSignin':
|
||||||
// These messages are displayed in line on the sign in page
|
// These messages are displayed in line on the sign in page
|
||||||
res.status(302).setHeader('Location', `${signinPageUrl}?error=${error}`)
|
res.redirect(`${signinPageUrl}?error=${error}`)
|
||||||
res.end()
|
|
||||||
return false
|
return false
|
||||||
case 'Configuration':
|
case 'Configuration':
|
||||||
statusCode = 500
|
statusCode = 500
|
||||||
heading = <h1>Server error</h1>
|
heading = <h1>Server error</h1>
|
||||||
message =
|
message = (
|
||||||
<div>
|
<div>
|
||||||
<div className='message'>
|
<div className='message'>
|
||||||
<p>There is a problem with the server configuration.</p>
|
<p>There is a problem with the server configuration.</p>
|
||||||
<p>Check the server logs for more information.</p>
|
<p>Check the server logs for more information.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case 'AccessDenied':
|
case 'AccessDenied':
|
||||||
statusCode = 403
|
statusCode = 403
|
||||||
heading = <h1>Access Denied</h1>
|
heading = <h1>Access Denied</h1>
|
||||||
message =
|
message = (
|
||||||
<div>
|
<div>
|
||||||
<div className='message'>
|
<div className='message'>
|
||||||
<p>You do not have permission to sign in.</p>
|
<p>You do not have permission to sign in.</p>
|
||||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case 'Verification':
|
case 'Verification':
|
||||||
// @TODO Check if user is signed in already with the same email address.
|
// @TODO Check if user is signed in already with the same email address.
|
||||||
// If they are, no need to display this message, can just direct to callbackUrl
|
// If they are, no need to display this message, can just direct to callbackUrl
|
||||||
statusCode = 403
|
statusCode = 403
|
||||||
heading = <h1>Unable to sign in</h1>
|
heading = <h1>Unable to sign in</h1>
|
||||||
message =
|
message = (
|
||||||
<div>
|
<div>
|
||||||
<div className='message'>
|
<div className='message'>
|
||||||
<p>The sign in link is no longer valid.</p>
|
<p>The sign in link is no longer valid.</p>
|
||||||
@@ -57,6 +58,7 @@ export default ({ baseUrl, basePath, error, res }) => {
|
|||||||
</div>
|
</div>
|
||||||
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
<p><a className='button' href={signinPageUrl}>Sign in</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import verifyRequest from './verify-request'
|
|||||||
import error from './error'
|
import error from './error'
|
||||||
import css from '../../css'
|
import css from '../../css'
|
||||||
|
|
||||||
function render (req, res, page, props, done) {
|
export default function renderPage (req, res, page, props = {}) {
|
||||||
|
props.baseUrl = req.options.baseUrl
|
||||||
|
props.basePath = req.options.basePath
|
||||||
let html = ''
|
let html = ''
|
||||||
switch (page) {
|
switch (page) {
|
||||||
case 'signin':
|
case 'signin':
|
||||||
@@ -18,7 +20,7 @@ function render (req, res, page, props, done) {
|
|||||||
break
|
break
|
||||||
case 'error':
|
case 'error':
|
||||||
html = error({ ...props, res })
|
html = error({ ...props, res })
|
||||||
if (html === false) return done()
|
if (html === false) return res.end()
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
html = error(props)
|
html = error(props)
|
||||||
@@ -27,9 +29,5 @@ function render (req, res, page, props, done) {
|
|||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html')
|
res.setHeader('Content-Type', 'text/html')
|
||||||
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div class="page">${html}</div></body></html>`)
|
res.send(`<!DOCTYPE html><head><style type="text/css">${css()}</style><meta name="viewport" content="width=device-width, initial-scale=1"></head><body><div class="page">${html}</div></body></html>`)
|
||||||
done()
|
res.end()
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
render
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||||
import render from 'preact-render-to-string'
|
import render from 'preact-render-to-string'
|
||||||
|
|
||||||
export default ({ req, csrfToken, providers, callbackUrl }) => {
|
export default function signin ({ req, csrfToken, providers, callbackUrl }) {
|
||||||
const { email, error } = req.query
|
const { email, error } = req.query
|
||||||
|
|
||||||
// We only want to render providers
|
// We only want to render providers
|
||||||
@@ -59,8 +59,8 @@ export default ({ req, csrfToken, providers, callbackUrl }) => {
|
|||||||
<button type='submit' className='button'>Sign in with {provider.name}</button>
|
<button type='submit' className='button'>Sign in with {provider.name}</button>
|
||||||
</form>}
|
</form>}
|
||||||
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
|
{(provider.type === 'email' || provider.type === 'credentials') && (i > 0) &&
|
||||||
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
|
providersToRender[i - 1].type !== 'email' && providersToRender[i - 1].type !== 'credentials' &&
|
||||||
<hr />}
|
<hr />}
|
||||||
{provider.type === 'email' &&
|
{provider.type === 'email' &&
|
||||||
<form action={provider.signinUrl} method='POST'>
|
<form action={provider.signinUrl} method='POST'>
|
||||||
<input type='hidden' name='csrfToken' value={csrfToken} />
|
<input type='hidden' name='csrfToken' value={csrfToken} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||||
import render from 'preact-render-to-string'
|
import render from 'preact-render-to-string'
|
||||||
|
|
||||||
export default ({ baseUrl, basePath, csrfToken }) => {
|
export default function signout ({ baseUrl, basePath, csrfToken }) {
|
||||||
return render(
|
return render(
|
||||||
<div className='signout'>
|
<div className='signout'>
|
||||||
<h1>Are you sure you want to sign out?</h1>
|
<h1>Are you sure you want to sign out?</h1>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
import { h } from 'preact' // eslint-disable-line no-unused-vars
|
||||||
import render from 'preact-render-to-string'
|
import render from 'preact-render-to-string'
|
||||||
|
|
||||||
export default ({ baseUrl }) => {
|
export default function verifyRequest ({ baseUrl }) {
|
||||||
return render(
|
return render(
|
||||||
<div className='verify-request'>
|
<div className='verify-request'>
|
||||||
<h1>Check your email</h1>
|
<h1>Check your email</h1>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Handle callbacks from login services
|
|
||||||
import oAuthCallback from '../lib/oauth/callback'
|
import oAuthCallback from '../lib/oauth/callback'
|
||||||
import callbackHandler from '../lib/callback-handler'
|
import callbackHandler from '../lib/callback-handler'
|
||||||
import cookie from '../lib/cookie'
|
import * as cookie from '../lib/cookie'
|
||||||
import logger from '../../lib/logger'
|
import logger from '../../lib/logger'
|
||||||
import dispatchEvent from '../lib/dispatch-event'
|
import dispatchEvent from '../lib/dispatch-event'
|
||||||
|
|
||||||
export default async (req, res, options, done) => {
|
/** Handle callbacks from login services */
|
||||||
|
export default async function callback (req, res) {
|
||||||
const {
|
const {
|
||||||
provider: providerName,
|
provider: providerName,
|
||||||
providers,
|
providers,
|
||||||
@@ -20,132 +20,135 @@ export default async (req, res, options, done) => {
|
|||||||
events,
|
events,
|
||||||
callbacks,
|
callbacks,
|
||||||
csrfToken,
|
csrfToken,
|
||||||
redirect
|
session: {
|
||||||
} = options
|
jwt: useJwtSession,
|
||||||
|
maxAge: sessionMaxAge
|
||||||
|
}
|
||||||
|
} = req.options
|
||||||
const provider = providers[providerName]
|
const provider = providers[providerName]
|
||||||
const { type } = provider
|
const { type } = provider
|
||||||
const useJwtSession = options.session.jwt
|
|
||||||
const sessionMaxAge = options.session.maxAge
|
|
||||||
|
|
||||||
// Get session ID (if set)
|
// Get session ID (if set)
|
||||||
const sessionToken = req.cookies ? req.cookies[cookies.sessionToken.name] : null
|
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
|
||||||
|
|
||||||
if (type === 'oauth') {
|
if (type === 'oauth') {
|
||||||
try {
|
try {
|
||||||
oAuthCallback(req, provider, csrfToken, async (error, profile, account, OAuthProfile) => {
|
const { profile, account, OAuthProfile } = await oAuthCallback(req, csrfToken)
|
||||||
try {
|
try {
|
||||||
if (error) {
|
// Make it easier to debug when adding a new provider
|
||||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make it easier to debug when adding a new provider
|
// If we don't have a profile object then either something went wrong
|
||||||
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
// or the user cancelled signin in. We don't know which, so we just
|
||||||
|
// direct the user to the signup page for now. We could do something
|
||||||
|
// else in future.
|
||||||
|
//
|
||||||
|
// Note: In oAuthCallback an error is logged with debug info, so it
|
||||||
|
// should at least be visible to developers what happened if it is an
|
||||||
|
// error with the provider.
|
||||||
|
if (!profile) {
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/signin`)
|
||||||
|
}
|
||||||
|
|
||||||
// If we don't have a profile object then either something went wrong
|
// Check if user is allowed to sign in
|
||||||
// or the user cancelled signin in. We don't know which, so we just
|
// Attempt to get Profile from OAuth provider details before invoking
|
||||||
// direct the user to the signup page for now. We could do something
|
// signIn callback - but if no user object is returned, that is fine
|
||||||
// else in future.
|
// (that just means it's a new user signing in for the first time).
|
||||||
//
|
let userOrProfile = profile
|
||||||
// Note: In oAuthCallback an error is logged with debug info, so it
|
if (adapter) {
|
||||||
// should at least be visible to developers what happened if it is an
|
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
|
||||||
// error with the provider.
|
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
||||||
if (!profile) {
|
if (userFromProviderAccountId) {
|
||||||
return redirect(`${baseUrl}${basePath}/signin`)
|
userOrProfile = userFromProviderAccountId
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is allowed to sign in
|
|
||||||
// Attempt to get Profile from OAuth provider details before invoking
|
|
||||||
// signIn callback - but if no user object is returned, that is fine
|
|
||||||
// (that just means it's a new user signing in for the first time).
|
|
||||||
let userOrProfile = profile
|
|
||||||
if (adapter) {
|
|
||||||
const { getUserByProviderAccountId } = await adapter.getAdapter(options)
|
|
||||||
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
|
||||||
if (userFromProviderAccountId) {
|
|
||||||
userOrProfile = userFromProviderAccountId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
|
||||||
if (signInCallbackResponse === false) {
|
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
|
||||||
} else {
|
|
||||||
return redirect(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign user in
|
|
||||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
|
|
||||||
|
|
||||||
if (useJwtSession) {
|
|
||||||
const defaultJwtPayload = {
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
picture: user.image
|
|
||||||
}
|
|
||||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
|
||||||
|
|
||||||
// Sign and encrypt token
|
|
||||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
|
||||||
|
|
||||||
// Set cookie expiry date
|
|
||||||
const cookieExpires = new Date()
|
|
||||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
|
||||||
|
|
||||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
|
||||||
} else {
|
|
||||||
// Save Session Token in cookie
|
|
||||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
|
||||||
}
|
|
||||||
|
|
||||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
|
||||||
|
|
||||||
// Handle first logins on new accounts
|
|
||||||
// e.g. option to send users to a new account landing page on initial login
|
|
||||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
|
||||||
if (isNewUser && pages.newUser) {
|
|
||||||
return redirect(pages.newUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callback URL is already verified at this point, so safe to use if specified
|
|
||||||
return redirect(callbackUrl || baseUrl)
|
|
||||||
} catch (error) {
|
|
||||||
if (error.name === 'AccountNotLinkedError') {
|
|
||||||
// If the email on the account is already linked, but nto with this oAuth account
|
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
|
|
||||||
} else if (error.name === 'CreateUserError') {
|
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
|
|
||||||
} else {
|
|
||||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
try {
|
||||||
|
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
||||||
|
if (signInCallbackResponse === false) {
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||||
|
} else if (typeof signInCallbackResponse === 'string') {
|
||||||
|
return res.redirect(signInCallbackResponse)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||||
|
}
|
||||||
|
// TODO: Remove in a future major release
|
||||||
|
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||||
|
return res.redirect(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign user in
|
||||||
|
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||||
|
|
||||||
|
if (useJwtSession) {
|
||||||
|
const defaultJwtPayload = {
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
picture: user.image,
|
||||||
|
sub: user.id?.toString()
|
||||||
|
}
|
||||||
|
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||||
|
|
||||||
|
// Sign and encrypt token
|
||||||
|
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||||
|
|
||||||
|
// Set cookie expiry date
|
||||||
|
const cookieExpires = new Date()
|
||||||
|
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||||
|
|
||||||
|
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||||
|
} else {
|
||||||
|
// Save Session Token in cookie
|
||||||
|
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||||
|
}
|
||||||
|
|
||||||
|
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||||
|
|
||||||
|
// Handle first logins on new accounts
|
||||||
|
// e.g. option to send users to a new account landing page on initial login
|
||||||
|
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||||
|
if (isNewUser && pages.newUser) {
|
||||||
|
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback URL is already verified at this point, so safe to use if specified
|
||||||
|
return res.redirect(callbackUrl || baseUrl)
|
||||||
|
} catch (error) {
|
||||||
|
if (error.name === 'AccountNotLinkedError') {
|
||||||
|
// If the email on the account is already linked, but not with this OAuth account
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
|
||||||
|
} else if (error.name === 'CreateUserError') {
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
|
||||||
|
} else {
|
||||||
|
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.name === 'OAuthCallbackError') {
|
||||||
|
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||||
|
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||||
|
}
|
||||||
logger.error('OAUTH_CALLBACK_ERROR', error)
|
logger.error('OAUTH_CALLBACK_ERROR', error)
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||||
}
|
}
|
||||||
} else if (type === 'email') {
|
} else if (type === 'email') {
|
||||||
try {
|
try {
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(options)
|
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
|
||||||
const verificationToken = req.query.token
|
const verificationToken = req.query.token
|
||||||
const email = req.query.email
|
const email = req.query.email
|
||||||
|
|
||||||
// Verify email and verification token exist in database
|
// Verify email and verification token exist in database
|
||||||
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
|
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If verification token is valid, delete verification request token from
|
// If verification token is valid, delete verification request token from
|
||||||
@@ -160,24 +163,28 @@ export default async (req, res, options, done) => {
|
|||||||
try {
|
try {
|
||||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||||
if (signInCallbackResponse === false) {
|
if (signInCallbackResponse === false) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||||
|
} else if (typeof signInCallbackResponse === 'string') {
|
||||||
|
return res.redirect(signInCallbackResponse)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||||
} else {
|
|
||||||
return redirect(error)
|
|
||||||
}
|
}
|
||||||
|
// TODO: Remove in a future major release
|
||||||
|
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||||
|
return res.redirect(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign user in
|
// Sign user in
|
||||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, options)
|
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||||
|
|
||||||
if (useJwtSession) {
|
if (useJwtSession) {
|
||||||
const defaultJwtPayload = {
|
const defaultJwtPayload = {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
picture: user.image
|
picture: user.image,
|
||||||
|
sub: user.id?.toString()
|
||||||
}
|
}
|
||||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||||
|
|
||||||
@@ -200,32 +207,28 @@ export default async (req, res, options, done) => {
|
|||||||
// e.g. option to send users to a new account landing page on initial login
|
// e.g. option to send users to a new account landing page on initial login
|
||||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||||
if (isNewUser && pages.newUser) {
|
if (isNewUser && pages.newUser) {
|
||||||
return redirect(pages.newUser)
|
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback URL is already verified at this point, so safe to use if specified
|
// Callback URL is already verified at this point, so safe to use if specified
|
||||||
if (callbackUrl) {
|
return res.redirect(callbackUrl || baseUrl)
|
||||||
return redirect(callbackUrl)
|
|
||||||
} else {
|
|
||||||
return redirect(baseUrl)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'CreateUserError') {
|
if (error.name === 'CreateUserError') {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
||||||
} else {
|
} else {
|
||||||
logger.error('CALLBACK_EMAIL_ERROR', error)
|
logger.error('CALLBACK_EMAIL_ERROR', error)
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === 'credentials' && req.method === 'POST') {
|
} else if (type === 'credentials' && req.method === 'POST') {
|
||||||
if (!useJwtSession) {
|
if (!useJwtSession) {
|
||||||
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!provider.authorize) {
|
if (!provider.authorize) {
|
||||||
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const credentials = req.body
|
const credentials = req.body
|
||||||
@@ -234,13 +237,13 @@ export default async (req, res, options, done) => {
|
|||||||
try {
|
try {
|
||||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||||
} else {
|
} else {
|
||||||
return redirect(error)
|
return res.redirect(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,13 +253,13 @@ export default async (req, res, options, done) => {
|
|||||||
try {
|
try {
|
||||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||||
if (signInCallbackResponse === false) {
|
if (signInCallbackResponse === false) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||||
} else {
|
} else {
|
||||||
return redirect(error)
|
return res.redirect(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,9 +281,8 @@ export default async (req, res, options, done) => {
|
|||||||
|
|
||||||
await dispatchEvent(events.signIn, { user, account })
|
await dispatchEvent(events.signIn, { user, account })
|
||||||
|
|
||||||
return redirect(callbackUrl || baseUrl)
|
return res.redirect(callbackUrl || baseUrl)
|
||||||
} else {
|
} else {
|
||||||
res.status(500).end(`Error: Callback for provider type ${type} not supported`)
|
return res.status(500).end(`Error: Callback for provider type ${type} not supported`)
|
||||||
return done()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
// Return a JSON object with a list of all outh providers currently configured
|
/**
|
||||||
// and their signin and callback URLs. This makes it possible to automatically
|
* Return a JSON object with a list of all outh providers currently configured
|
||||||
// generate buttons for all providers when rendering client side.
|
* and their signin and callback URLs. This makes it possible to automatically
|
||||||
export default (req, res, options, done) => {
|
* generate buttons for all providers when rendering client side.
|
||||||
const { providers } = options
|
*/
|
||||||
|
export default function providers (req, res) {
|
||||||
|
const { providers } = req.options
|
||||||
|
|
||||||
const result = {}
|
const result = Object.entries(providers)
|
||||||
Object.entries(providers).map(([provider, providerConfig]) => {
|
.reduce((acc, [provider, providerConfig]) => ({
|
||||||
result[provider] = {
|
...acc,
|
||||||
id: provider,
|
[provider]: {
|
||||||
name: providerConfig.name,
|
id: provider,
|
||||||
type: providerConfig.type,
|
name: providerConfig.name,
|
||||||
signinUrl: providerConfig.signinUrl,
|
type: providerConfig.type,
|
||||||
callbackUrl: providerConfig.callbackUrl
|
signinUrl: providerConfig.signinUrl,
|
||||||
}
|
callbackUrl: providerConfig.callbackUrl
|
||||||
})
|
}
|
||||||
|
}), {})
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.json(result)
|
res.json(result)
|
||||||
return done()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
// Return a session object (without any private fields) for Single Page App clients
|
import * as cookie from '../lib/cookie'
|
||||||
import cookie from '../lib/cookie'
|
|
||||||
import logger from '../../lib/logger'
|
import logger from '../../lib/logger'
|
||||||
import dispatchEvent from '../lib/dispatch-event'
|
import dispatchEvent from '../lib/dispatch-event'
|
||||||
|
|
||||||
export default async (req, res, options, done) => {
|
/**
|
||||||
const { cookies, adapter, jwt, events, callbacks } = options
|
* Return a session object (without any private fields)
|
||||||
const useJwtSession = options.session.jwt
|
* for Single Page App clients
|
||||||
const sessionMaxAge = options.session.maxAge
|
*/
|
||||||
|
export default async function session (req, res) {
|
||||||
|
const { cookies, adapter, jwt, events, callbacks } = req.options
|
||||||
|
const useJwtSession = req.options.session.jwt
|
||||||
|
const sessionMaxAge = req.options.session.maxAge
|
||||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||||
|
|
||||||
if (!sessionToken) {
|
if (!sessionToken) {
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.json({})
|
res.json({})
|
||||||
return done()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = {}
|
let response = {}
|
||||||
@@ -58,7 +61,7 @@ export default async (req, res, options, done) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const { getUser, getSession, updateSession } = await adapter.getAdapter(options)
|
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
|
||||||
const session = await getSession(sessionToken)
|
const session = await getSession(sessionToken)
|
||||||
if (session) {
|
if (session) {
|
||||||
// Trigger update to session object to update session expiry
|
// Trigger update to session object to update session expiry
|
||||||
@@ -100,5 +103,5 @@ export default async (req, res, options, done) => {
|
|||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
res.json(response)
|
res.json(response)
|
||||||
return done()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// Handle requests to /api/auth/signin
|
|
||||||
import oAuthSignin from '../lib/signin/oauth'
|
import oAuthSignin from '../lib/signin/oauth'
|
||||||
import emailSignin from '../lib/signin/email'
|
import emailSignin from '../lib/signin/email'
|
||||||
import logger from '../../lib/logger'
|
import logger from '../../lib/logger'
|
||||||
|
|
||||||
export default async (req, res, options, done) => {
|
/** Handle requests to /api/auth/signin */
|
||||||
|
export default async function signin (req, res) {
|
||||||
const {
|
const {
|
||||||
provider: providerName,
|
provider: providerName,
|
||||||
providers,
|
providers,
|
||||||
@@ -11,35 +11,30 @@ export default async (req, res, options, done) => {
|
|||||||
basePath,
|
basePath,
|
||||||
adapter,
|
adapter,
|
||||||
callbacks,
|
callbacks,
|
||||||
csrfToken,
|
csrfToken
|
||||||
redirect
|
} = req.options
|
||||||
} = options
|
|
||||||
const provider = providers[providerName]
|
const provider = providers[providerName]
|
||||||
const { type } = provider
|
const { type } = provider
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
res.status(500).end(`Error: Type not specified for ${provider}`)
|
res.status(500)
|
||||||
return done()
|
return res.end(`Error: Type not specified for ${provider}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'oauth' && req.method === 'POST') {
|
if (type === 'oauth' && req.method === 'POST') {
|
||||||
const authParams = { ...req.query }
|
try {
|
||||||
delete authParams.nextauth // This is probably not intended to be sent to the provider, remove
|
const oAuthSigninUrl = await oAuthSignin(provider, csrfToken)
|
||||||
|
return res.redirect(oAuthSigninUrl)
|
||||||
oAuthSignin(provider, csrfToken, (error, oAuthSigninUrl) => {
|
} catch (error) {
|
||||||
if (error) {
|
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return redirect(oAuthSigninUrl)
|
|
||||||
}, authParams)
|
|
||||||
} else if (type === 'email' && req.method === 'POST') {
|
} else if (type === 'email' && req.method === 'POST') {
|
||||||
if (!adapter) {
|
if (!adapter) {
|
||||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||||
}
|
}
|
||||||
const { getUserByEmail } = await adapter.getAdapter(options)
|
const { getUserByEmail } = await adapter.getAdapter(req.options)
|
||||||
|
|
||||||
// Note: Technically the part of the email address local mailbox element
|
// Note: Technically the part of the email address local mailbox element
|
||||||
// (everything before the @ symbol) should be treated as 'case sensitive'
|
// (everything before the @ symbol) should be treated as 'case sensitive'
|
||||||
@@ -54,29 +49,32 @@ export default async (req, res, options, done) => {
|
|||||||
|
|
||||||
// Check if user is allowed to sign in
|
// Check if user is allowed to sign in
|
||||||
try {
|
try {
|
||||||
const signinCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||||
if (signinCallbackResponse === false) {
|
if (signInCallbackResponse === false) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||||
|
} else if (typeof signInCallbackResponse === 'string') {
|
||||||
|
return res.redirect(signInCallbackResponse)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||||
} else {
|
|
||||||
return redirect(error)
|
|
||||||
}
|
}
|
||||||
|
// TODO: Remove in a future major release
|
||||||
|
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||||
|
return res.redirect(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await emailSignin(email, provider, options)
|
await emailSignin(email, provider, req.options)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('SIGNIN_EMAIL_ERROR', error)
|
logger.error('SIGNIN_EMAIL_ERROR', error)
|
||||||
return redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||||
provider.id
|
provider.id
|
||||||
)}&type=${encodeURIComponent(provider.type)}`)
|
)}&type=${encodeURIComponent(provider.type)}`)
|
||||||
} else {
|
} else {
|
||||||
return redirect(`${baseUrl}${basePath}/signin`)
|
return res.redirect(`${baseUrl}${basePath}/signin`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// Handle requests to /api/auth/signout
|
import * as cookie from '../lib/cookie'
|
||||||
import cookie from '../lib/cookie'
|
|
||||||
import logger from '../../lib/logger'
|
import logger from '../../lib/logger'
|
||||||
import dispatchEvent from '../lib/dispatch-event'
|
import dispatchEvent from '../lib/dispatch-event'
|
||||||
|
|
||||||
export default async (req, res, options, done) => {
|
/** Handle requests to /api/auth/signout */
|
||||||
const { adapter, cookies, events, jwt, callbackUrl, redirect } = options
|
export default async function signout (req, res) {
|
||||||
const useJwtSession = options.session.jwt
|
const { adapter, cookies, events, jwt, callbackUrl } = req.options
|
||||||
|
const useJwtSession = req.options.session.jwt
|
||||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||||
|
|
||||||
if (useJwtSession) {
|
if (useJwtSession) {
|
||||||
@@ -18,7 +18,7 @@ export default async (req, res, options, done) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get session from database
|
// Get session from database
|
||||||
const { getSession, deleteSession } = await adapter.getAdapter(options)
|
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Dispatch signout event
|
// Dispatch signout event
|
||||||
@@ -43,5 +43,5 @@ export default async (req, res, options, done) => {
|
|||||||
maxAge: 0
|
maxAge: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
return redirect(callbackUrl)
|
return res.redirect(callbackUrl)
|
||||||
}
|
}
|
||||||
|
|||||||
1430
test/docker/app/package-lock.json
generated
1430
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
|||||||
"author": "Iain Collins <me@iaincollins.com>",
|
"author": "Iain Collins <me@iaincollins.com>",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "^9.5.0",
|
"next": "^9.5.4",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-dom": "^16.13.1"
|
"react-dom": "^16.13.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ const options = {
|
|||||||
|
|
||||||
// You can define your own encode/decode functions for signing and encryption
|
// You can define your own encode/decode functions for signing and encryption
|
||||||
// if you want to override the default behaviour.
|
// if you want to override the default behaviour.
|
||||||
// encode: async ({ secret, token, maxAge }) => {},
|
// async encode({ secret, token, maxAge }) {},
|
||||||
// decode: async ({ secret, token, maxAge }) => {},
|
// async decode({ secret, token, maxAge }) {},
|
||||||
},
|
},
|
||||||
|
|
||||||
// You can define custom pages to override the built-in pages.
|
// You can define custom pages to override the built-in pages.
|
||||||
@@ -101,10 +101,10 @@ const options = {
|
|||||||
// when an action is performed.
|
// when an action is performed.
|
||||||
// https://next-auth.js.org/configuration/callbacks
|
// https://next-auth.js.org/configuration/callbacks
|
||||||
callbacks: {
|
callbacks: {
|
||||||
// signIn: async (user, account, profile) => { return Promise.resolve(true) },
|
// async signIn(user, account, profile) { return Promise.resolve(true) },
|
||||||
// redirect: async (url, baseUrl) => { return Promise.resolve(baseUrl) },
|
// async redirect(url, baseUrl) { return Promise.resolve(baseUrl) },
|
||||||
// session: async (session, user) => { return Promise.resolve(session) },
|
// async session(session, user) { return Promise.resolve(session) },
|
||||||
// jwt: async (token, user, account, profile, isNewUser) => { return Promise.resolve(token) }
|
// async jwt(token, user, account, profile, isNewUser) { return Promise.resolve(token) }
|
||||||
},
|
},
|
||||||
|
|
||||||
// Events are useful for logging
|
// Events are useful for logging
|
||||||
|
|||||||
@@ -34,3 +34,10 @@ services:
|
|||||||
service: postgres
|
service: postgres
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
|
fauna:
|
||||||
|
extends:
|
||||||
|
file: databases/fauna.yml
|
||||||
|
service: fauna
|
||||||
|
ports:
|
||||||
|
- 8443:8443
|
||||||
|
|||||||
7
test/docker/databases/fauna.yml
Normal file
7
test/docker/databases/fauna.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: '2'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
fauna:
|
||||||
|
image: fauna/faunadb
|
||||||
|
restart: always
|
||||||
200
test/fauna.js
Normal file
200
test/fauna.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
const Adapters = require('../adapters');
|
||||||
|
const assert = require('assert');
|
||||||
|
const fauna = require('faunadb');
|
||||||
|
const q = fauna.query;
|
||||||
|
|
||||||
|
const adminClient = new fauna.Client({
|
||||||
|
secret: 'secret',
|
||||||
|
domain: 'localhost',
|
||||||
|
port: '8443',
|
||||||
|
scheme: 'http'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authenticated client against the new DB used for tests
|
||||||
|
let client = null;
|
||||||
|
|
||||||
|
const InitialiseDb = async () => {
|
||||||
|
await adminClient.query(
|
||||||
|
q.CreateDatabase({name: 'nextauth'})
|
||||||
|
);
|
||||||
|
|
||||||
|
const key = await adminClient.query(
|
||||||
|
q.CreateKey({
|
||||||
|
database: q.Database('nextauth'),
|
||||||
|
role: 'server'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
client = new fauna.Client({
|
||||||
|
secret: key.secret,
|
||||||
|
domain: 'localhost',
|
||||||
|
port: '8443',
|
||||||
|
scheme: 'http'
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.query(q.CreateCollection({name: 'account'}));
|
||||||
|
await client.query(q.CreateCollection({name: 'session'}));
|
||||||
|
await client.query(q.CreateCollection({name: 'user'}));
|
||||||
|
await client.query(q.CreateCollection({name: 'verification_request'}));
|
||||||
|
|
||||||
|
await client.query(q.CreateIndex({
|
||||||
|
name: 'account_by_provider_account_id',
|
||||||
|
source: q.Collection('account'),
|
||||||
|
unique: true,
|
||||||
|
terms: [
|
||||||
|
{ field: ['data', 'providerId'] },
|
||||||
|
{ field: ['data', 'providerAccountId'] }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
await client.query(q.CreateIndex({
|
||||||
|
name: 'session_by_token',
|
||||||
|
source: q.Collection('session'),
|
||||||
|
unique: true,
|
||||||
|
terms: [
|
||||||
|
{ field: ['data', 'sessionToken'] }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
await client.query(q.CreateIndex({
|
||||||
|
name: 'user_by_email',
|
||||||
|
source: q.Collection('user'),
|
||||||
|
unique: true,
|
||||||
|
terms: [
|
||||||
|
{ field: ['data', 'email'] }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
|
||||||
|
await client.query(q.CreateIndex({
|
||||||
|
name: 'verification_request_by_token',
|
||||||
|
source: q.Collection('verification_request'),
|
||||||
|
unique: true,
|
||||||
|
terms: [
|
||||||
|
{ field: ['data', 'token'] }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const RunTests = async (adapter) => {
|
||||||
|
// createUser
|
||||||
|
const newUserResult = await adapter.createUser({
|
||||||
|
name: 'test user',
|
||||||
|
email: 'user@name.test',
|
||||||
|
image: 'https://www.gravatar.com/avatar/0'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(newUserResult.name, 'test user');
|
||||||
|
assert(newUserResult.createdAt !== null);
|
||||||
|
|
||||||
|
const userId = newUserResult.id;
|
||||||
|
|
||||||
|
// getUser
|
||||||
|
const user = await adapter.getUser(newUserResult.id);
|
||||||
|
assert.strictEqual(user.id, userId);
|
||||||
|
|
||||||
|
// getUserByEmail
|
||||||
|
const userByEmaiil = await adapter.getUserByEmail('user@name.test');
|
||||||
|
assert.strictEqual(userByEmaiil.id, userId);
|
||||||
|
|
||||||
|
// updateUser
|
||||||
|
const update = {
|
||||||
|
...user,
|
||||||
|
name: 'updated name'
|
||||||
|
};
|
||||||
|
const updatedUser = await adapter.updateUser(update);
|
||||||
|
assert.strictEqual(updatedUser.name, 'updated name');
|
||||||
|
assert.strictEqual(updatedUser.id, userId);
|
||||||
|
|
||||||
|
// linkAccount
|
||||||
|
const account = await adapter.linkAccount(
|
||||||
|
userId,
|
||||||
|
'github',
|
||||||
|
'oauth',
|
||||||
|
756832,
|
||||||
|
undefined,
|
||||||
|
'b7e3b00f2c596abc445f11abc445f1104c1b2b',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
assert.strictEqual(account.userId, userId);
|
||||||
|
assert.strictEqual(account.providerId, 'github');
|
||||||
|
assert(account.createdAt !== null);
|
||||||
|
|
||||||
|
// getUserByProviderAccountId
|
||||||
|
const userByProviderAccountId = await adapter.getUserByProviderAccountId('github', 756832);
|
||||||
|
assert.strictEqual(userByProviderAccountId.email, user.email);
|
||||||
|
|
||||||
|
// createSession
|
||||||
|
const newSession = await adapter.createSession(user);
|
||||||
|
assert(newSession.sessionToken !== null);
|
||||||
|
assert(newSession.createdAt !== null);
|
||||||
|
assert(newSession.expires !== null);
|
||||||
|
|
||||||
|
// getSession
|
||||||
|
const session = await adapter.getSession(newSession.sessionToken);
|
||||||
|
assert.strictEqual(session.sessionToken, newSession.sessionToken);
|
||||||
|
|
||||||
|
// updateSession
|
||||||
|
const updatedSession = await adapter.updateSession(session);
|
||||||
|
assert(updatedSession.expires !== session.expires);
|
||||||
|
|
||||||
|
// deleteSession
|
||||||
|
await adapter.deleteSession(session.sessionToken);
|
||||||
|
|
||||||
|
// unlinkAccount
|
||||||
|
await adapter.unlinkAccount(userId, 'github', 756832);
|
||||||
|
|
||||||
|
// deleteUser
|
||||||
|
await adapter.deleteUser(userId);
|
||||||
|
|
||||||
|
// createVerificationRequest
|
||||||
|
let requestSent = false;
|
||||||
|
const newVerificationRequest = await adapter.createVerificationRequest(
|
||||||
|
'user@test.test',
|
||||||
|
'http://localhost/callback/email?email=test@test.test&token=123',
|
||||||
|
'123',
|
||||||
|
'abc',
|
||||||
|
{
|
||||||
|
sendVerificationRequest: ({}) => {
|
||||||
|
requestSent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.strictEqual(newVerificationRequest.identifier, 'user@test.test');
|
||||||
|
assert(newVerificationRequest.token !== null && newVerificationRequest.token !== '');
|
||||||
|
assert(requestSent === true);
|
||||||
|
|
||||||
|
// getVerificationRequest
|
||||||
|
const verificationRequest = await adapter.getVerificationRequest('user@test.test', '123', 'abc');
|
||||||
|
assert.strictEqual(verificationRequest.identifier, 'user@test.test');
|
||||||
|
assert.strictEqual(verificationRequest.token, newVerificationRequest.token);
|
||||||
|
|
||||||
|
// deleteVerificationRequest
|
||||||
|
await adapter.deleteVerificationRequest('user@test.test', '123', 'abc');
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialise collections and create indexes
|
||||||
|
await InitialiseDb();
|
||||||
|
|
||||||
|
const adapterFactory = Adapters.Fauna.Adapter({faunaClient: client});
|
||||||
|
const adapter = await adapterFactory.getAdapter({baseUrl: 'http://localhost'});
|
||||||
|
|
||||||
|
await RunTests(adapter);
|
||||||
|
console.log('FaunaDB loaded ok');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('FaunaDB error', error);
|
||||||
|
error = true;
|
||||||
|
} finally {
|
||||||
|
// Clean up the DB
|
||||||
|
await adminClient.query(
|
||||||
|
q.Delete(q.Database('nextauth'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retCode = error ? 1 : 0;
|
||||||
|
process.exit(retCode);
|
||||||
|
})();
|
||||||
@@ -3,7 +3,7 @@ id: callbacks
|
|||||||
title: Callbacks
|
title: Callbacks
|
||||||
---
|
---
|
||||||
|
|
||||||
Callbacks are asynchronous functions you can use to control what happens when an action is performed.
|
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.
|
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.
|
||||||
|
|
||||||
@@ -16,17 +16,17 @@ You can specify a handler for any of the callbacks below.
|
|||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
...
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
signIn: async (user, account, profile) => {
|
async signIn(user, account, profile) {
|
||||||
return Promise.resolve(true)
|
return true
|
||||||
},
|
},
|
||||||
redirect: async (url, baseUrl) => {
|
async redirect(url, baseUrl) {
|
||||||
return Promise.resolve(baseUrl)
|
return baseUrl
|
||||||
},
|
},
|
||||||
session: async (session, user) => {
|
async session(session, user) {
|
||||||
return Promise.resolve(session)
|
return session
|
||||||
},
|
},
|
||||||
jwt: async (token, user, account, profile, isNewUser) => {
|
async jwt(token, user, account, profile, isNewUser) {
|
||||||
return Promise.resolve(token)
|
return token
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
}
|
}
|
||||||
@@ -39,27 +39,29 @@ The documentation below shows how to implement each callback, their default beha
|
|||||||
Use the `signIn()` callback to control if a user is allowed to sign in.
|
Use the `signIn()` callback to control if a user is allowed to sign in.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
/**
|
/**
|
||||||
* @param {object} user User object
|
* @param {object} user User object
|
||||||
* @param {object} account Provider account
|
* @param {object} account Provider account
|
||||||
* @param {object} profile Provider profile
|
* @param {object} profile Provider profile
|
||||||
* @return {boolean} Return `true` (or a modified JWT) to allow sign in
|
* @return {boolean|string} Return `true` to allow sign in
|
||||||
* Return `false` to deny access
|
* Return `false` to deny access
|
||||||
|
* Return `string` to redirect to (eg.: "/unauthorized")
|
||||||
*/
|
*/
|
||||||
signIn: async (user, account, profile) => {
|
async signIn(user, account, profile) {
|
||||||
const isAllowedToSignIn = true
|
const isAllowedToSignIn = true
|
||||||
if (isAllowedToSignIn) {
|
if (isAllowedToSignIn) {
|
||||||
return Promise.resolve(true)
|
return true
|
||||||
} else {
|
} else {
|
||||||
// Return false to display a default error message
|
// Return false to display a default error message
|
||||||
return Promise.resolve(false)
|
return false
|
||||||
// You can also Reject this callback with an Error or with a URL:
|
// Or you can return a URL to redirect to:
|
||||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
// return '/unauthorized'
|
||||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
|
* When using the **Email Provider** the `signIn()` callback is triggered both when the user makes a **Verification Request** (before they are sent email with a link that will allow them to sign in) and again *after* they activate the link in the sign in email.
|
||||||
@@ -89,18 +91,20 @@ The redirect callback is called anytime the user is redirected to a callback URL
|
|||||||
By default only URLs on the same URL as the site are allowed, you can use the redirect callback to customise that behaviour.
|
By default only URLs on the same URL as the site are allowed, you can use the redirect callback to customise that behaviour.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
/**
|
/**
|
||||||
* @param {string} url URL provided as callback URL by the client
|
* @param {string} url URL provided as callback URL by the client
|
||||||
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
* @param {string} baseUrl Default base URL of site (can be used as fallback)
|
||||||
* @return {string} URL the client will be redirect to
|
* @return {string} URL the client will be redirect to
|
||||||
*/
|
*/
|
||||||
redirect: async (url, baseUrl) => {
|
async redirect(url, baseUrl) {
|
||||||
return url.startsWith(baseUrl)
|
return url.startsWith(baseUrl)
|
||||||
? Promise.resolve(url)
|
? url
|
||||||
: Promise.resolve(baseUrl)
|
: baseUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -118,6 +122,7 @@ e.g. `getSession()`, `useSession()`, `/api/auth/session`
|
|||||||
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
|
* When using JSON Web Tokens for sessions, the JWT payload is provided instead.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
/**
|
/**
|
||||||
* @param {object} session Session object
|
* @param {object} session Session object
|
||||||
@@ -125,11 +130,12 @@ callbacks: {
|
|||||||
* JSON Web Token (if not using database sessions)
|
* JSON Web Token (if not using database sessions)
|
||||||
* @return {object} Session that will be returned to the client
|
* @return {object} Session that will be returned to the client
|
||||||
*/
|
*/
|
||||||
session: async (session, user) => {
|
async session(session, user) {
|
||||||
session.foo = 'bar' // Add property to session
|
session.foo = 'bar' // Add property to session
|
||||||
return Promise.resolve(session)
|
return session
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
@@ -148,7 +154,7 @@ If using JSON Web Tokens instead of database sessions, you should use the User I
|
|||||||
## JWT callback
|
## JWT callback
|
||||||
|
|
||||||
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
|
This JSON Web Token callback is called whenever a JSON Web Token is created (i.e. at sign
|
||||||
in) or updated (i.e whenever a session is accesed in the client).
|
in) or updated (i.e whenever a session is accessed in the client).
|
||||||
|
|
||||||
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
|
e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
|
||||||
|
|
||||||
@@ -158,6 +164,7 @@ e.g. `/api/auth/signin`, `getSession()`, `useSession()`, `/api/auth/session`
|
|||||||
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
|
The contents *user*, *account*, *profile* and *isNewUser* will vary depending on the provider and on if you are using a database or not. If you want to pass data such as User ID, OAuth Access Token, etc. to the browser, you can persist it in the token and use the `session()` callback to return it.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
/**
|
/**
|
||||||
* @param {object} token Decrypted JSON Web Token
|
* @param {object} token Decrypted JSON Web Token
|
||||||
@@ -167,13 +174,14 @@ callbacks: {
|
|||||||
* @param {boolean} isNewUser True if new user (only available on sign in)
|
* @param {boolean} isNewUser True if new user (only available on sign in)
|
||||||
* @return {object} JSON Web Token that will be saved
|
* @return {object} JSON Web Token that will be saved
|
||||||
*/
|
*/
|
||||||
jwt: async (token, user, account, profile, isNewUser) => {
|
async jwt(token, user, account, profile, isNewUser) {
|
||||||
const isSignIn = (user) ? true : false
|
const isSignIn = (user) ? true : false
|
||||||
// Add auth_time to token on signin in
|
// Add auth_time to token on signin in
|
||||||
if (isSignIn) { token.auth_time = Math.floor(Date.now() / 1000) }
|
if (isSignIn) { token.auth_time = Math.floor(Date.now() / 1000) }
|
||||||
return Promise.resolve(token)
|
return token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ Install module:
|
|||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
database: 'postgres://username:password@127.0.0.1:3306/database_name'
|
database: 'postgres://username:password@127.0.0.1:5432/database_name'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Microsoft SQL Server
|
### Microsoft SQL Server
|
||||||
@@ -166,7 +166,7 @@ Install module:
|
|||||||
#### Example
|
#### Example
|
||||||
|
|
||||||
```js
|
```js
|
||||||
database: 'mongodb://username:password@127.0.0.1:3306/database_name'
|
database: 'mongodb://username:password@127.0.0.1:27017/database_name'
|
||||||
```
|
```
|
||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ You can specify a handler for any of these events below, for debugging or for an
|
|||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
...
|
...
|
||||||
events: {
|
events: {
|
||||||
signIn: async (message) => { /* on successful sign in */ },
|
async signIn(message) { /* on successful sign in */ },
|
||||||
signOut: async (message) => { /* on signout */ },
|
async signOut(message) { /* on signout */ },
|
||||||
createUser: async (message) => { /* user created */ },
|
async createUser(message) { /* user created */ },
|
||||||
linkAccount: async (message) => { /* account linked to a user */ },
|
async linkAccount(message) { /* account linked to a user */ },
|
||||||
session: async (message) => { /* session is active */ },
|
async session(message) { /* session is active */ },
|
||||||
error: async (message) => { /* error in authentication flow */ }
|
async error(message) { /* error in authentication flow */ }
|
||||||
}
|
}
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -120,8 +120,8 @@ jwt: {
|
|||||||
|
|
||||||
// You can define your own encode/decode functions for signing and encryption
|
// You can define your own encode/decode functions for signing and encryption
|
||||||
// if you want to override the default behaviour.
|
// if you want to override the default behaviour.
|
||||||
// encode: async ({ secret, token, maxAge }) => {},
|
// async encode({ secret, token, maxAge }) {},
|
||||||
// decode: async ({ secret, token, maxAge }) => {},
|
// async decode({ secret, token, maxAge }) {},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -132,8 +132,8 @@ An example JSON Web Token contains a payload like this:
|
|||||||
name: 'Iain Collins',
|
name: 'Iain Collins',
|
||||||
email: 'me@iaincollins.com',
|
email: 'me@iaincollins.com',
|
||||||
picture: 'https://example.com/image.jpg',
|
picture: 'https://example.com/image.jpg',
|
||||||
"iat": 1594601838,
|
iat: 1594601838,
|
||||||
"exp": 1597193838
|
exp: 1597193838
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -230,17 +230,17 @@ You can specify a handler for any of the callbacks below.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
callbacks: {
|
callbacks: {
|
||||||
signIn: async (user, account, profile) => {
|
async signIn(user, account, profile) {
|
||||||
return Promise.resolve(true)
|
return true
|
||||||
},
|
},
|
||||||
redirect: async (url, baseUrl) => {
|
async redirect(url, baseUrl) {
|
||||||
return Promise.resolve(baseUrl)
|
return baseUrl
|
||||||
},
|
},
|
||||||
session: async (session, user) => {
|
async session(session, user) {
|
||||||
return Promise.resolve(session)
|
return session
|
||||||
},
|
},
|
||||||
jwt: async (token, user, account, profile, isNewUser) => {
|
async jwt(token, user, account, profile, isNewUser) {
|
||||||
return Promise.resolve(token)
|
return token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -264,20 +264,22 @@ The content of the message object varies depending on the flow (e.g. OAuth or Em
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
events: {
|
events: {
|
||||||
signIn: async (message) => { /* on successful sign in */ },
|
async signIn(message) { /* on successful sign in */ },
|
||||||
signOut: async (message) => { /* on signout */ },
|
async signOut(message) { /* on signout */ },
|
||||||
createUser: async (message) => { /* user created */ },
|
async createUser(message) { /* user created */ },
|
||||||
linkAccount: async (message) => { /* account linked to a user */ },
|
async linkAccount(message) { /* account linked to a user */ },
|
||||||
session: async (message) => { /* session is active */ },
|
async session(message) { /* session is active */ },
|
||||||
error: async (message) => { /* error in authentication flow */ }
|
async error(message) { /* error in authentication flow */ }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See the [events documentation](/configuration/events) for more information on how to use the events functions.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### adapter
|
### adapter
|
||||||
|
|
||||||
* **Default value**: *Adapater.Default()*
|
* **Default value**: *Adapter.Default()*
|
||||||
* **Required**: *No*
|
* **Required**: *No*
|
||||||
|
|
||||||
#### Description
|
#### Description
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ To add a custom login page, for example. You can use the `pages` option:
|
|||||||
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
|
In order to get the available authentication providers and the URLs to use for them, you can make a request to the API endpoint `/api/auth/providers`:
|
||||||
|
|
||||||
```jsx title="pages/auth/signin.js"
|
```jsx title="pages/auth/signin.js"
|
||||||
import React from 'react'
|
|
||||||
import { providers, signIn } from 'next-auth/client'
|
import { providers, signIn } from 'next-auth/client'
|
||||||
|
|
||||||
export default function SignIn({ providers }) {
|
export default function SignIn({ providers }) {
|
||||||
@@ -55,7 +54,6 @@ SignIn.getInitialProps = async (context) => {
|
|||||||
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
|
If you create a custom sign in form for email sign in, you will need to submit both fields for the **email** address and **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/signin/email**.
|
||||||
|
|
||||||
```jsx title="pages/auth/email-signin.js"
|
```jsx title="pages/auth/email-signin.js"
|
||||||
import React from 'react'
|
|
||||||
import { csrfToken } from 'next-auth/client'
|
import { csrfToken } from 'next-auth/client'
|
||||||
|
|
||||||
export default function SignIn({ csrfToken }) {
|
export default function SignIn({ csrfToken }) {
|
||||||
@@ -89,7 +87,6 @@ signIn('email', { email: 'jsmith@example.com' })
|
|||||||
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
|
If you create a sign in form for credentials based authentication, you will need to pass a **csrfToken** from **/api/auth/csrf** in a POST request to **/api/auth/callback/credentials**.
|
||||||
|
|
||||||
```jsx title="pages/auth/credentials-signin.js"
|
```jsx title="pages/auth/credentials-signin.js"
|
||||||
import React from 'react'
|
|
||||||
import { csrfToken } from 'next-auth/client'
|
import { csrfToken } from 'next-auth/client'
|
||||||
|
|
||||||
export default function SignIn({ csrfToken }) {
|
export default function SignIn({ csrfToken }) {
|
||||||
|
|||||||
@@ -14,24 +14,31 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
|||||||
* [Apple](/providers/apple)
|
* [Apple](/providers/apple)
|
||||||
* [Atlassian](/providers/atlassian)
|
* [Atlassian](/providers/atlassian)
|
||||||
* [Auth0](/providers/auth0)
|
* [Auth0](/providers/auth0)
|
||||||
|
* [Azure Active Directory B2C](/providers/azure-ad-b2c)
|
||||||
* [Basecamp](/providers/basecamp)
|
* [Basecamp](/providers/basecamp)
|
||||||
* [Battle.net](/providers/battlenet)
|
* [Battle.net](/providers/battle.net)
|
||||||
* [Box](/providers/box)
|
* [Box](/providers/box)
|
||||||
|
* [Bungie](/providers/bungie)
|
||||||
* [Amazon Cognito](/providers/cognito)
|
* [Amazon Cognito](/providers/cognito)
|
||||||
* [Discord](/providers/discord)
|
* [Discord](/providers/discord)
|
||||||
* [Facebook](/providers/facebook)
|
* [Facebook](/providers/facebook)
|
||||||
|
* [Foursquare](/providers/foursquare)
|
||||||
* [FusionAuth](/providers/fusionauth)
|
* [FusionAuth](/providers/fusionauth)
|
||||||
* [GitHub](/providers/github)
|
* [GitHub](/providers/github)
|
||||||
* [GitLab](/providers/gitlab)
|
* [GitLab](/providers/gitlab)
|
||||||
* [Google](/providers/google)
|
* [Google](/providers/google)
|
||||||
* [IdentityServer4](/providers/identity-server4)
|
* [IdentityServer4](/providers/identity-server4)
|
||||||
* [LinkedIn](/providers/LinkedIn)
|
* [LinkedIn](/providers/linkedin)
|
||||||
* [Mixer](/providers/Mixer)
|
* [Mail.ru](/providers/mailru)
|
||||||
* [Okta](/providers/Okta)
|
* [Mixer](/providers/mixer)
|
||||||
|
* [Netlify](/providers/netlify)
|
||||||
|
* [Okta](/providers/okta)
|
||||||
* [Slack](/providers/slack)
|
* [Slack](/providers/slack)
|
||||||
* [Spotify](/providers/spotify)
|
* [Spotify](/providers/spotify)
|
||||||
|
* [Strava](/providers/strava)
|
||||||
* [Twitch](/providers/Twitch)
|
* [Twitch](/providers/Twitch)
|
||||||
* [Twitter](/providers/twitter)
|
* [Twitter](/providers/twitter)
|
||||||
|
* [VK](/providers/vk)
|
||||||
* [Yandex](/providers/yandex)
|
* [Yandex](/providers/yandex)
|
||||||
|
|
||||||
### Using a built-in OAuth provider
|
### Using a built-in OAuth provider
|
||||||
@@ -56,6 +63,7 @@ NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1
|
|||||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
...
|
...
|
||||||
providers: [
|
providers: [
|
||||||
Providers.Twitter({
|
Providers.Twitter({
|
||||||
@@ -79,19 +87,19 @@ You can use an OAuth provider that isn't built-in by using a custom object.
|
|||||||
|
|
||||||
As an example of what this looks like, this is the the provider object returned for the Google provider:
|
As an example of what this looks like, this is the the provider object returned for the Google provider:
|
||||||
|
|
||||||
```json
|
```js
|
||||||
{
|
{
|
||||||
id: 'google',
|
id: "google",
|
||||||
name: 'Google',
|
name: "Google",
|
||||||
type: 'oauth',
|
type: "oauth",
|
||||||
version: '2.0',
|
version: "2.0",
|
||||||
scope: 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
|
scope: "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
|
||||||
params: { grant_type: 'authorization_code' },
|
params: { grant_type: "authorization_code" },
|
||||||
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
|
accessTokenUrl: "https://accounts.google.com/o/oauth2/token",
|
||||||
requestTokenUrl: 'https://accounts.google.com/o/oauth2/auth',
|
requestTokenUrl: "https://accounts.google.com/o/oauth2/auth",
|
||||||
authorizationUrl: 'https://accounts.google.com/o/oauth2/auth?response_type=code',
|
authorizationUrl: "https://accounts.google.com/o/oauth2/auth?response_type=code",
|
||||||
profileUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
|
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||||
profile: (profile) => {
|
async profile(profile) {
|
||||||
return {
|
return {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
@@ -99,13 +107,14 @@ As an example of what this looks like, this is the the provider object returned
|
|||||||
image: profile.picture
|
image: profile.picture
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clientId: '',
|
clientId: "",
|
||||||
clientSecret: ''
|
clientSecret: ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
You can replace all the options in this JSON object with the ones from your custom provider – be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
You can replace all the options in this JSON object with the ones from your custom provider - be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
...
|
...
|
||||||
providers: [
|
providers: [
|
||||||
Providers.Twitter({
|
Providers.Twitter({
|
||||||
@@ -126,27 +135,24 @@ providers: [
|
|||||||
|
|
||||||
### OAuth provider options
|
### OAuth provider options
|
||||||
|
|
||||||
| Name | Description | Required |
|
| Name | Description | Type | Required |
|
||||||
| :--------------: | :-------------------------------------------------: | :------: |
|
| :-----------------: | :---------------------------------------------------------: | :-----------------------------: | :------: |
|
||||||
| id | Unique ID for the provider | Yes |
|
| id | Unique ID for the provider | `string` | Yes |
|
||||||
| name | Descriptive name for the provider | Yes |
|
| name | Descriptive name for the provider | `string` | Yes |
|
||||||
| type | Type of provider, in this case it should be `oauth` | Yes |
|
| type | Type of provider, in this case it should be `oauth` | `oauth`, `email`, `credentials` | Yes |
|
||||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | Yes |
|
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
|
||||||
| scope | OAuth access scopes (expects array or string) | No |
|
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
|
||||||
| params | Additional authorization URL parameters | No |
|
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
|
||||||
| accessTokenUrl | Endpoint to retrieve an access token | Yes |
|
| clientId | Client ID of the OAuth provider | `string` | Yes |
|
||||||
| requestTokenUrl | Endpoint to retrieve a request token | No |
|
| clientSecret | Client Secret of the OAuth provider | `string` | No |
|
||||||
| authorizationUrl | Endpoint to request authorization from the user | Yes |
|
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | No |
|
||||||
| profileUrl | Endpoint to retrieve the user's profile | No |
|
| params | Additional authorization URL parameters | `object` | No |
|
||||||
| profile | An object with the user's info | No |
|
| requestTokenUrl | Endpoint to retrieve a request token | `string` | No |
|
||||||
| clientId | Client ID of the OAuth provider | Yes |
|
| authorizationParams | Additional params to be sent to the authorization endpoint | `object` | No |
|
||||||
| clientSecret | Client Secret of the OAuth provider | No |
|
| profileUrl | Endpoint to retrieve the user's profile | `string` | No |
|
||||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | No |
|
| profile | An callback returning an object with the user's info | `object` | No |
|
||||||
| state | Set to `false` for services that do not support `state` verfication | No |
|
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||||
|
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
|
||||||
:::note
|
|
||||||
Feel free to open a PR for your custom configuration if you've created one for a provider that others may be interested in so we can add it to the list of built-in OAuth providers!
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Sign in with Email
|
## Sign in with Email
|
||||||
|
|
||||||
@@ -157,6 +163,8 @@ Adding support for signing in via email in addition to one or more OAuth service
|
|||||||
Configuration is similar to other providers, but the options are different:
|
Configuration is similar to other providers, but the options are different:
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
providers: [
|
providers: [
|
||||||
Providers.Email({
|
Providers.Email({
|
||||||
server: process.env.EMAIL_SERVER,
|
server: process.env.EMAIL_SERVER,
|
||||||
@@ -164,6 +172,7 @@ providers: [
|
|||||||
// maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
|
// maxAge: 24 * 60 * 60, // How long email links are valid for (default 24h)
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
See the [Email provider documentation](/providers/email) for more information on how to configure email sign in.
|
See the [Email provider documentation](/providers/email) for more information on how to configure email sign in.
|
||||||
@@ -193,7 +202,7 @@ providers: [
|
|||||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||||
password: { label: "Password", type: "password" }
|
password: { label: "Password", type: "password" }
|
||||||
},
|
},
|
||||||
authorize: async (credentials) => {
|
async authorize(credentials) {
|
||||||
const user = (credentials) => {
|
const user = (credentials) => {
|
||||||
// You need to provide your own logic here that takes the credentials
|
// You need to provide your own logic here that takes the credentials
|
||||||
// submitted and returns either a object representing a user or value
|
// submitted and returns either a object representing a user or value
|
||||||
@@ -203,9 +212,9 @@ providers: [
|
|||||||
}
|
}
|
||||||
if (user) {
|
if (user) {
|
||||||
// Any user object returned here will be saved in the JSON Web Token
|
// Any user object returned here will be saved in the JSON Web Token
|
||||||
return Promise.resolve(user)
|
return user
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(null)
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -220,7 +229,7 @@ The Credentials provider can only be used if JSON Web Tokens are enabled for ses
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
<!-- React Image Component -->
|
<!-- React Image Component -->
|
||||||
export const Image = ({ children, src, alt = '' }) => (
|
export const Image = ({ children, src, alt = '' }) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
padding: '0.2rem',
|
padding: '0.2rem',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ title: Contributors
|
|||||||
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
|
* <a href="https://github.com/geraldnolan">Gerald Nolan</a>
|
||||||
* <a href="https://github.com/lluia">Lluis Agusti</a>
|
* <a href="https://github.com/lluia">Lluis Agusti</a>
|
||||||
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
|
* <a href="https://github.com/JeffersonBledsoe">Jefferson Bledsoe</a>
|
||||||
|
* <a href="https://github.com/balazsorban44">Balázs Orbán</a>
|
||||||
|
|
||||||
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._
|
_Special thanks to Lori Karikari for creating most of the providers, to Nico Domino for creating this site, to Fredrik Pettersen for creating the Prisma adapter, to Gerald Nolan for adding support for Sign in with Apple, to Lluis Agusti for work to add TypeScript definitions and to Jefferson Bledsoe for working on automating testing._
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ If you are using a Credentials Provider, NextAuth.js will not persist users or s
|
|||||||
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
|
In _most cases_ it does not make sense to specify a database in NextAuth.js options and support a Credentials Provider.
|
||||||
|
|
||||||
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
|
#### CALLBACK_CREDENTIALS_HANDLER_ERROR
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Session Handling
|
### Session Handling
|
||||||
@@ -127,3 +128,9 @@ They all indicate a problem interacting with the database.
|
|||||||
This error occurs when the Email Authentication Provider is unable to send an email.
|
This error occurs when the Email Authentication Provider is unable to send an email.
|
||||||
|
|
||||||
Check your mail server configuration.
|
Check your mail server configuration.
|
||||||
|
|
||||||
|
#### MISSING_NEXTAUTH_API_ROUTE_ERROR
|
||||||
|
|
||||||
|
This error happens when `[...nextauth].js` file is not found inside `pages/api/auth`.
|
||||||
|
|
||||||
|
Make sure the file is there and the filename is written correctly.
|
||||||
@@ -23,7 +23,7 @@ You can use also NextAuth.js with any database using a custom database adapter,
|
|||||||
|
|
||||||
### What authentication services does NextAuth.js support?
|
### What authentication services does NextAuth.js support?
|
||||||
|
|
||||||
NextAuth.js includes built-in support for signing in with Apple, Atlassian, Auth0, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, FusionAuth, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Okta, Slack, Spotify, Twitch, Twitter and Yandex.
|
NextAuth.js includes built-in support for signing in with Apple, Atlassian, Auth0, Azure Active Directory B2C, Google, Battle.net, Box, AWS Cognito, Discord, Facebook, Foursquare, FusionAuth, GitHub, GitLab, Google, Open ID Identity Server, Mixer, Netlify, Okta, Slack, Spotify, Strava, Twitch, Twitter and Yandex.
|
||||||
|
|
||||||
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
|
NextAuth.js also supports email for passwordless sign in, which is useful for account recovery or for people who are not able to use an account with the configured OAuth services (e.g. due to service outage, account suspension or otherwise becoming locked out of an account).
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ NextAuth.js records Refresh Tokens and Access Tokens on sign in (if supplied by
|
|||||||
|
|
||||||
You can then look them up from the database or persist them to the JSON Web Token.
|
You can then look them up from the database or persist them to the JSON Web Token.
|
||||||
|
|
||||||
Note: NextAuth.js does not current handle Access Token rotation for OAuth providers for you, if this is something you need, currently you will need to write the logic to handle that yourself.
|
Note: NextAuth.js does not currently handle Access Token rotation for OAuth providers for you, if this is something you need, currently you will need to write the logic to handle that yourself.
|
||||||
|
|
||||||
### When I sign in with another account with the same email address, why are accounts not linked automatically?
|
### When I sign in with another account with the same email address, why are accounts not linked automatically?
|
||||||
|
|
||||||
|
|||||||
@@ -41,13 +41,14 @@ It works best when the [`<Provider>`](#provider) is added to `pages/_app.js`.
|
|||||||
```jsx
|
```jsx
|
||||||
import { useSession } from 'next-auth/client'
|
import { useSession } from 'next-auth/client'
|
||||||
|
|
||||||
export default () => {
|
export default function Component() {
|
||||||
const [ session, loading ] = useSession()
|
const [ session, loading ] = useSession()
|
||||||
|
|
||||||
return <>
|
if(session) {
|
||||||
{session && <p>Signed in as {session.user.email}</p>}
|
return <p>Signed in as {session.user.email}</p>
|
||||||
{!session && <p><a href="/api/auth/signin">Sign in</a></p>}
|
}
|
||||||
</>
|
|
||||||
|
return <a href="/api/auth/signin">Sign in</a>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -169,7 +170,7 @@ The `signIn()` method can be called from the client in different ways, as shown
|
|||||||
import { signIn } from 'next-auth/client'
|
import { signIn } from 'next-auth/client'
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<button onClick={signIn}>Sign in</button>
|
<button onClick={() => signIn()}>Sign in</button>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -224,7 +225,7 @@ It reloads the page in the browser when complete.
|
|||||||
import { signOut } from 'next-auth/client'
|
import { signOut } from 'next-auth/client'
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<button onClick={signOut}>Sign out</button>
|
<button onClick={() => signOut()}>Sign out</button>
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ To add NextAuth.js to a project create a file called `[...nextauth].js` in `page
|
|||||||
import NextAuth from 'next-auth'
|
import NextAuth from 'next-auth'
|
||||||
import Providers from 'next-auth/providers'
|
import Providers from 'next-auth/providers'
|
||||||
|
|
||||||
const options = {
|
export default NextAuth({
|
||||||
// Configure one or more authentication providers
|
// Configure one or more authentication providers
|
||||||
providers: [
|
providers: [
|
||||||
Providers.GitHub({
|
Providers.GitHub({
|
||||||
@@ -33,9 +33,7 @@ const options = {
|
|||||||
|
|
||||||
// A database is optional, but required to persist accounts in a database
|
// A database is optional, but required to persist accounts in a database
|
||||||
database: process.env.DATABASE_URL,
|
database: process.env.DATABASE_URL,
|
||||||
}
|
})
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
All requests to `/api/auth/*` (signin, callback, signout, etc) will automatically be handed by NextAuth.js.
|
All requests to `/api/auth/*` (signin, callback, signout, etc) will automatically be handed by NextAuth.js.
|
||||||
@@ -49,7 +47,6 @@ See the [options documentation](/configuration/options) for how to configure pro
|
|||||||
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
The `useSession()` React Hook in the NextAuth.js client is the easiest way to check if someone is signed in.
|
||||||
|
|
||||||
```jsx title="pages/index.js"
|
```jsx title="pages/index.js"
|
||||||
import React from 'react'
|
|
||||||
import { signIn, signOut, useSession } from 'next-auth/client'
|
import { signIn, signOut, useSession } from 'next-auth/client'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
@@ -58,11 +55,11 @@ export default function Page() {
|
|||||||
return <>
|
return <>
|
||||||
{!session && <>
|
{!session && <>
|
||||||
Not signed in <br/>
|
Not signed in <br/>
|
||||||
<button onClick={signIn}>Sign in</button>
|
<button onClick={() => signIn()}>Sign in</button>
|
||||||
</>}
|
</>}
|
||||||
{session && <>
|
{session && <>
|
||||||
Signed in as {session.user.email} <br/>
|
Signed in as {session.user.email} <br/>
|
||||||
<button onClick={signOut}>Sign out</button>
|
<button onClick={() => signOut()}>Sign out</button>
|
||||||
</>}
|
</>}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -103,5 +100,5 @@ NEXTAUTH_URL=https://example.com
|
|||||||
:::tip
|
:::tip
|
||||||
In production, this needs to be set as an environment variable on the service you use to deploy your app.
|
In production, this needs to be set as an environment variable on the service you use to deploy your app.
|
||||||
|
|
||||||
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `now env` command.
|
To set environment variables on Vercel, you can use the [dashboard](https://vercel.com/dashboard) or the `vercel env pull` [command](https://vercel.com/docs/build-step#development-environment-variables).
|
||||||
:::
|
:::
|
||||||
|
|||||||
22
www/docs/getting-started/typescript.md
Normal file
22
www/docs/getting-started/typescript.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
id: typescript
|
||||||
|
title: TypeScript Support
|
||||||
|
---
|
||||||
|
|
||||||
|
Currently, NextAuth.js relies on the community to provide TypeScript types. You can download it from [DefinitelyTyped](https://www.npmjs.com/package/@types/next-auth).
|
||||||
|
|
||||||
|
Add it to your project with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm i -D @types/next-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn add -D @types/next-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find an initial Pull Request at [next-auth#516](https://github.com/nextauthjs/next-auth/pull/516) adding TypeScript. At the time of this writing, it looks like we would like to go from a complete migration to a more relaxed, incremental rewrite.
|
||||||
|
|
||||||
|
Feel free to open a Pull Request, if you would like to contribute!
|
||||||
@@ -26,13 +26,12 @@ providers: [
|
|||||||
Providers.Apple({
|
Providers.Apple({
|
||||||
clientId: process.env.APPLE_ID,
|
clientId: process.env.APPLE_ID,
|
||||||
clientSecret: {
|
clientSecret: {
|
||||||
appleId: process.env.APPLE_ID,
|
|
||||||
teamId: process.env.APPLE_TEAM_ID,
|
teamId: process.env.APPLE_TEAM_ID,
|
||||||
privateKey: process.env.APPLE_PRIVATE_KEY,
|
privateKey: process.env.APPLE_PRIVATE_KEY,
|
||||||
keyId: process.env.APPLE_KEY_ID,
|
keyId: process.env.APPLE_KEY_ID,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
]
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -65,10 +64,9 @@ import Providers from `next-auth/providers`
|
|||||||
providers: [
|
providers: [
|
||||||
Providers.Apple({
|
Providers.Apple({
|
||||||
clientId: process.env.APPLE_ID,
|
clientId: process.env.APPLE_ID,
|
||||||
clientSecret: process.env.APPLE_KEY_SECRET,
|
clientSecret: process.env.APPLE_KEY_SECRET
|
||||||
clientSecretCallback: false
|
|
||||||
})
|
})
|
||||||
}
|
]
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
28
www/docs/providers/azure-ad-b2c.md
Normal file
28
www/docs/providers/azure-ad-b2c.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
id: azure-ad-b2c
|
||||||
|
title: Azure Active Directory B2C
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from 'next-auth/providers';
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.AzureADB2C({
|
||||||
|
clientId: process.env.AZURE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.AZURE_CLIENT_SECRET,
|
||||||
|
scope: 'offline_access User.Read',
|
||||||
|
tenantId: process.env.AZURE_TENANT_ID,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
...
|
||||||
|
```
|
||||||
134
www/docs/providers/bungie.md
Normal file
134
www/docs/providers/bungie.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
---
|
||||||
|
id: bungie
|
||||||
|
title: Bungie
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://github.com/Bungie-net/api/wiki/OAuth-Documentation
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://www.bungie.net/en/Application
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.Bungie({
|
||||||
|
clientId: process.env.BUNGIE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.BUNGIE_SECRET,
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': provess.env.BUNGIE_API_KEY
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
Bungie require all sites to run HTTPS (including local development instances).
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
Bungie doesn't allow you to use localhost as the website URL, instead you need to use https://127.0.0.1:3000
|
||||||
|
:::
|
||||||
|
|
||||||
|
Navigate to https://www.bungie.net/en/Application and fill in the required details:
|
||||||
|
|
||||||
|
* Application name
|
||||||
|
* Application Status
|
||||||
|
* Website
|
||||||
|
* OAuth Client Type
|
||||||
|
- Confidential
|
||||||
|
* Redirect URL
|
||||||
|
- https://localhost:3000/api/auth/callback/bungie
|
||||||
|
* Scope
|
||||||
|
- `Access items like your Bungie.net notifications, memberships, and recent Bungie.Net forum activity.`
|
||||||
|
* Origin Header
|
||||||
|
|
||||||
|
The following guide may be helpful:
|
||||||
|
|
||||||
|
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||||
|
|
||||||
|
### Example server
|
||||||
|
|
||||||
|
You will need to edit your host file and point your site at `127.0.0.1`
|
||||||
|
|
||||||
|
[How to edit my host file?](https://phoenixnap.com/kb/how-to-edit-hosts-file-in-windows-mac-or-linux)
|
||||||
|
|
||||||
|
On Windows (Run Powershell as administrator)
|
||||||
|
|
||||||
|
```ps
|
||||||
|
Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.example.com" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
127.0.0.1 dev.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Create certificate
|
||||||
|
|
||||||
|
|
||||||
|
Creating a certificate for localhost is easy with openssl. Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||||
|
-newkey rsa:2048 -nodes -sha256 \
|
||||||
|
-subj '/CN=localhost' -extensions EXT -config <( \
|
||||||
|
printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth")
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
**Windows**
|
||||||
|
|
||||||
|
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
|
||||||
|
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if it’s not already done.
|
||||||
|
|
||||||
|
Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.cnf`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
req -x509 -out localhost.crt -keyout localhost.key \
|
||||||
|
-newkey rsa:2048 -nodes -sha256 \
|
||||||
|
-subj '/CN=localhost'
|
||||||
|
```
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Create directory `certificates` and place `localhost.key` and `localhost.crt`
|
||||||
|
|
||||||
|
|
||||||
|
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Bungie integration locally:
|
||||||
|
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { createServer } = require('https')
|
||||||
|
const { parse } = require('url')
|
||||||
|
const next = require('next')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const dev = process.env.NODE_ENV !== 'production'
|
||||||
|
const app = next({ dev })
|
||||||
|
const handle = app.getRequestHandler()
|
||||||
|
|
||||||
|
const httpsOptions = {
|
||||||
|
key: fs.readFileSync('./certificates/localhost.key'),
|
||||||
|
cert: fs.readFileSync('./certificates/localhost.crt')
|
||||||
|
}
|
||||||
|
|
||||||
|
app.prepare().then(() => {
|
||||||
|
createServer(httpsOptions, (req, res) => {
|
||||||
|
const parsedUrl = parse(req.url, true)
|
||||||
|
handle(req, res, parsedUrl)
|
||||||
|
}).listen(3000, err => {
|
||||||
|
if (err) throw err
|
||||||
|
console.log('> Ready on https://localhost:3000')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
@@ -27,9 +27,9 @@ The Credentials provider is specified like other providers, except that you need
|
|||||||
|
|
||||||
If you return `false` or `null` then an error will be displayed advising the user to check their details.
|
If you return `false` or `null` then an error will be displayed advising the user to check their details.
|
||||||
|
|
||||||
3. `Promise.Rejected()` with an Error or a URL.
|
3. You can throw an Error or a URL (a string).
|
||||||
|
|
||||||
If you reject the promise with an Error, the user will be sent to the error page with the error message as a query parameter. If you reject the promise with a URL (a string), the user will be redirected to the URL.
|
If you throw an Error, the user will be sent to the error page with the error message as a query parameter. If throw a URL (a string), the user will be redirected to the URL.
|
||||||
|
|
||||||
```js title="pages/api/auth/[...nextauth].js"
|
```js title="pages/api/auth/[...nextauth].js"
|
||||||
import Providers from `next-auth/providers`
|
import Providers from `next-auth/providers`
|
||||||
@@ -45,19 +45,19 @@ providers: [
|
|||||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||||
password: { label: "Password", type: "password" }
|
password: { label: "Password", type: "password" }
|
||||||
},
|
},
|
||||||
authorize: async (credentials) => {
|
async authorize(credentials) {
|
||||||
// Add logic here to look up the user from the credentials supplied
|
// Add logic here to look up the user from the credentials supplied
|
||||||
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
const user = { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
// Any object returned will be saved in `user` property of the JWT
|
// Any object returned will be saved in `user` property of the JWT
|
||||||
return Promise.resolve(user)
|
return user
|
||||||
} else {
|
} else {
|
||||||
// If you return null or false then the credentials will be rejected
|
// If you return null or false then the credentials will be rejected
|
||||||
return Promise.resolve(null)
|
return null
|
||||||
// You can also Reject this callback with an Error or with a URL:
|
// You can also Reject this callback with an Error or with a URL:
|
||||||
// return Promise.reject(new Error('error message')) // Redirect to error page
|
// throw new Error('error message') // Redirect to error page
|
||||||
// return Promise.reject('/path/to/redirect') // Redirect to a URL
|
// throw '/path/to/redirect' // Redirect to a URL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -82,9 +82,9 @@ As with all providers, the order you specify them is the order they are displaye
|
|||||||
Providers.Credentials({
|
Providers.Credentials({
|
||||||
id: 'domain-login',
|
id: 'domain-login',
|
||||||
name: "Domain Account",
|
name: "Domain Account",
|
||||||
authorize: async (credentials) => {
|
async authorize(credentials) {
|
||||||
const user = { /* add function to get user */ }
|
const user = { /* add function to get user */ }
|
||||||
return Promise.resolve(user)
|
return user
|
||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
domain: { label: "Domain", type: "text ", placeholder: "CORPNET", value: "CORPNET" },
|
domain: { label: "Domain", type: "text ", placeholder: "CORPNET", value: "CORPNET" },
|
||||||
@@ -95,9 +95,9 @@ As with all providers, the order you specify them is the order they are displaye
|
|||||||
Providers.Credentials({
|
Providers.Credentials({
|
||||||
id: 'intranet-credentials',
|
id: 'intranet-credentials',
|
||||||
name: "Two Factor Auth",
|
name: "Two Factor Auth",
|
||||||
authorize: async (credentials) => {
|
async authorize(credentials) {
|
||||||
const user = { /* add function to get user */ }
|
const user = { /* add function to get user */ }
|
||||||
return Promise.resolve(user)
|
return user
|
||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
email: { label: "Username", type: "text ", placeholder: "jsmith" },
|
email: { label: "Username", type: "text ", placeholder: "jsmith" },
|
||||||
|
|||||||
@@ -21,6 +21,6 @@ providers: [
|
|||||||
clientId: process.env.DISCORD_CLIENT_ID,
|
clientId: process.env.DISCORD_CLIENT_ID,
|
||||||
clientSecret: process.env.DISCORD_CLIENT_SECRET
|
clientSecret: process.env.DISCORD_CLIENT_SECRET
|
||||||
})
|
})
|
||||||
}
|
]
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|||||||
30
www/docs/providers/foursquare.md
Normal file
30
www/docs/providers/foursquare.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
id: foursquare
|
||||||
|
title: Foursquare
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://developer.foursquare.com/docs/places-api/authentication/#web-applications
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://developer.foursquare.com/
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
Foursquare requires an additional `apiVersion` parameter in [`YYYYMMDD` format](https://developer.foursquare.com/docs/places-api/versioning/), which essentially states "I'm prepared for API changes up to this date".
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.Foursquare({
|
||||||
|
clientId: process.env.FOURSQUARE_CLIENT_ID,
|
||||||
|
clientSecret: process.env.FOURSQUARE_CLIENT_SECRET,
|
||||||
|
apiVersion: 'YYYYMMDD'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -59,13 +59,13 @@ You can use this property to restrict access to people with verified accounts at
|
|||||||
const options = {
|
const options = {
|
||||||
...
|
...
|
||||||
callbacks: {
|
callbacks: {
|
||||||
signIn: async (user, account, profile) => {
|
async signIn(user, account, profile) {
|
||||||
if (account.provider === 'google' &&
|
if (account.provider === 'google' &&
|
||||||
profile.verified_email === true &&
|
profile.verified_email === true &&
|
||||||
profile.email.endsWith('@example.com')) {
|
profile.email.endsWith('@example.com')) {
|
||||||
return Promise.resolve(true)
|
return true
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve(false)
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
25
www/docs/providers/mailru.md
Normal file
25
www/docs/providers/mailru.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
id: mailru
|
||||||
|
title: Mail.ru
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://o2.mail.ru/docs
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://o2.mail.ru/app/
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.MailRu({
|
||||||
|
clientId: process.env.MAILRU_CLIENT_ID,
|
||||||
|
clientSecret: process.env.MAILRU_CLIENT_SECRET
|
||||||
|
})
|
||||||
|
]
|
||||||
|
...
|
||||||
26
www/docs/providers/netlify.md
Normal file
26
www/docs/providers/netlify.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
id: netlify
|
||||||
|
title: Netlify
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://www.netlify.com/blog/2016/10/10/integrating-with-netlify-oauth2/
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://github.com/netlify/netlify-oauth-example
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.Netlify({
|
||||||
|
clientId: process.env.NETLIFY_CLIENT_ID,
|
||||||
|
clientSecret: process.env.NETLIFY_CLIENT_SECRET
|
||||||
|
})
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
22
www/docs/providers/strava.md
Normal file
22
www/docs/providers/strava.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
id: strava
|
||||||
|
title: Strava
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
http://developers.strava.com/docs/reference/
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from 'next-auth/providers'
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.Strava({
|
||||||
|
clientId: process.env.STRAVA_CLIENT_ID,
|
||||||
|
clientSecret: process.env.STRAVA_CLIENT_SECRET,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
49
www/docs/providers/vk.md
Normal file
49
www/docs/providers/vk.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
id: vk
|
||||||
|
title: vk.com
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
https://vk.com/dev/first_guide
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
https://vk.com/apps?act=manage
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Providers from `next-auth/providers`
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.VK({
|
||||||
|
clientId: process.env.VK_CLIENT_ID,
|
||||||
|
clientSecret: process.env.VK_CLIENT_SECRET
|
||||||
|
})
|
||||||
|
]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
By default the provider uses `5.126` version of the API. See https://vk.com/dev/versions for more info.
|
||||||
|
:::
|
||||||
|
|
||||||
|
If you want to use a different version, you can pass it to provider's options object:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// pages/api/auth/[...nextauth].js
|
||||||
|
|
||||||
|
const apiVersion = "5.126"
|
||||||
|
...
|
||||||
|
providers: [
|
||||||
|
Providers.VK({
|
||||||
|
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}`,
|
||||||
|
})
|
||||||
|
]
|
||||||
|
...
|
||||||
|
```
|
||||||
@@ -74,7 +74,7 @@ import { PrismaClient } from '@prisma/client'
|
|||||||
|
|
||||||
const prisma = new PrismaClient()
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
const options = {
|
export default NextAuth({
|
||||||
providers: [
|
providers: [
|
||||||
Providers.Google({
|
Providers.Google({
|
||||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||||
@@ -82,9 +82,7 @@ const options = {
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
adapter: Adapters.Prisma.Adapter({ prisma }),
|
adapter: Adapters.Prisma.Adapter({ prisma }),
|
||||||
}
|
})
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|||||||
@@ -58,10 +58,31 @@ CREATE TABLE verification_requests
|
|||||||
created_at datetime NOT NULL DEFAULT getdate(),
|
created_at datetime NOT NULL DEFAULT getdate(),
|
||||||
updated_at datetime NOT NULL DEFAULT getdate()
|
updated_at datetime NOT NULL DEFAULT getdate()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX compound_id
|
||||||
|
ON accounts(compound_id);
|
||||||
|
|
||||||
|
CREATE INDEX provider_account_id
|
||||||
|
ON accounts(provider_account_id);
|
||||||
|
|
||||||
|
CREATE INDEX provider_id
|
||||||
|
ON accounts(provider_id);
|
||||||
|
|
||||||
|
CREATE INDEX user_id
|
||||||
|
ON accounts(user_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX session_token
|
||||||
|
ON sessions(session_token);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX access_token
|
||||||
|
ON sessions(access_token);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX email
|
||||||
|
ON users(email);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX token
|
||||||
|
ON verification_requests(token);
|
||||||
```
|
```
|
||||||
|
|
||||||
:::warning
|
When using NextAuth.js with SQL Server for the first time, run NextAuth.js once against your database with `?synchronize=true` on the connection string and export the schema that is created.
|
||||||
The above schema is incomplete, it does not include indexes.
|
:::
|
||||||
|
|
||||||
When using NextAuth.js with SQL Server fir the first time, run NextAuth.js once against your database with `?syncronize=true` on the connection string and export the schema that is created.
|
|
||||||
:::
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ CREATE TABLE users
|
|||||||
name VARCHAR(255),
|
name VARCHAR(255),
|
||||||
email VARCHAR(255),
|
email VARCHAR(255),
|
||||||
email_verified TIMESTAMPTZ,
|
email_verified TIMESTAMPTZ,
|
||||||
image VARCHAR(255),
|
image TEXT,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id)
|
PRIMARY KEY (id)
|
||||||
@@ -87,4 +87,4 @@ CREATE UNIQUE INDEX email
|
|||||||
CREATE UNIQUE INDEX token
|
CREATE UNIQUE INDEX token
|
||||||
ON verification_requests(token);
|
ON verification_requests(token);
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ const Adapter = (config, options = {}) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.resolve({
|
return {
|
||||||
createUser,
|
createUser,
|
||||||
getUser,
|
getUser,
|
||||||
getUserByEmail,
|
getUserByEmail,
|
||||||
@@ -166,7 +166,7 @@ const Adapter = (config, options = {}) => {
|
|||||||
createVerificationRequest,
|
createVerificationRequest,
|
||||||
getVerificationRequest,
|
getVerificationRequest,
|
||||||
deleteVerificationRequest
|
deleteVerificationRequest
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const ldap = require("ldapjs");
|
|||||||
import NextAuth from "next-auth";
|
import NextAuth from "next-auth";
|
||||||
import Providers from "next-auth/providers";
|
import Providers from "next-auth/providers";
|
||||||
|
|
||||||
const options = {
|
export default NextAuth({
|
||||||
providers: [
|
providers: [
|
||||||
Providers.Credentials({
|
Providers.Credentials({
|
||||||
name: "LDAP",
|
name: "LDAP",
|
||||||
@@ -22,7 +22,7 @@ const options = {
|
|||||||
username: { label: "DN", type: "text", placeholder: "" },
|
username: { label: "DN", type: "text", placeholder: "" },
|
||||||
password: { label: "Password", type: "password" },
|
password: { label: "Password", type: "password" },
|
||||||
},
|
},
|
||||||
authorize: async (credentials) => {
|
async authorize(credentials) {
|
||||||
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
|
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
|
||||||
const client = ldap.createClient({
|
const client = ldap.createClient({
|
||||||
url: process.env.LDAP_URI,
|
url: process.env.LDAP_URI,
|
||||||
@@ -47,7 +47,7 @@ const options = {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
jwt: async (token, user, account, profile, isNewUser) => {
|
async jwt(token, user, account, profile, isNewUser) {
|
||||||
const isSignIn = user ? true : false;
|
const isSignIn = user ? true : false;
|
||||||
if (isSignIn) {
|
if (isSignIn) {
|
||||||
token.username = user.username;
|
token.username = user.username;
|
||||||
@@ -55,7 +55,7 @@ const options = {
|
|||||||
}
|
}
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
session: async (session, user) => {
|
async session(session, user) {
|
||||||
return { ...session, user: { username: user.username } };
|
return { ...session, user: { username: user.username } };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -64,9 +64,7 @@ const options = {
|
|||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
encryption: true, // Very important to encrypt the JWT, otherwise you're leaking username+password into the browser
|
encryption: true, // Very important to encrypt the JWT, otherwise you're leaking username+password into the browser
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The idea is that once one is authenticated with the LDAP server, one can pass through both the username/DN and password to the JWT stored in the browser.
|
The idea is that once one is authenticated with the LDAP server, one can pass through both the username/DN and password to the JWT stored in the browser.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ id: securing-pages-and-api-routes
|
|||||||
title: Securing pages and API routes
|
title: Securing pages and API routes
|
||||||
---
|
---
|
||||||
|
|
||||||
You can easily protect client and server side side rendered pages and API routes with NextAuth.js.
|
You can easily protect client and server side rendered pages and API routes with NextAuth.js.
|
||||||
|
|
||||||
_You can find working examples of the approaches shown below in the [example project](https://github.com/iaincollins/next-auth-example/)._
|
_You can find working examples of the approaches shown below in the [example project](https://github.com/iaincollins/next-auth-example/)._
|
||||||
|
|
||||||
@@ -48,14 +48,13 @@ export default function Page() {
|
|||||||
|
|
||||||
if (typeof window !== 'undefined' && loading) return null
|
if (typeof window !== 'undefined' && loading) return null
|
||||||
|
|
||||||
if (!session) return <p>Access Denied</p>
|
if (session) {
|
||||||
|
return <>
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Protected Page</h1>
|
<h1>Protected Page</h1>
|
||||||
<p>You can view this page because you are signed in.</p>
|
<p>You can view this page because you are signed in.</p>
|
||||||
</>
|
</>
|
||||||
)
|
}
|
||||||
|
return <p>Access Denied</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerSideProps(context) {
|
export async function getServerSideProps(context) {
|
||||||
@@ -67,7 +66,7 @@ export async function getServerSideProps(context) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately avalible on page load to `useSession`.
|
This example assumes you have configured `_app.js` to pass the `session` prop through so that it's immediately available on page load to `useSession`.
|
||||||
|
|
||||||
```js title="pages/_app.js"
|
```js title="pages/_app.js"
|
||||||
import { Provider } from 'next-auth/client'
|
import { Provider } from 'next-auth/client'
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Second, a cypress file for environment variables. These can be defined in `cypre
|
|||||||
{
|
{
|
||||||
"GOOGLE_USER": "username@company.com",
|
"GOOGLE_USER": "username@company.com",
|
||||||
"GOOGLE_PW": "password",
|
"GOOGLE_PW": "password",
|
||||||
"COOKIE_NAME": "__Secure-next-auth.session-token",
|
"COOKIE_NAME": "next-auth.session-token",
|
||||||
"SITE_NAME": "http://localhost:3000"
|
"SITE_NAME": "http://localhost:3000"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -111,8 +111,11 @@ describe('Login page', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Cookies.defaults({
|
Cypress.Cookies.defaults({
|
||||||
whitelist: cookieName,
|
preserve: cookieName,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// remove the two lines below if you need to stay logged in
|
||||||
|
// for your remaining tests
|
||||||
cy.visit('/api/auth/signout')
|
cy.visit('/api/auth/signout')
|
||||||
cy.get('form').submit()
|
cy.get('form').submit()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ import Adapters from "next-auth/adapters"
|
|||||||
|
|
||||||
import Models from "../../../models"
|
import Models from "../../../models"
|
||||||
|
|
||||||
const options = {
|
export default NextAuth({
|
||||||
providers: [
|
providers: [
|
||||||
// Your providers
|
// Your providers
|
||||||
],
|
],
|
||||||
@@ -77,9 +77,7 @@ const options = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
})
|
||||||
|
|
||||||
export default (req, res) => NextAuth(req, res, options)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,3 +48,23 @@ You can use [node-jose-tools](https://www.npmjs.com/package/node-jose-tools) to
|
|||||||
|
|
||||||
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY
|
#### JWT_AUTO_GENERATED_ENCRYPTION_KEY
|
||||||
|
|
||||||
|
#### SIGNIN_CALLBACK_REJECT_REDIRECT
|
||||||
|
|
||||||
|
You returned something in the `signIn` callback, that is being deprecated.
|
||||||
|
|
||||||
|
You probably had something similar in the callback:
|
||||||
|
```js
|
||||||
|
return Promise.reject("/some/url")
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```js
|
||||||
|
throw "/some/url"
|
||||||
|
```
|
||||||
|
|
||||||
|
To remedy this, simply return the url instead:
|
||||||
|
|
||||||
|
```js
|
||||||
|
return "/some/url"
|
||||||
|
```
|
||||||
@@ -17,7 +17,7 @@ module.exports = {
|
|||||||
alt: 'NextAuth Logo',
|
alt: 'NextAuth Logo',
|
||||||
src: 'img/logo/logo-xs.png'
|
src: 'img/logo/logo-xs.png'
|
||||||
},
|
},
|
||||||
links: [
|
items: [
|
||||||
{
|
{
|
||||||
to: '/getting-started/introduction',
|
to: '/getting-started/introduction',
|
||||||
activeBasePath: 'docs',
|
activeBasePath: 'docs',
|
||||||
@@ -42,20 +42,18 @@ module.exports = {
|
|||||||
position: 'right'
|
position: 'right'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'https://github.com/iaincollins/next-auth',
|
href: 'https://github.com/nextauthjs/next-auth',
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
position: 'right'
|
position: 'right'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
announcementBar: {
|
announcementBar: {
|
||||||
id: 'release-candiate-announcement',
|
id: 'release-candiate-announcement',
|
||||||
content: 'NextAuth.js v2.0 has been released <a target="_blank" rel="noopener noreferrer" href="https://www.npmjs.com/package/next-auth">npm i next-auth</a>',
|
content: 'NextAuth.js now has automatic 🤖 releases 🎉! Check out the <a href="https://next-auth-git-canary.nextauthjs.vercel.app">Canary documentation 📚</a>',
|
||||||
backgroundColor: '#2DB2F9',
|
backgroundColor: '#2DB2F9',
|
||||||
textColor: '#fff'
|
textColor: '#fff'
|
||||||
},
|
},
|
||||||
*/
|
|
||||||
footer: {
|
footer: {
|
||||||
links: [
|
links: [
|
||||||
{
|
{
|
||||||
@@ -68,6 +66,10 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
label: 'Contributors',
|
label: 'Contributors',
|
||||||
to: '/contributors'
|
to: '/contributors'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Canary docs',
|
||||||
|
to: 'https://next-auth-git-canary.nextauthjs.vercel.app/'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -76,7 +78,7 @@ module.exports = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'GitHub',
|
label: 'GitHub',
|
||||||
to: 'https://github.com/iaincollins/next-auth'
|
to: 'https://github.com/nextauthjs/next-auth'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'NPM',
|
label: 'NPM',
|
||||||
@@ -98,7 +100,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
copyright: 'NextAuth.js © Iain Collins 2020'
|
copyright: 'NextAuth.js © Iain Collins 2021'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
presets: [
|
presets: [
|
||||||
@@ -108,7 +110,7 @@ module.exports = {
|
|||||||
docs: {
|
docs: {
|
||||||
routeBasePath: '/',
|
routeBasePath: '/',
|
||||||
sidebarPath: require.resolve('./sidebars.js'),
|
sidebarPath: require.resolve('./sidebars.js'),
|
||||||
editUrl: 'https://github.com/iaincollins/next-auth/edit/main/www'
|
editUrl: 'https://github.com/nextauthjs/next-auth/edit/main/www'
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
customCss: require.resolve('./src/css/index.css')
|
customCss: require.resolve('./src/css/index.css')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user