mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35583a513d | ||
|
|
665d91019f | ||
|
|
f2b816b7b9 | ||
|
|
2e770fb0bf | ||
|
|
e83e7231fb | ||
|
|
4593ec8b01 | ||
|
|
12517f629b | ||
|
|
77012bc00c | ||
|
|
60fdf26a56 | ||
|
|
0fae0c7a8e | ||
|
|
eba79f4445 | ||
|
|
e3bb9881ea | ||
|
|
827049cb35 | ||
|
|
ad8100d402 | ||
|
|
7b5defff16 | ||
|
|
bc9805d1ba | ||
|
|
c823016b36 | ||
|
|
ca0f4c6fba | ||
|
|
c0d2f2d852 | ||
|
|
71f63117a9 | ||
|
|
d04ce29314 | ||
|
|
d2882f1958 | ||
|
|
66db563ca5 | ||
|
|
9619077363 | ||
|
|
013ccb4cb0 | ||
|
|
6eb41259d1 | ||
|
|
141f8d07e2 | ||
|
|
ffd0601ab0 | ||
|
|
7864d4705d | ||
|
|
98dc82e5d6 | ||
|
|
86baefdd9d | ||
|
|
332e237c3e | ||
|
|
2fce08c0b5 | ||
|
|
adf3fb669f | ||
|
|
5323be3594 | ||
|
|
6df0d04a1e | ||
|
|
aa9c1e7c96 | ||
|
|
66473054f5 | ||
|
|
e8ddbc5c11 | ||
|
|
dfe4620056 | ||
|
|
848224e2c5 | ||
|
|
aee376cc57 | ||
|
|
0d2a81cd39 | ||
|
|
61e99c9489 | ||
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e | ||
|
|
fb04ab4e76 | ||
|
|
07e2a83ccb | ||
|
|
065d9eb310 | ||
|
|
5da19f3c9a | ||
|
|
88ec3bad71 | ||
|
|
5ab7868533 | ||
|
|
835dda0899 | ||
|
|
ad4709764a | ||
|
|
55a2932973 | ||
|
|
49cb7e5bd7 | ||
|
|
b95182ded7 | ||
|
|
be28672fd4 | ||
|
|
e26c5fc905 | ||
|
|
543f812eb3 | ||
|
|
0c9f9777c5 | ||
|
|
34f334a71d | ||
|
|
172ad02f8c | ||
|
|
eed0001524 | ||
|
|
a2705fb5b9 | ||
|
|
cb1e5a7174 | ||
|
|
8cba5d06b5 | ||
|
|
c52ce57296 | ||
|
|
4dae822806 | ||
|
|
901f6fb189 | ||
|
|
bb2237d0f9 | ||
|
|
fab7ce8f94 |
@@ -1,4 +0,0 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
15
.env.example
15
.env.example
@@ -1,15 +0,0 @@
|
||||
# Rename file to .env and populate values
|
||||
# to be able to run tests
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
NEXTAUTH_TWITTER_ID=
|
||||
NEXTAUTH_TWITTER_SECRET=
|
||||
NEXTAUTH_TWITTER_USERNAME=
|
||||
NEXTAUTH_TWITTER_PASSWORD=
|
||||
NEXTAUTH_GITHUB_ID=
|
||||
NEXTAUTH_GITHUB_SECRET=
|
||||
NEXTAUTH_GITHUB_USERNAME=
|
||||
NEXTAUTH_GITHUB_PASSWORD=
|
||||
NEXTAUTH_GOOGLE_ID=
|
||||
NEXTAUTH_GOOGLE_SECRET=
|
||||
NEXTAUTH_GOOGLE_USERNAME=
|
||||
NEXTAUTH_GOOGLE_PASSWORD=
|
||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/types/ @balazsorban44 @lluia
|
||||
0
FUNDING.yml → .github/FUNDING.yml
vendored
0
FUNDING.yml → .github/FUNDING.yml
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
49
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,33 +2,42 @@
|
||||
name: Bug report
|
||||
about: Report a defect with NextAuth.js
|
||||
labels: bug
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of the bug in NextAuth.js.
|
||||
## Description 🐜
|
||||
|
||||
Do not report bugs with your own project here, ask from help by raising a question instead - this helps us a lot with administration overhead.
|
||||
Please provide a clear and concise description of the bug in NextAuth.js.
|
||||
|
||||
**Steps to reproduce**
|
||||
Steps to reproduce the behavior.
|
||||
🚧 – _Do not report bugs with your own project here; ask for help [by raising a question instead](https://github.com/nextauthjs/next-auth/issues/new?assignees=&labels=question&template=question.md) - this helps us a lot with administration overhead._
|
||||
|
||||
Include a link to public repository which can be used to reproduce the behaviour.
|
||||
## How to reproduce ☕️
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
|
||||
**Screenshots or error logs**
|
||||
If applicable add screenshots or error logs to help explain the problem.
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
**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).*
|
||||
## Screenshots / Logs 📽
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**Help us help you**. We can address the bug you found much faster if you provide contextual screenshots or screen recordings showcasing the issue.
|
||||
|
||||
See [Kap](https://getkap.co/) for a good, easy-to-use, cross-platform screen recording tool.
|
||||
|
||||
## Environment 🖥
|
||||
|
||||
Please run this command:
|
||||
|
||||
```
|
||||
$ npx envinfo --system --binaries --browsers --npmPackages "{next-auth}"
|
||||
```
|
||||
|
||||
and paste the output here.
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help fix this bug, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
41
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,25 +2,38 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for NextAuth.js
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Summary of proposed feature**
|
||||
A clear and concise description of the feature being proposed.
|
||||
## Summary 💭
|
||||
|
||||
**Purpose of proposed feature**
|
||||
A clear and concise description of why this feature is necessary and what problems it solves.
|
||||
A clear and concise summary of the feature being proposed.
|
||||
|
||||
**Detail about proposed feature**
|
||||
A detailed description of how the proposal might work (if you have one).
|
||||
## Description 📓
|
||||
|
||||
**Potential problems**
|
||||
Describe any potential problems or potential limitations or caveats that might apply to the proposed solution.
|
||||
Please provide a more in-depth description of the feature proposed.
|
||||
|
||||
**Describe any alternatives you've considered**
|
||||
A clear and concise description of any alternative options you've considered.
|
||||
Make sure you provide plenty of [links]() to external documentation and inline code examples like so:
|
||||
|
||||
**Additional context**
|
||||
Any other context, screenshots, etc.
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
*Please indicate if you are willing and able to help implement the proposed feature.*
|
||||
Take time thinking about what you want to say and help us understand your proposal making sure that this description contains:
|
||||
|
||||
- **purpose of the feature**
|
||||
- **potential problems**
|
||||
- **potential alternatives**
|
||||
|
||||
You can use one of the templates set up on **CodeSandbox** to better illustrate your idea:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help implement this feature, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/question.md
vendored
37
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -2,24 +2,31 @@
|
||||
name: Question
|
||||
about: Ask a question about NextAuth.js or for help using it
|
||||
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**
|
||||
<!-- A clear and concise question. -->
|
||||
## Question 💬
|
||||
|
||||
**What are you trying to do**
|
||||
<!-- A description of what you are trying to do, for context. -->
|
||||
Please provide an in-depth description of the question you have.
|
||||
|
||||
**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. -->
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
**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).*
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
* [ ] Found the documentation helpful
|
||||
* [ ] Found documentation but was incomplete
|
||||
* [ ] Could not find relevant documentation
|
||||
* [ ] Found the example project helpful
|
||||
* [ ] Did not find the example project helpful
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
|
||||
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/typescript.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: TypeScript
|
||||
about: Ask a question about NextAuth.js TypeScript integration
|
||||
labels:
|
||||
- question
|
||||
- TypeScript
|
||||
assignees:
|
||||
- lluia
|
||||
- balazsorban44
|
||||
---
|
||||
|
||||
## Question 💬
|
||||
|
||||
Please provide an in-depth description of the question you have when using NextAuth.js on a Typescript project or when consuming the built-in types for `next-auth`.
|
||||
|
||||
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
|
||||
|
||||
```js
|
||||
function myAwesomeNextAuthFeature() {
|
||||
return 💚
|
||||
}
|
||||
```
|
||||
|
||||
**NOTE:** Questions will be converted to Discussions. You can find them [here](https://github.com/nextauthjs/next-auth/discussions)!
|
||||
|
||||
## How to reproduce ☕️
|
||||
|
||||
We encourage you to use the template set-up on **CodeSandbox** as a playground to represent your question or doubt:
|
||||
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
## Contributing 🙌🏽
|
||||
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
In case you're willing to help answer this TypeScript question, please let us know here, and we'll reach you 😊 . Otherwise, you can have a look at the issues labelled with [`"good first issue"`](https://github.com/nextauthjs/next-auth/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and pick any of them.
|
||||
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
35
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -16,26 +16,33 @@ merge of your pull request!
|
||||
|
||||
<!-- What changes are being made? (What feature/bug is being fixed here?) -->
|
||||
|
||||
**What**:
|
||||
## Reasoning 💡
|
||||
|
||||
<!-- Why are these changes necessary? -->
|
||||
<!-- What changes are being made? What feature/bug is being fixed here? -->
|
||||
|
||||
**Why**:
|
||||
## Checklist 🧢
|
||||
|
||||
<!-- How were these changes implemented? -->
|
||||
<!-- Feel free cross items ( like this `~[] item~` ) if they're irrelevant to your changes.
|
||||
|
||||
**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" -->
|
||||
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 -->
|
||||
<!-- In your opinion, is this ready to be merged as soon as it's reviewed? -->
|
||||
|
||||
## Affected issues 🎟
|
||||
|
||||
<!--
|
||||
Please [scout and link issues](https://github.com/nextauthjs/next-auth/issues) that might be solved by this PR.
|
||||
|
||||
If you write `"Fixes"` or `"Closes"` before the issue link like so:
|
||||
|
||||
```
|
||||
Fixes #359
|
||||
```
|
||||
|
||||
the connected issue will be automatically closed once the PR is merged and hence help with maintenance of the library 😊
|
||||
|
||||
-->
|
||||
|
||||
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@@ -4,22 +4,22 @@ name: Lint/Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
node-version: [12, 14, 16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
@@ -29,4 +29,4 @@ jobs:
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
- run: npm run build
|
||||
|
||||
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10, 12, 14]
|
||||
node-version: [12, 14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -2,14 +2,14 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'beta'
|
||||
- 'next'
|
||||
- '3.x'
|
||||
- "main"
|
||||
- "beta"
|
||||
- "next"
|
||||
- "3.x"
|
||||
pull_request:
|
||||
jobs:
|
||||
release:
|
||||
name: 'Release'
|
||||
name: "Release"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- run: npm run build
|
||||
- run: npx semantic-release@17
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
2
.github/workflows/types.yml
vendored
2
.github/workflows/types.yml
vendored
@@ -19,6 +19,8 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -25,9 +25,27 @@ node_modules
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
.next
|
||||
www/providers.json
|
||||
src/providers/index.js
|
||||
/internals
|
||||
/adapters.d.ts
|
||||
/adapters.js
|
||||
/client.d.ts
|
||||
/client.js
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/jwt.d.ts
|
||||
/jwt.js
|
||||
/providers.d.ts
|
||||
/providers.js
|
||||
/errors.js
|
||||
/errors.d.ts
|
||||
|
||||
# Development app
|
||||
app/next-auth
|
||||
app/dist/css
|
||||
app/package-lock.json
|
||||
app/yarn.lock
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
@@ -40,4 +58,4 @@ src/providers/index.js
|
||||
/_work
|
||||
|
||||
# Prisma migrations
|
||||
/prisma/migrations
|
||||
/prisma/migrations
|
||||
@@ -32,15 +32,15 @@ cd next-auth
|
||||
|
||||
2. Install packages:
|
||||
```sh
|
||||
npm i
|
||||
npm i && npm run dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
Copy `.env.local.example` to `.env.local`, and add your env variables for each provider you want to test.
|
||||
Copy `app/.env.local.example` to `app/.env.local`, and add your env variables for each provider you want to test.
|
||||
|
||||
> NOTE: You can add any environment variables to .env.local that you would like to use in your dev app.
|
||||
> You can find the next-auth config under`pages/api/auth/[...nextauth].js`.
|
||||
> You can find the next-auth config under`app/pages/api/auth/[...nextauth].js`.
|
||||
|
||||
1. Start the dev application/server:
|
||||
```sh
|
||||
@@ -57,7 +57,9 @@ If you need an example project to link to, you can use [next-auth-example](https
|
||||
|
||||
When running `npm run dev`, you start a Next.js dev server on `http://localhost:3000`, which includes hot reloading out of the box. Make changes on any of the files in `src` and see the changes immediately.
|
||||
|
||||
>NOTE: When working on CSS, you will need to manually refresh the page after changes. (Improving this through a PR is very welcome!)
|
||||
> NOTE: When working on CSS, you will have to manually refresh the page after changes. The reason for this is our pages using CSS are server-side rendered. (Improving this through a PR is very welcome!)
|
||||
|
||||
> NOTE: The setup is as follows: The development application lives inside the `app` folder, and whenever you make a change to the `src` folder in the root (where next-auth is), it gets copied into `app` every time (gitignored), so Next.js can pick them up and apply hot reloading. This is to avoid some annoying issues with how symlinks are working with different React builds, and also to provide a super-fast feedback loop while developing core features.
|
||||
|
||||
#### Providers
|
||||
|
||||
@@ -71,7 +73,7 @@ You can look at the existing built-in providers for inspiration.
|
||||
|
||||
#### 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, PostgreSQL, and MongoDB databases on localhost.
|
||||
|
||||
It will use port `3306`, `5432`, and `27017` on localhost respectively; please make sure those ports are not used by other services on localhost.
|
||||
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -1,30 +0,0 @@
|
||||
# Multi stage build to allow us to improve performance
|
||||
FROM node:10-alpine as base
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Install basic dependancies (Next.js, React)
|
||||
COPY test/docker/app/package*.json ./
|
||||
RUN npm ci --only=production
|
||||
|
||||
FROM node:10-alpine as app
|
||||
COPY --from=base /usr/src/app ./
|
||||
|
||||
# Copy last build of library into the image and install dependences for it.
|
||||
# This ensures the build is valid and package.json contains everything needed
|
||||
# to actually run the library.
|
||||
# Note: You must run `npm run build` first to build a release of the library
|
||||
RUN mkdir -p node_modules/next-auth
|
||||
# Copy all entrypoints for the library (if creating a new one, add it here)
|
||||
COPY index.js providers.js adapters.js client.js jwt.js node_modules/next-auth/
|
||||
# Copy the dist dir
|
||||
COPY dist node_modules/next-auth/dist
|
||||
# Copy the package.json for the library and install it's dependences
|
||||
COPY package*.json node_modules/next-auth/
|
||||
RUN cd node_modules/next-auth/ && npm ci --only=production
|
||||
|
||||
# Copy test pages across
|
||||
COPY test/docker/app/pages ./pages
|
||||
|
||||
RUN npm run build
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
6
app/README.md
Normal file
6
app/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# NextAuth.js Development App
|
||||
|
||||
This folder contains a Next.js app using NextAuth.js for local development. See the following section on how to start:
|
||||
|
||||
[Setting up local environment
|
||||
](https://github.com/nextauthjs/next-auth/blob/main/CONTRIBUTING.md#setting-up-local-environment)
|
||||
5
app/jsconfig.json
Normal file
5
app/jsconfig.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
0
next-env.d.ts → app/next-env.d.ts
vendored
0
next-env.d.ts → app/next-env.d.ts
vendored
19
app/next.config.js
Normal file
19
app/next.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
webpack(config) {
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
...config.resolve.alias,
|
||||
"next-auth$": path.join(process.cwd(), "next-auth/server"),
|
||||
"next-auth/client$": path.join(process.cwd(), "next-auth/client"),
|
||||
"next-auth/jwt$": path.join(process.cwd(), "next-auth/lib/jwt"),
|
||||
"next-auth/adapters": path.join(process.cwd(), "next-auth/adapters"),
|
||||
"next-auth/providers": path.join(process.cwd(), "next-auth/providers"),
|
||||
},
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
}
|
||||
25
app/package.json
Normal file
25
app/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "next-auth-app",
|
||||
"version": "1.0.0",
|
||||
"description": "NextAuth.js Developer app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "npm-run-all --parallel copy:app dev:css dev:next",
|
||||
"dev:next": "next dev",
|
||||
"copy:app": "cpx \"../src/**/*\" next-auth --watch",
|
||||
"copy:css": "cpx \"../dist/css/**/*\" dist/css --watch",
|
||||
"watch:css": "cd .. && npm run watch:css",
|
||||
"dev:css": "npm-run-all --parallel watch:css copy:css",
|
||||
"start": "next start"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Provider } from 'next-auth/client'
|
||||
import './styles.css'
|
||||
import { Provider } from "next-auth/client"
|
||||
import "./styles.css"
|
||||
|
||||
// Use the <Provider> to improve performance and allow components that call
|
||||
// `useSession()` anywhere in your application to access the `session` object.
|
||||
export default function App ({ Component, pageProps }) {
|
||||
export default function App({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
// Provider options are not required but can be useful in situations where
|
||||
@@ -21,7 +21,7 @@ export default function App ({ Component, pageProps }) {
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0
|
||||
keepAlive: 0,
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
@@ -1,5 +1,9 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
import NextAuth from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import Auth0Provider from "next-auth/providers/auth0"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
|
||||
// import Adapters from 'next-auth/adapters'
|
||||
// import { PrismaClient } from '@prisma/client'
|
||||
@@ -28,15 +32,15 @@ export default NextAuth({
|
||||
// }
|
||||
// },
|
||||
providers: [
|
||||
Providers.Email({
|
||||
EmailProvider({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
Providers.GitHub({
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Auth0({
|
||||
Auth0Provider({
|
||||
clientId: process.env.AUTH0_ID,
|
||||
clientSecret: process.env.AUTH0_SECRET,
|
||||
domain: process.env.AUTH0_DOMAIN,
|
||||
@@ -45,36 +49,36 @@ export default NextAuth({
|
||||
// authorizationParams: {
|
||||
// response_mode: 'form_post'
|
||||
// }
|
||||
protection: 'pkce'
|
||||
protection: "pkce",
|
||||
}),
|
||||
Providers.Twitter({
|
||||
TwitterProvider({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
Providers.Credentials({
|
||||
name: 'Credentials',
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
password: { label: 'Password', type: 'password' }
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize (credentials) {
|
||||
if (credentials.password === 'password') {
|
||||
async authorize(credentials, req) {
|
||||
if (credentials.password === "password") {
|
||||
return {
|
||||
id: 1,
|
||||
name: 'Fill Murray',
|
||||
email: 'bill@fillmurray.com',
|
||||
image: 'https://www.fillmurray.com/64/64'
|
||||
name: "Fill Murray",
|
||||
email: "bill@fillmurray.com",
|
||||
image: "https://www.fillmurray.com/64/64",
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
debug: false,
|
||||
theme: 'auto'
|
||||
theme: "auto",
|
||||
|
||||
// Default Database Adapter (TypeORM)
|
||||
// database: process.env.DATABASE_URL
|
||||
@@ -82,6 +86,6 @@ export default NextAuth({
|
||||
// Prisma Database Adapter
|
||||
// To configure this app to use the schema in `prisma/schema.prisma` run:
|
||||
// npx prisma generate
|
||||
// npx prisma migrate dev --preview-feature
|
||||
// npx prisma migrate dev
|
||||
// adapter: Adapters.Prisma.Adapter({ prisma })
|
||||
})
|
||||
22
config/babel.config.js
Normal file
22
config/babel.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// We aim to have the same support as Next.js
|
||||
// https://nextjs.org/docs/getting-started#system-requirements
|
||||
// https://nextjs.org/docs/basic-features/supported-browsers-features
|
||||
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
comments: false,
|
||||
overrides: [
|
||||
{
|
||||
test: ["../src/client/**"],
|
||||
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
|
||||
},
|
||||
{
|
||||
test: ["../src/server/pages/**"],
|
||||
presets: ["preact"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const MODULE_ENTRIES = {
|
||||
PROVIDERS: "providers",
|
||||
ADAPTERS: "adapters",
|
||||
JWT: "jwt",
|
||||
ERRORS: "errors",
|
||||
}
|
||||
|
||||
// Building submodule entries
|
||||
@@ -17,6 +18,7 @@ const BUILD_TARGETS = {
|
||||
[`${MODULE_ENTRIES.ADAPTERS}.js`]: "module.exports = require('./dist/adapters').default\n",
|
||||
[`${MODULE_ENTRIES.PROVIDERS}.js`]: "module.exports = require('./dist/providers').default\n",
|
||||
[`${MODULE_ENTRIES.JWT}.js`]: "module.exports = require('./dist/lib/jwt').default\n",
|
||||
[`${MODULE_ENTRIES.ERRORS}.js`]: "module.exports = require('./dist/lib/errors').default\n",
|
||||
}
|
||||
|
||||
Object.entries(BUILD_TARGETS).forEach(([target, content]) => {
|
||||
@@ -34,6 +36,7 @@ const TYPES_TARGETS = [
|
||||
`${MODULE_ENTRIES.ADAPTERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.PROVIDERS}.d.ts`,
|
||||
`${MODULE_ENTRIES.JWT}.d.ts`,
|
||||
`${MODULE_ENTRIES.ERRORS}.d.ts`,
|
||||
"internals",
|
||||
]
|
||||
|
||||
@@ -52,7 +55,9 @@ TYPES_TARGETS.forEach((target) => {
|
||||
|
||||
const providersDir = path.join(process.cwd(), "/src/providers")
|
||||
|
||||
const files = fs.readdirSync(providersDir, "utf8")
|
||||
const files = fs
|
||||
.readdirSync(providersDir, "utf8")
|
||||
.filter((file) => file !== "index.js")
|
||||
|
||||
let importLines = ""
|
||||
let exportLines = `export default {\n`
|
||||
|
||||
5222
package-lock.json
generated
5222
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
99
package.json
99
package.json
@@ -6,31 +6,40 @@
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"main": "index.js",
|
||||
"types": "./index.d.ts",
|
||||
"keywords": [
|
||||
"react",
|
||||
"nodejs",
|
||||
"oauth",
|
||||
"jwt",
|
||||
"oauth2",
|
||||
"authentication",
|
||||
"nextjs",
|
||||
"csrf",
|
||||
"oidc",
|
||||
"nextauth"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/server/index.js",
|
||||
"./jwt": "./dist/lib/jwt.js",
|
||||
"./adapters": "./dist/adapters/index.js",
|
||||
"./client": "./dist/client/index.js",
|
||||
"./providers": "./dist/providers/index.js",
|
||||
"./providers/*": "./dist/providers/*.js",
|
||||
"./errors": "./dist/lib/errors.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.json src --out-dir dist",
|
||||
"build:js": "node ./config/build.js && babel --config-file ./config/babel.config.js src --out-dir dist",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir dist && node config/wrap-css.js",
|
||||
"dev:with-css": "next | npm run watch:css",
|
||||
"dev": "next",
|
||||
"dev:setup": "npm run build:css && cd app && npm i",
|
||||
"dev": "cd app && npm run dev",
|
||||
"watch": "npm run watch:js | npm run watch:css",
|
||||
"watch:js": "babel --config-file ./config/babel.config.json --watch src --out-dir dist",
|
||||
"watch:js": "babel --config-file ./config/babel.config.js --watch src --out-dir dist",
|
||||
"watch:css": "postcss --config config/postcss.config.js --watch src/**/*.css --base src --dir dist",
|
||||
"test:app:start": "docker-compose -f test/docker/app.yml up -d",
|
||||
"test:app:rebuild": "npm run build && docker-compose -f test/docker/app.yml up -d --build",
|
||||
"test:app:stop": "docker-compose -f test/docker/app.yml down",
|
||||
"test": "npm run test:app:rebuild && npm run test:integration && npm run test:app:stop && npm run test:types",
|
||||
"test:db": "npm run test:db:mysql && npm run test:db:postgres && npm run test:db:mongodb && npm run test:db:mssql",
|
||||
"test:db:mysql": "node test/mysql.js",
|
||||
"test:db:postgres": "node test/postgres.js",
|
||||
"test:db:mongodb": "node test/mongodb.js",
|
||||
"test:db:mssql": "node test/mssql.js",
|
||||
"test:integration": "mocha test/integration",
|
||||
"test:types": "dtslint types",
|
||||
"db:start": "docker-compose -f test/docker/databases.yml up -d",
|
||||
"db:stop": "docker-compose -f test/docker/databases.yml down",
|
||||
"test": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
@@ -44,13 +53,17 @@
|
||||
"adapters.d.ts",
|
||||
"client.js",
|
||||
"client.d.ts",
|
||||
"errors.js",
|
||||
"errors.d.ts",
|
||||
"jwt.js",
|
||||
"jwt.d.ts",
|
||||
"internals"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"crypto-js": "^4.0.0",
|
||||
"@babel/runtime": "^7.14.0",
|
||||
"@next-auth/prisma-legacy-adapter": "0.0.1-canary.127",
|
||||
"@next-auth/typeorm-legacy-adapter": "0.0.2-canary.129",
|
||||
"futoin-hkdf": "^1.3.2",
|
||||
"jose": "^1.27.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
@@ -59,13 +72,11 @@
|
||||
"pkce-challenge": "^2.1.0",
|
||||
"preact": "^10.4.1",
|
||||
"preact-render-to-string": "^5.1.14",
|
||||
"querystring": "^0.2.0",
|
||||
"require_optional": "^1.0.1",
|
||||
"typeorm": "^0.2.30"
|
||||
"querystring": "^0.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17",
|
||||
"react-dom": "16.13.1 || ^17"
|
||||
"react-dom": "^16.13.1 || ^17"
|
||||
},
|
||||
"peerOptionalDependencies": {
|
||||
"mongodb": "^3.5.9",
|
||||
@@ -77,8 +88,9 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
@@ -99,19 +111,10 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
@@ -143,6 +146,32 @@
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
"main",
|
||||
{
|
||||
"name": "beta",
|
||||
"prerelease": true
|
||||
},
|
||||
{
|
||||
"name": "next",
|
||||
"prerelease": true
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/npm",
|
||||
[
|
||||
"@semantic-release/github",
|
||||
{
|
||||
"releasedLabels": false,
|
||||
"successComment": false
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
branches: [
|
||||
'+([0-9])?(.{+([0-9]),x}).x',
|
||||
'main',
|
||||
{ name: 'beta', prerelease: true },
|
||||
{ name: 'next', prerelease: true }
|
||||
]
|
||||
}
|
||||
36
src/adapters/error-handler.js
Normal file
36
src/adapters/error-handler.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { UnknownError } from "../lib/errors"
|
||||
|
||||
/**
|
||||
* Handles adapter induced errors.
|
||||
* @param {import("types/adapters").AdapterInstance} adapter
|
||||
* @param {import("types").LoggerInstance} logger
|
||||
* @return {import("types/adapters").AdapterInstance}
|
||||
*/
|
||||
export default function adapterErrorHandler(adapter, logger) {
|
||||
return Object.keys(adapter).reduce((acc, method) => {
|
||||
const name = capitalize(method)
|
||||
const code = upperSnake(name, adapter.displayName)
|
||||
|
||||
const adapterMethod = adapter[method]
|
||||
acc[method] = async (...args) => {
|
||||
try {
|
||||
logger.debug(code, ...args)
|
||||
return await adapterMethod(...args)
|
||||
} catch (error) {
|
||||
logger.error(`${code}_ERROR`, error)
|
||||
const e = new UnknownError(error)
|
||||
e.name = `${name}Error`
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function capitalize(s) {
|
||||
return `${s[0].toUpperCase()}${s.slice(1)}`
|
||||
}
|
||||
|
||||
function upperSnake(s, prefix = "ADAPTER") {
|
||||
return `${prefix}_${s.replace(/([A-Z])/g, "_$1")}`.toUpperCase()
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
const Adapter = (config, options = {}) => {
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`ADAPTER_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('createUser', profile)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('getUser', id)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('getUserByEmail', email)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('getUserByProviderAccountId', providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('updateUser', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('deleteUser', userId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('linkAccount', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return null
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('unlinkAccount', userId, providerId, providerAccountId)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('createSession', user)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('getSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('updateSession', session)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('deleteSession', sessionToken)
|
||||
return null
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('createVerificationRequest', identifier)
|
||||
return null
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('getVerificationRequest', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('deleteVerification', identifier, token)
|
||||
return null
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import TypeORM from './typeorm'
|
||||
import Prisma from './prisma'
|
||||
import * as TypeORM from "./typeorm"
|
||||
import * as Prisma from "./prisma"
|
||||
|
||||
export { TypeORM, Prisma }
|
||||
|
||||
export default {
|
||||
Default: TypeORM.Adapter,
|
||||
TypeORM,
|
||||
Prisma
|
||||
Prisma,
|
||||
}
|
||||
|
||||
6
src/adapters/prisma.js
Normal file
6
src/adapters/prisma.js
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/prisma-legacy
|
||||
*/
|
||||
|
||||
export { PrismaLegacyAdapter as Adapter } from "@next-auth/prisma-legacy-adapter"
|
||||
@@ -1,340 +0,0 @@
|
||||
import { createHash, randomBytes } from 'crypto'
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
|
||||
const Adapter = (config) => {
|
||||
const {
|
||||
prisma,
|
||||
modelMapping = {
|
||||
User: 'user',
|
||||
Account: 'account',
|
||||
Session: 'session',
|
||||
VerificationRequest: 'verificationRequest'
|
||||
}
|
||||
} = config
|
||||
|
||||
const { User, Account, Session, VerificationRequest } = modelMapping
|
||||
|
||||
function getCompoundId (providerId, providerAccountId) {
|
||||
return createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
}
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`PRISMA_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
return prisma[User].create({
|
||||
data: {
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.image,
|
||||
emailVerified: profile.emailVerified ? profile.emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
try {
|
||||
return prisma[User].findUnique({ where: { id } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return prisma[User].findUnique({ where: { email } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await prisma[Account].findUnique({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
if (!account) { return null }
|
||||
return prisma[User].findUnique({ where: { id: account.userId } })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
try {
|
||||
const { id, name, email, image, emailVerified } = user
|
||||
return prisma[User].update({
|
||||
where: { id },
|
||||
data: {
|
||||
name,
|
||||
email,
|
||||
image,
|
||||
emailVerified: emailVerified ? emailVerified.toISOString() : null
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
try {
|
||||
return prisma[User].delete({ where: { id: userId } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_USER_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_USER_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
return prisma[Account].create({
|
||||
data: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
compoundId: getCompoundId(providerId, providerAccountId),
|
||||
providerAccountId: `${providerAccountId}`,
|
||||
providerId,
|
||||
providerType,
|
||||
accessTokenExpires,
|
||||
userId
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
try {
|
||||
return prisma[Account].delete({ where: { compoundId: getCompoundId(providerId, providerAccountId) } })
|
||||
} catch (error) {
|
||||
logger.error('UNLINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('UNLINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
return prisma[Session].create({
|
||||
data: {
|
||||
expires,
|
||||
userId: user.id,
|
||||
sessionToken: randomBytes(32).toString('hex'),
|
||||
accessToken: randomBytes(32).toString('hex')
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await prisma[Session].findUnique({ where: { sessionToken } })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > session.expires) {
|
||||
await prisma[Session].delete({ where: { sessionToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
const { id, expires } = session
|
||||
return prisma[Session].update({ where: { id }, data: { expires: expires.toISOString() } })
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return prisma[Session].delete({ where: { sessionToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires.toISOString()
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const verificationRequest = await prisma[VerificationRequest].create({
|
||||
data: {
|
||||
identifier,
|
||||
token: hashedToken,
|
||||
expires
|
||||
}
|
||||
})
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await prisma[VerificationRequest].findFirst({
|
||||
where: {
|
||||
identifier,
|
||||
token: hashedToken
|
||||
}
|
||||
})
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > verificationRequest.expires) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await prisma[VerificationRequest].deleteMany({ where: { identifier, token: hashedToken } })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter
|
||||
}
|
||||
9
src/adapters/typeorm.js
Normal file
9
src/adapters/typeorm.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Source code can be found at:
|
||||
* https://github.com/nextauthjs/adapters/tree/canary/packages/typeorm-legacy
|
||||
*/
|
||||
|
||||
export {
|
||||
TypeORMLegacyAdapter as Adapter,
|
||||
Models,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
@@ -1,384 +0,0 @@
|
||||
import { createConnection, getConnection } from 'typeorm'
|
||||
import { createHash } from 'crypto'
|
||||
import require_optional from 'require_optional' // eslint-disable-line camelcase
|
||||
|
||||
import { CreateUserError } from '../../lib/errors'
|
||||
import adapterConfig from './lib/config'
|
||||
import adapterTransform from './lib/transform'
|
||||
import Models from './models'
|
||||
|
||||
import { updateConnectionEntities } from './lib/utils'
|
||||
|
||||
const Adapter = (typeOrmConfig, options = {}) => {
|
||||
// Ensure typeOrmConfigObject is normalized to an object
|
||||
const typeOrmConfigObject = (typeof typeOrmConfig === 'string')
|
||||
? adapterConfig.parseConnectionString(typeOrmConfig)
|
||||
: typeOrmConfig
|
||||
|
||||
// Load any custom models passed as an option, default to built in models
|
||||
const { models: customModels = {} } = options
|
||||
const models = {
|
||||
User: customModels.User ? customModels.User : Models.User,
|
||||
Account: customModels.Account ? customModels.Account : Models.Account,
|
||||
Session: customModels.Session ? customModels.Session : Models.Session,
|
||||
VerificationRequest: customModels.VerificationRequest ? customModels.VerificationRequest : Models.VerificationRequest
|
||||
}
|
||||
|
||||
// The models are designed for ANSI SQL databases first (as a baseline).
|
||||
// For databases that use a different pragma, we transform the models at run
|
||||
// time *unless* the models are user supplied (in which case we don't do
|
||||
// anything to do them). This function updates arguments by reference.
|
||||
adapterTransform(typeOrmConfigObject, models, options)
|
||||
|
||||
const config = adapterConfig.loadConfig(typeOrmConfigObject, { ...options, models })
|
||||
|
||||
// Create objects from models that can be consumed by functions in the adapter
|
||||
const User = models.User.model
|
||||
const Account = models.Account.model
|
||||
const Session = models.Session.model
|
||||
const VerificationRequest = models.VerificationRequest.model
|
||||
|
||||
let connection = null
|
||||
|
||||
async function getAdapter (appOptions) {
|
||||
const { logger } = appOptions
|
||||
// Display debug output if debug option enabled
|
||||
function debug (debugCode, ...args) {
|
||||
logger.debug(`TYPEORM_${debugCode}`, ...args)
|
||||
}
|
||||
|
||||
// Helper function to reuse / restablish connections
|
||||
// (useful if they drop when after being idle)
|
||||
async function _connect () {
|
||||
// Get current connection by name
|
||||
connection = getConnection(config.name)
|
||||
|
||||
// If connection is no longer established, reconnect
|
||||
if (!connection.isConnected) { connection = await connection.connect() }
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
// If no connection, create new connection
|
||||
try {
|
||||
connection = await createConnection(config)
|
||||
} catch (error) {
|
||||
if (error.name === 'AlreadyHasActiveConnectionError') {
|
||||
// If creating connection fails because it's already
|
||||
// been re-established, check it's really up
|
||||
await _connect()
|
||||
} else {
|
||||
logger.error('ADAPTER_CONNECTION_ERROR', error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If the connection object already exists, ensure it's valid
|
||||
await _connect()
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await updateConnectionEntities(connection, config.entities)
|
||||
}
|
||||
|
||||
// Get manager from connection object
|
||||
// https://github.com/typeorm/typeorm/blob/master/docs/entity-manager-api.md
|
||||
const { manager } = connection
|
||||
|
||||
// The models are primarily designed for ANSI SQL database, but some
|
||||
// flexiblity is required in the adapter to support non-SQL databases such
|
||||
// as MongoDB which have different pragmas.
|
||||
//
|
||||
// TypeORM does some abstraction, but doesn't handle everything (e.g. it
|
||||
// handles translating `id` and `_id` in models, but not queries) so we
|
||||
// need to handle somethings in the adapter to make it compatible.
|
||||
let idKey = 'id'
|
||||
let ObjectId
|
||||
if (config.type === 'mongodb') {
|
||||
idKey = '_id'
|
||||
// Using a dynamic import causes problems for some compilers/bundlers
|
||||
// that don't handle dynamic imports. To try and work around this we are
|
||||
// using the same method mongodb uses to load Object ID type, which is to
|
||||
// use the require_optional loader.
|
||||
const mongodb = require_optional('mongodb')
|
||||
ObjectId = mongodb.ObjectId
|
||||
}
|
||||
|
||||
// These values are stored as seconds, but to use them with dates in
|
||||
// JavaScript we convert them to milliseconds.
|
||||
//
|
||||
// Use a conditional to default to 30 day session age if not set - it should
|
||||
// always be set but a meaningful fallback is helpful to facilitate testing.
|
||||
if (appOptions && (!appOptions.session || !appOptions.session.maxAge)) {
|
||||
debug('GET_ADAPTER', 'Session expiry not configured (defaulting to 30 days')
|
||||
}
|
||||
const defaultSessionMaxAge = 30 * 24 * 60 * 60 * 1000
|
||||
const sessionMaxAge = (appOptions && appOptions.session && appOptions.session.maxAge)
|
||||
? appOptions.session.maxAge * 1000
|
||||
: defaultSessionMaxAge
|
||||
const sessionUpdateAge = (appOptions && appOptions.session && appOptions.session.updateAge)
|
||||
? appOptions.session.updateAge * 1000
|
||||
: 0
|
||||
|
||||
async function createUser (profile) {
|
||||
debug('CREATE_USER', profile)
|
||||
try {
|
||||
// Create user account
|
||||
const user = new User(profile.name, profile.email, profile.image, profile.emailVerified)
|
||||
return await manager.save(user)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_USER_ERROR', error)
|
||||
return Promise.reject(new CreateUserError(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser (id) {
|
||||
debug('GET_USER', id)
|
||||
|
||||
// In the very specific case of both using JWT for storing session data
|
||||
// and using MongoDB to store user data, the ID is a string rather than
|
||||
// an ObjectId and we need to turn it into an ObjectId.
|
||||
//
|
||||
// In all other scenarios it is already an ObjectId, because it will have
|
||||
// come from another MongoDB query.
|
||||
if (ObjectId && !(id instanceof ObjectId)) {
|
||||
id = ObjectId(id)
|
||||
}
|
||||
|
||||
try {
|
||||
return manager.findOne(User, { [idKey]: id })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByEmail (email) {
|
||||
debug('GET_USER_BY_EMAIL', email)
|
||||
try {
|
||||
if (!email) { return Promise.resolve(null) }
|
||||
return manager.findOne(User, { email })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_EMAIL_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_EMAIL_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserByProviderAccountId (providerId, providerAccountId) {
|
||||
debug('GET_USER_BY_PROVIDER_ACCOUNT_ID', providerId, providerAccountId)
|
||||
try {
|
||||
const account = await manager.findOne(Account, { providerId, providerAccountId })
|
||||
if (!account) { return null }
|
||||
return manager.findOne(User, { [idKey]: account.userId })
|
||||
} catch (error) {
|
||||
logger.error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error)
|
||||
return Promise.reject(new Error('GET_USER_BY_PROVIDER_ACCOUNT_ID_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUser (user) {
|
||||
debug('UPDATE_USER', user)
|
||||
return manager.save(User, user)
|
||||
}
|
||||
|
||||
async function deleteUser (userId) {
|
||||
debug('DELETE_USER', userId)
|
||||
// @TODO Delete user from DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function linkAccount (userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires) {
|
||||
debug('LINK_ACCOUNT', userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
try {
|
||||
// Create provider account linked to user
|
||||
const account = new Account(userId, providerId, providerType, providerAccountId, refreshToken, accessToken, accessTokenExpires)
|
||||
return manager.save(account)
|
||||
} catch (error) {
|
||||
logger.error('LINK_ACCOUNT_ERROR', error)
|
||||
return Promise.reject(new Error('LINK_ACCOUNT_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkAccount (userId, providerId, providerAccountId) {
|
||||
debug('UNLINK_ACCOUNT', userId, providerId, providerAccountId)
|
||||
// @TODO Get current user from DB
|
||||
// @TODO Delete [provider] object from user object
|
||||
// @TODO Save changes to user object in DB
|
||||
return false
|
||||
}
|
||||
|
||||
async function createSession (user) {
|
||||
debug('CREATE_SESSION', user)
|
||||
try {
|
||||
let expires = null
|
||||
if (sessionMaxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + sessionMaxAge)
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
const session = new Session(user.id, expires)
|
||||
|
||||
return manager.save(session)
|
||||
} catch (error) {
|
||||
logger.error('CREATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getSession (sessionToken) {
|
||||
debug('GET_SESSION', sessionToken)
|
||||
try {
|
||||
const session = await manager.findOne(Session, { sessionToken })
|
||||
|
||||
// Check session has not expired (do not return it if it has)
|
||||
if (session && session.expires && new Date() > new Date(session.expires)) {
|
||||
// @TODO Delete old sessions from database
|
||||
return null
|
||||
}
|
||||
|
||||
return session
|
||||
} catch (error) {
|
||||
logger.error('GET_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('GET_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSession (session, force) {
|
||||
debug('UPDATE_SESSION', session)
|
||||
try {
|
||||
if (sessionMaxAge && (sessionUpdateAge || sessionUpdateAge === 0) && session.expires) {
|
||||
// Calculate last updated date, to throttle write updates to database
|
||||
// Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge
|
||||
// e.g. ({expiry date} - 30 days) + 1 hour
|
||||
//
|
||||
// Default for sessionMaxAge is 30 days.
|
||||
// Default for sessionUpdateAge is 1 hour.
|
||||
const dateSessionIsDueToBeUpdated = new Date(session.expires)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() - sessionMaxAge)
|
||||
dateSessionIsDueToBeUpdated.setTime(dateSessionIsDueToBeUpdated.getTime() + sessionUpdateAge)
|
||||
|
||||
// Trigger update of session expiry date and write to database, only
|
||||
// if the session was last updated more than {sessionUpdateAge} ago
|
||||
if (new Date() > dateSessionIsDueToBeUpdated) {
|
||||
const newExpiryDate = new Date()
|
||||
newExpiryDate.setTime(newExpiryDate.getTime() + sessionMaxAge)
|
||||
session.expires = newExpiryDate
|
||||
} else if (!force) {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// If session MaxAge, session UpdateAge or session.expires are
|
||||
// missing then don't even try to save changes, unless force is set.
|
||||
if (!force) { return null }
|
||||
}
|
||||
|
||||
return manager.save(Session, session)
|
||||
} catch (error) {
|
||||
logger.error('UPDATE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('UPDATE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteSession (sessionToken) {
|
||||
debug('DELETE_SESSION', sessionToken)
|
||||
try {
|
||||
return await manager.delete(Session, { sessionToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_SESSION_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_SESSION_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function createVerificationRequest (identifier, url, token, secret, provider) {
|
||||
debug('CREATE_VERIFICATION_REQUEST', identifier)
|
||||
try {
|
||||
const { baseUrl } = appOptions
|
||||
const { sendVerificationRequest, maxAge } = provider
|
||||
|
||||
// Store hashed token (using secret as salt) so that tokens cannot be exploited
|
||||
// even if the contents of the database is compromised.
|
||||
// @TODO Use bcrypt function here instead of simple salted hash
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
|
||||
let expires = null
|
||||
if (maxAge) {
|
||||
const dateExpires = new Date()
|
||||
dateExpires.setTime(dateExpires.getTime() + (maxAge * 1000))
|
||||
expires = dateExpires
|
||||
}
|
||||
|
||||
// Save to database
|
||||
const newVerificationRequest = new VerificationRequest(identifier, hashedToken, expires)
|
||||
const verificationRequest = await manager.save(newVerificationRequest)
|
||||
|
||||
// With the verificationCallback on a provider, you can send an email, or queue
|
||||
// an email to be sent, or perform some other action (e.g. send a text message)
|
||||
await sendVerificationRequest({ identifier, url, token, baseUrl, provider })
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('CREATE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('CREATE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function getVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('GET_VERIFICATION_REQUEST', identifier, token)
|
||||
try {
|
||||
// Hash token provided with secret before trying to match it with database
|
||||
// @TODO Use bcrypt instead of salted SHA-256 hash for token
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
const verificationRequest = await manager.findOne(VerificationRequest, { identifier, token: hashedToken })
|
||||
|
||||
if (verificationRequest && verificationRequest.expires && new Date() > new Date(verificationRequest.expires)) {
|
||||
// Delete verification entry so it cannot be used again
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
return null
|
||||
}
|
||||
|
||||
return verificationRequest
|
||||
} catch (error) {
|
||||
logger.error('GET_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('GET_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVerificationRequest (identifier, token, secret, provider) {
|
||||
debug('DELETE_VERIFICATION', identifier, token)
|
||||
try {
|
||||
// Delete verification entry so it cannot be used again
|
||||
const hashedToken = createHash('sha256').update(`${token}${secret}`).digest('hex')
|
||||
await manager.delete(VerificationRequest, { identifier, token: hashedToken })
|
||||
} catch (error) {
|
||||
logger.error('DELETE_VERIFICATION_REQUEST_ERROR', error)
|
||||
return Promise.reject(new Error('DELETE_VERIFICATION_REQUEST_ERROR', error))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
createUser,
|
||||
getUser,
|
||||
getUserByEmail,
|
||||
getUserByProviderAccountId,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
linkAccount,
|
||||
unlinkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
createVerificationRequest,
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getAdapter
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Adapter,
|
||||
Models
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { EntitySchema } from 'typeorm'
|
||||
|
||||
const parseConnectionString = (configString) => {
|
||||
if (typeof configString !== 'string') { return configString }
|
||||
|
||||
// If the input is URL string, automatically convert the string to an object
|
||||
// to make configuration easier (in most use cases).
|
||||
//
|
||||
// TypeORM accepts connection string as a 'url' option, but unfortunately
|
||||
// not for all databases (e.g. SQLite) or for all options, so we handle
|
||||
// parsing it in this function.
|
||||
try {
|
||||
const parsedUrl = new URL(configString)
|
||||
const config = {}
|
||||
|
||||
if (parsedUrl.protocol.startsWith('mongodb+srv')) {
|
||||
// Special case handling is required for mongodb+srv with TypeORM
|
||||
config.type = 'mongodb'
|
||||
config.url = configString.replace(/\?(.*)$/, '')
|
||||
config.useNewUrlParser = true
|
||||
} else {
|
||||
config.type = parsedUrl.protocol.replace(/:$/, '')
|
||||
config.host = parsedUrl.hostname
|
||||
config.port = Number(parsedUrl.port)
|
||||
config.username = parsedUrl.username
|
||||
config.password = parsedUrl.password
|
||||
config.database = parsedUrl.pathname.replace(/^\//, '').replace(/\?(.*)$/, '')
|
||||
config.options = {}
|
||||
}
|
||||
|
||||
// This option is recommended by mongodb
|
||||
if (config.type === 'mongodb') {
|
||||
config.useUnifiedTopology = true
|
||||
}
|
||||
|
||||
// Prevents warning about deprecated option (sets default value)
|
||||
if (config.type === 'mssql') {
|
||||
config.options.enableArithAbort = true
|
||||
}
|
||||
|
||||
if (parsedUrl.search) {
|
||||
parsedUrl.search.replace(/^\?/, '').split('&').forEach(keyValuePair => {
|
||||
let [key, value] = keyValuePair.split('=')
|
||||
// Converts true/false strings to actual boolean values
|
||||
if (value === 'true') { value = true }
|
||||
if (value === 'false') { value = false }
|
||||
config[key] = value
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
} catch (error) {
|
||||
// If URL parsing fails for any reason, try letting TypeORM handle it
|
||||
return {
|
||||
url: configString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const loadConfig = (config, { models, namingStrategy }) => {
|
||||
const defaultConfig = {
|
||||
name: 'nextauth',
|
||||
autoLoadEntities: true,
|
||||
entities: [
|
||||
new EntitySchema(models.User.schema),
|
||||
new EntitySchema(models.Account.schema),
|
||||
new EntitySchema(models.Session.schema),
|
||||
new EntitySchema(models.VerificationRequest.schema)
|
||||
],
|
||||
timezone: 'Z', // Required for timestamps to be treated as UTC in MySQL
|
||||
logging: false,
|
||||
namingStrategy
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
...config
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
parseConnectionString,
|
||||
loadConfig
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// Inspired by https://github.com/tonivj5/typeorm-naming-strategies
|
||||
import { DefaultNamingStrategy } from 'typeorm'
|
||||
import { snakeCase, camelCase } from 'typeorm/util/StringUtils'
|
||||
|
||||
export class SnakeCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise table names (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || snakeCase(`${className}s`)
|
||||
}
|
||||
|
||||
columnName (propertyName, customName, embeddedPrefixes) {
|
||||
return `${snakeCase(embeddedPrefixes.join('_'))}${customName || snakeCase(propertyName)}`
|
||||
}
|
||||
|
||||
relationName (propertyName) {
|
||||
return snakeCase(propertyName)
|
||||
}
|
||||
|
||||
joinColumnName (relationName, referencedColumnName) {
|
||||
return snakeCase(`${relationName}_${referencedColumnName}`)
|
||||
}
|
||||
|
||||
joinTableName (firstTableName, secondTableName, firstPropertyName, secondPropertyName) {
|
||||
return snakeCase(`${firstTableName}_${firstPropertyName.replace(/\./gi, '_')}_${secondTableName}`)
|
||||
}
|
||||
|
||||
joinTableColumnName (tableName, propertyName, columnName) {
|
||||
return snakeCase(`${tableName}_${(columnName || propertyName)}`)
|
||||
}
|
||||
|
||||
classTableInheritanceParentColumnName (parentTableName, parentTableIdPropertyName) {
|
||||
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`)
|
||||
}
|
||||
|
||||
eagerJoinRelationAlias (alias, propertyPath) {
|
||||
return `${alias}__${propertyPath.replace('.', '_')}`
|
||||
}
|
||||
}
|
||||
|
||||
export class CamelCaseNamingStrategy extends DefaultNamingStrategy {
|
||||
// Pluralise collection names, uses (set customName to override)
|
||||
tableName (className, customName) {
|
||||
return customName || camelCase(`${className}s`)
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
// Perform transforms on SQL models so they can be used with other databases
|
||||
import { SnakeCaseNamingStrategy, CamelCaseNamingStrategy } from './naming-strategies'
|
||||
|
||||
const postgresTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for Postgres databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For Postgres we need to use the `timestamp with time zone` type
|
||||
// aka `timestamptz` to store timestamps correctly in UTC.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'timestamptz'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for MySQL databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// For MySQL we default milisecond precision of all timestamps to 6 digits.
|
||||
// This ensures all timestamp fields use the same precision (unless explictly
|
||||
// configured otherwise) and that values in MySQL match those Postgress.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
// If precision explictly set (including to null) don't change it
|
||||
if (typeof models[model].schema.columns[column].precision === 'undefined') {
|
||||
models[model].schema.columns[column].precision = 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mongodbTransform = (models, options) => {
|
||||
// A CamelCase naming strategy is used for all document databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// Important!
|
||||
//
|
||||
// 1. You must set 'objectId: true' on one property on a model in MongoDB.
|
||||
//
|
||||
// 'objectId' MUST be set on the primary ID field. This overrides other
|
||||
// values on that object in TypeORM (e.g. type: 'int' or 'primary').
|
||||
//
|
||||
// 2. Other properties that are Object IDs in the same model MUST be set to
|
||||
// type: 'objectId' (and should not be set to `objectId: true`).
|
||||
//
|
||||
// If you set 'objectId: true' on multiple properties on a model you will
|
||||
// see the result of queries like find() is wrong. You will see the same
|
||||
// Object ID in every property of type Object ID in the result (but the
|
||||
// database will look fine); so use `type: 'objectId'` for them instead.
|
||||
for (const model in models) {
|
||||
delete models[model].schema.columns.id.type
|
||||
models[model].schema.columns.id.objectId = true
|
||||
}
|
||||
|
||||
// Ensure reference to User ID in other models are Object IDs
|
||||
// This needs to done for any properties that reference another entity by ID
|
||||
models.Account.schema.columns.userId.type = 'objectId'
|
||||
models.Session.schema.columns.userId.type = 'objectId'
|
||||
|
||||
// The options `unique: true` and `nullable: true` don't work the same
|
||||
// with MongoDB as they do with SQL databases like MySQL and Postgres,
|
||||
// we need to create a sparse index to only allow unique values, while
|
||||
// still allowing multiple entires to omit the email address.
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
unique: true,
|
||||
sparse: true,
|
||||
columns: ['email']
|
||||
})
|
||||
}
|
||||
|
||||
const sqliteTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQLite databases
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQLite does not support `timestamp` fields so we remap them to `datetime`
|
||||
// in all models.
|
||||
//
|
||||
// `timestamp` is an ANSI SQL specification and widely supported by other
|
||||
// databases so this transform is a specific workaround required for SQLite.
|
||||
//
|
||||
// NB: SQLite adds 'create' and 'update' fields to allow rows, but that is
|
||||
// specific to SQLite and so we ignore that behaviour.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mssqlTransform = (models, options) => {
|
||||
// Apply snake case naming strategy for SQL Server databases
|
||||
if (!options.namingStrategy) {
|
||||
// @TODO Add TitleCase instead as more common MSSQL convention?
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
|
||||
// SQL Server deprecated TIMESTAMP in favor of ROWVERSION.
|
||||
// But ROWVERSION is not what it was intended in the other adapters.
|
||||
for (const model in models) {
|
||||
for (const column in models[model].schema.columns) {
|
||||
if (models[model].schema.columns[column].type === 'timestamp') {
|
||||
models[model].schema.columns[column].type = 'datetime'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Support UNIQUE on on User.email that allows duplicate NULL values
|
||||
// Note: This is ANSI SQL behaviour for UNIQUE not default in SQL Server
|
||||
delete models.User.schema.columns.email.unique
|
||||
|
||||
if (!models.User.schema.indices) { models.User.schema.indices = [] }
|
||||
|
||||
models.User.schema.indices.push({
|
||||
name: 'email',
|
||||
columns: ['email'],
|
||||
unique: true,
|
||||
where: 'email IS NOT NULL'
|
||||
})
|
||||
}
|
||||
|
||||
export default (config, models, options) => {
|
||||
// @TODO Refactor into switch statement
|
||||
if ((config.type && config.type.startsWith('mongodb')) ||
|
||||
(config.url && config.url.startsWith('mongodb'))) {
|
||||
mongodbTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('postgres')) ||
|
||||
(config.url && config.url.startsWith('postgres'))) {
|
||||
postgresTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mysql')) ||
|
||||
(config.url && config.url.startsWith('mysql'))) {
|
||||
mysqlTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('sqlite')) ||
|
||||
(config.url && config.url.startsWith('sqlite'))) {
|
||||
sqliteTransform(models, options)
|
||||
} else if ((config.type && config.type.startsWith('mssql')) ||
|
||||
(config.url && config.url.startsWith('mssql'))) {
|
||||
mssqlTransform(models, options)
|
||||
} else {
|
||||
// For all other SQL databases (e.g. MySQL) apply snake case naming
|
||||
// strategy, but otherwise use the models and schemas as they are.
|
||||
if (!options.namingStrategy) {
|
||||
options.namingStrategy = new SnakeCaseNamingStrategy()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
const entitiesChanged = (prevEntities, newEntities) => {
|
||||
if (prevEntities.length !== newEntities.length) return true
|
||||
for (let i = 0; i < prevEntities.length; i++) {
|
||||
if (prevEntities[i] !== newEntities[i]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const updateConnectionEntities = async (connection, entities) => {
|
||||
// Check if the entities passed have changed and if so replace them
|
||||
// and re-sync the typeorm connection.
|
||||
if (!connection || !entitiesChanged(connection.options.entities, entities)) return
|
||||
connection.options.entities = entities
|
||||
connection.buildMetadatas()
|
||||
if (connection.options.synchronize) {
|
||||
await connection.synchronize()
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { createHash } from 'crypto'
|
||||
|
||||
export class Account {
|
||||
constructor (
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
// The compound ID ensures there is only one entry for a given provider and account
|
||||
this.compoundId = createHash('sha256').update(`${providerId}:${providerAccountId}`).digest('hex')
|
||||
this.userId = userId
|
||||
this.providerType = providerType
|
||||
this.providerId = providerId
|
||||
this.providerAccountId = providerAccountId
|
||||
this.refreshToken = refreshToken
|
||||
this.accessToken = accessToken
|
||||
this.accessTokenExpires = accessTokenExpires
|
||||
}
|
||||
}
|
||||
|
||||
export const AccountSchema = {
|
||||
name: 'Account',
|
||||
target: Account,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
compoundId: {
|
||||
// The compound ID ensures that there there is only one instance of an
|
||||
// OAuth account in a way that works across different databases.
|
||||
// It is not used for anything else.
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
providerType: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
providerAccountId: {
|
||||
type: 'varchar'
|
||||
},
|
||||
refreshToken: {
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessToken: {
|
||||
// AccessTokens are not (yet) automatically rotated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'text',
|
||||
nullable: true
|
||||
},
|
||||
accessTokenExpires: {
|
||||
// AccessTokens expiry times are not (yet) updated by NextAuth.js
|
||||
// You can update it using the refreshToken and the accessTokenUrl endpoint for the provider
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
},
|
||||
indices: [
|
||||
{
|
||||
name: 'userId',
|
||||
columns: ['userId']
|
||||
},
|
||||
{
|
||||
name: 'providerId',
|
||||
columns: ['providerId']
|
||||
},
|
||||
{
|
||||
name: 'providerAccountId',
|
||||
columns: ['providerAccountId']
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Account, AccountSchema } from './account'
|
||||
import { User, UserSchema } from './user'
|
||||
import { Session, SessionSchema } from './session'
|
||||
import { VerificationRequest, VerificationRequestSchema } from './verification-request'
|
||||
|
||||
export default {
|
||||
Account: {
|
||||
model: Account,
|
||||
schema: AccountSchema
|
||||
},
|
||||
User: {
|
||||
model: User,
|
||||
schema: UserSchema
|
||||
},
|
||||
Session: {
|
||||
model: Session,
|
||||
schema: SessionSchema
|
||||
},
|
||||
VerificationRequest: {
|
||||
model: VerificationRequest,
|
||||
schema: VerificationRequestSchema
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
export class Session {
|
||||
constructor (userId, expires, sessionToken, accessToken) {
|
||||
this.userId = userId
|
||||
this.expires = expires
|
||||
this.sessionToken = sessionToken || randomBytes(32).toString('hex')
|
||||
this.accessToken = accessToken || randomBytes(32).toString('hex')
|
||||
}
|
||||
}
|
||||
|
||||
export const SessionSchema = {
|
||||
name: 'Session',
|
||||
target: Session,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
userId: {
|
||||
// This property is set to `type: objectId` on MongoDB databases
|
||||
type: 'int'
|
||||
},
|
||||
expires: {
|
||||
// The date the session expires (is updated when a session is active)
|
||||
type: 'timestamp'
|
||||
},
|
||||
sessionToken: {
|
||||
// The sessionToken should never be exposed to client side JavaScript
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
accessToken: {
|
||||
// The accessToken can be safely exposed to client side JavaScript to
|
||||
// to identify the owner of a session without exposing the sessionToken
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
export class User {
|
||||
constructor (name, email, image, emailVerified) {
|
||||
if (name) { this.name = name }
|
||||
if (email) { this.email = email }
|
||||
if (image) { this.image = image }
|
||||
if (emailVerified) {
|
||||
const currentDate = new Date()
|
||||
this.emailVerified = currentDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const UserSchema = {
|
||||
name: 'User',
|
||||
target: User,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
name: {
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
email: {
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
unique: true,
|
||||
nullable: true
|
||||
},
|
||||
emailVerified: {
|
||||
// Contains a timestamp of the last time an action was performed that
|
||||
// confirmed this email address was active and used by the user (e.g.
|
||||
// when an email sign in link is clicked on and verified). Is null
|
||||
// if the email address specified has never been verified.
|
||||
type: 'timestamp',
|
||||
nullable: true
|
||||
},
|
||||
image: {
|
||||
// A URL that points to an avatar to use for the user.
|
||||
// This is inherited from the one in the OAuth provider profile on
|
||||
// initial sign in, if one is specified in that profile.
|
||||
type: 'varchar',
|
||||
nullable: true
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// This model is used for sign in emails, but is designed to support other
|
||||
// mechanisms in future (e.g. 2FA via text message or short codes)
|
||||
export class VerificationRequest {
|
||||
constructor (identifier, token, expires) {
|
||||
if (identifier) { this.identifier = identifier }
|
||||
if (token) { this.token = token }
|
||||
if (expires) { this.expires = expires }
|
||||
}
|
||||
}
|
||||
|
||||
export const VerificationRequestSchema = {
|
||||
name: 'VerificationRequest',
|
||||
target: VerificationRequest,
|
||||
columns: {
|
||||
id: {
|
||||
// This property has `objectId: true` instead of `type: int` in MongoDB
|
||||
primary: true,
|
||||
type: 'int',
|
||||
generated: true
|
||||
},
|
||||
identifier: {
|
||||
// An email address, phone number, username or other unique identifier
|
||||
// associated with the request (used to track who it was on behalf of)
|
||||
type: 'varchar'
|
||||
},
|
||||
token: {
|
||||
// The token used verify the request (maybe hashed or encrypted)
|
||||
type: 'varchar',
|
||||
unique: true
|
||||
},
|
||||
expires: {
|
||||
// After this time, the request will no longer ve valid
|
||||
type: 'timestamp'
|
||||
},
|
||||
createdAt: {
|
||||
type: 'timestamp',
|
||||
createDate: true
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'timestamp',
|
||||
updateDate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export async function getSession (ctx) {
|
||||
return session
|
||||
}
|
||||
|
||||
async function getCsrfToken (ctx) {
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
}
|
||||
|
||||
@@ -355,6 +355,22 @@ function BroadcastChannel (name = 'nextauth.message') {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases. These should be removed in a newer release, as it only
|
||||
// creates problems for bundlers and adds confusion to users. TypeScript declarations
|
||||
// will provide sufficient help when importing
|
||||
export {
|
||||
setOptions as options,
|
||||
getSession as session,
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout
|
||||
}
|
||||
|
||||
export default {
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
|
||||
@@ -1,39 +1,98 @@
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'UnknownError'
|
||||
constructor(error) {
|
||||
// Support passing error or string
|
||||
super(error?.message ?? error)
|
||||
this.name = "UnknownError"
|
||||
if (error instanceof Error) {
|
||||
this.stack = error.stack
|
||||
}
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
toJSON() {
|
||||
return {
|
||||
error: {
|
||||
name: this.name,
|
||||
message: this.message
|
||||
// stack: this.stack
|
||||
}
|
||||
name: this.name,
|
||||
message: this.message,
|
||||
stack: this.stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'CreateUserError'
|
||||
}
|
||||
}
|
||||
|
||||
// Thrown when an Email address is already associated with an account
|
||||
// but the user is trying an OAuth account that is not linked to it.
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'AccountNotLinkedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuthCallbackError extends UnknownError {
|
||||
constructor (message) {
|
||||
super(message)
|
||||
this.name = 'OAuthCallbackError'
|
||||
}
|
||||
name = "OAuthCallbackError"
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an Email address is already associated with an account
|
||||
* but the user is trying an OAuth account that is not linked to it.
|
||||
*/
|
||||
export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class CreateUserError extends UnknownError {
|
||||
name = "CreateUserError"
|
||||
}
|
||||
|
||||
export class GetUserError extends UnknownError {
|
||||
name = "GetUserError"
|
||||
}
|
||||
|
||||
export class GetUserByEmailError extends UnknownError {
|
||||
name = "GetUserByEmailError"
|
||||
}
|
||||
|
||||
export class GetUserByIdError extends UnknownError {
|
||||
name = "GetUserByIdError"
|
||||
}
|
||||
|
||||
export class GetUserByProviderAccountIdError extends UnknownError {
|
||||
name = "GetUserByProviderAccountIdError"
|
||||
}
|
||||
|
||||
export class UpdateUserError extends UnknownError {
|
||||
name = "UpdateUserError"
|
||||
}
|
||||
|
||||
export class DeleteUserError extends UnknownError {
|
||||
name = "DeleteUserError"
|
||||
}
|
||||
|
||||
export class LinkAccountError extends UnknownError {
|
||||
name = "LinkAccountError"
|
||||
}
|
||||
|
||||
export class UnlinkAccountError extends UnknownError {
|
||||
name = "UnlinkAccountError"
|
||||
}
|
||||
|
||||
export class CreateSessionError extends UnknownError {
|
||||
name = "CreateSessionError"
|
||||
}
|
||||
|
||||
export class GetSessionError extends UnknownError {
|
||||
name = "GetSessionError"
|
||||
}
|
||||
|
||||
export class UpdateSessionError extends UnknownError {
|
||||
name = "UpdateSessionError"
|
||||
}
|
||||
|
||||
export class DeleteSessionError extends UnknownError {
|
||||
name = "DeleteSessionError"
|
||||
}
|
||||
|
||||
export class CreateVerificationRequestError extends UnknownError {
|
||||
name = "CreateVerificationRequestError"
|
||||
}
|
||||
|
||||
export class GetVerificationRequestError extends UnknownError {
|
||||
name = "GetVerificationRequestError"
|
||||
}
|
||||
|
||||
export class DeleteVerificationRequestError extends UnknownError {
|
||||
name = "DeleteVerificationRequestError"
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import crypto from 'crypto'
|
||||
import jose from 'jose'
|
||||
import logger from './logger'
|
||||
import crypto from "crypto"
|
||||
import jose from "jose"
|
||||
import logger from "./logger"
|
||||
|
||||
// Set default algorithm to use for auto-generated signing key
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = 'HS512'
|
||||
const DEFAULT_SIGNATURE_ALGORITHM = "HS512"
|
||||
|
||||
// Set default algorithm for auto-generated symmetric encryption key
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = 'A256GCM'
|
||||
const DEFAULT_ENCRYPTION_ALGORITHM = "A256GCM"
|
||||
|
||||
// Use encryption or not by default
|
||||
const DEFAULT_ENCRYPTION_ENABLED = false
|
||||
|
||||
const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
|
||||
async function encode ({
|
||||
export async function encode({
|
||||
token = {},
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
secret,
|
||||
signingKey,
|
||||
signingOptions = {
|
||||
expiresIn: `${maxAge}s`
|
||||
expiresIn: `${maxAge}s`,
|
||||
},
|
||||
encryptionKey,
|
||||
encryptionOptions = {
|
||||
alg: 'dir',
|
||||
alg: "dir",
|
||||
enc: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
zip: 'DEF'
|
||||
zip: "DEF",
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
// Signing Key
|
||||
const _signingKey = signingKey
|
||||
@@ -49,7 +49,7 @@ async function encode ({
|
||||
return signedToken
|
||||
}
|
||||
|
||||
async function decode ({
|
||||
export async function decode({
|
||||
secret,
|
||||
token,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
@@ -57,14 +57,14 @@ async function decode ({
|
||||
verificationKey = signingKey, // Optional (defaults to encryptionKey)
|
||||
verificationOptions = {
|
||||
maxTokenAge: `${maxAge}s`,
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM]
|
||||
algorithms: [DEFAULT_SIGNATURE_ALGORITHM],
|
||||
},
|
||||
encryptionKey,
|
||||
decryptionKey = encryptionKey, // Optional (defaults to encryptionKey)
|
||||
decryptionOptions = {
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM]
|
||||
algorithms: [DEFAULT_ENCRYPTION_ALGORITHM],
|
||||
},
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED
|
||||
encryption = DEFAULT_ENCRYPTION_ENABLED,
|
||||
} = {}) {
|
||||
if (!token) return null
|
||||
|
||||
@@ -77,8 +77,12 @@ async function decode ({
|
||||
: getDerivedEncryptionKey(secret)
|
||||
|
||||
// Decrypt token
|
||||
const decryptedToken = jose.JWE.decrypt(token, _encryptionKey, decryptionOptions)
|
||||
tokenToVerify = decryptedToken.toString('utf8')
|
||||
const decryptedToken = jose.JWE.decrypt(
|
||||
token,
|
||||
_encryptionKey,
|
||||
decryptionOptions
|
||||
)
|
||||
tokenToVerify = decryptedToken.toString("utf8")
|
||||
}
|
||||
|
||||
// Signing Key
|
||||
@@ -99,17 +103,22 @@ async function decode ({
|
||||
* raw?: boolean
|
||||
* }} params
|
||||
*/
|
||||
async function getToken (params) {
|
||||
export async function getToken(params) {
|
||||
const {
|
||||
req,
|
||||
// Use secure prefix for cookie name, unless URL is NEXTAUTH_URL is http://
|
||||
// or not set (e.g. development or test instance) case use unprefixed name
|
||||
secureCookie = !(!process.env.NEXTAUTH_URL || process.env.NEXTAUTH_URL.startsWith('http://')),
|
||||
cookieName = (secureCookie) ? '__Secure-next-auth.session-token' : 'next-auth.session-token',
|
||||
secureCookie = !(
|
||||
!process.env.NEXTAUTH_URL ||
|
||||
process.env.NEXTAUTH_URL.startsWith("http://")
|
||||
),
|
||||
cookieName = secureCookie
|
||||
? "__Secure-next-auth.session-token"
|
||||
: "next-auth.session-token",
|
||||
raw = false,
|
||||
decode: _decode = decode
|
||||
decode: _decode = decode,
|
||||
} = params
|
||||
if (!req) throw new Error('Must pass `req` to JWT getToken()')
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
|
||||
// Try to get token from cookie
|
||||
let token = req.cookies[cookieName]
|
||||
@@ -117,8 +126,8 @@ async function getToken (params) {
|
||||
// If cookie not found in cookie look for bearer token in authorization header.
|
||||
// This allows clients that pass through tokens in headers rather than as
|
||||
// cookies to use this helper function.
|
||||
if (!token && req.headers.authorization?.split(' ')[0] === 'Bearer') {
|
||||
const urlEncodedToken = req.headers.authorization.split(' ')[1]
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
@@ -138,7 +147,7 @@ let DERIVED_SIGNING_KEY_WARNING = false
|
||||
let DERIVED_ENCRYPTION_KEY_WARNING = false
|
||||
|
||||
// Do the better hkdf of Node.js one added in `v15.0.0` and Third Party one
|
||||
function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
|
||||
function hkdf(secret, { byteLength, encryptionInfo, digest = "sha256" }) {
|
||||
if (crypto.hkdfSync) {
|
||||
return Buffer.from(
|
||||
crypto.hkdfSync(
|
||||
@@ -150,39 +159,50 @@ function hkdf (secret, { byteLength, encryptionInfo, digest = 'sha256' }) {
|
||||
)
|
||||
)
|
||||
}
|
||||
return require('futoin-hkdf')(secret, byteLength, { info: encryptionInfo, hash: digest })
|
||||
return require("futoin-hkdf")(secret, byteLength, {
|
||||
info: encryptionInfo,
|
||||
hash: digest,
|
||||
})
|
||||
}
|
||||
|
||||
function getDerivedSigningKey (secret) {
|
||||
function getDerivedSigningKey(secret) {
|
||||
if (!DERIVED_SIGNING_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_SIGNING_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_SIGNING_KEY")
|
||||
DERIVED_SIGNING_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 64,
|
||||
encryptionInfo: 'NextAuth.js Generated Signing Key'
|
||||
encryptionInfo: "NextAuth.js Generated Signing Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_SIGNATURE_ALGORITHM,
|
||||
use: "sig",
|
||||
kid: "nextauth-auto-generated-signing-key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_SIGNATURE_ALGORITHM, use: 'sig', kid: 'nextauth-auto-generated-signing-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
function getDerivedEncryptionKey (secret) {
|
||||
function getDerivedEncryptionKey(secret) {
|
||||
if (!DERIVED_ENCRYPTION_KEY_WARNING) {
|
||||
logger.warn('JWT_AUTO_GENERATED_ENCRYPTION_KEY')
|
||||
logger.warn("JWT_AUTO_GENERATED_ENCRYPTION_KEY")
|
||||
DERIVED_ENCRYPTION_KEY_WARNING = true
|
||||
}
|
||||
|
||||
const buffer = hkdf(secret, {
|
||||
byteLength: 32,
|
||||
encryptionInfo: 'NextAuth.js Generated Encryption Key'
|
||||
encryptionInfo: "NextAuth.js Generated Encryption Key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, {
|
||||
alg: DEFAULT_ENCRYPTION_ALGORITHM,
|
||||
use: "enc",
|
||||
kid: "nextauth-auto-generated-encryption-key",
|
||||
})
|
||||
const key = jose.JWK.asKey(buffer, { alg: DEFAULT_ENCRYPTION_ALGORITHM, use: 'enc', kid: 'nextauth-auto-generated-encryption-key' })
|
||||
return key
|
||||
}
|
||||
|
||||
export default {
|
||||
encode,
|
||||
decode,
|
||||
getToken
|
||||
getToken,
|
||||
}
|
||||
|
||||
20
src/providers/42.js
Normal file
20
src/providers/42.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: '42-school',
|
||||
name: '42 School',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.intra.42.fr/oauth/token',
|
||||
authorizationUrl:
|
||||
'https://api.intra.42.fr/oauth/authorize?response_type=code',
|
||||
profileUrl: 'https://api.intra.42.fr/v2/me',
|
||||
profile: (profile) => ({
|
||||
id: profile.id,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
name: profile.usual_full_name,
|
||||
}),
|
||||
...options,
|
||||
}
|
||||
}
|
||||
54
src/providers/dropbox.js
Normal file
54
src/providers/dropbox.js
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* @param {import("../server").Provider} options
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // pages/api/auth/[...nextauth].js
|
||||
* import Providers from `next-auth/providers`
|
||||
* ...
|
||||
* providers: [
|
||||
* Providers.Dropbox({
|
||||
* clientId: process.env.DROPBOX_CLIENT_ID,
|
||||
* clientSecret: process.env.DROPBOX_CLIENT_SECRET
|
||||
* })
|
||||
* ]
|
||||
* ...
|
||||
*
|
||||
* // pages/index
|
||||
* import { signIn } from "next-auth/client"
|
||||
* ...
|
||||
* <button onClick={() => signIn("dropbox")}>
|
||||
* Sign in
|
||||
* </button>
|
||||
* ...
|
||||
* ```
|
||||
* *Resources:*
|
||||
* - [NextAuth.js Documentation](https://next-auth.js.org/providers/dropbox)
|
||||
* - [Dropbox Documentation](https://developers.dropbox.com/oauth-guide)
|
||||
* - [Configuration](https://www.dropbox.com/developers/apps)
|
||||
*/
|
||||
export default function Dropbox(options) {
|
||||
return {
|
||||
id: 'dropbox',
|
||||
name: 'Dropbox',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: 'account_info.read',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://api.dropboxapi.com/oauth2/token',
|
||||
authorizationUrl:
|
||||
'https://www.dropbox.com/oauth2/authorize?token_access_type=offline&response_type=code',
|
||||
profileUrl: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.account_id,
|
||||
name: profile.name.display_name,
|
||||
email: profile.email,
|
||||
image: profile.profile_photo_url,
|
||||
email_verified: profile.email_verified
|
||||
}
|
||||
},
|
||||
protection: ["state", "pkce"],
|
||||
...options
|
||||
}
|
||||
}
|
||||
22
src/providers/mailchimp.js
Normal file
22
src/providers/mailchimp.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export default function Mailchimp(options) {
|
||||
return {
|
||||
id: 'mailchimp',
|
||||
name: 'Mailchimp',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: { grant_type: 'authorization_code' },
|
||||
accessTokenUrl: 'https://login.mailchimp.com/oauth2/token',
|
||||
authorizationUrl: 'https://login.mailchimp.com/oauth2/authorize?response_type=code',
|
||||
profileUrl: 'https://login.mailchimp.com/oauth2/metadata',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
id: profile.login.login_id,
|
||||
name: profile.accountname,
|
||||
email: profile.login.email,
|
||||
image: null
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export default function Twitter(options) {
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(/_normal\.jpg$/, ".jpg"),
|
||||
image: profile.profile_image_url_https.replace(/_normal\.(jpg|png|gif)$/, ".$1"),
|
||||
}
|
||||
},
|
||||
...options,
|
||||
|
||||
23
src/providers/wordpress.js
Normal file
23
src/providers/wordpress.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default function WordPress(options) {
|
||||
return {
|
||||
id: "wordpress",
|
||||
name: "WordPress.com",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
scope: "auth",
|
||||
params: { grant_type: "authorization_code" },
|
||||
accessTokenUrl: "https://public-api.wordpress.com/oauth2/token",
|
||||
authorizationUrl:
|
||||
"https://public-api.wordpress.com/oauth2/authorize?response_type=code",
|
||||
profileUrl: "https://public-api.wordpress.com/rest/v1/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.ID,
|
||||
name: profile.display_name,
|
||||
email: profile.email,
|
||||
image: profile.avatar_URL,
|
||||
}
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
24
src/providers/workos.js
Normal file
24
src/providers/workos.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function WorkOS(options) {
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: 'https://api.workos.com/sso/token/',
|
||||
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
|
||||
profileUrl: 'https://api.workos.com/sso/profile/',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
name: `${profile.first_name} ${profile.last_name}`
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import adapters from '../adapters'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import logger, { setLogger } from '../lib/logger'
|
||||
import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
import parseProviders from './lib/providers'
|
||||
import * as routes from './routes'
|
||||
import renderPage from './pages'
|
||||
import createSecret from './lib/create-secret'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-res'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import * as pkce from './lib/oauth/pkce-handler'
|
||||
import * as state from './lib/oauth/state-handler'
|
||||
import adapters from "../adapters"
|
||||
import jwt from "../lib/jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import * as cookie from "./lib/cookie"
|
||||
import * as defaultEvents from "./lib/default-events"
|
||||
import * as defaultCallbacks from "./lib/default-callbacks"
|
||||
import parseProviders from "./lib/providers"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import createSecret from "./lib/create-secret"
|
||||
import callbackUrlHandler from "./lib/callback-url-handler"
|
||||
import extendRes from "./lib/extend-res"
|
||||
import csrfTokenHandler from "./lib/csrf-token-handler"
|
||||
import * as pkce from "./lib/oauth/pkce-handler"
|
||||
import * as state from "./lib/oauth/state-handler"
|
||||
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
* @param {import("next").NextApiResponse} res
|
||||
* @param {import("types").NextAuthOptions} userOptions
|
||||
*/
|
||||
async function NextAuthHandler (req, res, userOptions) {
|
||||
async function NextAuthHandler(req, res, userOptions) {
|
||||
if (userOptions.logger) {
|
||||
setLogger(userOptions.logger)
|
||||
}
|
||||
@@ -39,13 +39,15 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// to avoid early termination of calls to the serverless function
|
||||
// (and then return that promise when we are done) - eslint
|
||||
// complains but I'm not sure there is another way to do this.
|
||||
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
extendRes(req, res, resolve)
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
|
||||
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)
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
|
||||
return res.status(500).end(`Error: ${error}`)
|
||||
}
|
||||
|
||||
@@ -53,31 +55,48 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
providerId = nextauth[1],
|
||||
error = nextauth[1]
|
||||
error = nextauth[1],
|
||||
} = req.query
|
||||
|
||||
// @todo refactor all existing references to baseUrl and basePath
|
||||
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||
const { basePath, baseUrl } = parseUrl(
|
||||
process.env.NEXTAUTH_URL || process.env.VERCEL_URL
|
||||
)
|
||||
|
||||
const cookies = {
|
||||
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
|
||||
...cookie.defaultCookies(
|
||||
userOptions.useSecureCookies || baseUrl.startsWith("https://")
|
||||
),
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userOptions.cookies
|
||||
...userOptions.cookies,
|
||||
}
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
|
||||
const providers = parseProviders({
|
||||
providers: userOptions.providers,
|
||||
baseUrl,
|
||||
basePath,
|
||||
})
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
// Protection only works on OAuth 2.x providers
|
||||
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
|
||||
// When provider.state is undefined, we still want this to pass
|
||||
if (!provider.protection && provider.state !== false) {
|
||||
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ['state']
|
||||
} else if (typeof provider.protection === 'string') {
|
||||
provider.protection = [provider.protection]
|
||||
// TODO:
|
||||
// - rename to `checks` in 4.x, so it is similar to `openid-client`
|
||||
// - stop supporting `protection` as string
|
||||
// - remove `state` property
|
||||
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
|
||||
// Priority: (protection array > protection string) > state > default
|
||||
if (provider.protection) {
|
||||
provider.protection = Array.isArray(provider.protection)
|
||||
? provider.protection
|
||||
: [provider.protection]
|
||||
} else if (provider.state !== undefined) {
|
||||
provider.protection = [provider.state ? "state" : "none"]
|
||||
} else {
|
||||
// Default to state, as we did in 3.1
|
||||
// REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ["state"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +105,16 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// Parse database / adapter
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
|
||||
const adapter =
|
||||
userOptions.adapter ??
|
||||
(userOptions.database && adapters.Default(userOptions.database))
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
req.options = {
|
||||
debug: false,
|
||||
pages: {},
|
||||
theme: 'auto',
|
||||
theme: "auto",
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
@@ -111,7 +132,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
...userOptions.session
|
||||
...userOptions.session,
|
||||
},
|
||||
// JWT options
|
||||
jwt: {
|
||||
@@ -119,20 +140,20 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
maxAge, // same as session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userOptions.jwt
|
||||
...userOptions.jwt,
|
||||
},
|
||||
// Event messages
|
||||
events: {
|
||||
...defaultEvents,
|
||||
...userOptions.events
|
||||
...userOptions.events,
|
||||
},
|
||||
// Callback functions
|
||||
callbacks: {
|
||||
...defaultCallbacks,
|
||||
...userOptions.callbacks
|
||||
...userOptions.callbacks,
|
||||
},
|
||||
pkce: {},
|
||||
logger
|
||||
logger,
|
||||
}
|
||||
|
||||
csrfTokenHandler(req, res)
|
||||
@@ -141,65 +162,74 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
const render = renderPage(req, res)
|
||||
const { pages } = req.options
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (req.method === "GET") {
|
||||
switch (action) {
|
||||
case 'providers':
|
||||
case "providers":
|
||||
return routes.providers(req, res)
|
||||
case 'session':
|
||||
case "session":
|
||||
return routes.session(req, res)
|
||||
case 'csrf':
|
||||
case "csrf":
|
||||
return res.json({ csrfToken: req.options.csrfToken })
|
||||
case 'signin':
|
||||
case "signin":
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) { signinUrl = `${signinUrl}&error=${error}` }
|
||||
let signinUrl = `${pages.signIn}${
|
||||
pages.signIn.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) {
|
||||
signinUrl = `${signinUrl}&error=${error}`
|
||||
}
|
||||
return res.redirect(signinUrl)
|
||||
}
|
||||
|
||||
return render.signin()
|
||||
case 'signout':
|
||||
if (pages.signOut) {
|
||||
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
|
||||
}
|
||||
case "signout":
|
||||
if (pages.signOut) return res.redirect(pages.signOut)
|
||||
|
||||
return render.signout()
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
if (await pkce.handleCallback(req, res)) return
|
||||
if (await state.handleCallback(req, res)) return
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case 'verify-request':
|
||||
case "verify-request":
|
||||
if (pages.verifyRequest) {
|
||||
return res.redirect(pages.verifyRequest)
|
||||
}
|
||||
return render.verifyRequest()
|
||||
case 'error':
|
||||
case "error":
|
||||
if (pages.error) {
|
||||
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
|
||||
return res.redirect(
|
||||
`${pages.error}${
|
||||
pages.error.includes("?") ? "&" : "?"
|
||||
}error=${error}`
|
||||
)
|
||||
}
|
||||
|
||||
// These error messages are displayed in line on the sign in page
|
||||
if ([
|
||||
'Signin',
|
||||
'OAuthSignin',
|
||||
'OAuthCallback',
|
||||
'OAuthCreateAccount',
|
||||
'EmailCreateAccount',
|
||||
'Callback',
|
||||
'OAuthAccountNotLinked',
|
||||
'EmailSignin',
|
||||
'CredentialsSignin'
|
||||
].includes(error)) {
|
||||
if (
|
||||
[
|
||||
"Signin",
|
||||
"OAuthSignin",
|
||||
"OAuthCallback",
|
||||
"OAuthCreateAccount",
|
||||
"EmailCreateAccount",
|
||||
"Callback",
|
||||
"OAuthAccountNotLinked",
|
||||
"EmailSignin",
|
||||
"CredentialsSignin",
|
||||
].includes(error)
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
default:
|
||||
}
|
||||
} else if (req.method === 'POST') {
|
||||
} else if (req.method === "POST") {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (req.options.csrfTokenVerified && provider) {
|
||||
if (await pkce.handleSignin(req, res)) return
|
||||
@@ -208,16 +238,19 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
case 'signout':
|
||||
case "signout":
|
||||
// Verified CSRF Token required for signout
|
||||
if (req.options.csrfTokenVerified) {
|
||||
return routes.signout(req, res)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
|
||||
if (
|
||||
provider.type === "credentials" &&
|
||||
!req.options.csrfTokenVerified
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
@@ -226,31 +259,33 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case '_log':
|
||||
case "_log":
|
||||
if (userOptions.logger) {
|
||||
try {
|
||||
const {
|
||||
code = 'CLIENT_ERROR',
|
||||
level = 'error',
|
||||
message = '[]'
|
||||
code = "CLIENT_ERROR",
|
||||
level = "error",
|
||||
message = "[]",
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error('LOGGER_ERROR', error)
|
||||
logger.error("LOGGER_ERROR", error)
|
||||
}
|
||||
}
|
||||
return res.end()
|
||||
default:
|
||||
}
|
||||
}
|
||||
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
return res
|
||||
.status(400)
|
||||
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
/** Tha main entry point to next-auth */
|
||||
export default function NextAuth (...args) {
|
||||
export default function NextAuth(...args) {
|
||||
if (args.length === 1) {
|
||||
return (req, res) => NextAuthHandler(req, res, args[0])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AccountNotLinkedError } from '../../lib/errors'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import { AccountNotLinkedError } from "../../lib/errors"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* This function handles the complex flow of signing users in, and either creating,
|
||||
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
|
||||
* 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.
|
||||
* @param {import("types").Session} sessionToken
|
||||
* @param {import("types").Profile} profile
|
||||
* @param {import("types").Account} account
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
*/
|
||||
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
|
||||
export default async function callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
providerAccount,
|
||||
options
|
||||
) {
|
||||
// Input validation
|
||||
if (!profile) throw new Error('Missing profile')
|
||||
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
|
||||
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
|
||||
if (!profile) throw new Error("Missing profile")
|
||||
if (!providerAccount?.id || !providerAccount.type)
|
||||
throw new Error("Missing or invalid provider account")
|
||||
if (!["email", "oauth"].includes(providerAccount.type))
|
||||
throw new Error("Provider not supported")
|
||||
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: {
|
||||
jwt: useJwtSession
|
||||
}
|
||||
session: { jwt: useJwtSession },
|
||||
} = options
|
||||
|
||||
// If no adapter is configured then we don't have a database and cannot
|
||||
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
user: profile,
|
||||
account: providerAccount,
|
||||
session: {}
|
||||
session: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
linkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession
|
||||
} = await adapter.getAdapter(options)
|
||||
deleteSession,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
|
||||
|
||||
let session = null
|
||||
let user = null
|
||||
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
}
|
||||
|
||||
if (providerAccount.type === 'email') {
|
||||
if (providerAccount.type === "email") {
|
||||
// If signing in with an email, check if an account with the same email address exists already
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// If they are not already signed in as the same user, this flow will
|
||||
// sign them out of the current session and sign them in as the new user
|
||||
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else if (providerAccount.type === 'oauth') {
|
||||
} else if (providerAccount.type === "oauth") {
|
||||
// If signing in with oauth account, check to see if the account exists already
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(
|
||||
providerAccount.provider,
|
||||
providerAccount.id
|
||||
)
|
||||
if (userByProviderAccountId) {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in with this account, we don't need to do anything
|
||||
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
// If the user is currently signed in, but the new account they are signing in
|
||||
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
// If there is no active session, but the account being signed in with is already
|
||||
// associated with a valid user then create session to sign the user in.
|
||||
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
|
||||
session = useJwtSession
|
||||
? {}
|
||||
: await createSession(userByProviderAccountId)
|
||||
return {
|
||||
session,
|
||||
user: userByProviderAccountId,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else {
|
||||
if (isSignedIn) {
|
||||
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
// As they are already signed in, we don't need to do anything after linking them
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
//
|
||||
// OAuth providers should require email address verification to prevent this, but in
|
||||
// practice that is not always the case; this helps protect against that.
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
||||
// we do already have an account with the same email address as the one in the
|
||||
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
isNewUser = true
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
let httpMethod = 'GET'
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
@@ -238,8 +239,15 @@ async function getOAuth2 (provider, accessToken, results) {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
/** Dropbox requires POST instead of GET
|
||||
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
|
||||
*/
|
||||
if (provider.id === 'dropbox') {
|
||||
httpMethod = 'POST'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
|
||||
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { randomBytes } from "crypto"
|
||||
import adapterErrorHandler from "../../../adapters/error-handler"
|
||||
|
||||
export default async function email (email, provider, options) {
|
||||
/**
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {import("types/providers").EmailConfig} provider
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
* @returns
|
||||
*/
|
||||
export default async function email(email, provider, options) {
|
||||
try {
|
||||
const { baseUrl, basePath, adapter } = options
|
||||
const { baseUrl, basePath, adapter, logger } = options
|
||||
|
||||
const { createVerificationRequest } = await adapter.getAdapter(options)
|
||||
const { createVerificationRequest } = adapterErrorHandler(
|
||||
await adapter.getAdapter(options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Prefer provider specific secret, but use default secret if none specified
|
||||
const secret = provider.secret || options.secret
|
||||
|
||||
// Generate token
|
||||
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
|
||||
const token =
|
||||
(await provider.generateVerificationToken?.()) ??
|
||||
randomBytes(32).toString("hex")
|
||||
|
||||
// Send email with link containing token (the unhashed version)
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
|
||||
provider.id
|
||||
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
|
||||
// @TODO Create invite (send secret so can be hashed)
|
||||
await createVerificationRequest(email, url, token, secret, provider, options)
|
||||
await createVerificationRequest(
|
||||
email,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider,
|
||||
options
|
||||
)
|
||||
|
||||
// Return promise
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import oAuthCallback from '../lib/oauth/callback'
|
||||
import callbackHandler from '../lib/callback-handler'
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import oAuthCallback from "../lib/oauth/callback"
|
||||
import callbackHandler from "../lib/callback-handler"
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Handle callbacks from login services
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function callback (req, res) {
|
||||
export default async function callback(req, res) {
|
||||
const {
|
||||
provider,
|
||||
adapter,
|
||||
@@ -22,21 +22,23 @@ export default async function callback (req, res) {
|
||||
jwt,
|
||||
events,
|
||||
callbacks,
|
||||
session: {
|
||||
jwt: useJwtSession,
|
||||
maxAge: sessionMaxAge
|
||||
}
|
||||
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
// Get session ID (if set)
|
||||
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
|
||||
|
||||
if (provider.type === 'oauth') {
|
||||
if (provider.type === "oauth") {
|
||||
try {
|
||||
const { profile, account, OAuthProfile } = await oAuthCallback(req)
|
||||
try {
|
||||
// Make it easier to debug when adding a new provider
|
||||
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
||||
logger.debug("OAUTH_CALLBACK_RESPONSE", {
|
||||
profile,
|
||||
account,
|
||||
OAuthProfile,
|
||||
})
|
||||
|
||||
// If we don't have a profile object then either something went wrong
|
||||
// or the user cancelled signing in. We don't know which, so we just
|
||||
@@ -56,52 +58,85 @@ export default async function callback (req, res) {
|
||||
// (that just means it's a new user signing in for the first time).
|
||||
let userOrProfile = profile
|
||||
if (adapter) {
|
||||
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
||||
const { getUserByProviderAccountId } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(
|
||||
account.provider,
|
||||
account.id
|
||||
)
|
||||
if (userFromProviderAccountId) {
|
||||
userOrProfile = userFromProviderAccountId
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
||||
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(
|
||||
`${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.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
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()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||
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))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
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 })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -110,94 +145,145 @@ export default async function callback (req, res) {
|
||||
// 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)}`)
|
||||
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 (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`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`
|
||||
)
|
||||
} else if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'OAuthCallbackError') {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', 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 res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'email') {
|
||||
} else if (provider.type === "email") {
|
||||
try {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const {
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest,
|
||||
getUserByEmail,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(req.options), logger)
|
||||
const verificationToken = req.query.token
|
||||
const email = req.query.email
|
||||
|
||||
// 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) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
||||
}
|
||||
|
||||
// If verification token is valid, delete verification request token from
|
||||
// the database so it cannot be used again
|
||||
await deleteVerificationRequest(email, verificationToken, secret, provider)
|
||||
await deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
)
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = {
|
||||
id: provider.id,
|
||||
type: "email",
|
||||
providerAccountId: email,
|
||||
}
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
profile,
|
||||
account,
|
||||
{ email }
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
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()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
profile,
|
||||
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))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
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 })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -206,55 +292,93 @@ export default async function callback (req, res) {
|
||||
// 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)}`)
|
||||
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 === 'CreateUserError') {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
||||
if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=EmailCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('CALLBACK_EMAIL_ERROR', error)
|
||||
logger.error("CALLBACK_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'credentials' && req.method === 'POST') {
|
||||
} else if (provider.type === "credentials" && req.method === "POST") {
|
||||
if (!useJwtSession) {
|
||||
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_JWT_ERROR",
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
if (!provider.authorize) {
|
||||
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const credentials = req.body
|
||||
|
||||
let userObjectReturnedFromAuthorizeHandler
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
|
||||
credentials, {...req, options: {}, cookies: {}}
|
||||
)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
return res
|
||||
.status(401)
|
||||
.redirect(
|
||||
`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
const user = userObjectReturnedFromAuthorizeHandler
|
||||
const account = { id: provider.id, type: 'credentials' }
|
||||
const account = { id: provider.id, type: "credentials" }
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
user,
|
||||
account,
|
||||
credentials
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
return res
|
||||
.status(403)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
@@ -263,22 +387,33 @@ export default async function callback (req, res) {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
userObjectReturnedFromAuthorizeHandler,
|
||||
false
|
||||
)
|
||||
|
||||
// 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))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account })
|
||||
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
}
|
||||
return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
return res
|
||||
.status(500)
|
||||
.end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Return a session object (without any private fields)
|
||||
* for Single Page App clients
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function session (req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks } = req.options
|
||||
export default async function session(req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionMaxAge = req.options.session.maxAge
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
@@ -24,7 +26,9 @@ export default async function session (req, res) {
|
||||
|
||||
// Generate new session expiry date
|
||||
const sessionExpiresDate = new Date()
|
||||
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
|
||||
sessionExpiresDate.setTime(
|
||||
sessionExpiresDate.getTime() + sessionMaxAge * 1000
|
||||
)
|
||||
const sessionExpires = sessionExpiresDate.toISOString()
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
@@ -33,14 +37,17 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: decodedJwt.name || null,
|
||||
email: decodedJwt.email || null,
|
||||
image: decodedJwt.picture || null
|
||||
image: decodedJwt.picture || null,
|
||||
},
|
||||
expires: sessionExpires
|
||||
expires: sessionExpires,
|
||||
}
|
||||
|
||||
// Pass Session and JSON Web Token through to the session callback
|
||||
const jwtPayload = await callbacks.jwt(decodedJwt)
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
jwtPayload
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
@@ -49,17 +56,29 @@ export default async function session (req, res) {
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: sessionExpires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
|
||||
await dispatchEvent(events.session, {
|
||||
session: sessionPayload,
|
||||
jwt: jwtPayload,
|
||||
})
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error('JWT_SESSION_ERROR', error)
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
logger.error("JWT_SESSION_ERROR", error)
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
|
||||
const { getUser, getSession, updateSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const session = await getSession(sessionToken)
|
||||
if (session) {
|
||||
// Trigger update to session object to update session expiry
|
||||
@@ -73,29 +92,38 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image
|
||||
image: user.image,
|
||||
},
|
||||
accessToken: session.accessToken,
|
||||
expires: session.expires
|
||||
expires: session.expires,
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
user
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
|
||||
// Set cookie again to update expiry
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, {
|
||||
expires: session.expires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload })
|
||||
} else if (sessionToken) {
|
||||
// If sessionToken was found set but it's not valid for a session then
|
||||
// remove the sessionToken cookie from browser.
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('SESSION_ERROR', error)
|
||||
logger.error("SESSION_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
import getAuthorizationUrl from '../lib/signin/oauth'
|
||||
import emailSignin from '../lib/signin/email'
|
||||
import logger from '../../lib/logger'
|
||||
import getAuthorizationUrl from "../lib/signin/oauth"
|
||||
import emailSignin from "../lib/signin/email"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signin */
|
||||
export default async function signin (req, res) {
|
||||
/**
|
||||
* Handle requests to /api/auth/signin
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signin(req, res) {
|
||||
const {
|
||||
provider,
|
||||
baseUrl,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks
|
||||
callbacks,
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
if (!provider.type) {
|
||||
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
|
||||
}
|
||||
|
||||
if (provider.type === 'oauth' && req.method === 'POST') {
|
||||
if (provider.type === "oauth" && req.method === "POST") {
|
||||
try {
|
||||
const authorizationUrl = await getAuthorizationUrl(req)
|
||||
return res.redirect(authorizationUrl)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
logger.error("SIGNIN_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
} else if (provider.type === 'email' && req.method === 'POST') {
|
||||
} else if (provider.type === "email" && req.method === "POST") {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
const { getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const { getUserByEmail } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Note: Technically the part of the email address local mailbox element
|
||||
// (everything before the @ symbol) should be treated as 'case sensitive'
|
||||
@@ -39,36 +47,43 @@ export default async function signin (req, res) {
|
||||
const email = req.body.email?.toLowerCase() ?? null
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = { id: provider.id, type: "email", providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, {
|
||||
email,
|
||||
verificationRequest: true,
|
||||
})
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSignin(email, provider, req.options)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_EMAIL_ERROR', error)
|
||||
logger.error("SIGNIN_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signin`)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signout */
|
||||
export default async function signout (req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl } = req.options
|
||||
/**
|
||||
* Handle requests to /api/auth/signout
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signout(req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
|
||||
@@ -18,7 +22,10 @@ export default async function signout (req, res) {
|
||||
}
|
||||
} else {
|
||||
// Get session from database
|
||||
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
|
||||
const { getSession, deleteSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
try {
|
||||
// Dispatch signout event
|
||||
@@ -33,14 +40,14 @@ export default async function signout (req, res) {
|
||||
await deleteSession(sessionToken)
|
||||
} catch (error) {
|
||||
// If error, log it but continue
|
||||
logger.error('SIGNOUT_ERROR', error)
|
||||
logger.error("SIGNOUT_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Session Token
|
||||
cookie.set(res, cookies.sessionToken.name, '', {
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0
|
||||
maxAge: 0,
|
||||
})
|
||||
|
||||
return res.redirect(callbackUrl)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Start test app with local databases inside the container.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
# https://docs.docker.com/compose/compose-file/compose-file-v2/
|
||||
version: '2.3'
|
||||
|
||||
services:
|
||||
|
||||
app:
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: Dockerfile
|
||||
environment:
|
||||
# Set env vars in your current terminal or in .env in the root directory
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||
- NEXTAUTH_DATABASE_URL=${NEXTAUTH_DATABASE_URL}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXTAUTH_JWT_SESSIONS=${NEXTAUTH_JWT_SESSIONS}
|
||||
- NEXTAUTH_AUTH0_ID=${NEXTAUTH_AUTH0_ID}
|
||||
- NEXTAUTH_AUTH0_SECRET=${NEXTAUTH_AUTH0_SECRET}
|
||||
- NEXTAUTH_AUTH0_DOMAIN=${NEXTAUTH_AUTH0_DOMAIN}
|
||||
- NEXTAUTH_FACEBOOK_ID=${NEXTAUTH_FACEBOOK_ID}
|
||||
- NEXTAUTH_FACEBOOK_SECRET=${NEXTAUTH_FACEBOOK_SECRET}
|
||||
- NEXTAUTH_GITHUB_ID=${NEXTAUTH_GITHUB_ID}
|
||||
- NEXTAUTH_GITHUB_SECRET=${NEXTAUTH_GITHUB_SECRET}
|
||||
- NEXTAUTH_GOOGLE_ID=${NEXTAUTH_GOOGLE_ID}
|
||||
- NEXTAUTH_GOOGLE_SECRET=${NEXTAUTH_GOOGLE_SECRET}
|
||||
- NEXTAUTH_TWITTER_ID=${NEXTAUTH_TWITTER_ID}
|
||||
- NEXTAUTH_TWITTER_SECRET=${NEXTAUTH_TWITTER_SECRET}
|
||||
- NEXTAUTH_EMAIL_SERVER=${NEXTAUTH_EMAIL_SERVER}
|
||||
- NEXTAUTH_EMAIL_FROM=${NEXTAUTH_EMAIL_FROM}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
# mongo:
|
||||
# extends:
|
||||
# file: databases/mongo.yml
|
||||
# service: mongo
|
||||
|
||||
# mssql:
|
||||
# extends:
|
||||
# file: databases/mssql.yml
|
||||
# service: mssql
|
||||
|
||||
# mysql:
|
||||
# extends:
|
||||
# file: databases/mysql.yml
|
||||
# service: mysql
|
||||
|
||||
# postgres:
|
||||
# extends:
|
||||
# file: databases/postgres.yml
|
||||
# service: postgres
|
||||
2521
test/docker/app/package-lock.json
generated
2521
test/docker/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "next-auth-test",
|
||||
"version": "0.0.1",
|
||||
"description": "Test application for NextAuth.js",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"next": "^10.0.6",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Provider } from 'next-auth/client'
|
||||
|
||||
export default function App ({ Component, pageProps }) {
|
||||
return (
|
||||
<Provider
|
||||
options={{
|
||||
// Client Max Age controls how often the useSession in the client should
|
||||
// contact the server to sync the session state. Value in seconds.
|
||||
// e.g.
|
||||
// * 0 - Disabled (always use cache value)
|
||||
// * 60 - Sync session state with server if it's older than 60 seconds
|
||||
clientMaxAge: 0,
|
||||
// Keep Alive tells windows / tabs that are signed in to keep sending
|
||||
// a keep alive request (which extends the current session expiry) to
|
||||
// prevent sessions in open windows from expiring. Value in seconds.
|
||||
//
|
||||
// Note: If a session has expired when keep alive is triggered, all open
|
||||
// windows / tabs will be updated to reflect the user is signed out.
|
||||
keepAlive: 0
|
||||
}}
|
||||
session={pageProps.session}
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import NextAuth from 'next-auth'
|
||||
import Providers from 'next-auth/providers'
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
const options = {
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Email({
|
||||
server: process.env.NEXTAUTH_EMAIL_SERVER,
|
||||
from: process.env.NEXTAUTH_EMAIL_FROM
|
||||
}),
|
||||
Providers.Apple({
|
||||
clientId: process.env.NEXTAUTH_APPLE_ID,
|
||||
clientSecret: {
|
||||
appleId: process.env.NEXTAUTH_APPLE_ID,
|
||||
teamId: process.env.NEXTAUTH_APPLE_TEAM_ID,
|
||||
privateKey: process.env.NEXTAUTH_APPLE_PRIVATE_KEY,
|
||||
keyId: process.env.NEXTAUTH_APPLE_KEY_ID
|
||||
}
|
||||
}),
|
||||
Providers.Auth0({
|
||||
clientId: process.env.NEXTAUTH_AUTH0_ID,
|
||||
clientSecret: process.env.NEXTAUTH_AUTH0_SECRET,
|
||||
domain: process.env.NEXTAUTH_AUTH0_DOMAIN
|
||||
}),
|
||||
Providers.Facebook({
|
||||
clientId: process.env.NEXTAUTH_FACEBOOK_ID,
|
||||
clientSecret: process.env.NEXTAUTH_FACEBOOK_SECRET
|
||||
}),
|
||||
Providers.GitHub({
|
||||
clientId: process.env.NEXTAUTH_GITHUB_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GITHUB_SECRET
|
||||
}),
|
||||
Providers.Google({
|
||||
clientId: process.env.NEXTAUTH_GOOGLE_ID,
|
||||
clientSecret: process.env.NEXTAUTH_GOOGLE_SECRET
|
||||
}),
|
||||
Providers.Twitter({
|
||||
clientId: process.env.NEXTAUTH_TWITTER_ID,
|
||||
clientSecret: process.env.NEXTAUTH_TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
// Database optional. MySQL, Maria DB, Postgres and MongoDB are supported.
|
||||
// https://next-auth.js.org/configuration/database
|
||||
//
|
||||
// Notes:
|
||||
// * You must to install an appropriate node_module for your database
|
||||
// * The Email provider requires a database (OAuth providers do not)
|
||||
database: process.env.NEXTAUTH_DATABASE_URL,
|
||||
|
||||
// The secret should be set to a reasonably long random string.
|
||||
// It is used to sign cookies and to sign and encrypt JSON Web Tokens, unless
|
||||
// a seperate secret is defined explicitly for encrypting the JWT.
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
|
||||
session: {
|
||||
// Use JSON Web Tokens for session instead of database sessions.
|
||||
// This option can be used with or without a database for users/accounts.
|
||||
// Note: `jwt` is automatically set to `true` if no database is specified.
|
||||
jwt: true
|
||||
|
||||
// Seconds - How long until an idle session expires and is no longer valid.
|
||||
// maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
|
||||
// Seconds - Throttle how frequently to write to database to extend a session.
|
||||
// Use it to limit write operations. Set to 0 to always update the database.
|
||||
// Note: This option is ignored if using JSON Web Tokens
|
||||
// updateAge: 24 * 60 * 60, // 24 hours
|
||||
},
|
||||
|
||||
// JSON Web tokens are only used for sessions if the `jwt: true` session
|
||||
// option is set - or by default if no database is specified.
|
||||
// https://next-auth.js.org/configuration/options#jwt
|
||||
jwt: {
|
||||
// A secret to use for key generation (you should set this explicitly)
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// Set to true to use encryption (default: false)
|
||||
// encryption: true,
|
||||
|
||||
// You can define your own encode/decode functions for signing and encryption
|
||||
// if you want to override the default behaviour.
|
||||
// async encode({ secret, token, maxAge }) {},
|
||||
// async decode({ secret, token, maxAge }) {},
|
||||
},
|
||||
|
||||
// You can define custom pages to override the built-in pages.
|
||||
// The routes shown here are the default URLs that will be used when a custom
|
||||
// pages is not specified for that route.
|
||||
// https://next-auth.js.org/configuration/pages
|
||||
pages: {
|
||||
// signIn: '/api/auth/signin', // Displays signin buttons
|
||||
// signOut: '/api/auth/signout', // Displays form with sign out button
|
||||
// error: '/api/auth/error', // Error code passed in query string as ?error=
|
||||
// verifyRequest: '/api/auth/verify-request', // Used for check email page
|
||||
// newUser: null // If set, new users will be directed here on first sign in
|
||||
},
|
||||
|
||||
// Callbacks are asynchronous functions you can use to control what happens
|
||||
// when an action is performed.
|
||||
// https://next-auth.js.org/configuration/callbacks
|
||||
callbacks: {
|
||||
// async signIn(user, account, profile) { return Promise.resolve(true) },
|
||||
// async redirect(url, baseUrl) { return Promise.resolve(baseUrl) },
|
||||
// async session(session, user) { return Promise.resolve(session) },
|
||||
// async jwt(token, user, account, profile, isNewUser) { return Promise.resolve(token) }
|
||||
},
|
||||
|
||||
// Events are useful for logging
|
||||
// https://next-auth.js.org/configuration/events
|
||||
events: { },
|
||||
|
||||
// Enable debug messages in the console if you are having problems
|
||||
debug: false
|
||||
}
|
||||
|
||||
export default (req, res) => NextAuth(req, res, options)
|
||||
@@ -1,3 +0,0 @@
|
||||
export default (req, res) => {
|
||||
res.send(JSON.stringify(process.env, null, 2))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import jwt from 'next-auth/jwt'
|
||||
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
|
||||
if (session) {
|
||||
res.send({ content: 'Protected content.' })
|
||||
} else {
|
||||
res.send({ content: 'Unprotected content.' })
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { getSession } from 'next-auth/client'
|
||||
|
||||
export default async (req, res) => {
|
||||
const session = await getSession({ req })
|
||||
res.send(JSON.stringify(session, null, 2))
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import Package from 'next-auth/package.json'
|
||||
|
||||
export default (req, res) => {
|
||||
res.send(Package.version)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function IndexPage () {
|
||||
return (
|
||||
<div id='nextauth-test-app'>
|
||||
<h1>NextAuth.js Test App</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useSession } from 'next-auth/client'
|
||||
|
||||
export default function TestPage () {
|
||||
const [ session, loading ] = useSession()
|
||||
|
||||
return (
|
||||
<div id='nextauth-test-page'>
|
||||
<h1>NextAuth.js Test Page</h1>
|
||||
{session && <p id="nextauth-signed-in">Signed in</p>}
|
||||
{!session && !loading && <p id="nextauth-signed-out">Signed out</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
# Start Mongo, MSSQL, MySQL and Postgres databases on the current host running
|
||||
# on their respective default ports. This is intended for developer convenience
|
||||
# to make it easier to develop and test features manually.
|
||||
#
|
||||
# Note: Uses Docker Compose v2 as v3 doesn't currently support extends.
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
extends:
|
||||
file: databases/mongo.yml
|
||||
service: mongo
|
||||
ports:
|
||||
- "27017:27017"
|
||||
|
||||
mssql:
|
||||
extends:
|
||||
file: databases/mssql.yml
|
||||
service: mssql
|
||||
ports:
|
||||
- "1433:1433"
|
||||
|
||||
mysql:
|
||||
extends:
|
||||
file: databases/mysql.yml
|
||||
service: mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
postgres:
|
||||
extends:
|
||||
file: databases/postgres.yml
|
||||
service: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mongo:
|
||||
image: bitnami/mongodb
|
||||
restart: always
|
||||
environment:
|
||||
MONGODB_USERNAME: nextauth
|
||||
MONGODB_PASSWORD: password
|
||||
MONGODB_DATABASE: nextauth
|
||||
@@ -1,13 +0,0 @@
|
||||
version: "2"
|
||||
|
||||
services:
|
||||
mssql:
|
||||
image: mcr.microsoft.com/mssql/server:2017-latest
|
||||
restart: always
|
||||
environment:
|
||||
SA_PASSWORD: Pa55w0rd # minimum password complexity
|
||||
ACCEPT_EULA: Y
|
||||
# WARN: command overrides, default image start sequence, start.sh starts 'sql-server'
|
||||
command: '/var/setup/start.sh'
|
||||
volumes:
|
||||
- ./mssql:/var/setup # mount setup files
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# see https://github.com/Microsoft/mssql-docker
|
||||
# no way to know when sql server is ready
|
||||
until /opt/mssql-tools/bin/sqlcmd -S 127.0.01 -U sa -P Pa55w0rd -d master -i /var/setup/setup.sql
|
||||
do sleep 1;
|
||||
done
|
||||
echo "NEXT_AUTH: setup completed"
|
||||
@@ -1,29 +0,0 @@
|
||||
USE master;
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.syslogins
|
||||
where name = 'nextauth')
|
||||
CREATE LOGIN nextauth
|
||||
WITH PASSWORD = 'password',
|
||||
CHECK_POLICY = OFF;
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists (select name
|
||||
from sys.databases
|
||||
where name = 'nextauth' )
|
||||
CREATE database nextauth
|
||||
GO
|
||||
/* did you tear down the container ? */
|
||||
if not exists(select [name]
|
||||
from sys.sysusers
|
||||
where name= 'nextauth')
|
||||
CREATE USER nextauth
|
||||
WITH DEFAULT_SCHEMA =[dbo];
|
||||
GO
|
||||
/*
|
||||
* Adding user as sysadmin,
|
||||
* So you can easily drop/create/re-create/alter the database
|
||||
* You will need to login to 'master' to do that
|
||||
*/
|
||||
exec sp_addsrvrolemember @loginame = N'nextauth', @rolename = N'sysadmin'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user