mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35583a513d | ||
|
|
665d91019f | ||
|
|
f2b816b7b9 | ||
|
|
2e770fb0bf | ||
|
|
e83e7231fb | ||
|
|
4593ec8b01 | ||
|
|
12517f629b | ||
|
|
77012bc00c | ||
|
|
60fdf26a56 | ||
|
|
0fae0c7a8e | ||
|
|
eba79f4445 | ||
|
|
e3bb9881ea | ||
|
|
827049cb35 | ||
|
|
ad8100d402 | ||
|
|
7b5defff16 | ||
|
|
bc9805d1ba | ||
|
|
c823016b36 | ||
|
|
ca0f4c6fba | ||
|
|
c0d2f2d852 | ||
|
|
71f63117a9 | ||
|
|
d04ce29314 | ||
|
|
d2882f1958 | ||
|
|
66db563ca5 | ||
|
|
9619077363 | ||
|
|
013ccb4cb0 | ||
|
|
6eb41259d1 | ||
|
|
141f8d07e2 | ||
|
|
ffd0601ab0 | ||
|
|
7864d4705d | ||
|
|
98dc82e5d6 | ||
|
|
86baefdd9d | ||
|
|
332e237c3e | ||
|
|
2fce08c0b5 | ||
|
|
adf3fb669f | ||
|
|
5323be3594 | ||
|
|
6df0d04a1e | ||
|
|
aa9c1e7c96 | ||
|
|
66473054f5 | ||
|
|
e8ddbc5c11 | ||
|
|
dfe4620056 | ||
|
|
848224e2c5 | ||
|
|
aee376cc57 | ||
|
|
0d2a81cd39 | ||
|
|
61e99c9489 | ||
|
|
0eb4159737 | ||
|
|
9f0008375f | ||
|
|
0cf1823e70 | ||
|
|
7f39669053 | ||
|
|
7b82d6e985 | ||
|
|
53b0a7aa74 | ||
|
|
fbb09303af | ||
|
|
ff05ac1e41 | ||
|
|
a6f6c1590d | ||
|
|
52c2466b9e |
@@ -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 😊
|
||||
|
||||
-->
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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
|
||||
|
||||
22
config/babel.config.js
Normal file
22
config/babel.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// We aim to have the same support as Next.js
|
||||
// https://nextjs.org/docs/getting-started#system-requirements
|
||||
// https://nextjs.org/docs/basic-features/supported-browsers-features
|
||||
|
||||
module.exports = {
|
||||
presets: [["@babel/preset-env", { targets: { node: "10.13" } }]],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-optional-catch-binding",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
comments: false,
|
||||
overrides: [
|
||||
{
|
||||
test: ["../src/client/**"],
|
||||
presets: [["@babel/preset-env", { targets: { ie: "11" } }]],
|
||||
},
|
||||
{
|
||||
test: ["../src/server/pages/**"],
|
||||
presets: ["preact"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
5044
package-lock.json
generated
5044
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
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": "echo \"Write some tests...\"; npm run test:types",
|
||||
"test:types": "dtslint types --onlyTestTsNext",
|
||||
"prepublishOnly": "npm run build",
|
||||
"publish:beta": "npm publish --tag beta",
|
||||
"publish:canary": "npm publish --tag canary",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
@@ -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,9 +88,9 @@
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.4",
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@prisma/client": "^2.16.1",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.2",
|
||||
"@babel/plugin-transform-runtime": "^7.13.15",
|
||||
"@babel/preset-env": "^7.14.2",
|
||||
"@semantic-release/commit-analyzer": "^8.0.1",
|
||||
"@semantic-release/github": "^7.2.0",
|
||||
"@semantic-release/npm": "7.0.8",
|
||||
@@ -124,19 +111,10 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.3.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"mocha": "^8.1.3",
|
||||
"mongodb": "^3.5.9",
|
||||
"mssql": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"next": "^10.0.5",
|
||||
"pg": "^8.2.1",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "^2.2.1",
|
||||
"prisma": "^2.16.1",
|
||||
"puppeteer": "^5.2.1",
|
||||
"puppeteer-extra": "^3.1.15",
|
||||
"puppeteer-extra-plugin-stealth": "^2.6.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"typescript": "^4.1.3"
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,6 +355,22 @@ function BroadcastChannel (name = 'nextauth.message') {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Some methods are exported with more than one name. This provides some
|
||||
// flexibility over how they can be invoked and backwards compatibility
|
||||
// with earlier releases. These should be removed in a newer release, as it only
|
||||
// creates problems for bundlers and adds confusion to users. TypeScript declarations
|
||||
// will provide sufficient help when importing
|
||||
export {
|
||||
setOptions as options,
|
||||
getSession as session,
|
||||
getProviders as providers,
|
||||
getCsrfToken as csrfToken,
|
||||
signIn as signin,
|
||||
signOut as signout
|
||||
}
|
||||
|
||||
export default {
|
||||
getSession,
|
||||
getCsrfToken,
|
||||
|
||||
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,
|
||||
|
||||
24
src/providers/workos.js
Normal file
24
src/providers/workos.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export default function WorkOS(options) {
|
||||
return {
|
||||
id: 'workos',
|
||||
name: 'WorkOS',
|
||||
type: 'oauth',
|
||||
version: '2.0',
|
||||
scope: '',
|
||||
params: {
|
||||
grant_type: 'authorization_code',
|
||||
client_id: options.clientId,
|
||||
client_secret: options.clientSecret
|
||||
},
|
||||
accessTokenUrl: 'https://api.workos.com/sso/token/',
|
||||
authorizationUrl: `https://api.workos.com/sso/authorize/?response_type=code&domain=${options.domain}`,
|
||||
profileUrl: 'https://api.workos.com/sso/profile/',
|
||||
profile: (profile) => {
|
||||
return {
|
||||
...profile,
|
||||
name: `${profile.first_name} ${profile.last_name}`
|
||||
}
|
||||
},
|
||||
...options
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import adapters from '../adapters'
|
||||
import jwt from '../lib/jwt'
|
||||
import parseUrl from '../lib/parse-url'
|
||||
import logger, { setLogger } from '../lib/logger'
|
||||
import * as cookie from './lib/cookie'
|
||||
import * as defaultEvents from './lib/default-events'
|
||||
import * as defaultCallbacks from './lib/default-callbacks'
|
||||
import parseProviders from './lib/providers'
|
||||
import * as routes from './routes'
|
||||
import renderPage from './pages'
|
||||
import createSecret from './lib/create-secret'
|
||||
import callbackUrlHandler from './lib/callback-url-handler'
|
||||
import extendRes from './lib/extend-res'
|
||||
import csrfTokenHandler from './lib/csrf-token-handler'
|
||||
import * as pkce from './lib/oauth/pkce-handler'
|
||||
import * as state from './lib/oauth/state-handler'
|
||||
import adapters from "../adapters"
|
||||
import jwt from "../lib/jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import * as cookie from "./lib/cookie"
|
||||
import * as defaultEvents from "./lib/default-events"
|
||||
import * as defaultCallbacks from "./lib/default-callbacks"
|
||||
import parseProviders from "./lib/providers"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import createSecret from "./lib/create-secret"
|
||||
import callbackUrlHandler from "./lib/callback-url-handler"
|
||||
import extendRes from "./lib/extend-res"
|
||||
import csrfTokenHandler from "./lib/csrf-token-handler"
|
||||
import * as pkce from "./lib/oauth/pkce-handler"
|
||||
import * as state from "./lib/oauth/state-handler"
|
||||
|
||||
// To work properly in production with OAuth providers the NEXTAUTH_URL
|
||||
// environment variable must be set.
|
||||
if (!process.env.NEXTAUTH_URL) {
|
||||
logger.warn('NEXTAUTH_URL', 'NEXTAUTH_URL environment variable not set')
|
||||
logger.warn("NEXTAUTH_URL", "NEXTAUTH_URL environment variable not set")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ if (!process.env.NEXTAUTH_URL) {
|
||||
* @param {import("next").NextApiResponse} res
|
||||
* @param {import("types").NextAuthOptions} userOptions
|
||||
*/
|
||||
async function NextAuthHandler (req, res, userOptions) {
|
||||
async function NextAuthHandler(req, res, userOptions) {
|
||||
if (userOptions.logger) {
|
||||
setLogger(userOptions.logger)
|
||||
}
|
||||
@@ -39,13 +39,15 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// to avoid early termination of calls to the serverless function
|
||||
// (and then return that promise when we are done) - eslint
|
||||
// complains but I'm not sure there is another way to do this.
|
||||
return new Promise(async resolve => { // eslint-disable-line no-async-promise-executor
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
extendRes(req, res, resolve)
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = 'Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly.'
|
||||
const error =
|
||||
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
|
||||
|
||||
logger.error('MISSING_NEXTAUTH_API_ROUTE_ERROR', error)
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
|
||||
return res.status(500).end(`Error: ${error}`)
|
||||
}
|
||||
|
||||
@@ -53,31 +55,48 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
nextauth,
|
||||
action = nextauth[0],
|
||||
providerId = nextauth[1],
|
||||
error = nextauth[1]
|
||||
error = nextauth[1],
|
||||
} = req.query
|
||||
|
||||
// @todo refactor all existing references to baseUrl and basePath
|
||||
const { basePath, baseUrl } = parseUrl(process.env.NEXTAUTH_URL || process.env.VERCEL_URL)
|
||||
const { basePath, baseUrl } = parseUrl(
|
||||
process.env.NEXTAUTH_URL || process.env.VERCEL_URL
|
||||
)
|
||||
|
||||
const cookies = {
|
||||
...cookie.defaultCookies(userOptions.useSecureCookies || baseUrl.startsWith('https://')),
|
||||
...cookie.defaultCookies(
|
||||
userOptions.useSecureCookies || baseUrl.startsWith("https://")
|
||||
),
|
||||
// Allow user cookie options to override any cookie settings above
|
||||
...userOptions.cookies
|
||||
...userOptions.cookies,
|
||||
}
|
||||
|
||||
const secret = createSecret({ userOptions, basePath, baseUrl })
|
||||
|
||||
const providers = parseProviders({ providers: userOptions.providers, baseUrl, basePath })
|
||||
const providers = parseProviders({
|
||||
providers: userOptions.providers,
|
||||
baseUrl,
|
||||
basePath,
|
||||
})
|
||||
const provider = providers.find(({ id }) => id === providerId)
|
||||
|
||||
// Protection only works on OAuth 2.x providers
|
||||
if (provider?.type === 'oauth' && provider.version?.startsWith('2')) {
|
||||
// When provider.state is undefined, we still want this to pass
|
||||
if (!provider.protection && provider.state !== false) {
|
||||
// Default to state, as we did in 3.1 REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ['state']
|
||||
} else if (typeof provider.protection === 'string') {
|
||||
provider.protection = [provider.protection]
|
||||
// TODO:
|
||||
// - rename to `checks` in 4.x, so it is similar to `openid-client`
|
||||
// - stop supporting `protection` as string
|
||||
// - remove `state` property
|
||||
if (provider?.type === "oauth" && provider.version?.startsWith("2")) {
|
||||
// Priority: (protection array > protection string) > state > default
|
||||
if (provider.protection) {
|
||||
provider.protection = Array.isArray(provider.protection)
|
||||
? provider.protection
|
||||
: [provider.protection]
|
||||
} else if (provider.state !== undefined) {
|
||||
provider.protection = [provider.state ? "state" : "none"]
|
||||
} else {
|
||||
// Default to state, as we did in 3.1
|
||||
// REVIEW: should we use "pkce" or "none" as default?
|
||||
provider.protection = ["state"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,14 +105,16 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
// Parse database / adapter
|
||||
// If adapter is provided, use it (advanced usage, overrides database)
|
||||
// If database URI or config object is provided, use it (simple usage)
|
||||
const adapter = userOptions.adapter ?? (userOptions.database && adapters.Default(userOptions.database))
|
||||
const adapter =
|
||||
userOptions.adapter ??
|
||||
(userOptions.database && adapters.Default(userOptions.database))
|
||||
|
||||
// User provided options are overriden by other options,
|
||||
// except for the options with special handling above
|
||||
req.options = {
|
||||
debug: false,
|
||||
pages: {},
|
||||
theme: 'auto',
|
||||
theme: "auto",
|
||||
// Custom options override defaults
|
||||
...userOptions,
|
||||
// These computed settings can have values in userOptions but we override them
|
||||
@@ -111,7 +132,7 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
jwt: !adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60, // Sessions updated only if session is greater than this value (0 = always, 24*60*60 = every 24 hours)
|
||||
...userOptions.session
|
||||
...userOptions.session,
|
||||
},
|
||||
// JWT options
|
||||
jwt: {
|
||||
@@ -119,20 +140,20 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
maxAge, // same as session maxAge,
|
||||
encode: jwt.encode,
|
||||
decode: jwt.decode,
|
||||
...userOptions.jwt
|
||||
...userOptions.jwt,
|
||||
},
|
||||
// Event messages
|
||||
events: {
|
||||
...defaultEvents,
|
||||
...userOptions.events
|
||||
...userOptions.events,
|
||||
},
|
||||
// Callback functions
|
||||
callbacks: {
|
||||
...defaultCallbacks,
|
||||
...userOptions.callbacks
|
||||
...userOptions.callbacks,
|
||||
},
|
||||
pkce: {},
|
||||
logger
|
||||
logger,
|
||||
}
|
||||
|
||||
csrfTokenHandler(req, res)
|
||||
@@ -141,65 +162,74 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
const render = renderPage(req, res)
|
||||
const { pages } = req.options
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (req.method === "GET") {
|
||||
switch (action) {
|
||||
case 'providers':
|
||||
case "providers":
|
||||
return routes.providers(req, res)
|
||||
case 'session':
|
||||
case "session":
|
||||
return routes.session(req, res)
|
||||
case 'csrf':
|
||||
case "csrf":
|
||||
return res.json({ csrfToken: req.options.csrfToken })
|
||||
case 'signin':
|
||||
case "signin":
|
||||
if (pages.signIn) {
|
||||
let signinUrl = `${pages.signIn}${pages.signIn.includes('?') ? '&' : '?'}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) { signinUrl = `${signinUrl}&error=${error}` }
|
||||
let signinUrl = `${pages.signIn}${
|
||||
pages.signIn.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${req.options.callbackUrl}`
|
||||
if (error) {
|
||||
signinUrl = `${signinUrl}&error=${error}`
|
||||
}
|
||||
return res.redirect(signinUrl)
|
||||
}
|
||||
|
||||
return render.signin()
|
||||
case 'signout':
|
||||
if (pages.signOut) {
|
||||
return res.redirect(`${pages.signOut}${pages.signOut.includes('?') ? '&' : '?'}error=${error}`)
|
||||
}
|
||||
case "signout":
|
||||
if (pages.signOut) return res.redirect(pages.signOut)
|
||||
|
||||
return render.signout()
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
if (await pkce.handleCallback(req, res)) return
|
||||
if (await state.handleCallback(req, res)) return
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case 'verify-request':
|
||||
case "verify-request":
|
||||
if (pages.verifyRequest) {
|
||||
return res.redirect(pages.verifyRequest)
|
||||
}
|
||||
return render.verifyRequest()
|
||||
case 'error':
|
||||
case "error":
|
||||
if (pages.error) {
|
||||
return res.redirect(`${pages.error}${pages.error.includes('?') ? '&' : '?'}error=${error}`)
|
||||
return res.redirect(
|
||||
`${pages.error}${
|
||||
pages.error.includes("?") ? "&" : "?"
|
||||
}error=${error}`
|
||||
)
|
||||
}
|
||||
|
||||
// These error messages are displayed in line on the sign in page
|
||||
if ([
|
||||
'Signin',
|
||||
'OAuthSignin',
|
||||
'OAuthCallback',
|
||||
'OAuthCreateAccount',
|
||||
'EmailCreateAccount',
|
||||
'Callback',
|
||||
'OAuthAccountNotLinked',
|
||||
'EmailSignin',
|
||||
'CredentialsSignin'
|
||||
].includes(error)) {
|
||||
if (
|
||||
[
|
||||
"Signin",
|
||||
"OAuthSignin",
|
||||
"OAuthCallback",
|
||||
"OAuthCreateAccount",
|
||||
"EmailCreateAccount",
|
||||
"Callback",
|
||||
"OAuthAccountNotLinked",
|
||||
"EmailSignin",
|
||||
"CredentialsSignin",
|
||||
].includes(error)
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?error=${error}`)
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
default:
|
||||
}
|
||||
} else if (req.method === 'POST') {
|
||||
} else if (req.method === "POST") {
|
||||
switch (action) {
|
||||
case 'signin':
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
if (req.options.csrfTokenVerified && provider) {
|
||||
if (await pkce.handleSignin(req, res)) return
|
||||
@@ -208,16 +238,19 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
case 'signout':
|
||||
case "signout":
|
||||
// Verified CSRF Token required for signout
|
||||
if (req.options.csrfTokenVerified) {
|
||||
return routes.signout(req, res)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signout?csrf=true`)
|
||||
case 'callback':
|
||||
case "callback":
|
||||
if (provider) {
|
||||
// Verified CSRF Token required for credentials providers only
|
||||
if (provider.type === 'credentials' && !req.options.csrfTokenVerified) {
|
||||
if (
|
||||
provider.type === "credentials" &&
|
||||
!req.options.csrfTokenVerified
|
||||
) {
|
||||
return res.redirect(`${baseUrl}${basePath}/signin?csrf=true`)
|
||||
}
|
||||
|
||||
@@ -226,31 +259,33 @@ async function NextAuthHandler (req, res, userOptions) {
|
||||
return routes.callback(req, res)
|
||||
}
|
||||
break
|
||||
case '_log':
|
||||
case "_log":
|
||||
if (userOptions.logger) {
|
||||
try {
|
||||
const {
|
||||
code = 'CLIENT_ERROR',
|
||||
level = 'error',
|
||||
message = '[]'
|
||||
code = "CLIENT_ERROR",
|
||||
level = "error",
|
||||
message = "[]",
|
||||
} = req.body
|
||||
|
||||
logger[level](code, ...JSON.parse(message))
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error('LOGGER_ERROR', error)
|
||||
logger.error("LOGGER_ERROR", error)
|
||||
}
|
||||
}
|
||||
return res.end()
|
||||
default:
|
||||
}
|
||||
}
|
||||
return res.status(400).end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
return res
|
||||
.status(400)
|
||||
.end(`Error: HTTP ${req.method} is not supported for ${req.url}`)
|
||||
})
|
||||
}
|
||||
|
||||
/** Tha main entry point to next-auth */
|
||||
export default function NextAuth (...args) {
|
||||
export default function NextAuth(...args) {
|
||||
if (args.length === 1) {
|
||||
return (req, res) => NextAuthHandler(req, res, args[0])
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AccountNotLinkedError } from '../../lib/errors'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import { AccountNotLinkedError } from "../../lib/errors"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* This function handles the complex flow of signing users in, and either creating,
|
||||
@@ -12,20 +13,29 @@ import dispatchEvent from '../lib/dispatch-event'
|
||||
* All verification (e.g. OAuth flows or email address verificaiton flows) are
|
||||
* done prior to this handler being called to avoid additonal complexity in this
|
||||
* handler.
|
||||
* @param {import("types").Session} sessionToken
|
||||
* @param {import("types").Profile} profile
|
||||
* @param {import("types").Account} account
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
*/
|
||||
export default async function callbackHandler (sessionToken, profile, providerAccount, options) {
|
||||
export default async function callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
providerAccount,
|
||||
options
|
||||
) {
|
||||
// Input validation
|
||||
if (!profile) throw new Error('Missing profile')
|
||||
if (!providerAccount?.id || !providerAccount.type) throw new Error('Missing or invalid provider account')
|
||||
if (!['email', 'oauth'].includes(providerAccount.type)) throw new Error('Provider not supported')
|
||||
if (!profile) throw new Error("Missing profile")
|
||||
if (!providerAccount?.id || !providerAccount.type)
|
||||
throw new Error("Missing or invalid provider account")
|
||||
if (!["email", "oauth"].includes(providerAccount.type))
|
||||
throw new Error("Provider not supported")
|
||||
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: {
|
||||
jwt: useJwtSession
|
||||
}
|
||||
session: { jwt: useJwtSession },
|
||||
} = options
|
||||
|
||||
// If no adapter is configured then we don't have a database and cannot
|
||||
@@ -34,7 +44,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
user: profile,
|
||||
account: providerAccount,
|
||||
session: {}
|
||||
session: {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +57,8 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
linkAccount,
|
||||
createSession,
|
||||
getSession,
|
||||
deleteSession
|
||||
} = await adapter.getAdapter(options)
|
||||
deleteSession,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(options), options.logger)
|
||||
|
||||
let session = null
|
||||
let user = null
|
||||
@@ -74,9 +84,11 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
}
|
||||
|
||||
if (providerAccount.type === 'email') {
|
||||
if (providerAccount.type === "email") {
|
||||
// If signing in with an email, check if an account with the same email address exists already
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// If they are not already signed in as the same user, this flow will
|
||||
// sign them out of the current session and sign them in as the new user
|
||||
@@ -107,11 +119,14 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else if (providerAccount.type === 'oauth') {
|
||||
} else if (providerAccount.type === "oauth") {
|
||||
// If signing in with oauth account, check to see if the account exists already
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
|
||||
const userByProviderAccountId = await getUserByProviderAccountId(
|
||||
providerAccount.provider,
|
||||
providerAccount.id
|
||||
)
|
||||
if (userByProviderAccountId) {
|
||||
if (isSignedIn) {
|
||||
// If the user is already signed in with this account, we don't need to do anything
|
||||
@@ -122,7 +137,7 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
// If the user is currently signed in, but the new account they are signing in
|
||||
@@ -132,11 +147,13 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
}
|
||||
// If there is no active session, but the account being signed in with is already
|
||||
// associated with a valid user then create session to sign the user in.
|
||||
session = useJwtSession ? {} : await createSession(userByProviderAccountId)
|
||||
session = useJwtSession
|
||||
? {}
|
||||
: await createSession(userByProviderAccountId)
|
||||
return {
|
||||
session,
|
||||
user: userByProviderAccountId,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
} else {
|
||||
if (isSignedIn) {
|
||||
@@ -151,13 +168,16 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
// As they are already signed in, we don't need to do anything after linking them
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +198,9 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
//
|
||||
// OAuth providers should require email address verification to prevent this, but in
|
||||
// practice that is not always the case; this helps protect against that.
|
||||
const userByEmail = profile.email ? await getUserByEmail(profile.email) : null
|
||||
const userByEmail = profile.email
|
||||
? await getUserByEmail(profile.email)
|
||||
: null
|
||||
if (userByEmail) {
|
||||
// We end up here when we don't have an account with the same [provider].id *BUT*
|
||||
// we do already have an account with the same email address as the one in the
|
||||
@@ -207,14 +229,17 @@ export default async function callbackHandler (sessionToken, profile, providerAc
|
||||
providerAccount.accessToken,
|
||||
providerAccount.accessTokenExpires
|
||||
)
|
||||
await dispatchEvent(events.linkAccount, { user, providerAccount: providerAccount })
|
||||
await dispatchEvent(events.linkAccount, {
|
||||
user,
|
||||
providerAccount: providerAccount,
|
||||
})
|
||||
|
||||
session = useJwtSession ? {} : await createSession(user)
|
||||
isNewUser = true
|
||||
return {
|
||||
session,
|
||||
user,
|
||||
isNewUser
|
||||
isNewUser,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ async function getOAuth2AccessToken (code, provider, codeVerifier) {
|
||||
*/
|
||||
async function getOAuth2 (provider, accessToken, results) {
|
||||
let url = provider.profileUrl
|
||||
let httpMethod = 'GET'
|
||||
const headers = { ...provider.headers }
|
||||
|
||||
if (this._useAuthorizationHeaderForGET) {
|
||||
@@ -238,8 +239,15 @@ async function getOAuth2 (provider, accessToken, results) {
|
||||
url = prepareProfileUrl({ provider, url, results })
|
||||
}
|
||||
|
||||
/** Dropbox requires POST instead of GET
|
||||
* Read more: https://www.dropbox.com/developers/reference/auth-types#user
|
||||
*/
|
||||
if (provider.id === 'dropbox') {
|
||||
httpMethod = 'POST'
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._request('GET', url, headers, null, accessToken, (error, profileData) => {
|
||||
this._request(httpMethod, url, headers, null, accessToken, (error, profileData) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
import { randomBytes } from "crypto"
|
||||
import adapterErrorHandler from "../../../adapters/error-handler"
|
||||
|
||||
export default async function email (email, provider, options) {
|
||||
/**
|
||||
*
|
||||
* @param {string} email
|
||||
* @param {import("types/providers").EmailConfig} provider
|
||||
* @param {import("types/internals").AppOptions} options
|
||||
* @returns
|
||||
*/
|
||||
export default async function email(email, provider, options) {
|
||||
try {
|
||||
const { baseUrl, basePath, adapter } = options
|
||||
const { baseUrl, basePath, adapter, logger } = options
|
||||
|
||||
const { createVerificationRequest } = await adapter.getAdapter(options)
|
||||
const { createVerificationRequest } = adapterErrorHandler(
|
||||
await adapter.getAdapter(options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Prefer provider specific secret, but use default secret if none specified
|
||||
const secret = provider.secret || options.secret
|
||||
|
||||
// Generate token
|
||||
const token = await provider.generateVerificationToken?.() ?? randomBytes(32).toString('hex')
|
||||
const token =
|
||||
(await provider.generateVerificationToken?.()) ??
|
||||
randomBytes(32).toString("hex")
|
||||
|
||||
// Send email with link containing token (the unhashed version)
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(provider.id)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
const url = `${baseUrl}${basePath}/callback/${encodeURIComponent(
|
||||
provider.id
|
||||
)}?email=${encodeURIComponent(email)}&token=${encodeURIComponent(token)}`
|
||||
|
||||
// @TODO Create invite (send secret so can be hashed)
|
||||
await createVerificationRequest(email, url, token, secret, provider, options)
|
||||
await createVerificationRequest(
|
||||
email,
|
||||
url,
|
||||
token,
|
||||
secret,
|
||||
provider,
|
||||
options
|
||||
)
|
||||
|
||||
// Return promise
|
||||
return Promise.resolve()
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import oAuthCallback from '../lib/oauth/callback'
|
||||
import callbackHandler from '../lib/callback-handler'
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import oAuthCallback from "../lib/oauth/callback"
|
||||
import callbackHandler from "../lib/callback-handler"
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Handle callbacks from login services
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function callback (req, res) {
|
||||
export default async function callback(req, res) {
|
||||
const {
|
||||
provider,
|
||||
adapter,
|
||||
@@ -22,21 +22,23 @@ export default async function callback (req, res) {
|
||||
jwt,
|
||||
events,
|
||||
callbacks,
|
||||
session: {
|
||||
jwt: useJwtSession,
|
||||
maxAge: sessionMaxAge
|
||||
}
|
||||
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
// Get session ID (if set)
|
||||
const sessionToken = req.cookies?.[cookies.sessionToken.name] ?? null
|
||||
|
||||
if (provider.type === 'oauth') {
|
||||
if (provider.type === "oauth") {
|
||||
try {
|
||||
const { profile, account, OAuthProfile } = await oAuthCallback(req)
|
||||
try {
|
||||
// Make it easier to debug when adding a new provider
|
||||
logger.debug('OAUTH_CALLBACK_RESPONSE', { profile, account, OAuthProfile })
|
||||
logger.debug("OAUTH_CALLBACK_RESPONSE", {
|
||||
profile,
|
||||
account,
|
||||
OAuthProfile,
|
||||
})
|
||||
|
||||
// If we don't have a profile object then either something went wrong
|
||||
// or the user cancelled signing in. We don't know which, so we just
|
||||
@@ -56,52 +58,85 @@ export default async function callback (req, res) {
|
||||
// (that just means it's a new user signing in for the first time).
|
||||
let userOrProfile = profile
|
||||
if (adapter) {
|
||||
const { getUserByProviderAccountId } = await adapter.getAdapter(req.options)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(account.provider, account.id)
|
||||
const { getUserByProviderAccountId } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const userFromProviderAccountId = await getUserByProviderAccountId(
|
||||
account.provider,
|
||||
account.id
|
||||
)
|
||||
if (userFromProviderAccountId) {
|
||||
userOrProfile = userFromProviderAccountId
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(userOrProfile, account, OAuthProfile)
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
userOrProfile,
|
||||
account,
|
||||
OAuthProfile
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=AccessDenied`
|
||||
)
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
const { user, session, isNewUser } = await callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
account,
|
||||
req.options
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, OAuthProfile, isNewUser)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
OAuthProfile,
|
||||
isNewUser
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -110,94 +145,145 @@ export default async function callback (req, res) {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
return res.redirect(
|
||||
`${pages.newUser}${
|
||||
pages.newUser.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
} catch (error) {
|
||||
if (error.name === 'AccountNotLinkedError') {
|
||||
if (error.name === "AccountNotLinkedError") {
|
||||
// If the email on the account is already linked, but not with this OAuth account
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`)
|
||||
} else if (error.name === 'CreateUserError') {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCreateAccount`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthAccountNotLinked`
|
||||
)
|
||||
} else if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=OAuthCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('OAUTH_CALLBACK_HANDLER_ERROR', error)
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'OAuthCallbackError') {
|
||||
logger.error('CALLBACK_OAUTH_ERROR', error)
|
||||
if (error.name === "OAuthCallbackError") {
|
||||
logger.error("CALLBACK_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthCallback`)
|
||||
}
|
||||
logger.error('OAUTH_CALLBACK_ERROR', error)
|
||||
logger.error("OAUTH_CALLBACK_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'email') {
|
||||
} else if (provider.type === "email") {
|
||||
try {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const { getVerificationRequest, deleteVerificationRequest, getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const {
|
||||
getVerificationRequest,
|
||||
deleteVerificationRequest,
|
||||
getUserByEmail,
|
||||
} = adapterErrorHandler(await adapter.getAdapter(req.options), logger)
|
||||
const verificationToken = req.query.token
|
||||
const email = req.query.email
|
||||
|
||||
// Verify email and verification token exist in database
|
||||
const invite = await getVerificationRequest(email, verificationToken, secret, provider)
|
||||
const invite = await getVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
)
|
||||
if (!invite) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Verification`)
|
||||
}
|
||||
|
||||
// If verification token is valid, delete verification request token from
|
||||
// the database so it cannot be used again
|
||||
await deleteVerificationRequest(email, verificationToken, secret, provider)
|
||||
await deleteVerificationRequest(
|
||||
email,
|
||||
verificationToken,
|
||||
secret,
|
||||
provider
|
||||
)
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = {
|
||||
id: provider.id,
|
||||
type: "email",
|
||||
providerAccountId: email,
|
||||
}
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email })
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
profile,
|
||||
account,
|
||||
{ email }
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
// Sign user in
|
||||
const { user, session, isNewUser } = await callbackHandler(sessionToken, profile, account, req.options)
|
||||
const { user, session, isNewUser } = await callbackHandler(
|
||||
sessionToken,
|
||||
profile,
|
||||
account,
|
||||
req.options
|
||||
)
|
||||
|
||||
if (useJwtSession) {
|
||||
const defaultJwtPayload = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, profile, isNewUser)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
profile,
|
||||
isNewUser
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, { expires: session.expires || null, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, session.sessionToken, {
|
||||
expires: session.expires || null,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
}
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account, isNewUser })
|
||||
@@ -206,55 +292,93 @@ export default async function callback (req, res) {
|
||||
// e.g. option to send users to a new account landing page on initial login
|
||||
// Note that the callback URL is preserved, so the journey can still be resumed
|
||||
if (isNewUser && pages.newUser) {
|
||||
return res.redirect(`${pages.newUser}${pages.newUser.includes('?') ? '&' : '?'}callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
||||
return res.redirect(
|
||||
`${pages.newUser}${
|
||||
pages.newUser.includes("?") ? "&" : "?"
|
||||
}callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
)
|
||||
}
|
||||
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
} catch (error) {
|
||||
if (error.name === 'CreateUserError') {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailCreateAccount`)
|
||||
if (error.name === "CreateUserError") {
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=EmailCreateAccount`
|
||||
)
|
||||
}
|
||||
logger.error('CALLBACK_EMAIL_ERROR', error)
|
||||
logger.error("CALLBACK_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Callback`)
|
||||
}
|
||||
} else if (provider.type === 'credentials' && req.method === 'POST') {
|
||||
} else if (provider.type === "credentials" && req.method === "POST") {
|
||||
if (!useJwtSession) {
|
||||
logger.error('CALLBACK_CREDENTIALS_JWT_ERROR', 'Signin in with credentials is only supported if JSON Web Tokens are enabled')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_JWT_ERROR",
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
if (!provider.authorize) {
|
||||
logger.error('CALLBACK_CREDENTIALS_HANDLER_ERROR', 'Must define an authorize() handler to use credentials authentication provider')
|
||||
return res.status(500).redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
return res
|
||||
.status(500)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
|
||||
const credentials = req.body
|
||||
|
||||
let userObjectReturnedFromAuthorizeHandler
|
||||
try {
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(credentials)
|
||||
userObjectReturnedFromAuthorizeHandler = await provider.authorize(
|
||||
credentials, {...req, options: {}, cookies: {}}
|
||||
)
|
||||
if (!userObjectReturnedFromAuthorizeHandler) {
|
||||
return res.status(401).redirect(`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(provider.id)}`)
|
||||
return res
|
||||
.status(401)
|
||||
.redirect(
|
||||
`${baseUrl}${basePath}/error?error=CredentialsSignin&provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}`
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
const user = userObjectReturnedFromAuthorizeHandler
|
||||
const account = { id: provider.id, type: 'credentials' }
|
||||
const account = { id: provider.id, type: "credentials" }
|
||||
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(user, account, credentials)
|
||||
const signInCallbackResponse = await callbacks.signIn(
|
||||
user,
|
||||
account,
|
||||
credentials
|
||||
)
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.status(403).redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
return res
|
||||
.status(403)
|
||||
.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error.message)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(
|
||||
error.message
|
||||
)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(error)
|
||||
}
|
||||
@@ -263,22 +387,33 @@ export default async function callback (req, res) {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
picture: user.image,
|
||||
sub: user.id?.toString()
|
||||
sub: user.id?.toString(),
|
||||
}
|
||||
const jwtPayload = await callbacks.jwt(defaultJwtPayload, user, account, userObjectReturnedFromAuthorizeHandler, false)
|
||||
const jwtPayload = await callbacks.jwt(
|
||||
defaultJwtPayload,
|
||||
user,
|
||||
account,
|
||||
userObjectReturnedFromAuthorizeHandler,
|
||||
false
|
||||
)
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + (sessionMaxAge * 1000))
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: cookieExpires.toISOString(), ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.signIn, { user, account })
|
||||
|
||||
return res.redirect(callbackUrl || baseUrl)
|
||||
}
|
||||
return res.status(500).end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
return res
|
||||
.status(500)
|
||||
.end(`Error: Callback for provider type ${provider.type} not supported`)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/**
|
||||
* Return a session object (without any private fields)
|
||||
* for Single Page App clients
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function session (req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks } = req.options
|
||||
export default async function session(req, res) {
|
||||
const { cookies, adapter, jwt, events, callbacks, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionMaxAge = req.options.session.maxAge
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
@@ -24,7 +26,9 @@ export default async function session (req, res) {
|
||||
|
||||
// Generate new session expiry date
|
||||
const sessionExpiresDate = new Date()
|
||||
sessionExpiresDate.setTime(sessionExpiresDate.getTime() + (sessionMaxAge * 1000))
|
||||
sessionExpiresDate.setTime(
|
||||
sessionExpiresDate.getTime() + sessionMaxAge * 1000
|
||||
)
|
||||
const sessionExpires = sessionExpiresDate.toISOString()
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
@@ -33,14 +37,17 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: decodedJwt.name || null,
|
||||
email: decodedJwt.email || null,
|
||||
image: decodedJwt.picture || null
|
||||
image: decodedJwt.picture || null,
|
||||
},
|
||||
expires: sessionExpires
|
||||
expires: sessionExpires,
|
||||
}
|
||||
|
||||
// Pass Session and JSON Web Token through to the session callback
|
||||
const jwtPayload = await callbacks.jwt(decodedJwt)
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, jwtPayload)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
jwtPayload
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
@@ -49,17 +56,29 @@ export default async function session (req, res) {
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token: jwtPayload })
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, { expires: sessionExpires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, newEncodedJwt, {
|
||||
expires: sessionExpires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload, jwt: jwtPayload })
|
||||
await dispatchEvent(events.session, {
|
||||
session: sessionPayload,
|
||||
jwt: jwtPayload,
|
||||
})
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error('JWT_SESSION_ERROR', error)
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
logger.error("JWT_SESSION_ERROR", error)
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { getUser, getSession, updateSession } = await adapter.getAdapter(req.options)
|
||||
const { getUser, getSession, updateSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
const session = await getSession(sessionToken)
|
||||
if (session) {
|
||||
// Trigger update to session object to update session expiry
|
||||
@@ -73,29 +92,38 @@ export default async function session (req, res) {
|
||||
user: {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image
|
||||
image: user.image,
|
||||
},
|
||||
accessToken: session.accessToken,
|
||||
expires: session.expires
|
||||
expires: session.expires,
|
||||
}
|
||||
|
||||
// Pass Session through to the session callback
|
||||
const sessionPayload = await callbacks.session(defaultSessionPayload, user)
|
||||
const sessionPayload = await callbacks.session(
|
||||
defaultSessionPayload,
|
||||
user
|
||||
)
|
||||
|
||||
// Return session payload as response
|
||||
response = sessionPayload
|
||||
|
||||
// Set cookie again to update expiry
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, { expires: session.expires, ...cookies.sessionToken.options })
|
||||
cookie.set(res, cookies.sessionToken.name, sessionToken, {
|
||||
expires: session.expires,
|
||||
...cookies.sessionToken.options,
|
||||
})
|
||||
|
||||
await dispatchEvent(events.session, { session: sessionPayload })
|
||||
} else if (sessionToken) {
|
||||
// If sessionToken was found set but it's not valid for a session then
|
||||
// remove the sessionToken cookie from browser.
|
||||
cookie.set(res, cookies.sessionToken.name, '', { ...cookies.sessionToken.options, maxAge: 0 })
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('SESSION_ERROR', error)
|
||||
logger.error("SESSION_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,43 @@
|
||||
import getAuthorizationUrl from '../lib/signin/oauth'
|
||||
import emailSignin from '../lib/signin/email'
|
||||
import logger from '../../lib/logger'
|
||||
import getAuthorizationUrl from "../lib/signin/oauth"
|
||||
import emailSignin from "../lib/signin/email"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signin */
|
||||
export default async function signin (req, res) {
|
||||
/**
|
||||
* Handle requests to /api/auth/signin
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signin(req, res) {
|
||||
const {
|
||||
provider,
|
||||
baseUrl,
|
||||
basePath,
|
||||
adapter,
|
||||
callbacks
|
||||
callbacks,
|
||||
logger,
|
||||
} = req.options
|
||||
|
||||
if (!provider.type) {
|
||||
return res.status(500).end(`Error: Type not specified for ${provider.name}`)
|
||||
}
|
||||
|
||||
if (provider.type === 'oauth' && req.method === 'POST') {
|
||||
if (provider.type === "oauth" && req.method === "POST") {
|
||||
try {
|
||||
const authorizationUrl = await getAuthorizationUrl(req)
|
||||
return res.redirect(authorizationUrl)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_OAUTH_ERROR', error)
|
||||
logger.error("SIGNIN_OAUTH_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=OAuthSignin`)
|
||||
}
|
||||
} else if (provider.type === 'email' && req.method === 'POST') {
|
||||
} else if (provider.type === "email" && req.method === "POST") {
|
||||
if (!adapter) {
|
||||
logger.error('EMAIL_REQUIRES_ADAPTER_ERROR')
|
||||
logger.error("EMAIL_REQUIRES_ADAPTER_ERROR")
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=Configuration`)
|
||||
}
|
||||
const { getUserByEmail } = await adapter.getAdapter(req.options)
|
||||
const { getUserByEmail } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
// Note: Technically the part of the email address local mailbox element
|
||||
// (everything before the @ symbol) should be treated as 'case sensitive'
|
||||
@@ -39,36 +47,43 @@ export default async function signin (req, res) {
|
||||
const email = req.body.email?.toLowerCase() ?? null
|
||||
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const profile = await getUserByEmail(email) || { email }
|
||||
const account = { id: provider.id, type: 'email', providerAccountId: email }
|
||||
const profile = (await getUserByEmail(email)) || { email }
|
||||
const account = { id: provider.id, type: "email", providerAccountId: email }
|
||||
|
||||
// Check if user is allowed to sign in
|
||||
try {
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, { email, verificationRequest: true })
|
||||
const signInCallbackResponse = await callbacks.signIn(profile, account, {
|
||||
email,
|
||||
verificationRequest: true,
|
||||
})
|
||||
if (signInCallbackResponse === false) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=AccessDenied`)
|
||||
} else if (typeof signInCallbackResponse === 'string') {
|
||||
} else if (typeof signInCallbackResponse === "string") {
|
||||
return res.redirect(signInCallbackResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/error?error=${encodeURIComponent(error)}`
|
||||
)
|
||||
}
|
||||
// TODO: Remove in a future major release
|
||||
logger.warn('SIGNIN_CALLBACK_REJECT_REDIRECT')
|
||||
logger.warn("SIGNIN_CALLBACK_REJECT_REDIRECT")
|
||||
return res.redirect(error)
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSignin(email, provider, req.options)
|
||||
} catch (error) {
|
||||
logger.error('SIGNIN_EMAIL_ERROR', error)
|
||||
logger.error("SIGNIN_EMAIL_ERROR", error)
|
||||
return res.redirect(`${baseUrl}${basePath}/error?error=EmailSignin`)
|
||||
}
|
||||
|
||||
return res.redirect(`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`)
|
||||
return res.redirect(
|
||||
`${baseUrl}${basePath}/verify-request?provider=${encodeURIComponent(
|
||||
provider.id
|
||||
)}&type=${encodeURIComponent(provider.type)}`
|
||||
)
|
||||
}
|
||||
return res.redirect(`${baseUrl}${basePath}/signin`)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as cookie from '../lib/cookie'
|
||||
import logger from '../../lib/logger'
|
||||
import dispatchEvent from '../lib/dispatch-event'
|
||||
import * as cookie from "../lib/cookie"
|
||||
import dispatchEvent from "../lib/dispatch-event"
|
||||
import adapterErrorHandler from "../../adapters/error-handler"
|
||||
|
||||
/** Handle requests to /api/auth/signout */
|
||||
export default async function signout (req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl } = req.options
|
||||
/**
|
||||
* Handle requests to /api/auth/signout
|
||||
* @param {import("types/internals").NextAuthRequest} req
|
||||
* @param {import("types/internals").NextAuthResponse} res
|
||||
*/
|
||||
export default async function signout(req, res) {
|
||||
const { adapter, cookies, events, jwt, callbackUrl, logger } = req.options
|
||||
const useJwtSession = req.options.session.jwt
|
||||
const sessionToken = req.cookies[cookies.sessionToken.name]
|
||||
|
||||
@@ -18,7 +22,10 @@ export default async function signout (req, res) {
|
||||
}
|
||||
} else {
|
||||
// Get session from database
|
||||
const { getSession, deleteSession } = await adapter.getAdapter(req.options)
|
||||
const { getSession, deleteSession } = adapterErrorHandler(
|
||||
await adapter.getAdapter(req.options),
|
||||
logger
|
||||
)
|
||||
|
||||
try {
|
||||
// Dispatch signout event
|
||||
@@ -33,14 +40,14 @@ export default async function signout (req, res) {
|
||||
await deleteSession(sessionToken)
|
||||
} catch (error) {
|
||||
// If error, log it but continue
|
||||
logger.error('SIGNOUT_ERROR', error)
|
||||
logger.error("SIGNOUT_ERROR", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Session Token
|
||||
cookie.set(res, cookies.sessionToken.name, '', {
|
||||
cookie.set(res, cookies.sessionToken.name, "", {
|
||||
...cookies.sessionToken.options,
|
||||
maxAge: 0
|
||||
maxAge: 0,
|
||||
})
|
||||
|
||||
return res.redirect(callbackUrl)
|
||||
|
||||
@@ -1,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)
|
||||
}
|
||||
})()
|
||||
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: {
|
||||
|
||||
@@ -156,9 +156,9 @@ Check out the content of all the params in addition `token`, to see what info yo
|
||||
:::
|
||||
|
||||
:::warning
|
||||
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** for all cookies on a domain is commonly imposed by browsers.
|
||||
NextAuth.js does not limit how much data you can store in a JSON Web Token, however a ~**4096 byte limit** per cookie is commonly imposed by browsers.
|
||||
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). You can store a key that can be used to look up that data in the `session()` callback.
|
||||
If you need to persist a large amount of data, you will need to persist it elsewhere (e.g. in a database). A common solution is to store a key in the cookie that can be used to look up the remaining data in the database, for example, in the `session()` callback.
|
||||
:::
|
||||
|
||||
## Session callback
|
||||
|
||||
@@ -7,17 +7,60 @@ Events are asynchronous functions that do not return a response, they are useful
|
||||
|
||||
You can specify a handler for any of these events below, for debugging or for an audit log.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
...
|
||||
events: {
|
||||
async signIn(message) { /* on successful sign in */ },
|
||||
async signOut(message) { /* on signout */ },
|
||||
async createUser(message) { /* user created */ },
|
||||
async linkAccount(message) { /* account linked to a user */ },
|
||||
async session(message) { /* session is active */ },
|
||||
async error(message) { /* error in authentication flow */ }
|
||||
}
|
||||
...
|
||||
```
|
||||
:::note
|
||||
Execution of your auth API will be blocked by an `await` on your event handler. If your event handler starts any burdensome work it should not block its own promise on that work.
|
||||
:::
|
||||
|
||||
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc) but typically contains a user object and/or contents of the JSON Web Token and other information relevant to the event.
|
||||
## Events
|
||||
|
||||
### signIn
|
||||
|
||||
Sent on successful sign in.
|
||||
|
||||
The message will be an object and contain:
|
||||
|
||||
- `user` (from your adapter or from the provider if a `credentials` type provider)
|
||||
- `account` (from your adapter or the provider)
|
||||
- `isNewUser` (whether your adapter had a user for this account already)
|
||||
|
||||
### signOut
|
||||
|
||||
Sent when the user signs out.
|
||||
|
||||
The message object is the JWT, if using them, or the adapter session object for the session that is being ended.
|
||||
|
||||
### createUser
|
||||
|
||||
Sent when the adapter is told to create a new user.
|
||||
|
||||
The message object will be the user.
|
||||
|
||||
### updateUser
|
||||
|
||||
Sent when the adapter is told to update an existing user. Currently this is only sent when the user verifies their email address.
|
||||
|
||||
The message object will be the user.
|
||||
|
||||
### linkAccount
|
||||
|
||||
Sent when an account in a given provider is linked to a user in our userbase. For example, when a user signs up with Twitter or when an existing user links their Google account.
|
||||
|
||||
The message will be an object and contain:
|
||||
|
||||
- `user`: The user object from your adapter
|
||||
- `providerAccount`: The object returned from the provider.
|
||||
|
||||
### session
|
||||
|
||||
Sent at the end of a request for the current session.
|
||||
|
||||
The message will be an object and contain:
|
||||
|
||||
- `session`: The session object from your adapter
|
||||
- `jwt`: If using JWT, the token for this session.
|
||||
|
||||
### error
|
||||
|
||||
Sent when an error occurs
|
||||
|
||||
The message could be any object relevant to describing the error.
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Options
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### NEXTAUTH_URL
|
||||
### NEXTAUTH_URL
|
||||
|
||||
When deploying to production, set the `NEXTAUTH_URL` environment variable to the canonical URL of your site.
|
||||
|
||||
@@ -37,10 +37,10 @@ Options are passed to NextAuth.js when initializing it in an API route.
|
||||
|
||||
### providers
|
||||
|
||||
* **Default value**: `[]`
|
||||
* **Required**: *Yes*
|
||||
- **Default value**: `[]`
|
||||
- **Required**: _Yes_
|
||||
|
||||
#### Description
|
||||
#### Description
|
||||
|
||||
An array of authentication providers for signing in (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order. This can be one of the built-in providers or an object with a custom provider.
|
||||
|
||||
@@ -50,10 +50,10 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
|
||||
### database
|
||||
|
||||
* **Default value**: `null`
|
||||
* **Required**: *No (unless using email provider)*
|
||||
- **Default value**: `null`
|
||||
- **Required**: _No (unless using email provider)_
|
||||
|
||||
#### Description
|
||||
#### Description
|
||||
|
||||
[A database connection string or configuration object.](/configuration/databases)
|
||||
|
||||
@@ -61,14 +61,14 @@ See the [providers documentation](/configuration/providers) for a list of suppor
|
||||
|
||||
### secret
|
||||
|
||||
* **Default value**: `string` (*SHA hash of the "options" object*)
|
||||
* **Required**: *No - but strongly recommended!*
|
||||
- **Default value**: `string` (_SHA hash of the "options" object_)
|
||||
- **Required**: _No - but strongly recommended!_
|
||||
|
||||
#### Description
|
||||
|
||||
A random string used to hash tokens, sign cookies and generate crytographic keys.
|
||||
|
||||
If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
If not specified, it uses a hash for all configuration options, including Client ID / Secrets for entropy.
|
||||
|
||||
The default behaviour is volatile, and it is strongly recommended you explicitly specify a value to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
|
||||
@@ -76,8 +76,8 @@ The default behaviour is volatile, and it is strongly recommended you explicitly
|
||||
|
||||
### session
|
||||
|
||||
* **Default value**: `object`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `object`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -90,14 +90,14 @@ 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: false,
|
||||
|
||||
jwt: false,
|
||||
|
||||
// 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
|
||||
// Note: This option is ignored if using JSON Web Tokens
|
||||
updateAge: 24 * 60 * 60, // 24 hours
|
||||
}
|
||||
```
|
||||
@@ -106,8 +106,8 @@ session: {
|
||||
|
||||
### jwt
|
||||
|
||||
* **Default value**: `object`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `object`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -124,18 +124,15 @@ jwt: {
|
||||
// This is used to generate the actual signingKey and produces a warning
|
||||
// message if not defined explicitly.
|
||||
// secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw',
|
||||
|
||||
// You can generate a signing key using `jose newkey -s 512 -t oct -a HS512`
|
||||
// This gives you direct knowledge of the key used to sign the token so you can use it
|
||||
// to authenticate indirectly (eg. to a database driver)
|
||||
// signingKey: {"kty":"oct","kid":"Dl893BEV-iVE-x9EC52TDmlJUgGm9oZ99_ZL025Hc5Q","alg":"HS512","k":"K7QqRmJOKRK2qcCKV_pi9PSBv3XP0fpTu30TP8xn4w01xR3ZMZM38yL2DnTVPVw6e4yhdh0jtoah-i4c_pZagA"},
|
||||
|
||||
// If you chose something other than the default algorithm for the signingKey (HS512)
|
||||
// you also need to configure the algorithm
|
||||
// verificationOptions: {
|
||||
// algorithms: ['HS256']
|
||||
// },
|
||||
|
||||
// Set to true to use encryption. Defaults to false (signing only).
|
||||
// encryption: true,
|
||||
// encryptionKey: "",
|
||||
@@ -143,7 +140,6 @@ jwt: {
|
||||
// decryptionOptions = {
|
||||
// algorithms: ['A256GCM']
|
||||
// },
|
||||
|
||||
// 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 }) {},
|
||||
@@ -168,13 +164,13 @@ An example JSON Web Token contains a payload like this:
|
||||
You can use the built-in `getToken()` helper method to verify and decrypt the token, like this:
|
||||
|
||||
```js
|
||||
import jwt from 'next-auth/jwt'
|
||||
import jwt from "next-auth/jwt"
|
||||
|
||||
const secret = process.env.JWT_SECRET
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret })
|
||||
console.log('JSON Web Token', token)
|
||||
console.log("JSON Web Token", token)
|
||||
res.end()
|
||||
}
|
||||
```
|
||||
@@ -185,10 +181,10 @@ _For convenience, this helper function is also able to read and decode tokens pa
|
||||
|
||||
The getToken() helper requires the following options:
|
||||
|
||||
* `req` - (object) Request object
|
||||
* `secret` - (string) JWT Secret
|
||||
- `req` - (object) Request object
|
||||
- `secret` - (string) JWT Secret
|
||||
|
||||
You must also pass *any options configured on the `jwt` option* to the helper.
|
||||
You must also pass _any options configured on the `jwt` option_ to the helper.
|
||||
|
||||
e.g. Including custom session `maxAge` and custom signing and/or encryption keys or options
|
||||
|
||||
@@ -196,15 +192,15 @@ e.g. Including custom session `maxAge` and custom signing and/or encryption keys
|
||||
|
||||
It also supports the following options:
|
||||
|
||||
* `secureCookie` - (boolean) Use secure prefixed cookie name
|
||||
- `secureCookie` - (boolean) Use secure prefixed cookie name
|
||||
|
||||
By default, the helper function will attempt to determine if it should use the secure prefixed cookie (e.g. `true` in production and `false` in development, unless NEXTAUTH_URL contains an HTTPS URL).
|
||||
|
||||
* `cookieName` - (string) Session token cookie name
|
||||
- `cookieName` - (string) Session token cookie name
|
||||
|
||||
The `secureCookie` option is ignored if `cookieName` is explicitly specified.
|
||||
|
||||
* `raw` - (boolean) Get raw token (not decoded)
|
||||
- `raw` - (boolean) Get raw token (not decoded)
|
||||
|
||||
If set to `true` returns the raw token without decrypting or verifying it.
|
||||
|
||||
@@ -216,8 +212,8 @@ The JWT is stored in the Session Token cookie, the same cookie used for tokens w
|
||||
|
||||
### pages
|
||||
|
||||
* **Default value**: `{}`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `{}`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -225,7 +221,7 @@ Specify URLs to be used if you want to create custom sign in, sign out and error
|
||||
|
||||
Pages specified will override the corresponding built-in page.
|
||||
|
||||
*For example:*
|
||||
_For example:_
|
||||
|
||||
```js
|
||||
pages: {
|
||||
@@ -243,8 +239,8 @@ See the documentation for the [pages option](/configuration/pages) for more info
|
||||
|
||||
### callbacks
|
||||
|
||||
* **Default value**: `object`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `object`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -277,8 +273,8 @@ See the [callbacks documentation](/configuration/callbacks) for more information
|
||||
|
||||
### events
|
||||
|
||||
* **Default value**: `object`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `object`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -286,27 +282,26 @@ Events are asynchronous functions that do not return a response, they are useful
|
||||
|
||||
You can specify a handler for any of these events below - e.g. for debugging or to create an audit log.
|
||||
|
||||
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc), but typically contains a user object and/or contents of the JSON Web Token and other information relevant to the event.
|
||||
The content of the message object varies depending on the flow (e.g. OAuth or Email authentication flow, JWT or database sessions, etc). See the [events documentation](/configuration/events) for more information on the form of each message object and how to use the events functions.
|
||||
|
||||
```js
|
||||
events: {
|
||||
async signIn(message) { /* on successful sign in */ },
|
||||
async signOut(message) { /* on signout */ },
|
||||
async createUser(message) { /* user created */ },
|
||||
async linkAccount(message) { /* account linked to a user */ },
|
||||
async updateUser(message) { /* user updated - e.g. their email was verified */ },
|
||||
async linkAccount(message) { /* account (e.g. Twitter) linked to a user */ },
|
||||
async session(message) { /* session is active */ },
|
||||
async error(message) { /* error in authentication flow */ }
|
||||
}
|
||||
```
|
||||
|
||||
See the [events documentation](/configuration/events) for more information on how to use the events functions.
|
||||
|
||||
---
|
||||
|
||||
### adapter
|
||||
|
||||
* **Default value**: *Adapter.Default()*
|
||||
* **Required**: *No*
|
||||
- **Default value**: _Adapter.Default()_
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -324,8 +319,8 @@ If the `adapter` option is specified it overrides the `database` option, only sp
|
||||
|
||||
### debug
|
||||
|
||||
* **Default value**: `false`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `false`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -335,14 +330,15 @@ Set debug to `true` to enable debug messages for authentication and database ope
|
||||
|
||||
### logger
|
||||
|
||||
* **Default value**: `console`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `console`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
Override any of the logger levels (`undefined` levels will use the built-in logger), and intercept logs in NextAuth. You can use this to send NextAuth logs to a third-party logging service.
|
||||
|
||||
Example:
|
||||
|
||||
```js title="/pages/api/auth/[...nextauth].js"
|
||||
import log from "logging-service"
|
||||
|
||||
@@ -371,8 +367,8 @@ If the `debug` level is defined by the user, it will be called regardless of the
|
||||
|
||||
### theme
|
||||
|
||||
* **Default value**: `"auto"`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `"auto"`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -388,8 +384,8 @@ Advanced options are passed the same way as basic options, but may have complex
|
||||
|
||||
### useSecureCookies
|
||||
|
||||
* **Default value**: `true` for HTTPS sites / `false` for HTTP sites
|
||||
* **Required**: *No*
|
||||
- **Default value**: `true` for HTTPS sites / `false` for HTTP sites
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
@@ -404,15 +400,15 @@ Properties on any custom `cookies` that are specified override this option.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Setting this option to *false* in production is a security risk and may allow sessions to hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
|
||||
Setting this option to _false_ in production is a security risk and may allow sessions to be hijacked if used in production. It is intended to support development and testing. Using this option is not recommended.
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
### cookies
|
||||
|
||||
* **Default value**: `{}`
|
||||
* **Required**: *No*
|
||||
- **Default value**: `{}`
|
||||
- **Required**: _No_
|
||||
|
||||
#### Description
|
||||
|
||||
|
||||
@@ -3,59 +3,123 @@ id: providers
|
||||
title: Providers
|
||||
---
|
||||
|
||||
Authentication Providers in NextAuth.js are services that can be used to sign in (OAuth, Email, etc).
|
||||
Authentication Providers in **NextAuth.js** are services that can be used to sign in a user.
|
||||
|
||||
## Sign in with OAuth
|
||||
There's four ways a user can be signed in:
|
||||
|
||||
NextAuth.js is designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0 and has built-in support for many popular OAuth sign-in services.
|
||||
- [Using a built-in OAuth Provider](#oauth-providers) (e.g Github, Twitter, Google, etc...)
|
||||
- [Using a custom OAuth Provider](#using-a-custom-provider)
|
||||
- [Using Email](#email-provider)
|
||||
- [Using Credentials](#credentials-provider)
|
||||
|
||||
### Built-in OAuth providers
|
||||
:::note
|
||||
NextAuth.js is designed to work with any OAuth service, it supports **OAuth 1.0**, **1.0A** and **2.0** and has built-in support for most popular sign-in services.
|
||||
:::
|
||||
|
||||
<ul>
|
||||
## OAuth Providers
|
||||
|
||||
### Available providers
|
||||
|
||||
<div className="provider-name-list">
|
||||
{Object.entries(require("../../providers.json"))
|
||||
.filter(([key]) => !["email", "credentials"].includes(key))
|
||||
.sort(([, a], [, b]) => a.localeCompare(b))
|
||||
.map(([key, name]) =>
|
||||
<li key={key}><a href={`/providers/${key}`}>{name}</a></li>
|
||||
.map(([key, name]) => (
|
||||
<span key={key}>
|
||||
<a href={`/providers/${key}`}>{name}</a>
|
||||
<span className="provider-name-list__comma">,</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
### Using a built-in OAuth provider
|
||||
### How to
|
||||
|
||||
1. Register your application at the developer portal of your provider. There are links above to the developer docs for most supported providers with details on how to register your application.
|
||||
|
||||
2. The redirect URI should follow this format:
|
||||
```
|
||||
[origin]/api/auth/callback/[provider]
|
||||
```
|
||||
For example, Twitter on `localhost` this would be:
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/twitter
|
||||
```
|
||||
|
||||
```
|
||||
[origin]/api/auth/callback/[provider]
|
||||
```
|
||||
|
||||
For example, Twitter on `localhost` this would be:
|
||||
|
||||
```
|
||||
http://localhost:3000/api/auth/callback/twitter
|
||||
```
|
||||
|
||||
3. Create a `.env` file at the root of your project and add the client ID and client secret. For Twitter this would be:
|
||||
|
||||
```
|
||||
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
|
||||
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
```
|
||||
```
|
||||
TWITTER_ID=YOUR_TWITTER_CLIENT_ID
|
||||
TWITTER_SECRET=YOUR_TWITTER_CLIENT_SECRET
|
||||
```
|
||||
|
||||
4. Now you can add the provider settings to the NextAuth options object. You can add as many OAuth providers as you like, as you can see `providers` is an array.
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
...
|
||||
```
|
||||
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.Twitter({
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET
|
||||
})
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
5. Once a provider has been setup, you can sign in at the following URL: `[origin]/api/auth/signin`. This is an unbranded auto-generated page with all the configured providers.
|
||||
|
||||
<Image src="/img/signin.png" alt="Signin Screenshot" />
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
| :-----------------: | :--------------------------------------------------------------: | :---------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `oauth` | `"oauth"` | Yes |
|
||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
|
||||
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | Yes |
|
||||
| params | Extra URL params sent when calling `accessTokenUrl` | `Object` | Yes |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
|
||||
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | `string` | Yes |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | `string` | Yes |
|
||||
| clientId | Client ID of the OAuth provider | `string` | Yes |
|
||||
| clientSecret | Client Secret of the OAuth provider | `string` | Yes |
|
||||
| profile | A callback returning an object with the user's info | `(profile, tokens) => Object` | Yes |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) | `"pkce"`,`"state"`,`"none"` | No |
|
||||
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `Object` | No |
|
||||
| authorizationParams | Additional params to be sent to the authorization endpoint | `Object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| region | Only when using BattleNet | `string` | No |
|
||||
| domain | Only when using certain Providers | `string` | No |
|
||||
| tenantId | Only when using Azure, Active Directory, B2C, FusionAuth | `string` | No |
|
||||
|
||||
:::tip
|
||||
Even if you are using a built-in provider, you can override any of these options to tweak the default configuration.
|
||||
|
||||
```js title=[...nextauth].js
|
||||
import Providers from "next-auth/providers"
|
||||
|
||||
Providers.Auth0({
|
||||
clientId: process.env.CLIENT_ID,
|
||||
clientSecret: process.env.CLIENT_SECRET,
|
||||
domain: process.env.DOMAIN,
|
||||
scope: "openid your_custom_scope", // We do provide a default, but this will override it if defined
|
||||
profile(profile) {
|
||||
return {} // Return the profile in a shape that is different from the built-in one.
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Using a custom provider
|
||||
|
||||
You can use an OAuth provider that isn't built-in by using a custom object.
|
||||
@@ -76,7 +140,7 @@ As an example of what this looks like, this is the provider object returned for
|
||||
profileUrl: "https://www.googleapis.com/oauth2/v1/userinfo?alt=json",
|
||||
async profile(profile, tokens) {
|
||||
// You can use the tokens, in case you want to fetch more profile information
|
||||
// For example several OAuth provider does not return e-mail by default.
|
||||
// For example several OAuth providers do not return email by default.
|
||||
// Depending on your provider, will have tokens like `access_token`, `id_token` and or `refresh_token`
|
||||
return {
|
||||
id: profile.id,
|
||||
@@ -89,7 +153,8 @@ As an example of what this looks like, this is the provider object returned for
|
||||
clientSecret: ""
|
||||
}
|
||||
```
|
||||
You can replace all the options in this JSON object with the ones from your custom provider - be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option:
|
||||
|
||||
Replace all the options in this JSON object with the ones from your custom provider - be sure to give it a unique ID and specify the correct OAuth version - and add it to the providers option when initializing the library:
|
||||
|
||||
```js title="pages/api/auth/[...nextauth].js"
|
||||
import Providers from `next-auth/providers`
|
||||
@@ -111,33 +176,24 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
### Adding a new provider
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily!
|
||||
|
||||
You only need to add two changes:
|
||||
|
||||
### OAuth provider options
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers)<br />
|
||||
• make sure you use a named default export, like this: `export default function YourProvider`
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
3. Add it to our [provider types](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts) (for TS projects)<br />
|
||||
• you just need to add your new provider name to [this list](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L56-L97)<br />
|
||||
• in case you new provider accepts some custom options, you can [add them here](https://github.com/nextauthjs/next-auth/blob/main/types/providers.d.ts#L48-L53)
|
||||
|
||||
| Name | Description | Type | Required |
|
||||
| :-----------------: | :--------------------------------------------------------------: | :-----------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case it should be `oauth` | `oauth`, `email`, `credentials` | Yes |
|
||||
| version | OAuth version (e.g. '1.0', '1.0a', '2.0') | `string` | Yes |
|
||||
| accessTokenUrl | Endpoint to retrieve an access token | `string` | Yes |
|
||||
| authorizationUrl | Endpoint to request authorization from the user | `string` | Yes |
|
||||
| clientId | Client ID of the OAuth provider | `string` | Yes |
|
||||
| clientSecret | Client Secret of the OAuth provider | `string` | No |
|
||||
| scope | OAuth access scopes (expects array or string) | `string` or `string[]` | No |
|
||||
| params | Additional authorization URL parameters | `object` | No |
|
||||
| requestTokenUrl | Endpoint to retrieve a request token | `string` | No |
|
||||
| authorizationParams | Additional params to be sent to the authorization endpoint | `object` | No |
|
||||
| profileUrl | Endpoint to retrieve the user's profile | `string` | No |
|
||||
| profile | An callback returning an object with the user's info | `object` | No |
|
||||
| idToken | Set to `true` for services that use ID Tokens (e.g. OpenID) | `boolean` | No |
|
||||
| headers | Any headers that should be sent to the OAuth provider | `object` | No |
|
||||
| protection | Additional security for OAuth login flows (defaults to `state`) |`[pkce]`,`[state]`,`[pkce,state]`| No |
|
||||
| state | Same as `protection: "state"`. Being deprecated, use protection. | `boolean` | No |
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
## Sign in with Email
|
||||
## Email Provider
|
||||
|
||||
### How to
|
||||
|
||||
The Email provider uses email to send "magic links" that can be used sign in, you will likely have seen them before if you have used software like Slack.
|
||||
|
||||
@@ -164,8 +220,21 @@ See the [Email provider documentation](/providers/email) for more information on
|
||||
The email provider requires a database, it cannot be used without one.
|
||||
:::
|
||||
|
||||
### Options
|
||||
|
||||
## Sign in with Credentials
|
||||
| Name | Description | Type | Required |
|
||||
| :---------------------: | :---------------------------------------------------------------------------------: | :------------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `email` | `"email"` | Yes |
|
||||
| server | Path or object pointing to the email server | `string` or `Object` | Yes |
|
||||
| sendVerificationRequest | Callback to execute when a verification request is sent | `(params) => Promise<undefined>` | Yes |
|
||||
| from | The email address from which emails are sent, default: "<no-reply@example.com>" | `string` | No |
|
||||
| maxAge | How long until the e-mail can be used to log the user in seconds. Defaults to 1 day | `number` | No |
|
||||
|
||||
## Credentials Provider
|
||||
|
||||
### How to
|
||||
|
||||
The Credentials provider allows you to handle signing in with arbitrary credentials, such as a username and password, two factor authentication or hardware device (e.g. YubiKey U2F / FIDO).
|
||||
|
||||
@@ -185,12 +254,14 @@ providers: [
|
||||
username: { label: "Username", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const user = (credentials) => {
|
||||
async authorize(credentials, req) {
|
||||
const user = (credentials, req) => {
|
||||
// You need to provide your own logic here that takes the credentials
|
||||
// submitted and returns either a object representing a user or value
|
||||
// that is false/null if the credentials are invalid.
|
||||
// e.g. return { id: 1, name: 'J Smith', email: 'jsmith@example.com' }
|
||||
// You can also use the request object to obtain additional parameters
|
||||
// (i.e., the request IP address)
|
||||
return null
|
||||
}
|
||||
if (user) {
|
||||
@@ -211,26 +282,12 @@ See the [Credentials provider documentation](/providers/credentials) for more in
|
||||
The Credentials provider can only be used if JSON Web Tokens are enabled for sessions. Users authenticated with the Credentials provider are not persisted in the database.
|
||||
:::
|
||||
|
||||
<!-- React Image Component -->
|
||||
export const Image = ({ children, src, alt = '' }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: '0.2rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<img alt={alt} src={src} />
|
||||
</div>
|
||||
)
|
||||
### Options
|
||||
|
||||
|
||||
## Adding a new built-in provider
|
||||
|
||||
If you think your custom provider might be useful to others, we encourage you to open a PR and add it to the built-in list so others can discover it much more easily! You only need to add two changes:
|
||||
1. Add your config: [`src/providers/{provider}.js`](https://github.com/nextauthjs/next-auth/tree/main/src/providers) (Make sure you use a named default export, like `export default function YourProvider`!)
|
||||
2. Add provider documentation: [`www/docs/providers/{provider}.md`](https://github.com/nextauthjs/next-auth/tree/main/www/docs/providers)
|
||||
|
||||
That's it! 🎉 Others will be able to discover this provider much more easily now!
|
||||
|
||||
You can look at the existing built-in providers for inspiration.
|
||||
| Name | Description | Type | Required |
|
||||
| :---------: | :-----------------------------------------------: | :-----------------------------------: | :------: |
|
||||
| id | Unique ID for the provider | `string` | Yes |
|
||||
| name | Descriptive name for the provider | `string` | Yes |
|
||||
| type | Type of provider, in this case `credentials` | `"credentials"` | Yes |
|
||||
| credentials | The credentials to sign-in with | `Object` | Yes |
|
||||
| authorize | Callback to execute once user is to be authorized | `(credentials, req) => Promise<User>` | Yes |
|
||||
|
||||
@@ -95,7 +95,7 @@ If you are unable to use an HS512 key (for example to interoperate with other se
|
||||
|
||||
````
|
||||
jwt: {
|
||||
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"}
|
||||
signingKey: {"kty":"oct","kid":"--","alg":"HS256","k":"--"},
|
||||
verificationOptions: {
|
||||
algorithms: ["HS256"]
|
||||
}
|
||||
|
||||
@@ -196,9 +196,9 @@ JSON Web Tokens can be used for session tokens, but are also used for lots of ot
|
||||
|
||||
NextAuth.js client includes advanced features to mitigate the downsides of using shorter session expiry times on the user experience, including automatic session token rotation, optionally sending keep alive messages to prevent short lived sessions from expiring if there is an window or tab open, background re-validation, and automatic tab/window syncing that keeps sessions in sync across windows any time session state changes or a window or tab gains or loses focus.
|
||||
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes in total for all cookies on a domain, though the exact limit varies between browsers, proxies and hosting services.
|
||||
* As with database session tokens, JSON Web Tokens are limited in the amount of data you can store in them. There is typically a limit of around 4096 bytes per cookie, though the exact limit varies between browsers, proxies and hosting services. If you want to support most browsers, then do not exceed 4096 bytes per cookie. If you want to save more data, you will need to persist your sessions in a database (Source: [browsercookielimits.iain.guru](http://browsercookielimits.iain.guru/))
|
||||
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~2 KB of data you probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server side key/value store).
|
||||
The more data you try to store in a token and the more other cookies you set, the closer you will come to this limit. If you wish to store more than ~4 KB of data you're probably at the point where you need to store a unique ID in the token and persist the data elsewhere (e.g. in a server-side key/value store).
|
||||
|
||||
* Data stored in an encrypted JSON Web Token (JWE) may be compromised at some point.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ id: typescript
|
||||
title: TypeScript
|
||||
---
|
||||
|
||||
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about how certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
|
||||
NextAuth.js comes with its own type definitions, so you can safely use it in your TypeScript projects. Even if you don't use TypeScript, IDEs like VSCode will pick this up, to provide you with a better developer experience. While you are typing, you will get suggestions about what certain objects/functions look like, and sometimes also links to documentation, examples and other useful resources.
|
||||
|
||||
Check out the example repository showcasing how to use `next-auth` on a Next.js application with TypeScript:
|
||||
https://github.com/nextauthjs/next-auth-typescript-example
|
||||
@@ -14,6 +14,43 @@ The types at [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyType
|
||||
|
||||
---
|
||||
|
||||
## Adapters
|
||||
|
||||
If you're writing your own custom Adapter, you can take advantage of the types to make sure your implementation conforms to what's expected:
|
||||
|
||||
```ts
|
||||
import type { Adapter } from "next-auth/adapters"
|
||||
|
||||
const MyAdapter: Adapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When writing your own custom Adapter in plain JavaScript, note that you can use **JSDoc** to get helpful editor hints and auto-completion like so:
|
||||
|
||||
```js
|
||||
/** @type { import("next-auth/adapters").Adapter } */
|
||||
const MyAdapter = () => {
|
||||
return {
|
||||
async getAdapter() {
|
||||
return {
|
||||
// your adapter methods here
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::note
|
||||
This will work in code editors with a strong TypeScript integration like VSCode or WebStorm. It might not work if you're using more lightweight editors like VIM or Atom.
|
||||
:::
|
||||
|
||||
## Module Augmentation
|
||||
|
||||
`next-auth` comes with certain types/interfaces, that are shared across submodules. Good examples are `Session` and `JWT`. Ideally, you should only need to create these types at a single place, and TS should pick them up in every location where they are referenced. Luckily, this is exactly what Module Augmentation can do for us. Define your shared interfaces in a single location, and get type-safety across your application, when you use `next-auth` (or one of its submodules).
|
||||
|
||||
34
www/docs/providers/42.md
Normal file
34
www/docs/providers/42.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: 42-school
|
||||
title: 42 School
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
https://api.intra.42.fr/apidoc/guides/web_application_flow
|
||||
|
||||
## Configuration
|
||||
|
||||
https://profile.intra.42.fr/oauth/applications/new
|
||||
|
||||
## Options
|
||||
|
||||
The **42 School Provider** comes with a set of default options:
|
||||
|
||||
- [42 School Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/42.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
providers: [
|
||||
Providers.FortyTwo({
|
||||
clientId: process.env.FORTY_TWO_CLIENT_ID,
|
||||
clientSecret: process.env.FORTY_TWO_CLIENT_SECRET
|
||||
})
|
||||
]
|
||||
...
|
||||
```
|
||||
@@ -11,6 +11,14 @@ https://developer.apple.com/sign-in-with-apple/get-started/
|
||||
|
||||
https://developer.apple.com/account/resources/identifiers/list/serviceId
|
||||
|
||||
## Options
|
||||
|
||||
The **Apple Provider** comes with a set of default options:
|
||||
|
||||
- [Apple Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/apple.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
There are two ways you can use the Sign in with Apple provider.
|
||||
@@ -25,7 +33,7 @@ import Providers from `next-auth/providers`
|
||||
providers: [
|
||||
Providers.Apple({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: {
|
||||
clientSecret: {
|
||||
teamId: process.env.APPLE_TEAM_ID,
|
||||
privateKey: process.env.APPLE_PRIVATE_KEY,
|
||||
keyId: process.env.APPLE_KEY_ID,
|
||||
@@ -40,18 +48,18 @@ providers: [
|
||||
You can convert your Apple key to a single line to use it in a environment variable.
|
||||
|
||||
**Mac**
|
||||
|
||||
|
||||
```bash
|
||||
awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' AuthKey_ID.k8
|
||||
```
|
||||
|
||||
|
||||
**Windows**
|
||||
|
||||
|
||||
```powershell
|
||||
$k8file = "AuthKey_ID.k8"
|
||||
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
|
||||
(Get-Content "C:\Users\$env:UserName\Downloads\${k8file}") -join "\n"
|
||||
```
|
||||
|
||||
|
||||
:::
|
||||
|
||||
### Pre-generated secret
|
||||
@@ -92,9 +100,9 @@ Apple doesn't allow you to use localhost in domains or subdomains.
|
||||
|
||||
The following guides may be helpful:
|
||||
|
||||
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||
- [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||
|
||||
* [Guide to configuring Sign in with Apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
|
||||
- [Guide to configuring Sign in with Apple](https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple)
|
||||
|
||||
### Example server
|
||||
|
||||
@@ -114,7 +122,6 @@ Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.e
|
||||
|
||||
#### Create certificate
|
||||
|
||||
|
||||
Creating a certificate for localhost is easy with openssl . Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
|
||||
|
||||
```bash
|
||||
@@ -127,7 +134,7 @@ openssl req -x509 -out localhost.crt -keyout localhost.key \
|
||||
:::tip
|
||||
**Windows**
|
||||
|
||||
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
|
||||
The OpenSSL executable is distributed with [Git](https://git-scm.com/download/win]9) for Windows.
|
||||
Once installed you will find the openssl.exe file in `C:/Program Files/Git/mingw64/bin` which you can add to the system PATH environment variable if it’s not already done.
|
||||
|
||||
Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.cnf`
|
||||
@@ -142,32 +149,30 @@ Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.
|
||||
|
||||
Create directory `certificates` and place `localhost.key` and `localhost.crt`
|
||||
|
||||
|
||||
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Apple integration locally:
|
||||
|
||||
|
||||
```js
|
||||
const { createServer } = require('https')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const fs = require('fs')
|
||||
const { createServer } = require("https")
|
||||
const { parse } = require("url")
|
||||
const next = require("next")
|
||||
const fs = require("fs")
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const dev = process.env.NODE_ENV !== "production"
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('./certificates/localhost.key'),
|
||||
cert: fs.readFileSync('./certificates/localhost.crt')
|
||||
key: fs.readFileSync("./certificates/localhost.key"),
|
||||
cert: fs.readFileSync("./certificates/localhost.crt"),
|
||||
}
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer(httpsOptions, (req, res) => {
|
||||
const parsedUrl = parse(req.url, true)
|
||||
handle(req, res, parsedUrl)
|
||||
}).listen(3000, err => {
|
||||
}).listen(3000, (err) => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on https://localhost:3000')
|
||||
console.log("> Ready on https://localhost:3000")
|
||||
})
|
||||
})
|
||||
```
|
||||
@@ -177,25 +182,28 @@ app.prepare().then(() => {
|
||||
If you want to pre-generate your secret, this is an example of the code you will need:
|
||||
|
||||
```js
|
||||
const jwt = require('jsonwebtoken')
|
||||
const fs = require('fs')
|
||||
const jwt = require("jsonwebtoken")
|
||||
const fs = require("fs")
|
||||
|
||||
const appleId = 'myapp.example.com'
|
||||
const keyId = ''
|
||||
const teamId = ''
|
||||
const privateKey = fs.readFileSync('path/to/key')
|
||||
const appleId = "myapp.example.com"
|
||||
const keyId = ""
|
||||
const teamId = ""
|
||||
const privateKey = fs.readFileSync("path/to/key")
|
||||
|
||||
const secret = jwt.sign(
|
||||
{
|
||||
iss: teamId,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + ( 86400 * 180 ), // 6 months
|
||||
aud: 'https://appleid.apple.com',
|
||||
sub: appleId
|
||||
}, privateKey, {
|
||||
algorithm: 'ES256',
|
||||
keyid: keyId
|
||||
})
|
||||
exp: Math.floor(Date.now() / 1000) + 86400 * 180, // 6 months
|
||||
aud: "https://appleid.apple.com",
|
||||
sub: appleId,
|
||||
},
|
||||
privateKey,
|
||||
{
|
||||
algorithm: "ES256",
|
||||
keyid: keyId,
|
||||
}
|
||||
)
|
||||
|
||||
console.log(secret)
|
||||
```
|
||||
|
||||
@@ -7,6 +7,14 @@ title: Atlassian
|
||||
|
||||
https://developer.atlassian.com/cloud/jira/platform/oauth-2-authorization-code-grants-3lo-for-apps/#implementing-oauth-2-0--3lo-
|
||||
|
||||
## Options
|
||||
|
||||
The **Atlassian Provider** comes with a set of default options:
|
||||
|
||||
- [Atlassian Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/atlassian.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -15,6 +15,14 @@ https://manage.auth0.com/dashboard
|
||||
Configure your application in Auth0 as a 'Regular Web Application' (not a 'Single Page App').
|
||||
:::
|
||||
|
||||
## Options
|
||||
|
||||
The **Auth0 Provider** comes with a set of default options:
|
||||
|
||||
- [Auth0 Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/auth0.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
@@ -32,4 +40,4 @@ providers: [
|
||||
|
||||
:::note
|
||||
`domain` should be the fully qualified domain – e.g. `dev-s6clz2lv.eu.auth0.com`
|
||||
:::
|
||||
:::
|
||||
|
||||
@@ -11,7 +11,16 @@ https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-c
|
||||
|
||||
https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant
|
||||
|
||||
## Options
|
||||
|
||||
The **Azure Active Directory Provider** comes with a set of default options:
|
||||
|
||||
- [Azure Active Directory Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/azure-ad-b2c.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
- In https://portal.azure.com/ -> Azure Active Directory create a new App Registration.
|
||||
- Make sure to remember / copy
|
||||
- Application (client) ID
|
||||
@@ -22,13 +31,13 @@ https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tena
|
||||
In `.env.local` create the follwing entries:
|
||||
|
||||
```
|
||||
AZURE_CLIENT_ID=<copy Application (client) ID here>
|
||||
AZURE_CLIENT_ID=<copy Application (client) ID here>
|
||||
AZURE_CLIENT_SECRET=<copy generated secret value here>
|
||||
AZURE_TENANT_ID=<copy the tenant id here>
|
||||
```
|
||||
|
||||
In `pages/api/auth/[...nextauth].js` find or add the AZURE entries:
|
||||
|
||||
|
||||
```js
|
||||
import Providers from 'next-auth/providers';
|
||||
...
|
||||
|
||||
@@ -11,9 +11,18 @@ https://github.com/basecamp/api/blob/master/sections/authentication.md
|
||||
|
||||
https://launchpad.37signals.com/integrations
|
||||
|
||||
## Options
|
||||
|
||||
The **Basecamp Provider** comes with a set of default options:
|
||||
|
||||
- [Basecamp Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/basecamp.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic profile information
|
||||
|
||||
```js
|
||||
import Providers from `next-auth/providers`
|
||||
...
|
||||
@@ -27,7 +36,7 @@ providers: [
|
||||
```
|
||||
|
||||
:::note
|
||||
Using the example above, it is only possible to retrieve profile information such as account id, email and name. If you wish to retrieve user data in relation to a specific team, you must provide a different profileUrl and a custom function to handle profile information as shown in the example below.
|
||||
Using the example above, it is only possible to retrieve profile information such as account id, email and name. If you wish to retrieve user data in relation to a specific team, you must provide a different profileUrl and a custom function to handle profile information as shown in the example below.
|
||||
:::
|
||||
|
||||
### Profile information in relation to specific team
|
||||
@@ -57,4 +66,4 @@ providers: [
|
||||
|
||||
:::tip
|
||||
The BASECAMP_TEAM_ID is found in the url path of your team's homepage. For example, if the url is `https://3.basecamp.com/1234567/projects`, then in this case the BASECAMP_TEAM_ID is 1234567
|
||||
:::
|
||||
:::
|
||||
|
||||
@@ -11,6 +11,14 @@ https://develop.battle.net/documentation/guides/using-oauth
|
||||
|
||||
https://develop.battle.net/access/clients
|
||||
|
||||
## Options
|
||||
|
||||
The **Battle.net Provider** comes with a set of default options:
|
||||
|
||||
- [Battle.net Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/battlenet.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -11,6 +11,14 @@ https://developer.box.com/reference/
|
||||
|
||||
https://developer.box.com/guides/sso-identities-and-app-users/connect-okta-to-app-users/configure-box/
|
||||
|
||||
## Options
|
||||
|
||||
The **Box Provider** comes with a set of default options:
|
||||
|
||||
- [Box Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/box.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -11,6 +11,14 @@ https://github.com/Bungie-net/api/wiki/OAuth-Documentation
|
||||
|
||||
https://www.bungie.net/en/Application
|
||||
|
||||
## Options
|
||||
|
||||
The **Bungie Provider** comes with a set of default options:
|
||||
|
||||
- [Bungie Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/bungie.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
@@ -28,8 +36,6 @@ providers: [
|
||||
...
|
||||
```
|
||||
|
||||
## Instructions
|
||||
|
||||
### Configuration
|
||||
|
||||
:::tip
|
||||
@@ -42,20 +48,20 @@ Bungie doesn't allow you to use localhost as the website URL, instead you need t
|
||||
|
||||
Navigate to https://www.bungie.net/en/Application and fill in the required details:
|
||||
|
||||
* Application name
|
||||
* Application Status
|
||||
* Website
|
||||
* OAuth Client Type
|
||||
- Application name
|
||||
- Application Status
|
||||
- Website
|
||||
- OAuth Client Type
|
||||
- Confidential
|
||||
* Redirect URL
|
||||
- Redirect URL
|
||||
- https://localhost:3000/api/auth/callback/bungie
|
||||
* Scope
|
||||
- Scope
|
||||
- `Access items like your Bungie.net notifications, memberships, and recent Bungie.Net forum activity.`
|
||||
* Origin Header
|
||||
- Origin Header
|
||||
|
||||
The following guide may be helpful:
|
||||
|
||||
* [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||
- [How to setup localhost with HTTPS with a Next.js app](https://medium.com/@anMagpie/secure-your-local-development-server-with-https-next-js-81ac6b8b3d68)
|
||||
|
||||
### Example server
|
||||
|
||||
@@ -75,7 +81,6 @@ Add-Content -Path C:\Windows\System32\drivers\etc\hosts -Value "127.0.0.1`tdev.e
|
||||
|
||||
#### Create certificate
|
||||
|
||||
|
||||
Creating a certificate for localhost is easy with openssl. Just put the following command in the terminal. The output will be two files: localhost.key and localhost.crt.
|
||||
|
||||
```bash
|
||||
@@ -103,32 +108,30 @@ Add environment variable `OPENSSL_CONF=C:/Program Files/Git/mingw64/ssl/openssl.
|
||||
|
||||
Create directory `certificates` and place `localhost.key` and `localhost.crt`
|
||||
|
||||
|
||||
You can create a `server.js` in the root of your project and run it with `node server.js` to test Sign in with Bungie integration locally:
|
||||
|
||||
|
||||
```js
|
||||
const { createServer } = require('https')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const fs = require('fs')
|
||||
const { createServer } = require("https")
|
||||
const { parse } = require("url")
|
||||
const next = require("next")
|
||||
const fs = require("fs")
|
||||
|
||||
const dev = process.env.NODE_ENV !== 'production'
|
||||
const dev = process.env.NODE_ENV !== "production"
|
||||
const app = next({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('./certificates/localhost.key'),
|
||||
cert: fs.readFileSync('./certificates/localhost.crt')
|
||||
key: fs.readFileSync("./certificates/localhost.key"),
|
||||
cert: fs.readFileSync("./certificates/localhost.crt"),
|
||||
}
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer(httpsOptions, (req, res) => {
|
||||
const parsedUrl = parse(req.url, true)
|
||||
handle(req, res, parsedUrl)
|
||||
}).listen(3000, err => {
|
||||
}).listen(3000, (err) => {
|
||||
if (err) throw err
|
||||
console.log('> Ready on https://localhost:3000')
|
||||
console.log("> Ready on https://localhost:3000")
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
@@ -13,6 +13,14 @@ https://console.aws.amazon.com/cognito/users/
|
||||
|
||||
You need to select your AWS region to go the the Cognito dashboard.
|
||||
|
||||
## Options
|
||||
|
||||
The **Amazon Cognito Provider** comes with a set of default options:
|
||||
|
||||
- [Amazon Cognito Provider options](https://github.com/nextauthjs/next-auth/blob/main/src/providers/cognito.js)
|
||||
|
||||
You can override any of the options to suit your own use case.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user