mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8a9e8aeb6 | ||
|
|
1fb308a6f4 | ||
|
|
613c303315 | ||
|
|
d24fe1cebb | ||
|
|
885b02ca95 | ||
|
|
f218697fd6 | ||
|
|
dbead0ad85 | ||
|
|
704ded5310 | ||
|
|
25fbcb4648 | ||
|
|
53a439b44b | ||
|
|
16a2e37fd6 | ||
|
|
0392a8df9a | ||
|
|
a459b95c5b | ||
|
|
13df7eb81d | ||
|
|
62f261209c | ||
|
|
da43d0d896 | ||
|
|
4b1271ba75 | ||
|
|
d30da0170f | ||
|
|
887b2985fc | ||
|
|
d2bbac1164 | ||
|
|
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 |
@@ -1,4 +0,0 @@
|
||||
# Exclude directories we don't need from Docker context to improve build time
|
||||
node_modules
|
||||
www
|
||||
src
|
||||
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 😊
|
||||
|
||||
-->
|
||||
|
||||
39
.github/workflows/tests.yml
vendored
Normal file
39
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
types:
|
||||
name: "Types"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Check types
|
||||
run: npm run test:types
|
||||
tests:
|
||||
name: "Integration Tests"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Init
|
||||
uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
- name: Install dependencies
|
||||
uses: bahmutov/npm-install@v1
|
||||
- name: Run tests
|
||||
run: npm test -- --ci
|
||||
27
.github/workflows/types.yml
vendored
27
.github/workflows/types.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Types
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- beta
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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
|
||||
run: npm run test:types
|
||||
@@ -32,7 +32,7 @@ cd next-auth
|
||||
|
||||
2. Install packages:
|
||||
```sh
|
||||
npm i && npm dev:setup
|
||||
npm i && npm run dev:setup
|
||||
```
|
||||
|
||||
3. Populate `.env.local`:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# You can use `openssl rand -hex 32` or
|
||||
# https://generate-secret.now.sh/32 to generate a secret.
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verificaion tokens.
|
||||
SECRET=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,6 +4,6 @@ import jwt from 'next-auth/jwt'
|
||||
const secret = process.env.SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
33
config/babel.config.js
Normal file
33
config/babel.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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"],
|
||||
},
|
||||
{
|
||||
test: ["../src/**/*.test.js"],
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
runtime: "automatic",
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
["@babel/preset-env", { "targets": { "esmodules": true } }]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
],
|
||||
"comments": false,
|
||||
"overrides": [
|
||||
{
|
||||
"test": ["../src/server/pages/**"],
|
||||
"presets": ["preact"]
|
||||
}
|
||||
]
|
||||
}
|
||||
2
config/jest-setup.js
Normal file
2
config/jest-setup.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import "@testing-library/jest-dom"
|
||||
import "whatwg-fetch"
|
||||
8
config/jest.config.js
Normal file
8
config/jest.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"\\.js$": ["babel-jest", { configFile: "./config/babel.config.js" }],
|
||||
},
|
||||
roots: ["../src"],
|
||||
setupFilesAfterEnv: ["./jest-setup.js"],
|
||||
testMatch: ["**/*.test.js"],
|
||||
}
|
||||
40718
package-lock.json
generated
40718
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
73
package.json
73
package.json
@@ -30,29 +30,16 @@
|
||||
},
|
||||
"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: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": "jest --config ./config/jest.config.js",
|
||||
"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"
|
||||
},
|
||||
@@ -74,7 +61,9 @@
|
||||
],
|
||||
"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",
|
||||
@@ -83,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",
|
||||
@@ -101,17 +88,22 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@babel/preset-react": "^7.13.13",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
"@semantic-release/release-notes-generator": "^9.0.1",
|
||||
"@testing-library/jest-dom": "^5.12.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@testing-library/user-event": "^13.1.9",
|
||||
"@types/react": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.22.0",
|
||||
"autoprefixer": "^9.7.6",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
@@ -121,25 +113,20 @@
|
||||
"eslint-config-prettier": "^8.2.0",
|
||||
"eslint-config-standard-with-typescript": "^19.0.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jest": "^24.3.6",
|
||||
"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",
|
||||
"jest": "^26.6.3",
|
||||
"msw": "^0.28.2",
|
||||
"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"
|
||||
"typescript": "^4.1.3",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false
|
||||
@@ -166,7 +153,23 @@
|
||||
"localStorage": "readonly",
|
||||
"location": "readonly",
|
||||
"fetch": "readonly"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*test.js"
|
||||
],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"jest"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"branches": [
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/client/__tests__/mocks.js
Normal file
87
src/client/__tests__/mocks.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { setupServer } from "msw/node"
|
||||
import { rest } from "msw"
|
||||
import { randomBytes } from "crypto"
|
||||
|
||||
export const mockSession = {
|
||||
user: {
|
||||
image: null,
|
||||
name: "John",
|
||||
email: "john@email.com",
|
||||
},
|
||||
expires: 123213139,
|
||||
}
|
||||
|
||||
export const mockProviders = {
|
||||
github: {
|
||||
id: "github",
|
||||
name: "Github",
|
||||
type: "oauth",
|
||||
signinUrl: "path/to/signin",
|
||||
callbackUrl: "path/to/callback",
|
||||
},
|
||||
credentials: {
|
||||
id: "credentials",
|
||||
name: "Credentials",
|
||||
type: "credentials",
|
||||
authorize: null,
|
||||
credentials: null,
|
||||
},
|
||||
email: {
|
||||
id: "email",
|
||||
type: "email",
|
||||
name: "Email",
|
||||
},
|
||||
}
|
||||
|
||||
export const mockCSRFToken = {
|
||||
csrfToken: randomBytes(32).toString("hex"),
|
||||
}
|
||||
|
||||
export const mockGithubResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/github/url",
|
||||
}
|
||||
|
||||
export const mockCredentialsResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/credentials/url",
|
||||
}
|
||||
|
||||
export const mockEmailResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/email/url",
|
||||
}
|
||||
|
||||
export const mockSignOutResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
url: "https://path/to/signout/url",
|
||||
}
|
||||
|
||||
export const server = setupServer(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSignOutResponse))
|
||||
),
|
||||
rest.get("/api/auth/session", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockSession))
|
||||
),
|
||||
rest.get("/api/auth/csrf", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCSRFToken))
|
||||
),
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockProviders))
|
||||
),
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
),
|
||||
rest.post("/api/auth/callback/credentials", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockCredentialsResponse))
|
||||
),
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(mockEmailResponse))
|
||||
),
|
||||
rest.post("/api/auth/_log", (req, res, ctx) => res(ctx.status(200)))
|
||||
)
|
||||
97
src/client/__tests__/session.test.js
Normal file
97
src/client/__tests__/session.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import { server, mockSession } from "./mocks"
|
||||
import logger from "../../lib/logger"
|
||||
import { useState, useEffect } from "react"
|
||||
import { getSession } from ".."
|
||||
import { getBroadcastEvents } from "./utils"
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => server.listen())
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
afterAll(() => server.close())
|
||||
|
||||
test("if it can fetch the session, it should store it in `localStorage`", async () => {
|
||||
render(<SessionFlow />)
|
||||
|
||||
// In the start, there is no session
|
||||
const noSession = await screen.findByText("No session")
|
||||
expect(noSession).toBeInTheDocument()
|
||||
|
||||
// After we fetched the session, it should have been rendered by `<SessionFlow />`
|
||||
const session = await screen.findByText(new RegExp(mockSession.user.name))
|
||||
expect(session).toBeInTheDocument()
|
||||
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "getSession",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
|
||||
test("if there's an error fetching the session, it should log it", async () => {
|
||||
server.use(
|
||||
rest.get("/api/auth/session", (req, res, ctx) => {
|
||||
return res(ctx.status(500), ctx.body("Server error"))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SessionFlow />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"session",
|
||||
new SyntaxError("Unexpected token S in JSON at position 0")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SessionFlow() {
|
||||
const [session, setSession] = useState(null)
|
||||
useEffect(() => {
|
||||
async function fetchUserSession() {
|
||||
try {
|
||||
const result = await getSession({})
|
||||
setSession(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
fetchUserSession()
|
||||
}, [])
|
||||
|
||||
if (session) {
|
||||
return <pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
}
|
||||
return <p>No session</p>
|
||||
}
|
||||
290
src/client/__tests__/sign-in.test.js
Normal file
290
src/client/__tests__/sign-in.test.js
Normal file
@@ -0,0 +1,290 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import logger from "../../lib/logger"
|
||||
import {
|
||||
server,
|
||||
mockCredentialsResponse,
|
||||
mockEmailResponse,
|
||||
mockGithubResponse,
|
||||
} from "./mocks"
|
||||
import { signIn } from ".."
|
||||
import { rest } from "msw"
|
||||
|
||||
const { location } = window
|
||||
|
||||
jest.mock("../../lib/logger", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
warn: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
proxyLogger(logger) {
|
||||
return logger
|
||||
},
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider, it redirects to the default sign-in page",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | type
|
||||
${""} | ${"no"}
|
||||
${"foo"} | ${"unknown"}
|
||||
`(
|
||||
"if $type provider supplied and no callback URL, redirects using the current location",
|
||||
async ({ provider }) => {
|
||||
render(<SignInFlow providerId={provider} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
`/api/auth/signin?callbackUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test.each`
|
||||
provider | mockUrl
|
||||
${`email`} | ${mockEmailResponse.url}
|
||||
${`credentials`} | ${mockCredentialsResponse.url}
|
||||
`(
|
||||
"$provider provider redirects if `redirect` is `true`",
|
||||
async ({ provider, mockUrl }) => {
|
||||
render(<SignInFlow providerId={provider} redirect={true} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrl)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
test("redirection can't be stopped using an oauth provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="github"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockGithubResponse.url)
|
||||
})
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'credentials' provider", async () => {
|
||||
render(
|
||||
<SignInFlow
|
||||
providerId="credentials"
|
||||
callbackUrl={callbackUrl}
|
||||
redirect={false}
|
||||
/>
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockCredentialsResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn`
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/credentials/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("redirection can be stopped using the 'email' provider", async () => {
|
||||
render(
|
||||
<SignInFlow providerId="email" callbackUrl={callbackUrl} redirect={false} />
|
||||
)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).not.toHaveBeenCalledWith(
|
||||
mockEmailResponse.url
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("signin-result").textContent).not.toBe(
|
||||
"no response"
|
||||
)
|
||||
})
|
||||
|
||||
// snapshot the expected return shape from `signIn` oauth
|
||||
expect(JSON.parse(screen.getByTestId("signin-result").textContent))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": null,
|
||||
"ok": true,
|
||||
"status": 200,
|
||||
"url": "https://path/to/email/url",
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
test("if callback URL contains a hash we force a window reload when re-directing", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/email", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockEmailResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="email" callbackUrl={mockUrlWithHash} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
// the browser will not refresh the page if the redirect URL contains a hash, hence we force it on the client, see #1289
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
test("params are propagated to the signin URL when supplied", async () => {
|
||||
let matchedParams = ""
|
||||
const authParams = "foo=bar&bar=foo"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signin/github", (req, res, ctx) => {
|
||||
matchedParams = req.url.search
|
||||
return res(ctx.status(200), ctx.json(mockGithubResponse))
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" authorizationParams={authParams} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(matchedParams).toEqual(`?${authParams}`)
|
||||
})
|
||||
})
|
||||
|
||||
test("when it fails to fetch the providers, it redirected back to signin page", async () => {
|
||||
const errorMsg = "Error when retrieving providers"
|
||||
|
||||
server.use(
|
||||
rest.get("/api/auth/providers", (req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json(errorMsg))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignInFlow providerId="github" />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledWith(`/api/auth/error`)
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
"CLIENT_FETCH_ERROR",
|
||||
"providers",
|
||||
errorMsg
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function SignInFlow({
|
||||
providerId,
|
||||
callbackUrl,
|
||||
redirect = true,
|
||||
authorizationParams = {},
|
||||
}) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function handleSignIn() {
|
||||
const result = await signIn(
|
||||
providerId,
|
||||
{
|
||||
callbackUrl,
|
||||
redirect,
|
||||
},
|
||||
authorizationParams
|
||||
)
|
||||
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signin-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={() => handleSignIn()}>Sign in</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
129
src/client/__tests__/sign-out.test.js
Normal file
129
src/client/__tests__/sign-out.test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { useState } from "react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import { server, mockSignOutResponse } from "./mocks"
|
||||
import { signOut } from ".."
|
||||
import { rest } from "msw"
|
||||
import { getBroadcastEvents } from "./utils"
|
||||
|
||||
const { location } = window
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen()
|
||||
delete window.location
|
||||
window.location = {
|
||||
...location,
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line no-proto
|
||||
jest.spyOn(window.localStorage.__proto__, "setItem")
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
server.resetHandlers()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
window.location = location
|
||||
server.close()
|
||||
})
|
||||
|
||||
const callbackUrl = "https://redirects/to"
|
||||
|
||||
test("by default it redirects to the current URL if the server did not provide one", async () => {
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ ...mockSignOutResponse, url: undefined }))
|
||||
)
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(window.location.href)
|
||||
})
|
||||
})
|
||||
|
||||
test("it redirects to the URL allowed by the server", async () => {
|
||||
render(<SignOutFlow callbackUrl={callbackUrl} />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.replace).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(
|
||||
mockSignOutResponse.url
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test("if url contains a hash during redirection a page reload happens", async () => {
|
||||
const mockUrlWithHash = "https://path/to/email/url#foo-bar-baz"
|
||||
|
||||
server.use(
|
||||
rest.post("/api/auth/signout", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
...mockSignOutResponse,
|
||||
url: mockUrlWithHash,
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.reload).toHaveBeenCalledTimes(1)
|
||||
expect(window.location.replace).toHaveBeenCalledWith(mockUrlWithHash)
|
||||
})
|
||||
})
|
||||
|
||||
test("will broadcast the signout event to other tabs", async () => {
|
||||
render(<SignOutFlow />)
|
||||
|
||||
userEvent.click(screen.getByRole("button"))
|
||||
|
||||
await waitFor(() => {
|
||||
const broadcastCalls = getBroadcastEvents()
|
||||
const [broadcastedEvent] = broadcastCalls
|
||||
|
||||
expect(broadcastCalls).toHaveLength(1)
|
||||
expect(broadcastedEvent.eventName).toBe("nextauth.message")
|
||||
expect(broadcastedEvent.value).toStrictEqual({
|
||||
data: {
|
||||
trigger: "signout",
|
||||
},
|
||||
event: "session",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function SignOutFlow({ callbackUrl, redirect = true }) {
|
||||
const [response, setResponse] = useState(null)
|
||||
|
||||
async function setSignOutRes() {
|
||||
const result = await signOut({ callbackUrl, redirect })
|
||||
setResponse(result)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p data-testid="signout-result">
|
||||
{response ? JSON.stringify(response) : "no response"}
|
||||
</p>
|
||||
<button onClick={() => setSignOutRes()}>Sign out</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
8
src/client/__tests__/utils.js
Normal file
8
src/client/__tests__/utils.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function getBroadcastEvents() {
|
||||
return window.localStorage.setItem.mock.calls
|
||||
.filter((call) => call[0] === "nextauth.message")
|
||||
.map(([eventName, value]) => {
|
||||
const { timestamp, ...rest } = JSON.parse(value)
|
||||
return { eventName, value: rest }
|
||||
})
|
||||
}
|
||||
@@ -8,9 +8,15 @@
|
||||
//
|
||||
// We use HTTP POST requests with CSRF Tokens to protect against CSRF attacks.
|
||||
|
||||
import { useState, useEffect, useContext, createContext, createElement } from 'react'
|
||||
import _logger, { proxyLogger } from '../lib/logger'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useContext,
|
||||
createContext,
|
||||
createElement,
|
||||
} from "react"
|
||||
import _logger, { proxyLogger } from "../lib/logger"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
// This behaviour mirrors the default behaviour for getting the site name that
|
||||
// happens server side in server/index.js
|
||||
@@ -22,8 +28,14 @@ import parseUrl from '../lib/parse-url'
|
||||
const __NEXTAUTH = {
|
||||
baseUrl: parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePath: parseUrl(process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL || process.env.VERCEL_URL).baseUrl,
|
||||
basePathServer: parseUrl(process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL).basePath,
|
||||
baseUrlServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL ||
|
||||
process.env.NEXTAUTH_URL ||
|
||||
process.env.VERCEL_URL
|
||||
).baseUrl,
|
||||
basePathServer: parseUrl(
|
||||
process.env.NEXTAUTH_URL_INTERNAL || process.env.NEXTAUTH_URL
|
||||
).basePath,
|
||||
keepAlive: 0,
|
||||
clientMaxAge: 0,
|
||||
// Properties starting with _ are used for tracking internal app state
|
||||
@@ -31,7 +43,7 @@ const __NEXTAUTH = {
|
||||
_clientSyncTimer: null,
|
||||
_eventListenersAdded: false,
|
||||
_clientSession: undefined,
|
||||
_getSession: () => {}
|
||||
_getSession: () => {},
|
||||
}
|
||||
|
||||
const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
@@ -39,7 +51,7 @@ const logger = proxyLogger(_logger, __NEXTAUTH.basePath)
|
||||
const broadcast = BroadcastChannel()
|
||||
|
||||
// Add event listners on load
|
||||
if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
if (typeof window !== "undefined" && !__NEXTAUTH._eventListenersAdded) {
|
||||
__NEXTAUTH._eventListenersAdded = true
|
||||
// Listen for storage events and update session if event fired from
|
||||
// another window (but suppress firing another event to avoid a loop)
|
||||
@@ -50,26 +62,30 @@ if (typeof window !== 'undefined' && !__NEXTAUTH._eventListenersAdded) {
|
||||
// on how the session object is being used in the client; it is
|
||||
// more robust to have each window/tab fetch it's own copy of the
|
||||
// session object rather than share it across instances.
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: 'storage' }))
|
||||
broadcast.receive(() => __NEXTAUTH._getSession({ event: "storage" }))
|
||||
|
||||
// Listen for document visibility change events and
|
||||
// if visibility of the document changes, re-fetch the session.
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: 'visibilitychange' })
|
||||
}, false)
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
() => {
|
||||
!document.hidden && __NEXTAUTH._getSession({ event: "visibilitychange" })
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
// Context to store session data globally
|
||||
/** @type {import("types/internals/client").SessionContext} */
|
||||
const SessionContext = createContext()
|
||||
|
||||
export function useSession (session) {
|
||||
export function useSession(session) {
|
||||
const context = useContext(SessionContext)
|
||||
if (context) return context
|
||||
return _useSessionHook(session)
|
||||
}
|
||||
|
||||
function _useSessionHook (session) {
|
||||
function _useSessionHook(session) {
|
||||
const [data, setData] = useState(session)
|
||||
const [loading, setLoading] = useState(!data)
|
||||
|
||||
@@ -77,7 +93,7 @@ function _useSessionHook (session) {
|
||||
__NEXTAUTH._getSession = async ({ event = null } = {}) => {
|
||||
try {
|
||||
const triggredByEvent = event !== null
|
||||
const triggeredByStorageEvent = event === 'storage'
|
||||
const triggeredByStorageEvent = event === "storage"
|
||||
|
||||
const clientMaxAge = __NEXTAUTH.clientMaxAge
|
||||
const clientLastSync = parseInt(__NEXTAUTH._clientLastSync)
|
||||
@@ -98,14 +114,19 @@ function _useSessionHook (session) {
|
||||
// tab or window that will come through as a triggeredByStorageEvent
|
||||
// event and will skip this logic)
|
||||
return
|
||||
} else if (clientMaxAge > 0 && currentTime < (clientLastSync + clientMaxAge)) {
|
||||
} else if (
|
||||
clientMaxAge > 0 &&
|
||||
currentTime < clientLastSync + clientMaxAge
|
||||
) {
|
||||
// If the session freshness is within clientMaxAge then don't request
|
||||
// it again on this call (avoids too many invokations).
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSession === undefined) { __NEXTAUTH._clientSession = null }
|
||||
if (clientSession === undefined) {
|
||||
__NEXTAUTH._clientSession = null
|
||||
}
|
||||
|
||||
// Update clientLastSync before making response to avoid repeated
|
||||
// invokations that would otherwise be triggered while we are still
|
||||
@@ -116,7 +137,7 @@ function _useSessionHook (session) {
|
||||
// tell getSession not to trigger an event when it calls to avoid an
|
||||
// infinate loop.
|
||||
const newClientSessionData = await getSession({
|
||||
triggerEvent: !triggeredByStorageEvent
|
||||
triggerEvent: !triggeredByStorageEvent,
|
||||
})
|
||||
|
||||
// Save session state internally, just so we can track that we've checked
|
||||
@@ -126,7 +147,7 @@ function _useSessionHook (session) {
|
||||
setData(newClientSessionData)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_USE_SESSION_ERROR', error)
|
||||
logger.error("CLIENT_USE_SESSION_ERROR", error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
@@ -137,40 +158,41 @@ function _useSessionHook (session) {
|
||||
return [data, loading]
|
||||
}
|
||||
|
||||
export async function getSession (ctx) {
|
||||
const session = await _fetchData('session', ctx)
|
||||
export async function getSession(ctx) {
|
||||
const session = await _fetchData("session", ctx)
|
||||
if (ctx?.triggerEvent ?? true) {
|
||||
broadcast.post({ event: 'session', data: { trigger: 'getSession' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "getSession" } })
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
export async function getCsrfToken (ctx) {
|
||||
return (await _fetchData('csrf', ctx))?.csrfToken
|
||||
export async function getCsrfToken(ctx) {
|
||||
return (await _fetchData("csrf", ctx))?.csrfToken
|
||||
}
|
||||
|
||||
export async function getProviders () {
|
||||
return _fetchData('providers')
|
||||
export async function getProviders() {
|
||||
return await _fetchData("providers")
|
||||
}
|
||||
|
||||
export async function signIn (provider, options = {}, authorizationParams = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signIn(provider, options = {}, authorizationParams = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const providers = await getProviders()
|
||||
|
||||
// Redirect to sign in page if no valid provider specified
|
||||
if (!(provider in providers)) {
|
||||
// If Provider not recognized, redirect to sign in page
|
||||
window.location = `${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
return
|
||||
if (!providers) {
|
||||
return window.location.replace(`${baseUrl}/error`)
|
||||
}
|
||||
const isCredentials = providers[provider].type === 'credentials'
|
||||
const isEmail = providers[provider].type === 'email'
|
||||
const canRedirectBeDisabled = isCredentials || isEmail
|
||||
|
||||
if (!(provider in providers)) {
|
||||
return window.location.replace(
|
||||
`${baseUrl}/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
const isCredentials = providers[provider].type === "credentials"
|
||||
const isEmail = providers[provider].type === "email"
|
||||
const isSupportingReturn = isCredentials || isEmail
|
||||
|
||||
const signInUrl = isCredentials
|
||||
? `${baseUrl}/callback/${provider}`
|
||||
@@ -179,72 +201,71 @@ export async function signIn (provider, options = {}, authorizationParams = {})
|
||||
// If is any other provider type, POST to provider URL with CSRF Token,
|
||||
// callback URL and any other parameters supplied.
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
...options,
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
|
||||
const _signInUrl = `${signInUrl}?${new URLSearchParams(authorizationParams)}`
|
||||
const res = await fetch(_signInUrl, fetchOptions)
|
||||
const data = await res.json()
|
||||
if (redirect || !canRedirectBeDisabled) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
|
||||
if (redirect || !isSupportingReturn) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
const error = new URL(data.url).searchParams.get('error')
|
||||
const error = new URL(data.url).searchParams.get("error")
|
||||
|
||||
if (res.ok) {
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
status: res.status,
|
||||
ok: res.ok,
|
||||
url: error ? null : data.url
|
||||
url: error ? null : data.url,
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOut (options = {}) {
|
||||
const {
|
||||
callbackUrl = window.location,
|
||||
redirect = true
|
||||
} = options
|
||||
export async function signOut(options = {}) {
|
||||
const { callbackUrl = window.location.href, redirect = true } = options
|
||||
const baseUrl = _apiBaseUrl()
|
||||
const fetchOptions = {
|
||||
method: 'post',
|
||||
method: "post",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
csrfToken: await getCsrfToken(),
|
||||
callbackUrl,
|
||||
json: true
|
||||
})
|
||||
json: true,
|
||||
}),
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/signout`, fetchOptions)
|
||||
const data = await res.json()
|
||||
broadcast.post({ event: 'session', data: { trigger: 'signout' } })
|
||||
broadcast.post({ event: "session", data: { trigger: "signout" } })
|
||||
|
||||
if (redirect) {
|
||||
const url = data.url ?? callbackUrl
|
||||
window.location = url
|
||||
window.location.replace(url)
|
||||
// If url contains a hash, the browser does not reload the page. We reload manually
|
||||
if (url.includes('#')) window.location.reload()
|
||||
if (url.includes("#")) window.location.reload()
|
||||
return
|
||||
}
|
||||
|
||||
await __NEXTAUTH._getSession({ event: 'storage' })
|
||||
await __NEXTAUTH._getSession({ event: "storage" })
|
||||
|
||||
return data
|
||||
}
|
||||
@@ -252,13 +273,18 @@ export async function signOut (options = {}) {
|
||||
// Method to set options. The documented way is to use the provider, but this
|
||||
// method is being left in as an alternative, that will be helpful if/when we
|
||||
// expose a vanilla JavaScript version that doesn't depend on React.
|
||||
export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {}) {
|
||||
export function setOptions({
|
||||
baseUrl,
|
||||
basePath,
|
||||
clientMaxAge,
|
||||
keepAlive,
|
||||
} = {}) {
|
||||
if (baseUrl) __NEXTAUTH.baseUrl = baseUrl
|
||||
if (basePath) __NEXTAUTH.basePath = basePath
|
||||
if (clientMaxAge) __NEXTAUTH.clientMaxAge = clientMaxAge
|
||||
if (keepAlive) {
|
||||
__NEXTAUTH.keepAlive = keepAlive
|
||||
if (typeof window === 'undefined') return
|
||||
if (typeof window === "undefined") return
|
||||
|
||||
// Clear existing timer (if there is one)
|
||||
if (__NEXTAUTH._clientSyncTimer !== null) {
|
||||
@@ -269,12 +295,12 @@ export function setOptions ({ baseUrl, basePath, clientMaxAge, keepAlive } = {})
|
||||
__NEXTAUTH._clientSyncTimer = setTimeout(async () => {
|
||||
// Only invoke keepalive when a session exists
|
||||
if (!__NEXTAUTH._clientSession) return
|
||||
await __NEXTAUTH._getSession({ event: 'timer' })
|
||||
await __NEXTAUTH._getSession({ event: "timer" })
|
||||
}, keepAlive * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider ({ children, session, options }) {
|
||||
export function Provider({ children, session, options }) {
|
||||
setOptions(options)
|
||||
return createElement(
|
||||
SessionContext.Provider,
|
||||
@@ -290,24 +316,25 @@ export function Provider ({ children, session, options }) {
|
||||
* work seemlessly in getInitialProps() on server side
|
||||
* pages *and* in _app.js.
|
||||
*/
|
||||
async function _fetchData (path, { ctx, req = ctx?.req } = {}) {
|
||||
async function _fetchData(path, { ctx, req = ctx?.req } = {}) {
|
||||
try {
|
||||
const baseUrl = await _apiBaseUrl()
|
||||
const options = req ? { headers: { cookie: req.headers.cookie } } : {}
|
||||
const res = await fetch(`${baseUrl}/${path}`, options)
|
||||
const data = await res.json()
|
||||
if (!res.ok) throw data
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error('CLIENT_FETCH_ERROR', path, error)
|
||||
logger.error("CLIENT_FETCH_ERROR", path, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function _apiBaseUrl () {
|
||||
if (typeof window === 'undefined') {
|
||||
function _apiBaseUrl() {
|
||||
if (typeof window === "undefined") {
|
||||
// NEXTAUTH_URL should always be set explicitly to support server side calls - log warning if not set
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
// Return absolute path when called server side
|
||||
@@ -318,7 +345,7 @@ function _apiBaseUrl () {
|
||||
}
|
||||
|
||||
/** Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC. */
|
||||
function _now () {
|
||||
function _now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
@@ -328,33 +355,48 @@ function _now () {
|
||||
*
|
||||
* https://caniuse.com/?search=broadcastchannel
|
||||
*/
|
||||
function BroadcastChannel (name = 'nextauth.message') {
|
||||
function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/**
|
||||
* Get notified by other tabs/windows.
|
||||
* @param {(message: import("types/internals/client").BroadcastMessage) => void} onReceive
|
||||
*/
|
||||
receive (onReceive) {
|
||||
if (typeof window === 'undefined') return
|
||||
window.addEventListener('storage', async (event) => {
|
||||
receive(onReceive) {
|
||||
if (typeof window === "undefined") return
|
||||
window.addEventListener("storage", async (event) => {
|
||||
if (event.key !== name) return
|
||||
/** @type {import("types/internals/client").BroadcastMessage} */
|
||||
const message = JSON.parse(event.newValue)
|
||||
if (message?.event !== 'session' || !message?.data) return
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
})
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post (message) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
localStorage.setItem(name,
|
||||
post(message) {
|
||||
if (typeof localStorage === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
JSON.stringify({ ...message, timestamp: _now() })
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -374,5 +416,5 @@ export default {
|
||||
providers: getProviders,
|
||||
csrfToken: getCsrfToken,
|
||||
signin: signIn,
|
||||
signout: signOut
|
||||
signout: signOut,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
26
src/providers/workos.js
Normal file
26
src/providers/workos.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default function WorkOS(options) {
|
||||
const domain = options.domain || 'api.workos.com';
|
||||
|
||||
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://${domain}/sso/token`,
|
||||
authorizationUrl: `https://${domain}/sso/authorize?response_type=code`,
|
||||
profileUrl: `https://${domain}/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()
|
||||
|
||||
@@ -15,9 +15,9 @@ export default async function getAuthorizationUrl (req) {
|
||||
if (provider.version?.startsWith('2.')) {
|
||||
// Handle OAuth v2.x
|
||||
let url = client.getAuthorizeUrl({
|
||||
scope: provider.scope,
|
||||
...params,
|
||||
redirect_uri: provider.callbackUrl,
|
||||
scope: provider.scope
|
||||
redirect_uri: provider.callbackUrl
|
||||
})
|
||||
|
||||
// If the authorizationUrl specified in the config has query parameters on it
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default function providers (req, res) {
|
||||
export default function providers(req, res) {
|
||||
const { providers } = req.options
|
||||
|
||||
const result = providers.reduce((acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
}, {})
|
||||
const result = providers.reduce(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
@@ -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,16 +0,0 @@
|
||||
# To be able to run tests:
|
||||
# 1. copy to the root folder and rename to .env
|
||||
# 2. Populate with values
|
||||
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,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" ]
|
||||
@@ -1,52 +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: ./test/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'
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# launch setup on the background & start server
|
||||
# otherise sqlservr won't start
|
||||
/var/setup/setup.sh & /opt/mssql/bin/sqlservr
|
||||
@@ -1,13 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
mysql:
|
||||
image: mysql
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_USER: nextauth
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_DATABASE: nextauth
|
||||
MYSQL_RANDOM_ROOT_PASSWORD: 'yes'
|
||||
@@ -1,11 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
postgres:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: nextauth
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: nextauth
|
||||
141
test/fixtures/schemas/mssql.json
vendored
141
test/fixtures/schemas/mssql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "datetime",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "datetime",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/mysql.json
vendored
141
test/fixtures/schemas/mysql.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "int",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "varchar(255)",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp(6)",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
141
test/fixtures/schemas/postgres.json
vendored
141
test/fixtures/schemas/postgres.json
vendored
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"users": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"name": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"email_verified": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"image": {
|
||||
"type": "character varying",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"accounts": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"compound_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_type": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"provider_account_id": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token": {
|
||||
"type": "text",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"access_token_expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"sessions": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"user_id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"session_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"access_token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
},
|
||||
"verification_requests": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"nullable": false
|
||||
},
|
||||
"identifier": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"token": {
|
||||
"type": "character varying",
|
||||
"nullable": false
|
||||
},
|
||||
"expires": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"created_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "timestamp with time zone",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
47
test/fixtures/sql/mssql.sql
vendored
47
test/fixtures/sql/mssql.sql
vendored
@@ -1,47 +0,0 @@
|
||||
-- FIXME Missing indexes!
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
compound_id varchar(255) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
provider_type varchar(255) NOT NULL,
|
||||
provider_id varchar(255) NOT NULL,
|
||||
provider_account_id varchar(255) NOT NULL,
|
||||
refresh_token text NULL,
|
||||
access_token text NULL,
|
||||
access_token_expires datetime NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
user_id int NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
session_token varchar(255) NOT NULL,
|
||||
access_token varchar(255) NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
name varchar(255) NULL,
|
||||
email varchar(255) NULL,
|
||||
email_verified datetime NULL,
|
||||
image varchar(255) NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id int IDENTITY(1,1) NOT NULL,
|
||||
identifier varchar(255) NOT NULL,
|
||||
token varchar(255) NOT NULL,
|
||||
expires datetime NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT getdate(),
|
||||
updated_at datetime NOT NULL DEFAULT getdate()
|
||||
);
|
||||
74
test/fixtures/sql/mysql.sql
vendored
74
test/fixtures/sql/mysql.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMP(6),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMP(6),
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMP(6) NOT NULL,
|
||||
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
74
test/fixtures/sql/postgres.sql
vendored
74
test/fixtures/sql/postgres.sql
vendored
@@ -1,74 +0,0 @@
|
||||
CREATE TABLE accounts
|
||||
(
|
||||
id SERIAL,
|
||||
compound_id VARCHAR(255) NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
provider_type VARCHAR(255) NOT NULL,
|
||||
provider_id VARCHAR(255) NOT NULL,
|
||||
provider_account_id VARCHAR(255) NOT NULL,
|
||||
refresh_token TEXT,
|
||||
access_token TEXT,
|
||||
access_token_expires TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE sessions
|
||||
(
|
||||
id SERIAL,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
session_token VARCHAR(255) NOT NULL,
|
||||
access_token VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE users
|
||||
(
|
||||
id SERIAL,
|
||||
name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
email_verified TIMESTAMPTZ,
|
||||
image VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE verification_requests
|
||||
(
|
||||
id SERIAL,
|
||||
identifier VARCHAR(255) NOT NULL,
|
||||
token VARCHAR(255) NOT NULL,
|
||||
expires TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX compound_id
|
||||
ON accounts(compound_id);
|
||||
|
||||
CREATE INDEX provider_account_id
|
||||
ON accounts(provider_account_id);
|
||||
|
||||
CREATE INDEX provider_id
|
||||
ON accounts(provider_id);
|
||||
|
||||
CREATE INDEX user_id
|
||||
ON accounts(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX session_token
|
||||
ON sessions(session_token);
|
||||
|
||||
CREATE UNIQUE INDEX access_token
|
||||
ON sessions(access_token);
|
||||
|
||||
CREATE UNIQUE INDEX email
|
||||
ON users(email);
|
||||
|
||||
CREATE UNIQUE INDEX token
|
||||
ON verification_requests(token);
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GITHUB_USERNAME: USERNAME,
|
||||
NEXTAUTH_GITHUB_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
describe('GitHub (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/github"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="login"]')
|
||||
await page.click('input[name="login"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="password"]')
|
||||
await page.click('input[name="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="/session"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,81 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_GOOGLE_USERNAME: USERNAME,
|
||||
NEXTAUTH_GOOGLE_PASSWORD: PASSWORD
|
||||
} = process.env
|
||||
|
||||
// This seems to stall because of a popup that is displayed only when using
|
||||
// puppeteer. See FIXME below. Would appreciate any help resolving it.
|
||||
describe.skip('Google (OAuth 2.0 flow)', function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/google"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[type="email"]')
|
||||
await page.click('input[type="email"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// FIXME Work out how to dismiss popup
|
||||
// A popup *only* appears when using puppeteer (not manually) but I can't
|
||||
// get the xPath selectors to work to dismiss it. This is as close as I got.
|
||||
// await page.waitForXPath("(//span[contains(text(), 'Got it')])[2]")
|
||||
// const element = await page.$x("(//span[contains(text(), 'Got it')])[2]")
|
||||
// await element.click()
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[type="password"]')
|
||||
await page.click('input[type="password"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('button[type="button"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
// Note: Google OAuth appends a # to the end of the URL in Chrome
|
||||
assert.equal(page.url(), `${CALLBACK_URL}#`)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,71 +0,0 @@
|
||||
require('dotenv').config()
|
||||
const assert = require('assert')
|
||||
const { puppeteer, puppeteerOptions } = require('../lib/puppeteer')
|
||||
|
||||
const BASE_URL = 'http://localhost:3000'
|
||||
const CALLBACK_URL = `${BASE_URL}/test`
|
||||
|
||||
const {
|
||||
NEXTAUTH_TWITTER_USERNAME: USERNAME,
|
||||
NEXTAUTH_TWITTER_PASSWORD: PASSWORD,
|
||||
} = process.env
|
||||
|
||||
describe('Twitter (OAuth 1.1 flow)', async function () {
|
||||
this.slow(5000)
|
||||
this.timeout(1000 * 60)
|
||||
let browser,page
|
||||
|
||||
before(async () => {
|
||||
browser = await puppeteer.launch(puppeteerOptions)
|
||||
page = await browser.newPage()
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should show button on sign in page', async function () {
|
||||
page.setDefaultTimeout(1000 * 60)
|
||||
await page.goto(`${BASE_URL}/api/auth/signin?callbackUrl=${encodeURIComponent(CALLBACK_URL)}`)
|
||||
await page.waitForSelector(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
await page.click(`form[action="${BASE_URL}/api/auth/signin/twitter"] button`)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be redirected to provider', async function () {
|
||||
// Enter username
|
||||
await page.waitForSelector('input[name="session[username_or_email]"]')
|
||||
await page.click('input[name="session[username_or_email]"]')
|
||||
await page.keyboard.type(USERNAME)
|
||||
|
||||
// Enter password
|
||||
await page.waitForSelector('input[name="session[password]"]')
|
||||
await page.click('input[name="session[password]"]')
|
||||
await page.keyboard.type(PASSWORD)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be able to sign in with provider', async function () {
|
||||
// Click submit
|
||||
await page.click('form[action="https://api.twitter.com/oauth/authenticate"] [type="submit"]')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be returned to callback URL', async function () {
|
||||
// Wait for page to return to callback URL
|
||||
await page.waitForSelector('#nextauth-test-page')
|
||||
|
||||
// Check we are at the correct callback URL
|
||||
assert.equal(page.url(), CALLBACK_URL)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
it('should be signed in', async function () {
|
||||
// Check we are signed in
|
||||
await page.waitForSelector('#nextauth-signed-in')
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
await browser.close()
|
||||
return Promise.resolve()
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
exports.compareSchemas = (expected, actual) => {
|
||||
const errors = []
|
||||
|
||||
// Check all models and properties that are expected exist
|
||||
for (const objectName in expected) {
|
||||
if (actual[objectName]) {
|
||||
for (const propertyName in expected[objectName]) {
|
||||
if (actual[objectName][propertyName]) {
|
||||
if (JSON.stringify(expected[objectName][propertyName]) !== JSON.stringify(actual[objectName][propertyName])) {
|
||||
errors.push(`${objectName}.${propertyName} does not match expected result`)
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName}.${propertyName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} not found (should exist)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for models and properties that exist but are not expected
|
||||
for (const objectName in actual) {
|
||||
if (expected[objectName]) {
|
||||
for (const propertyName in actual[objectName]) {
|
||||
if (!expected[objectName][propertyName]) {
|
||||
errors.push(`${objectName}.${propertyName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors.push(`${objectName} found (not expected)`)
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if no errors, else return array of errors
|
||||
return errors.length > 0 ? errors : true
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
const puppeteerExtra = require('puppeteer-extra')
|
||||
const StealthPlugin = require("puppeteer-extra-plugin-stealth")
|
||||
const stealthPlugin = StealthPlugin()
|
||||
|
||||
// Override Puppeteer user agent to set 'navigator.platform' explicitly to
|
||||
// prevent detection on some providers (e.g. GitHub OAuth) as they force 2FA
|
||||
// on sign in if they detect sign in from a platform they haven't seen before.
|
||||
const puppeteerExtraPluginUserAgentOverride = require("puppeteer-extra-plugin-stealth/evasions/user-agent-override")
|
||||
stealthPlugin.enabledEvasions.delete("user-agent-override")
|
||||
puppeteerExtra.use(stealthPlugin)
|
||||
const pluginUserAgentOverride = puppeteerExtraPluginUserAgentOverride({
|
||||
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36",
|
||||
platform: "MacIntel"
|
||||
})
|
||||
puppeteerExtra.use(pluginUserAgentOverride)
|
||||
|
||||
// CI is set to true by GitHub Actions to indicate is running in CD/CI
|
||||
const { CI } = process.env
|
||||
|
||||
const puppeteerOptions = {
|
||||
headless: true // Set to 'false' to debug more easily
|
||||
}
|
||||
|
||||
// When running on remote test runner (which is ARM) the executable path
|
||||
// needs to be set to 'chromium-browser' so it uses the ARM build of Chromium
|
||||
// not the x86 build that Puppeteer uses by default. Supporting this allows us
|
||||
// to test easily from remote locations that are outside cloud networks like
|
||||
// AWS, GPC, Azure, etc. and avoids tests being thwarted by IP blocklists.
|
||||
if (CI)
|
||||
puppeteerOptions.executablePath = 'chromium-browser'
|
||||
|
||||
module.exports = {
|
||||
puppeteer: puppeteerExtra,
|
||||
puppeteerOptions
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
// We can't connection a local MongoDB SRV instance but we can at least see if the URLs cause an error
|
||||
Adapters.Default('mongodb+srv://nextauth:password@127.0.0.1/nextauth?ssl=false&retryWrites=true')
|
||||
|
||||
// Connect to local MongoDB instance
|
||||
// Note: MongoDB doesn't thrown a connection error right away if is a
|
||||
// problem with the credentials or host configuration, but after a few
|
||||
// seconds it throws a Timeout error (which is caught by the adapter).
|
||||
const adapter = Adapters.Default('mongodb://nextauth:password@127.0.0.1:27017/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
// @TODO create objects in database, check format of objects returned
|
||||
|
||||
console.log('MongoDB loaded ok')
|
||||
process.exit()
|
||||
} catch (error) {
|
||||
console.error('MongoDB error', error)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
106
test/mssql.js
106
test/mssql.js
@@ -1,106 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const mssql = require('mssql');
|
||||
const Adapters = require('../adapters');
|
||||
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mssql.json');
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE));
|
||||
const TABLES = Object.keys(expectedSchema);
|
||||
const databaseUrl = `mssql://nextauth:password@127.0.0.1:1433/nextauth?synchronize=true`;
|
||||
|
||||
function printSchema() {
|
||||
return new Promise(async (resolve) => {
|
||||
/**
|
||||
* @type {import('mssql').ConnectionPool}
|
||||
*/
|
||||
let connection;
|
||||
try {
|
||||
connection = await mssql.connect(databaseUrl);
|
||||
// Invoke adapter to sync schema
|
||||
await (Adapters.Default(databaseUrl)).getAdapter();
|
||||
// query schema
|
||||
const { recordset } = await connection.query(
|
||||
`use [nextauth]; ` +
|
||||
TABLES.map(
|
||||
(table) =>
|
||||
`select * from INFORMATION_SCHEMA.COLUMNS` +
|
||||
` where TABLE_NAME = '${table}'`
|
||||
).join(' UNION ALL ')
|
||||
);
|
||||
// build result
|
||||
return resolve(
|
||||
TABLES.reduce(
|
||||
(out, next) => ({
|
||||
...out,
|
||||
[next]: collect(recordset, next),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
} finally {
|
||||
if (connection) {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const assert = require('assert');
|
||||
/** RUN */
|
||||
(async () => {
|
||||
try {
|
||||
const testResultSchema = await printSchema();
|
||||
const actualTables = Object.keys(testResultSchema);
|
||||
assert.equal(
|
||||
TABLES,
|
||||
actualTables.join(),
|
||||
`MSSQL Schema: Expected tables [${TABLES.join()}]\n to be [${actualTables.join()}]`
|
||||
);
|
||||
//cheap deepEquals, with hints
|
||||
for (const tableName of TABLES) {
|
||||
const newLocal = expectedSchema[tableName];
|
||||
for (const columnName of Object.keys(newLocal)) {
|
||||
const expected = expectedSchema[tableName][columnName];
|
||||
const actual = testResultSchema[tableName][columnName];
|
||||
for (const propKey of Object.keys(expected)) {
|
||||
assert.equal(
|
||||
expected[propKey],
|
||||
actual[propKey],
|
||||
`Expected ${tableName}.${columnName}.${propKey}=${actual[propKey]}` +
|
||||
` to be ${expected[propKey]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('mssql: schema ok');
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
})()
|
||||
.then(() => process.exit())
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(-1);
|
||||
});
|
||||
/** collect results */
|
||||
const collect = (records, tableName) => {
|
||||
const keys = Object.keys(expectedSchema[tableName]);
|
||||
const ret = records
|
||||
.filter((x) => x.TABLE_NAME === tableName)
|
||||
.reduce((out, x) => {
|
||||
if (keys.indexOf(x.COLUMN_NAME) === -1) return out; //map only required columns
|
||||
const nullable = x.IS_NULLABLE === 'YES';
|
||||
return {
|
||||
...out,
|
||||
[x.COLUMN_NAME]: {
|
||||
nullable,
|
||||
type: x.DATA_TYPE,
|
||||
default: (nullable && x.COLUMN_DEFAULT) || undefined,
|
||||
},
|
||||
};
|
||||
}, {});
|
||||
return ret;
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mysql = require('mysql')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/mysql.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('mysql://nextauth:password@127.0.0.1:3306/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = mysql.createConnection({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 3306,
|
||||
multipleStatements: true
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `DESCRIBE ${table}`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.Null === 'YES' ? true : false
|
||||
return {
|
||||
type: column.Type,
|
||||
nullable,
|
||||
default: nullable ? column.Default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].forEach(column => { users[column.Field] = getColumnSchema(column) })
|
||||
result[1].forEach(column => { accounts[column.Field] = getColumnSchema(column) })
|
||||
result[2].forEach(column => { sessions[column.Field] = getColumnSchema(column) })
|
||||
result[3].forEach(column => { verification_requests[column.Field] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('MySQL schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('MySQL schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('MySQL schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
@@ -1,73 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// Placeholder for schema test (will use test framework, this is temporary)
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { Client } = require('pg')
|
||||
|
||||
const { compareSchemas } = require('./lib/db')
|
||||
const Adapters = require('../adapters')
|
||||
|
||||
const TABLES = ['users', 'accounts', 'sessions', 'verification_requests']
|
||||
const SCHEMA_FILE = path.join(__dirname, '/fixtures/schemas/postgres.json')
|
||||
|
||||
function printSchema () {
|
||||
return new Promise(async (resolve) => {
|
||||
// Invoke adapter to sync schema
|
||||
const adapter = Adapters.Default('postgres://nextauth:password@127.0.0.1:5432/nextauth?synchronize=true')
|
||||
await adapter.getAdapter()
|
||||
|
||||
const connection = new Client({
|
||||
host: '127.0.0.1',
|
||||
user: 'nextauth',
|
||||
password: 'password',
|
||||
database: 'nextauth',
|
||||
port: 5432
|
||||
})
|
||||
|
||||
connection.connect()
|
||||
connection.query(
|
||||
TABLES.map(table => `SELECT column_name, data_type, character_maximum_length, is_nullable, column_default FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = '${table}' ORDER BY ordinal_position`).join(';'),
|
||||
(error, result) => {
|
||||
if (error) { throw error }
|
||||
|
||||
const getColumnSchema = (column) => {
|
||||
const nullable = column.is_nullable === 'YES' ? true : false
|
||||
return {
|
||||
type: column.data_type,
|
||||
nullable,
|
||||
default: nullable ? column.column_default : undefined
|
||||
}
|
||||
}
|
||||
|
||||
const users = {}
|
||||
const accounts = {}
|
||||
const sessions = {}
|
||||
const verification_requests = {}
|
||||
|
||||
result[0].rows.forEach(column => { users[column.column_name] = getColumnSchema(column) })
|
||||
result[1].rows.forEach(column => { accounts[column.column_name] = getColumnSchema(column) })
|
||||
result[2].rows.forEach(column => { sessions[column.column_name] = getColumnSchema(column) })
|
||||
result[3].rows.forEach(column => { verification_requests[column.column_name] = getColumnSchema(column) })
|
||||
|
||||
connection.end()
|
||||
|
||||
resolve({ users, accounts, sessions, verification_requests })
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const expectedSchema = JSON.parse(fs.readFileSync(SCHEMA_FILE))
|
||||
const testResultSchema = await printSchema()
|
||||
const compareResult = compareSchemas(expectedSchema, testResultSchema)
|
||||
if (compareResult === true) {
|
||||
console.log('Postgres schema ok')
|
||||
process.exit()
|
||||
} else {
|
||||
console.error('Postgres schema errors')
|
||||
compareResult.forEach(error => console.log(` * ${error}`))
|
||||
console.log('Postgres schema found:', JSON.stringify(testResultSchema, null, 2))
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
@@ -3,31 +3,15 @@
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"types": [
|
||||
"./types"
|
||||
],
|
||||
"next-auth": [
|
||||
"./src/server"
|
||||
],
|
||||
"next-auth/adapters": [
|
||||
"./src/adapters"
|
||||
],
|
||||
"next-auth/client": [
|
||||
"./src/client"
|
||||
],
|
||||
"next-auth/jwt": [
|
||||
"./src/lib/jwt"
|
||||
],
|
||||
"next-auth/providers": [
|
||||
"./src/providers"
|
||||
]
|
||||
"types": ["./types"],
|
||||
"next-auth": ["./src/server"],
|
||||
"next-auth/adapters": ["./src/adapters"],
|
||||
"next-auth/client": ["./src/client"],
|
||||
"next-auth/jwt": ["./src/lib/jwt"],
|
||||
"next-auth/providers": ["./src/providers"]
|
||||
},
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
@@ -44,9 +28,8 @@
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.js"
|
||||
"**/*.js",
|
||||
".eslintrc.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
365
types/adapters.d.ts
vendored
365
types/adapters.d.ts
vendored
@@ -1,245 +1,156 @@
|
||||
import { AppOptions } from "./internals"
|
||||
import { ConnectionOptions, EntitySchema } from "typeorm"
|
||||
import { User } from "."
|
||||
import { AppProvider } from "./providers"
|
||||
import { User, Profile, Session } from "."
|
||||
import { EmailConfig } from "./providers"
|
||||
|
||||
export interface Profile {
|
||||
id: string
|
||||
name: string
|
||||
email: string | null
|
||||
image?: string | null
|
||||
/** Legacy */
|
||||
|
||||
export {
|
||||
TypeORMAccountModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import {
|
||||
TypeORMAdapter,
|
||||
TypeORMAdapterModels,
|
||||
} from "@next-auth/typeorm-legacy-adapter"
|
||||
|
||||
import { PrismaLegacyAdapter } from "@next-auth/prisma-legacy-adapter"
|
||||
|
||||
export const TypeORM: {
|
||||
Models: TypeORMAdapterModels
|
||||
Adapter: TypeORMAdapter
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
userId: string | number | object
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
export const Prisma: {
|
||||
Adapter: PrismaLegacyAdapter
|
||||
}
|
||||
|
||||
export interface VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
}
|
||||
|
||||
export interface SendVerificationRequestParams {
|
||||
identifier: string
|
||||
url: string
|
||||
token: string
|
||||
baseUrl: string
|
||||
provider: AppProvider
|
||||
}
|
||||
|
||||
export type EmailAppProvider = AppProvider & {
|
||||
sendVerificationRequest: (
|
||||
params: SendVerificationRequestParams
|
||||
) => Promise<void>
|
||||
maxAge: number | undefined
|
||||
}
|
||||
|
||||
export interface AdapterInstance<
|
||||
TUser,
|
||||
TProfile,
|
||||
TSession,
|
||||
TVerificationRequest
|
||||
> {
|
||||
createUser: (profile: TProfile) => Promise<TUser>
|
||||
getUser: (id: string) => Promise<TUser | null>
|
||||
getUserByEmail: (email: string) => Promise<TUser | null>
|
||||
getUserByProviderAccountId: (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => Promise<TUser | null>
|
||||
updateUser: (user: TUser) => Promise<TUser>
|
||||
linkAccount: (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => Promise<void>
|
||||
createSession: (user: TUser) => Promise<TSession>
|
||||
getSession: (sessionToken: string) => Promise<TSession | null>
|
||||
updateSession: (session: TSession, force?: boolean) => Promise<TSession>
|
||||
deleteSession: (sessionToken: string) => Promise<void>
|
||||
createVerificationRequest?: (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => Promise<TVerificationRequest>
|
||||
getVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<TVerificationRequest | null>
|
||||
deleteVerificationRequest?: (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
interface Adapter<
|
||||
TUser extends User = any,
|
||||
TProfile extends Profile = any,
|
||||
TSession extends Session = any,
|
||||
TVerificationRequest extends VerificationRequest = any
|
||||
> {
|
||||
getAdapter: (
|
||||
appOptions: AppOptions
|
||||
) => Promise<AdapterInstance<TUser, TProfile, TSession, TVerificationRequest>>
|
||||
}
|
||||
|
||||
type Schema<T = any> = EntitySchema<T>["options"]
|
||||
|
||||
interface BuiltInAdapters {
|
||||
Default: TypeORMAdapter["Adapter"]
|
||||
TypeORM: TypeORMAdapter
|
||||
Prisma: PrismaAdapter
|
||||
declare const Adapters: {
|
||||
Default: TypeORMAdapter
|
||||
TypeORM: typeof TypeORM
|
||||
Prisma: typeof Prisma
|
||||
}
|
||||
export default Adapters
|
||||
|
||||
/**
|
||||
* TODO: fix auto-type schema
|
||||
* Using a custom adapter you can connect to any database backend or even several different databases.
|
||||
* Custom adapters created and maintained by our community can be found in the adapters repository.
|
||||
* Feel free to add a custom adapter from your project to the repository,
|
||||
* or even become a maintainer of a certain adapter.
|
||||
* Custom adapters can still be created and used in a project without being added to the repository.
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
|
||||
interface TypeORMAdapter<
|
||||
A extends TypeORMAccountModel = any,
|
||||
U extends TypeORMUserModel = any,
|
||||
S extends TypeORMSessionModel = any,
|
||||
VR extends TypeORMVerificationRequestModel = any
|
||||
> {
|
||||
Adapter: (
|
||||
typeOrmConfig: ConnectionOptions,
|
||||
options?: {
|
||||
models?: {
|
||||
Account?: {
|
||||
model: A
|
||||
schema: Schema<A>
|
||||
}
|
||||
User?: {
|
||||
model: U
|
||||
schema: Schema<U>
|
||||
}
|
||||
Session?: {
|
||||
model: S
|
||||
schema: Schema<S>
|
||||
}
|
||||
VerificationRequest?: {
|
||||
model: VR
|
||||
schema: Schema<VR>
|
||||
}
|
||||
}
|
||||
}
|
||||
) => Adapter<U, Profile, S, VR>
|
||||
Models: {
|
||||
Account: {
|
||||
model: TypeORMAccountModel
|
||||
schema: Schema<TypeORMAccountModel>
|
||||
}
|
||||
User: {
|
||||
model: TypeORMUserModel
|
||||
schema: Schema<TypeORMUserModel>
|
||||
}
|
||||
Session: {
|
||||
model: TypeORMSessionModel
|
||||
schema: Schema<TypeORMSessionModel>
|
||||
}
|
||||
VerificationRequest: {
|
||||
model: TypeORMVerificationRequestModel
|
||||
schema: Schema<TypeORMVerificationRequestModel>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface PrismaAdapter {
|
||||
Adapter: (config: {
|
||||
prisma: any
|
||||
modelMapping?: {
|
||||
User: string
|
||||
Account: string
|
||||
Session: string
|
||||
VerificationRequest: string
|
||||
}
|
||||
}) => Adapter
|
||||
}
|
||||
|
||||
declare class TypeORMAccountModel {
|
||||
compoundId: string
|
||||
userId: number
|
||||
providerType: string
|
||||
providerId: string
|
||||
providerAccountId: string
|
||||
refreshToken?: string
|
||||
accessToken?: string
|
||||
accessTokenExpires?: Date
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
export interface AdapterInstance<U = User, P = Profile, S = Session> {
|
||||
/** Used as a prefix for adapter related log messages. (Defaults to `ADAPTER_`) */
|
||||
displayName?: string
|
||||
createUser(profile: P): Promise<U>
|
||||
getUser(id: string): Promise<U | null>
|
||||
getUserByEmail(email: string | null): Promise<U | null>
|
||||
getUserByProviderAccountId(
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<U | null>
|
||||
updateUser(user: U): Promise<U>
|
||||
/** @todo Implement */
|
||||
deleteUser?(userId: string): Promise<void>
|
||||
linkAccount(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken?: string,
|
||||
accessToken?: string,
|
||||
accessTokenExpires?: Date
|
||||
)
|
||||
accessTokenExpires?: null
|
||||
): Promise<void>
|
||||
/** @todo Implement */
|
||||
unlinkAccount?(
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
): Promise<void>
|
||||
createSession(user: U): Promise<S>
|
||||
getSession(sessionToken: string): Promise<S | null>
|
||||
updateSession(session: S, force?: boolean): Promise<S | null>
|
||||
deleteSession(sessionToken: string): Promise<void>
|
||||
createVerificationRequest?(
|
||||
identifier: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailConfig & { maxAge: number; from: string }
|
||||
): Promise<void>
|
||||
getVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<{
|
||||
id: string
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
} | null>
|
||||
deleteVerificationRequest?(
|
||||
identifier: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: Required<EmailConfig>
|
||||
): Promise<void>
|
||||
}
|
||||
|
||||
declare class TypeORMUserModel implements User {
|
||||
name?: string
|
||||
email?: string
|
||||
image?: string
|
||||
emailVerified?: Date
|
||||
|
||||
constructor(
|
||||
name?: string,
|
||||
email?: string,
|
||||
image?: string,
|
||||
emailVerified?: Date
|
||||
)
|
||||
[x: string]: unknown
|
||||
}
|
||||
|
||||
declare class TypeORMSessionModel implements Session {
|
||||
userId: number
|
||||
expires: Date
|
||||
sessionToken: string
|
||||
accessToken: string
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
expires: Date,
|
||||
sessionToken?: string,
|
||||
accessToken?: string
|
||||
)
|
||||
}
|
||||
|
||||
declare class TypeORMVerificationRequestModel implements VerificationRequest {
|
||||
identifier: string
|
||||
token: string
|
||||
expires: Date
|
||||
|
||||
constructor(identifier: string, token: string, expires: Date)
|
||||
}
|
||||
|
||||
declare const Adapters: BuiltInAdapters
|
||||
|
||||
export default Adapters
|
||||
|
||||
export {
|
||||
Adapter,
|
||||
BuiltInAdapters as Adapters,
|
||||
TypeORMAdapter,
|
||||
TypeORMAccountModel,
|
||||
TypeORMUserModel,
|
||||
TypeORMSessionModel,
|
||||
TypeORMVerificationRequestModel,
|
||||
PrismaAdapter,
|
||||
/**
|
||||
* From an implementation perspective, an adapter in NextAuth.js is a function
|
||||
* which returns an async `getAdapter()` method, which in turn returns a list of functions
|
||||
* used to handle operations such as creating user, linking a user
|
||||
* and an OAuth account or handling reading and writing sessions.
|
||||
*
|
||||
* It uses this approach to allow database connection logic to live in the `getAdapter()` method.
|
||||
* By calling the function just before an action needs to happen,
|
||||
* it is possible to check database connection status and handle connecting / reconnecting
|
||||
* to a database as required.
|
||||
*
|
||||
* **Required methods**
|
||||
*
|
||||
* _(These methods are required for all sign in flows)_
|
||||
* - `createUser`
|
||||
* - `getUser`
|
||||
* - `getUserByEmail`
|
||||
* - `getUserByProviderAccountId`
|
||||
* - `linkAccount`
|
||||
* - `createSession`
|
||||
* - `getSession`
|
||||
* - `updateSession`
|
||||
* - `deleteSession`
|
||||
* - `updateUser`
|
||||
*
|
||||
* _(Required to support email / passwordless sign in)_
|
||||
*
|
||||
* - `createVerificationRequest`
|
||||
* - `getVerificationRequest`
|
||||
* - `deleteVerificationRequest`
|
||||
*
|
||||
* **Unimplemented methods**
|
||||
*
|
||||
* _(These methods will be required in a future release, but are not yet invoked)_
|
||||
* - `deleteUser`
|
||||
* - `unlinkAccount`
|
||||
*
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters) |
|
||||
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
|
||||
*/
|
||||
export type Adapter<
|
||||
C = unknown,
|
||||
O = Record<string, unknown>,
|
||||
U = unknown,
|
||||
P = unknown,
|
||||
S = unknown
|
||||
> = (
|
||||
client: C,
|
||||
options?: O
|
||||
) => {
|
||||
getAdapter(appOptions: AppOptions): Promise<AdapterInstance<U, P, S>>
|
||||
}
|
||||
|
||||
83
types/index.d.ts
vendored
83
types/index.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// Minimum TypeScript Version: 3.5
|
||||
// Minimum TypeScript Version: 3.6
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
@@ -111,7 +111,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#events) | [Events documentation](https://next-auth.js.org/configuration/events)
|
||||
*/
|
||||
events?: EventsOptions
|
||||
events?: Partial<JWTEventCallbacks | SessionEventCallbacks>
|
||||
/**
|
||||
* By default NextAuth.js uses a database adapter that uses TypeORM and supports MySQL, MariaDB, Postgres and MongoDB and SQLite databases.
|
||||
* An alternative adapter that uses Prisma, which currently supports MySQL, MariaDB and Postgres, is also included.
|
||||
@@ -127,7 +127,7 @@ export interface NextAuthOptions {
|
||||
* [Default adapter](https://next-auth.js.org/schemas/adapters#typeorm-adapter) |
|
||||
* [Community adapters](https://github.com/nextauthjs/adapters)
|
||||
*/
|
||||
adapter?: Adapter
|
||||
adapter?: ReturnType<Adapter>
|
||||
/**
|
||||
* Set debug to true to enable debug messages for authentication and database operations.
|
||||
* * **Default value**: `false`
|
||||
@@ -180,7 +180,7 @@ export interface NextAuthOptions {
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#theme) | [Pages documentation]("https://next-auth.js.org/configuration/pages")
|
||||
*/
|
||||
theme?: "auto" | "dark" | "light"
|
||||
theme?: Theme
|
||||
/**
|
||||
* When set to `true` then all cookies set by NextAuth.js will only be accessible from HTTPS URLs.
|
||||
* This option defaults to `false` on URLs that start with `http://` (e.g. http://localhost:3000) for developer convenience.
|
||||
@@ -215,6 +215,14 @@ export interface NextAuthOptions {
|
||||
cookies?: CookiesOptions
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the theme of the built-in pages.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#theme) |
|
||||
* [Pages](https://next-auth.js.org/configuration/pages)
|
||||
*/
|
||||
export type Theme = "auto" | "dark" | "light"
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
*
|
||||
@@ -233,6 +241,8 @@ export interface LoggerInstance {
|
||||
*/
|
||||
export interface TokenSet {
|
||||
accessToken: string
|
||||
/** Kept for historical reasons, check out `expires_in` */
|
||||
accessTokenExpires: null
|
||||
idToken?: string
|
||||
refreshToken?: string
|
||||
access_token: string
|
||||
@@ -340,20 +350,61 @@ export interface CookiesOptions {
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventType =
|
||||
| "signIn"
|
||||
| "signOut"
|
||||
| "createUser"
|
||||
| "updateUser"
|
||||
| "linkAccount"
|
||||
| "session"
|
||||
| "error"
|
||||
export type EventCallback<MessageType = unknown> = (
|
||||
message: MessageType
|
||||
) => Promise<void>
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventCallback = (message: any) => Promise<void>
|
||||
/**
|
||||
* If using a `credentials` type auth, the user is the raw response from your
|
||||
* credential provider.
|
||||
* For other providers, you'll get the User object from your adapter, the account,
|
||||
* and an indicator if the user was new to your Adapter.
|
||||
*/
|
||||
export interface SignInEventMessage {
|
||||
user: User
|
||||
account: Account
|
||||
isNewUser?: boolean
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/events) */
|
||||
export type EventsOptions = Partial<Record<EventType, EventCallback>>
|
||||
export interface LinkAccountEventMessage {
|
||||
user: User
|
||||
providerAccount: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The various event callbacks you can register for from next-auth
|
||||
*/
|
||||
export interface CommonEventCallbacks {
|
||||
signIn: EventCallback<SignInEventMessage>
|
||||
createUser: EventCallback<User>
|
||||
updateUser: EventCallback<User>
|
||||
linkAccount: EventCallback<LinkAccountEventMessage>
|
||||
error: EventCallback
|
||||
}
|
||||
/**
|
||||
* The event callbacks will take this form if you are using JWTs:
|
||||
* signOut will receive the JWT and session will receive the session and JWT.
|
||||
*/
|
||||
export interface JWTEventCallbacks extends CommonEventCallbacks {
|
||||
signOut: EventCallback<JWT>
|
||||
session: EventCallback<{
|
||||
session: Session
|
||||
jwt: JWT
|
||||
}>
|
||||
}
|
||||
/**
|
||||
* The event callbacks will take this form if you are using Sessions
|
||||
* and not using JWTs:
|
||||
* signOut will receive the underlying DB adapter's session object, and session
|
||||
* will receive the NextAuth client session with extra data.
|
||||
*/
|
||||
export interface SessionEventCallbacks extends CommonEventCallbacks {
|
||||
signOut: EventCallback<Session | null>
|
||||
session: EventCallback<{ session: Session }>
|
||||
}
|
||||
export type EventCallbacks = JWTEventCallbacks | SessionEventCallbacks
|
||||
|
||||
export type EventType = keyof EventCallbacks
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/pages) */
|
||||
export interface PagesOptions {
|
||||
|
||||
12
types/internals/index.d.ts
vendored
12
types/internals/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { NextApiRequest, NextApiResponse } from "./utils"
|
||||
import { NextAuthOptions } from ".."
|
||||
import { LoggerInstance, NextAuthOptions, SessionOptions, Theme } from ".."
|
||||
import { AppProvider } from "../providers"
|
||||
|
||||
/** Options that are the same both in internal and user provided options. */
|
||||
@@ -9,12 +9,7 @@ export type NextAuthSharedOptions =
|
||||
| "events"
|
||||
| "callbacks"
|
||||
| "cookies"
|
||||
| "secret"
|
||||
| "adapter"
|
||||
| "theme"
|
||||
| "debug"
|
||||
| "logger"
|
||||
| "session"
|
||||
|
||||
export interface AppOptions
|
||||
extends Required<Pick<NextAuthOptions, NextAuthSharedOptions>> {
|
||||
@@ -42,6 +37,11 @@ export interface AppOptions
|
||||
provider?: AppProvider
|
||||
csrfToken?: string
|
||||
csrfTokenVerified?: boolean
|
||||
secret: string
|
||||
theme: Theme
|
||||
debug: boolean
|
||||
logger: LoggerInstance
|
||||
session: Required<SessionOptions>
|
||||
}
|
||||
|
||||
export interface NextAuthRequest extends NextApiRequest {
|
||||
|
||||
31
types/providers.d.ts
vendored
31
types/providers.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import { Profile, TokenSet, User } from "."
|
||||
import { Awaitable } from "./internals/utils"
|
||||
import { Awaitable, NextApiRequest } from "./internals/utils"
|
||||
|
||||
export type ProviderType = "oauth" | "email" | "credentials"
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface OAuthConfig<P extends Record<string, unknown> = Profile>
|
||||
scope: string
|
||||
params: { grant_type: string }
|
||||
accessTokenUrl: string
|
||||
requestTokenUrl: string
|
||||
requestTokenUrl?: string
|
||||
authorizationUrl: string
|
||||
profileUrl: string
|
||||
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
|
||||
@@ -64,9 +64,11 @@ export type OAuthProviderType =
|
||||
| "Bungie"
|
||||
| "Cognito"
|
||||
| "Discord"
|
||||
| "Dropbox"
|
||||
| "EVEOnline"
|
||||
| "Facebook"
|
||||
| "FACEIT"
|
||||
| "FortyTwo"
|
||||
| "Foursquare"
|
||||
| "FusionAuth"
|
||||
| "GitHub"
|
||||
@@ -92,6 +94,7 @@ export type OAuthProviderType =
|
||||
| "Twitter"
|
||||
| "VK"
|
||||
| "WordPress"
|
||||
| "WorkOS"
|
||||
| "Yandex"
|
||||
| "Zoho"
|
||||
|
||||
@@ -112,7 +115,7 @@ interface CredentialsConfig<C extends Record<string, CredentialInput> = {}>
|
||||
extends CommonProviderOptions {
|
||||
type: "credentials"
|
||||
credentials: C
|
||||
authorize(credentials: Record<keyof C, string>): Awaitable<User | null>
|
||||
authorize(credentials: Record<keyof C, string>, req: NextApiRequest): Awaitable<User | null>
|
||||
}
|
||||
|
||||
export type CredentialsProvider = (
|
||||
@@ -132,19 +135,27 @@ export interface EmailConfigServerOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export type SendVerificationRequest = (params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}) => Awaitable<void>
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
|
||||
server: string | EmailConfigServerOptions
|
||||
/** @default "NextAuth <no-reply@example.com>" */
|
||||
from?: string
|
||||
/**
|
||||
* How long until the e-mail can be used to log the user in,
|
||||
* in seconds. Defaults to 1 day
|
||||
* @default 86400
|
||||
*/
|
||||
maxAge?: number
|
||||
sendVerificationRequest(params: {
|
||||
identifier: string
|
||||
url: string
|
||||
baseUrl: string
|
||||
token: string
|
||||
provider: EmailConfig
|
||||
}): Awaitable<void>
|
||||
sendVerificationRequest: SendVerificationRequest
|
||||
}
|
||||
|
||||
export type EmailProvider = (options: Partial<EmailConfig>) => EmailConfig
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import Providers, { AppProvider, OAuthConfig } from "next-auth/providers"
|
||||
import {
|
||||
Adapter,
|
||||
EmailAppProvider,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest,
|
||||
} from "next-auth/adapters"
|
||||
import Providers, { OAuthConfig } from "next-auth/providers"
|
||||
import { Adapter } from "next-auth/adapters"
|
||||
import NextAuth, * as NextAuthTypes from "next-auth"
|
||||
import { IncomingMessage, ServerResponse } from "http"
|
||||
import * as JWTType from "next-auth/jwt"
|
||||
import { Socket } from "net"
|
||||
import { NextApiRequest, NextApiResponse } from "internals/utils"
|
||||
import { AppOptions } from "internals"
|
||||
@@ -54,74 +47,88 @@ const exampleUser: NextAuthTypes.User = {
|
||||
email: "",
|
||||
}
|
||||
|
||||
const exampleSession: Session = {
|
||||
const exampleSession: NextAuthTypes.Session = {
|
||||
userId: "",
|
||||
accessToken: "",
|
||||
sessionToken: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const exampleVerificatoinRequest: VerificationRequest = {
|
||||
const exampleVerificationRequest = {
|
||||
id: "",
|
||||
identifier: "",
|
||||
token: "",
|
||||
expires: new Date(),
|
||||
}
|
||||
|
||||
const adapter: Adapter<
|
||||
NextAuthTypes.User,
|
||||
Profile,
|
||||
Session,
|
||||
VerificationRequest
|
||||
> = {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
createUser: async (profile: Profile) => exampleUser,
|
||||
getUser: async (id: string) => exampleUser,
|
||||
getUserByEmail: async (email: string) => exampleUser,
|
||||
getUserByProviderAccountId: async (
|
||||
providerId: string,
|
||||
providerAccountId: string
|
||||
) => exampleUser,
|
||||
updateUser: async (user: NextAuthTypes.User) => exampleUser,
|
||||
linkAccount: async (
|
||||
userId: string,
|
||||
providerId: string,
|
||||
providerType: string,
|
||||
providerAccountId: string,
|
||||
refreshToken: string,
|
||||
accessToken: string,
|
||||
accessTokenExpires: number
|
||||
) => undefined,
|
||||
createSession: async (user: NextAuthTypes.User) => exampleSession,
|
||||
getSession: async (sessionToken: string) => exampleSession,
|
||||
updateSession: async (session: Session, force?: boolean) =>
|
||||
exampleSession,
|
||||
deleteSession: async (sessionToken: string) => undefined,
|
||||
createVerificationRequest: async (
|
||||
email: string,
|
||||
url: string,
|
||||
token: string,
|
||||
secret: string,
|
||||
provider: EmailAppProvider,
|
||||
options: AppOptions
|
||||
) => exampleVerificatoinRequest,
|
||||
getVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => exampleVerificatoinRequest,
|
||||
deleteVerificationRequest: async (
|
||||
email: string,
|
||||
verificationToken: string,
|
||||
secret: string,
|
||||
provider: AppProvider
|
||||
) => undefined,
|
||||
}
|
||||
},
|
||||
const MyAdapter: Adapter<Record<string, unknown>> = () => {
|
||||
return {
|
||||
async getAdapter(appOptions: AppOptions) {
|
||||
return {
|
||||
async createUser(profile) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUser(id) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByEmail(email) {
|
||||
return exampleUser
|
||||
},
|
||||
async getUserByProviderAccountId(providerId, providerAccountId) {
|
||||
return exampleUser
|
||||
},
|
||||
async updateUser(user) {
|
||||
return exampleUser
|
||||
},
|
||||
async linkAccount(
|
||||
userId,
|
||||
providerId,
|
||||
providerType,
|
||||
providerAccountId,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
accessTokenExpires
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
async createSession(user) {
|
||||
return exampleSession
|
||||
},
|
||||
async getSession(sessionToken) {
|
||||
return exampleSession
|
||||
},
|
||||
async updateSession(session, force) {
|
||||
return exampleSession
|
||||
},
|
||||
async deleteSession(sessionToken) {
|
||||
return undefined
|
||||
},
|
||||
async createVerificationRequest(email, url, token, secret, provider) {
|
||||
return undefined
|
||||
},
|
||||
async getVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return exampleVerificationRequest
|
||||
},
|
||||
async deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
) {
|
||||
return undefined
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const allConfig = {
|
||||
const client = {} // Create a fake db client
|
||||
|
||||
const allConfig: NextAuthTypes.NextAuthOptions = {
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: "123",
|
||||
@@ -147,53 +154,43 @@ const allConfig = {
|
||||
},
|
||||
pages: pageOptions,
|
||||
callbacks: {
|
||||
async signIn(
|
||||
user: NextAuthTypes.User,
|
||||
account: Record<string, unknown>,
|
||||
profile: Record<string, unknown>
|
||||
) {
|
||||
async signIn(user, account, profile) {
|
||||
return true
|
||||
},
|
||||
async redirect(url: string, baseUrl: string) {
|
||||
async redirect(url, baseUrl) {
|
||||
return "path/to/foo"
|
||||
},
|
||||
async session(
|
||||
session: NextAuthTypes.Session,
|
||||
userOrToken: NextAuthTypes.User
|
||||
) {
|
||||
async session(session, userOrToken) {
|
||||
return { ...session }
|
||||
},
|
||||
async jwt(
|
||||
token: JWTType.JWT,
|
||||
user?: NextAuthTypes.User,
|
||||
account?: Record<string, unknown>,
|
||||
profile?: Record<string, unknown>,
|
||||
isNewUser?: boolean
|
||||
) {
|
||||
async jwt(token, user, account, profile, isNewUser) {
|
||||
return token
|
||||
},
|
||||
},
|
||||
events: {
|
||||
async signIn(message: string) {
|
||||
async signIn(message: NextAuthTypes.SignInEventMessage) {
|
||||
return undefined
|
||||
},
|
||||
async signOut(message: string) {
|
||||
async signOut(message: NextAuthTypes.Session | null) {
|
||||
return undefined
|
||||
},
|
||||
async createUser(message: string) {
|
||||
async createUser(message: NextAuthTypes.User) {
|
||||
return undefined
|
||||
},
|
||||
async linkAccount(message: string) {
|
||||
async updateUser(message: NextAuthTypes.User) {
|
||||
return undefined
|
||||
},
|
||||
async session(message: string) {
|
||||
async linkAccount(message: NextAuthTypes.LinkAccountEventMessage) {
|
||||
return undefined
|
||||
},
|
||||
async error(message: string) {
|
||||
async session(message: NextAuthTypes.Session) {
|
||||
return undefined
|
||||
},
|
||||
async error(message: any) {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
adapter,
|
||||
adapter: MyAdapter(client),
|
||||
useSecureCookies: true,
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
|
||||
71
www/docs/adapters/dynamodb.md
Normal file
71
www/docs/adapters/dynamodb.md
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
id: dynamodb
|
||||
title: DynamoDB Adapter
|
||||
---
|
||||
|
||||
# DynamoDB
|
||||
|
||||
This is the AWS DynamoDB Adapter for next-auth. This package can only be used in conjunction with the primary next-auth package. It is not a standalone package.
|
||||
|
||||
You need a table with a partition key `pk` and a sort key `sk`. Your table also needs a global secondary index named `GSI1` with `GSI1PK` as partition key and `GSI1SK` as sorting key. You can set whatever you want as the table name and the billing method.
|
||||
|
||||
You can find the full schema in the table structure section below.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/dynamodb-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/dynamodb-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
You need to pass `aws-sdk` to the adapter in addition to the table name.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import AWS from "aws-sdk";
|
||||
import NextAuth from "next-auth";
|
||||
import Providers from "next-auth/providers";
|
||||
import { DynamoDBAdapter } from "@next-auth/dynamodb-adapter"
|
||||
|
||||
AWS.config.update({
|
||||
accessKeyId: process.env.NEXT_AUTH_AWS_ACCESS_KEY,
|
||||
secretAccessKey: process.env.NEXT_AUTH_AWS_SECRET_KEY,
|
||||
region: process.env.NEXT_AUTH_AWS_REGION,
|
||||
});
|
||||
|
||||
export default NextAuth({
|
||||
// Configure one or more authentication providers
|
||||
providers: [
|
||||
Providers.GitHub({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
}),
|
||||
Providers.Email({
|
||||
server: process.env.EMAIL_SERVER,
|
||||
from: process.env.EMAIL_FROM,
|
||||
}),
|
||||
// ...add more providers here
|
||||
],
|
||||
adapter: DynamoDBAdapter({
|
||||
AWS,
|
||||
tableName: "next-auth-test",
|
||||
}),
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
(AWS secrets start with `NEXT_AUTH_` in order to not conflict with [Vercel's reserved environment variables](https://vercel.com/docs/environment-variables#reserved-environment-variables).)
|
||||
|
||||
## Schema
|
||||
|
||||
The table respects the single table design pattern. This has many advantages:
|
||||
|
||||
- Only one table to manage, monitor and provision.
|
||||
- Querying relations is faster than with multi-table schemas (for eg. retreiving all sessions for a user).
|
||||
- Only one table needs to be replicated, if you want to go multi-region.
|
||||
|
||||
Here is a schema of the table :
|
||||
|
||||

|
||||
84
www/docs/adapters/fauna.md
Normal file
84
www/docs/adapters/fauna.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
id: fauna
|
||||
title: FaunaDB Adapter
|
||||
---
|
||||
|
||||
# FaunaDB
|
||||
|
||||
This is the Fauna Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
You can find the Fauna schema and seed information in the docs at [next-auth.js.org/adapters/fauna](https://next-auth.js.org/adapters/fauna).
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/fauna-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/fauna-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import * as Fauna from "faunadb"
|
||||
import { FaunaAdapter } from "@next-auth/fauna-adapter"
|
||||
|
||||
const client = new Fauna.Client({
|
||||
secret: "secret",
|
||||
scheme: "http",
|
||||
domain: "localhost",
|
||||
port: 8443,
|
||||
})
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FaunaAdapter({ faunaClient: client})
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Schema
|
||||
|
||||
```javascript
|
||||
CreateCollection({ name: "accounts" })
|
||||
CreateCollection({ name: "sessions" })
|
||||
CreateCollection({ name: "users" })
|
||||
CreateCollection({ name: "verification_requests" })
|
||||
CreateIndex({
|
||||
name: "account_by_provider_account_id",
|
||||
source: Collection("accounts"),
|
||||
unique: true,
|
||||
terms: [
|
||||
{ field: ["data", "providerId"] },
|
||||
{ field: ["data", "providerAccountId"] },
|
||||
],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "session_by_token",
|
||||
source: Collection("sessions"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "sessionToken"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "user_by_email",
|
||||
source: Collection("users"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "email"] }],
|
||||
})
|
||||
CreateIndex({
|
||||
name: "verification_request_by_token",
|
||||
source: Collection("verification_requests"),
|
||||
unique: true,
|
||||
terms: [{ field: ["data", "token"] }, { field: ["data", "identifier"] }],
|
||||
})
|
||||
```
|
||||
73
www/docs/adapters/firebase.md
Normal file
73
www/docs/adapters/firebase.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: firebase
|
||||
title: Firebase Adapter
|
||||
---
|
||||
|
||||
# Firebase
|
||||
|
||||
This is the Firebase Adapter for [`next-auth`](https://next-auth.js.org). This package can only be used in conjunction with the primary `next-auth` package. It is not a standalone package.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Install `next-auth` and `@next-auth/firebase-adapter`
|
||||
|
||||
```js
|
||||
npm install next-auth @next-auth/firebase-adapter
|
||||
```
|
||||
|
||||
2. Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object.
|
||||
|
||||
```javascript title="pages/api/auth/[...nextauth].js"
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import { FirebaseAdapter } from "@next-auth/firebase-adapter"
|
||||
|
||||
import firebase from "firebase/app"
|
||||
import "firebase/firestore"
|
||||
|
||||
const firestore = (
|
||||
firebase.apps[0] ?? firebase.initializeApp(/* your config */)
|
||||
).firestore()
|
||||
|
||||
// For more information on each option (and a full list of options) go to
|
||||
// https://next-auth.js.org/configuration/options
|
||||
export default NextAuth({
|
||||
// https://next-auth.js.org/configuration/providers
|
||||
providers: [
|
||||
Providers.Google({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
],
|
||||
adapter: FirebaseAdapter(firestore),
|
||||
...
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
When initializing the firestore adapter, you must pass in the firebase config object with the details from your project. More details on how to obtain that config object can be found [here](https://support.google.com/firebase/answer/7015592).
|
||||
|
||||
An example firebase config looks like this:
|
||||
|
||||
```js
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyDOCAbC123dEf456GhI789jKl01-MnO",
|
||||
authDomain: "myapp-project-123.firebaseapp.com",
|
||||
databaseURL: "https://myapp-project-123.firebaseio.com",
|
||||
projectId: "myapp-project-123",
|
||||
storageBucket: "myapp-project-123.appspot.com",
|
||||
messagingSenderId: "65211879809",
|
||||
appId: "1:65211879909:web:3ae38ef1cdcb2e01fe5f0c",
|
||||
measurementId: "G-8GSGZQ44ST",
|
||||
}
|
||||
```
|
||||
|
||||
See [firebase.google.com/docs/web/setup](https://firebase.google.com/docs/web/setup) for more details.
|
||||
|
||||
:::tip **From Firebase**
|
||||
|
||||
**Caution**: We do not recommend manually modifying an app's Firebase config file or object. If you initialize an app with invalid or missing values for any of these required "Firebase options", then your end users may experience serious issues.
|
||||
|
||||
For open source projects, we generally do not recommend including the app's Firebase config file or object in source control because, in most cases, your users should create their own Firebase projects and point their apps to their own Firebase resources (via their own Firebase config file or object).
|
||||
:::
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user