mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
95 Commits
v4.0.0-bet
...
v4.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dcdb62dca | ||
|
|
1f4b7d8089 | ||
|
|
fedb84872d | ||
|
|
c0dddfb77f | ||
|
|
50fe115df6 | ||
|
|
cc17ddf8aa | ||
|
|
8644e553ed | ||
|
|
d1d0db43ea | ||
|
|
b01f6805d3 | ||
|
|
c44b860b9e | ||
|
|
22f74d7c4d | ||
|
|
2570168660 | ||
|
|
187a1474f5 | ||
|
|
4dc76749f2 | ||
|
|
35ee608d59 | ||
|
|
0f132de115 | ||
|
|
31426b9435 | ||
|
|
64b2a2c43b | ||
|
|
7beb3ff03b | ||
|
|
432876c011 | ||
|
|
15d1fab4c8 | ||
|
|
5e803cd34c | ||
|
|
76bf524e8e | ||
|
|
f9e0ef8d18 | ||
|
|
38cefdd548 | ||
|
|
b871b47d8b | ||
|
|
043b252940 | ||
|
|
e9ac11b4b2 | ||
|
|
ba39efb256 | ||
|
|
6502b63e9c | ||
|
|
0d7d8da2d9 | ||
|
|
f998bf2768 | ||
|
|
78fa33312f | ||
|
|
533ed949b3 | ||
|
|
1597369d30 | ||
|
|
41819882be | ||
|
|
b66afcc5cc | ||
|
|
da991de8a4 | ||
|
|
932d05da70 | ||
|
|
e8a58a01b6 | ||
|
|
91de463a5e | ||
|
|
4a9d871698 | ||
|
|
c2119b15de | ||
|
|
0ce15c4a18 | ||
|
|
ead715219a | ||
|
|
8faa7553dd | ||
|
|
90a6a0084b | ||
|
|
cb844a2436 | ||
|
|
74558d6cc2 | ||
|
|
d03125a77b | ||
|
|
66d16f8bf4 | ||
|
|
be74dd0e7e | ||
|
|
9bf867ddcf | ||
|
|
0f460c22da | ||
|
|
887cb00877 | ||
|
|
75ca097ff7 | ||
|
|
bcb9383aec | ||
|
|
b953963101 | ||
|
|
4649f1968b | ||
|
|
45f4a69a4e | ||
|
|
2155c93a3c | ||
|
|
d5958571a4 | ||
|
|
ebecaa6a4b | ||
|
|
1c5173a818 | ||
|
|
35ce332cc6 | ||
|
|
ec295287f1 | ||
|
|
46978ac02f | ||
|
|
f546e550dd | ||
|
|
ac5b4db0f2 | ||
|
|
8bbffdd08c | ||
|
|
a22a0a36fd | ||
|
|
797272afe1 | ||
|
|
13e56bcf2f | ||
|
|
b0f7f87c04 | ||
|
|
9c0851c0f9 | ||
|
|
f5b3c29ab1 | ||
|
|
b4f2a0106a | ||
|
|
9c095b0532 | ||
|
|
0475964a0f | ||
|
|
ad6c13cdc9 | ||
|
|
591aa7cc7e | ||
|
|
9abb392b4e | ||
|
|
b89ae87fb1 | ||
|
|
3687d17724 | ||
|
|
b04ff82fb9 | ||
|
|
c11915ba9c | ||
|
|
24ee459f97 | ||
|
|
ac4851d238 | ||
|
|
84094b0ee7 | ||
|
|
f09ab4a04f | ||
|
|
067364381b | ||
|
|
6ee36b6842 | ||
|
|
5a89ab69d3 | ||
|
|
665445818e | ||
|
|
67cf2a11bb |
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -10,7 +10,13 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide the following information:
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
### Important :exclamation:
|
||||
Please help us maintain this project more efficiently! Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
|
||||
- Docs related: https://github.com/nextauthjs/docs
|
||||
- Adapter related: https://github.com/nextauthjs/adapters
|
||||
|
||||
If you are in the correct repo, then proceed by providing the following information:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
11
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@@ -9,8 +9,14 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library. Please provide the following information:
|
||||
|
||||
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library.
|
||||
### Important :exclamation:
|
||||
Please help us maintain this project more efficiently! Before creating the issue make sure you shouldn't be creating it in one the below repos instead:
|
||||
- Docs related: https://github.com/nextauthjs/docs
|
||||
- Adapter related: https://github.com/nextauthjs/adapters
|
||||
|
||||
If you are in the correct repo, then proceed by providing the following information:
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
@@ -65,4 +71,3 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
It takes a lot of work 🏋🏻♀️ maintaining a library like `next-auth`; any contribution is more than welcome 💚
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -43,6 +43,8 @@ app/package-lock.json
|
||||
app/yarn.lock
|
||||
app/prisma/migrations
|
||||
app/prisma/dev.db*
|
||||
app/dist
|
||||
app/next-auth
|
||||
|
||||
# VS
|
||||
/.vs/slnx.sqlite-journal
|
||||
@@ -50,6 +52,9 @@ app/prisma/dev.db*
|
||||
/.vs
|
||||
.vscode
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
|
||||
29
README.md
29
README.md
@@ -32,6 +32,11 @@ NextAuth.js is a complete open source authentication solution for [Next.js](http
|
||||
|
||||
It is designed from the ground up to support Next.js and Serverless.
|
||||
|
||||
This is the core repo for NextAuth.js. Check the repos below if you are interested in additional information:
|
||||
|
||||
- Docs related: https://github.com/nextauthjs/docs
|
||||
- Adapter related: https://github.com/nextauthjs/adapters
|
||||
|
||||
## Getting Started
|
||||
|
||||
```
|
||||
@@ -81,7 +86,8 @@ Advanced options allow you to define your own routines to handle controlling wha
|
||||
|
||||
### TypeScript
|
||||
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
|
||||
NextAuth.js comes with built-in types. For more information and usage, check out
|
||||
the [TypeScript section](https://next-auth.js.org/getting-started/typescript) in the documentation.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -90,21 +96,24 @@ NextAuth.js comes with built-in types. For more information and usage, check out
|
||||
```javascript
|
||||
// pages/api/auth/[...nextauth].js
|
||||
import NextAuth from "next-auth"
|
||||
import Providers from "next-auth/providers"
|
||||
import AppleProvider from "next-auth/providers/apple"
|
||||
import GoogleProvider from "next-auth/providers/google"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
|
||||
export default NextAuth({
|
||||
secret: process.env.SECRET,
|
||||
providers: [
|
||||
// OAuth authentication providers
|
||||
Providers.Apple({
|
||||
AppleProvider({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: process.env.APPLE_SECRET,
|
||||
}),
|
||||
Providers.Google({
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
}),
|
||||
// Sign in with passwordless email link
|
||||
Providers.Email({
|
||||
EmailProvider({
|
||||
server: process.env.MAIL_SERVER,
|
||||
from: "<no-reply@example.com>",
|
||||
}),
|
||||
@@ -190,6 +199,13 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
|
||||
<div>Prisma</div><br />
|
||||
<sub>🥉 Bronze Financial Sponsor</sub>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://clerk.dev" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/49538330?s=200&v=4" alt="Prisma Logo" />
|
||||
</a><br />
|
||||
<div>Clerk</div><br />
|
||||
<sub>🥉 Bronze Financial Sponsor</sub>
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
<a href="https://checklyhq.com" target="_blank">
|
||||
<img width="128px" src="https://avatars.githubusercontent.com/u/25982255?v=4" alt="Checkly Logo" />
|
||||
@@ -212,7 +228,8 @@ We're happy to announce we've recently created an [OpenCollective](https://openc
|
||||
|
||||
## Contributing
|
||||
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
We're open to all community contributions! If you'd like to contribute in any way, please first read
|
||||
our [Contributing Guide](https://github.com/nextauthjs/next-auth/blob/canary/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
webpack(config) {
|
||||
config.experiments = {
|
||||
topLevelAwait: true,
|
||||
}
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
@@ -18,6 +21,9 @@ module.exports = {
|
||||
|
||||
return config
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"clean": "rm -rf .next",
|
||||
"dev": "npm-run-all --parallel dev:next watch:css copy:css ",
|
||||
"dev:next": "next dev",
|
||||
"build": "next build",
|
||||
"copy:css": "cpx \"../css/**/*\" src/css --watch",
|
||||
"watch:css": "cd .. && npm run watch:css",
|
||||
"start": "next start",
|
||||
|
||||
@@ -23,6 +23,8 @@ import CognitoProvider from "next-auth/providers/cognito"
|
||||
import SlackProvider from "next-auth/providers/slack"
|
||||
import Okta from "next-auth/providers/okta"
|
||||
import AzureB2C from "next-auth/providers/azure-ad-b2c"
|
||||
import OsuProvider from "next-auth/providers/osu"
|
||||
import AppleProvider from "next-auth/providers/apple"
|
||||
|
||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
@@ -42,15 +44,15 @@ export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
// E-mail
|
||||
// Start fake e-mail server with `npm run start:email`
|
||||
EmailProvider({
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
auth: null,
|
||||
secure: false,
|
||||
port: 1025,
|
||||
tls: { rejectUnauthorized: false },
|
||||
},
|
||||
}),
|
||||
// EmailProvider({
|
||||
// server: {
|
||||
// host: "127.0.0.1",
|
||||
// auth: null,
|
||||
// secure: false,
|
||||
// port: 1025,
|
||||
// tls: { rejectUnauthorized: false },
|
||||
// },
|
||||
// }),
|
||||
// Credentials
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
@@ -167,11 +169,16 @@ export const authOptions: NextAuthOptions = {
|
||||
tenantId: process.env.AZURE_B2C_TENANT_ID,
|
||||
primaryUserFlow: process.env.AZURE_B2C_PRIMARY_USER_FLOW,
|
||||
}),
|
||||
OsuProvider({
|
||||
clientId: process.env.OSU_CLIENT_ID,
|
||||
clientSecret: process.env.OSU_CLIENT_SECRET,
|
||||
}),
|
||||
AppleProvider({
|
||||
clientId: process.env.APPLE_ID,
|
||||
clientSecret: process.env.APPLE_SECRET,
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
secret: process.env.SECRET,
|
||||
},
|
||||
secret: process.env.SECRET,
|
||||
debug: true,
|
||||
theme: {
|
||||
colorScheme: "auto",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// This is an example of how to read a JSON Web Token from an API route
|
||||
import jwt from "next-auth/jwt"
|
||||
|
||||
const secret = process.env.SECRET
|
||||
import { getToken } from "next-auth/jwt"
|
||||
|
||||
export default async (req, res) => {
|
||||
const token = await jwt.getToken({ req, secret, encryption: true })
|
||||
const token = await getToken({ req, secret: process.env.SECRET })
|
||||
res.send(JSON.stringify(token, null, 2))
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ body {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
li,
|
||||
|
||||
@@ -31,7 +31,12 @@ module.exports = (api) => {
|
||||
comments: false,
|
||||
overrides: [
|
||||
{
|
||||
test: ["../src/react/index.tsx"],
|
||||
test: [
|
||||
"../src/react/index.tsx",
|
||||
"../src/lib/logger.ts",
|
||||
"../src/core/errors.ts",
|
||||
"../src/client/**",
|
||||
],
|
||||
presets: [
|
||||
["@babel/preset-env", { targets: { ie: 11 } }],
|
||||
["@babel/preset-react", { runtime: "automatic" }],
|
||||
|
||||
40
package-lock.json
generated
40
package-lock.json
generated
@@ -17,9 +17,10 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@panva/hkdf": "^1.0.0",
|
||||
"cookie": "^0.4.1",
|
||||
"jose": "^4.1.2",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.0.1",
|
||||
"openid-client": "^5.0.2",
|
||||
"preact": "^10.5.14",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -4833,7 +4834,6 @@
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -6935,20 +6935,6 @@
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -11464,9 +11450,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.0.1.tgz",
|
||||
"integrity": "sha512-Ks97p8A/RJld7W3NrdNqJ/6danhGONcRkyjSuRZwvllawjEwHhn/88w923CC/L7fBKzbDcmRH7btJ0gPr4AwCQ==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.0.2.tgz",
|
||||
"integrity": "sha512-Z3fuzy5S/ohIBonXdTFYAKIjMAf0ycRMW0Rs1cXSH1UBJDB2CJTcVR5u7MPlPui5lA1f+mQ9DktPNEwrYVoY+A==",
|
||||
"dependencies": {
|
||||
"jose": "^4.1.0",
|
||||
"lru-cache": "^6.0.0",
|
||||
@@ -18593,8 +18579,7 @@
|
||||
"cookie": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.17.2",
|
||||
@@ -20161,13 +20146,6 @@
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
@@ -23550,9 +23528,9 @@
|
||||
}
|
||||
},
|
||||
"openid-client": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.0.1.tgz",
|
||||
"integrity": "sha512-Ks97p8A/RJld7W3NrdNqJ/6danhGONcRkyjSuRZwvllawjEwHhn/88w923CC/L7fBKzbDcmRH7btJ0gPr4AwCQ==",
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.0.2.tgz",
|
||||
"integrity": "sha512-Z3fuzy5S/ohIBonXdTFYAKIjMAf0ycRMW0Rs1cXSH1UBJDB2CJTcVR5u7MPlPui5lA1f+mQ9DktPNEwrYVoY+A==",
|
||||
"requires": {
|
||||
"jose": "^4.1.0",
|
||||
"lru-cache": "^6.0.0",
|
||||
|
||||
29
package.json
29
package.json
@@ -5,6 +5,11 @@
|
||||
"homepage": "https://next-auth.js.org",
|
||||
"repository": "https://github.com/nextauthjs/next-auth.git",
|
||||
"author": "Iain Collins <me@iaincollins.com>",
|
||||
"contributors": [
|
||||
"Balázs Orbán <info@balazsorban.com>",
|
||||
"Nico Domino <yo@ndo.dev>",
|
||||
"Lluis Agusti <hi@llu.lu>"
|
||||
],
|
||||
"main": "index.js",
|
||||
"module": "index.js",
|
||||
"types": "index.d.ts",
|
||||
@@ -62,9 +67,10 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@panva/hkdf": "^1.0.0",
|
||||
"cookie": "^0.4.1",
|
||||
"jose": "^4.1.2",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.0.1",
|
||||
"openid-client": "^5.0.2",
|
||||
"preact": "^10.5.14",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
@@ -138,10 +144,7 @@
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"extends": [
|
||||
"standard-with-typescript",
|
||||
"prettier"
|
||||
],
|
||||
"extends": ["standard-with-typescript", "prettier"],
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"next-env.d.ts",
|
||||
@@ -165,18 +168,12 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"./**/*test.js"
|
||||
],
|
||||
"files": ["./**/*test.js"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:jest/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"jest"
|
||||
]
|
||||
"extends": ["plugin:jest/recommended"],
|
||||
"plugins": ["jest"]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -210,6 +207,10 @@
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/balazsorban44"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nextauth"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export async function fetchData<T = any>(
|
||||
return Object.keys(data).length > 0 ? data : null // Return null if data empty
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_FETCH_ERROR", {
|
||||
error,
|
||||
error: error as Error,
|
||||
path,
|
||||
...(req ? { header: req.headers } : {}),
|
||||
})
|
||||
@@ -84,9 +84,9 @@ export function BroadcastChannel(name = "nextauth.message") {
|
||||
return {
|
||||
/** Get notified by other tabs/windows. */
|
||||
receive(onReceive: (message: BroadcastMessage) => void) {
|
||||
const handler = (event) => {
|
||||
const handler = (event: StorageEvent) => {
|
||||
if (event.key !== name) return
|
||||
const message: BroadcastMessage = JSON.parse(event.newValue)
|
||||
const message: BroadcastMessage = JSON.parse(event.newValue ?? "{}")
|
||||
if (message?.event !== "session" || !message?.data) return
|
||||
|
||||
onReceive(message)
|
||||
@@ -95,7 +95,7 @@ export function BroadcastChannel(name = "nextauth.message") {
|
||||
return () => window.removeEventListener("storage", handler)
|
||||
},
|
||||
/** Notify other tabs/windows. */
|
||||
post(message) {
|
||||
post(message: Record<string, unknown>) {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(
|
||||
name,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { EventCallbacks, LoggerInstance } from ".."
|
||||
import { Adapter } from "../adapters"
|
||||
import type { EventCallbacks, LoggerInstance } from ".."
|
||||
import type { Adapter } from "../adapters"
|
||||
|
||||
/**
|
||||
* Same as the default `Error`, but it is JSON serializable.
|
||||
* @source https://iaincollins.medium.com/error-handling-in-javascript-a6172ccdf9af
|
||||
*/
|
||||
export class UnknownError extends Error {
|
||||
constructor(error) {
|
||||
code: string
|
||||
constructor(error: Error | string) {
|
||||
// Support passing error or string
|
||||
super(error?.message ?? error)
|
||||
super((error as Error)?.message ?? error)
|
||||
this.name = "UnknownError"
|
||||
this.code = (error as any).code
|
||||
if (error instanceof Error) {
|
||||
this.stack = error.stack
|
||||
}
|
||||
@@ -36,6 +38,31 @@ export class AccountNotLinkedError extends UnknownError {
|
||||
name = "AccountNotLinkedError"
|
||||
}
|
||||
|
||||
export class MissingAPIRoute extends UnknownError {
|
||||
name = "MissingAPIRouteError"
|
||||
code = "MISSING_NEXTAUTH_API_ROUTE_ERROR"
|
||||
}
|
||||
|
||||
export class MissingSecret extends UnknownError {
|
||||
name = "MissingSecretError"
|
||||
code = "NO_SECRET"
|
||||
}
|
||||
|
||||
export class MissingAuthorize extends UnknownError {
|
||||
name = "MissingAuthorizeError"
|
||||
code = "CALLBACK_CREDENTIALS_HANDLER_ERROR"
|
||||
}
|
||||
|
||||
export class MissingAdapter extends UnknownError {
|
||||
name = "MissingAdapterError"
|
||||
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
|
||||
}
|
||||
|
||||
export class UnsupportedStrategy extends UnknownError {
|
||||
name = "UnsupportedStrategyError"
|
||||
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
|
||||
}
|
||||
|
||||
type Method = (...args: any[]) => Promise<any>
|
||||
|
||||
export function upperSnake(s: string) {
|
||||
@@ -56,10 +83,10 @@ export function eventsErrorHandler(
|
||||
return Object.keys(methods).reduce<any>((acc, name) => {
|
||||
acc[name] = async (...args: any[]) => {
|
||||
try {
|
||||
const method: Method = methods[name]
|
||||
const method: Method = methods[name as keyof Method]
|
||||
return await method(...args)
|
||||
} catch (e) {
|
||||
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e)
|
||||
logger.error(`${upperSnake(name)}_EVENT_ERROR`, e as Error)
|
||||
}
|
||||
}
|
||||
return acc
|
||||
@@ -77,11 +104,11 @@ export function adapterErrorHandler(
|
||||
acc[name] = async (...args: any[]) => {
|
||||
try {
|
||||
logger.debug(`adapter_${name}`, { args })
|
||||
const method: Method = adapter[name as any]
|
||||
const method: Method = adapter[name as keyof Method]
|
||||
return await method(...args)
|
||||
} catch (error) {
|
||||
logger.error(`adapter_error_${name}`, error)
|
||||
const e = new UnknownError(error)
|
||||
logger.error(`adapter_error_${name}`, error as Error)
|
||||
const e = new UnknownError(error as Error)
|
||||
e.name = `${capitalize(name)}Error`
|
||||
throw e
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import logger from "../lib/logger"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import * as routes from "./routes"
|
||||
import renderPage from "./pages"
|
||||
import type { NextAuthOptions } from "./types"
|
||||
import { init } from "./init"
|
||||
import { Cookie } from "./lib/cookie"
|
||||
import { assertConfig } from "./lib/assert"
|
||||
import { SessionStore } from "./lib/cookie"
|
||||
|
||||
import { NextAuthAction } from "../lib/types"
|
||||
import type { NextAuthOptions } from "./types"
|
||||
import type { NextAuthAction } from "../lib/types"
|
||||
import type { Cookie } from "./lib/cookie"
|
||||
import type { ErrorType } from "./pages/error"
|
||||
|
||||
export interface IncomingRequest {
|
||||
/** @default "http://localhost:3000" */
|
||||
host?: string
|
||||
method: string
|
||||
cookies?: Record<string, any>
|
||||
host: string
|
||||
method?: string
|
||||
cookies?: Record<string, string>
|
||||
headers?: Record<string, any>
|
||||
query?: Record<string, any>
|
||||
body?: Record<string, any>
|
||||
@@ -35,7 +38,7 @@ export interface OutgoingResponse<
|
||||
cookies?: Cookie[]
|
||||
}
|
||||
|
||||
interface NextAuthHandlerParams {
|
||||
export interface NextAuthHandlerParams {
|
||||
req: IncomingRequest
|
||||
options: NextAuthOptions
|
||||
}
|
||||
@@ -44,7 +47,27 @@ export async function NextAuthHandler<
|
||||
Body extends string | Record<string, any> | any[]
|
||||
>(params: NextAuthHandlerParams): Promise<OutgoingResponse<Body>> {
|
||||
const { options: userOptions, req } = params
|
||||
const { action, providerId, error } = req
|
||||
|
||||
setLogger(userOptions.logger, userOptions.debug)
|
||||
|
||||
const assertionResult = assertConfig(params)
|
||||
|
||||
if (typeof assertionResult === "string") {
|
||||
logger.warn(assertionResult)
|
||||
} else if (assertionResult instanceof Error) {
|
||||
// Bail out early if there's an error in the user config
|
||||
const { pages, theme } = userOptions
|
||||
logger.error(assertionResult.code, assertionResult)
|
||||
if (pages?.error) {
|
||||
return {
|
||||
redirect: `${pages.error}?error=Configuration`,
|
||||
}
|
||||
}
|
||||
const render = renderPage({ theme })
|
||||
return render.error({ error: "configuration" })
|
||||
}
|
||||
|
||||
const { action, providerId, error, method = "GET" } = req
|
||||
|
||||
const { options, cookies } = await init({
|
||||
userOptions,
|
||||
@@ -54,23 +77,23 @@ export async function NextAuthHandler<
|
||||
callbackUrl: req.body?.callbackUrl ?? req.query?.callbackUrl,
|
||||
csrfToken: req.body?.csrfToken,
|
||||
cookies: req.cookies,
|
||||
isPost: req.method === "POST",
|
||||
isPost: method === "POST",
|
||||
})
|
||||
|
||||
const sessionToken =
|
||||
req.cookies?.[options.cookies.sessionToken.name] ||
|
||||
req.headers?.Authorization?.replace("Bearer ", "")
|
||||
const sessionStore = new SessionStore(
|
||||
options.cookies.sessionToken,
|
||||
req,
|
||||
options.logger
|
||||
)
|
||||
|
||||
const codeVerifier = req.cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
|
||||
if (req.method === "GET") {
|
||||
const render = renderPage({ options, query: req.query, cookies })
|
||||
if (method === "GET") {
|
||||
const render = renderPage({ ...options, query: req.query, cookies })
|
||||
const { pages } = options
|
||||
switch (action) {
|
||||
case "providers":
|
||||
return (await routes.providers(options.providers)) as any
|
||||
case "session":
|
||||
return (await routes.session({ options, sessionToken })) as any
|
||||
return (await routes.session({ options, sessionStore })) as any
|
||||
case "csrf":
|
||||
return {
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
@@ -96,11 +119,11 @@ export async function NextAuthHandler<
|
||||
const callback = await routes.callback({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
cookies: req.cookies,
|
||||
method,
|
||||
options,
|
||||
sessionToken,
|
||||
codeVerifier,
|
||||
sessionStore,
|
||||
})
|
||||
if (callback.cookies) cookies.push(...callback.cookies)
|
||||
return { ...callback, cookies }
|
||||
@@ -139,10 +162,10 @@ export async function NextAuthHandler<
|
||||
return { redirect: `${options.url}/signin?error=${error}`, cookies }
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
return render.error({ error: error as ErrorType })
|
||||
default:
|
||||
}
|
||||
} else if (req.method === "POST") {
|
||||
} else if (method === "POST") {
|
||||
switch (action) {
|
||||
case "signin":
|
||||
// Verified CSRF Token required for all sign in routes
|
||||
@@ -160,7 +183,7 @@ export async function NextAuthHandler<
|
||||
case "signout":
|
||||
// Verified CSRF Token required for signout
|
||||
if (options.csrfTokenVerified) {
|
||||
const signout = await routes.signout({ options, sessionToken })
|
||||
const signout = await routes.signout({ options, sessionStore })
|
||||
if (signout.cookies) cookies.push(...signout.cookies)
|
||||
return { ...signout, cookies }
|
||||
}
|
||||
@@ -178,11 +201,11 @@ export async function NextAuthHandler<
|
||||
const callback = await routes.callback({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
cookies: req.cookies,
|
||||
method,
|
||||
options,
|
||||
sessionToken,
|
||||
codeVerifier,
|
||||
sessionStore,
|
||||
})
|
||||
if (callback.cookies) cookies.push(...callback.cookies)
|
||||
return { ...callback, cookies }
|
||||
@@ -195,7 +218,7 @@ export async function NextAuthHandler<
|
||||
logger[level](code, metadata)
|
||||
} catch (error) {
|
||||
// If logging itself failed...
|
||||
logger.error("LOGGER_ERROR", error)
|
||||
logger.error("LOGGER_ERROR", error as Error)
|
||||
}
|
||||
}
|
||||
return {}
|
||||
@@ -205,6 +228,6 @@ export async function NextAuthHandler<
|
||||
|
||||
return {
|
||||
status: 400,
|
||||
body: `Error: Action ${action} with HTTP ${req.method} is not supported by NextAuth.js` as any,
|
||||
body: `Error: Action ${action} with HTTP ${method} is not supported by NextAuth.js` as any,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,6 @@ export async function init({
|
||||
options: InternalOptions
|
||||
cookies: cookie.Cookie[]
|
||||
}> {
|
||||
// If debug enabled, set ENV VAR so that logger logs debug messages
|
||||
if (userOptions.debug) {
|
||||
;(process.env._NEXTAUTH_DEBUG as any) = true
|
||||
}
|
||||
|
||||
const url = parseUrl(host)
|
||||
|
||||
const secret = createSecret({ userOptions, url })
|
||||
@@ -85,7 +80,8 @@ export async function init({
|
||||
providers,
|
||||
// Session options
|
||||
session: {
|
||||
jwt: !userOptions.adapter, // If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
// If no adapter specified, force use of JSON Web Tokens (stateless)
|
||||
strategy: userOptions.adapter ? "database" : "jwt",
|
||||
maxAge,
|
||||
updateAge: 24 * 60 * 60,
|
||||
...userOptions.session,
|
||||
|
||||
78
src/core/lib/assert.ts
Normal file
78
src/core/lib/assert.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
MissingAdapter,
|
||||
MissingAPIRoute,
|
||||
MissingAuthorize,
|
||||
MissingSecret,
|
||||
UnsupportedStrategy,
|
||||
} from "../errors"
|
||||
|
||||
import type { NextAuthHandlerParams } from ".."
|
||||
import type { WarningCode } from "../../lib/logger"
|
||||
|
||||
type ConfigError =
|
||||
| MissingAPIRoute
|
||||
| MissingSecret
|
||||
| UnsupportedStrategy
|
||||
| MissingAuthorize
|
||||
| MissingAdapter
|
||||
|
||||
/**
|
||||
* Verify that the user configured `next-auth` correctly.
|
||||
* Good place to mention deprecations as well.
|
||||
*
|
||||
* REVIEW: Make some of these and corresponding docs less Next.js specific?
|
||||
*/
|
||||
export function assertConfig(
|
||||
params: NextAuthHandlerParams
|
||||
): ConfigError | WarningCode | undefined {
|
||||
const { options, req } = params
|
||||
|
||||
// req.query isn't defined when asserting `getServerSession` for example
|
||||
if (!req.query?.nextauth && !req.action) {
|
||||
return new MissingAPIRoute(
|
||||
"Cannot find [...nextauth].{js,ts} in `/pages/api/auth`. Make sure the filename is written correctly."
|
||||
)
|
||||
}
|
||||
|
||||
if (!options.secret) {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return new MissingSecret("Please define a `secret` in production.")
|
||||
} else {
|
||||
return "NO_SECRET"
|
||||
}
|
||||
}
|
||||
|
||||
if (!req.host) return "NEXTAUTH_URL"
|
||||
|
||||
let hasCredentials, hasEmail
|
||||
|
||||
options.providers.forEach(({ type }) => {
|
||||
if (type === "credentials") hasCredentials = true
|
||||
else if (type === "email") hasEmail = true
|
||||
})
|
||||
|
||||
if (hasCredentials) {
|
||||
const dbStrategy = options.session?.strategy === "database"
|
||||
const onlyCredentials = !options.providers.some(
|
||||
(p) => p.type !== "credentials"
|
||||
)
|
||||
if (dbStrategy && onlyCredentials) {
|
||||
return new UnsupportedStrategy(
|
||||
"Signin in with credentials only supported if JWT strategy is enabled"
|
||||
)
|
||||
}
|
||||
|
||||
const credentialsNoAuthorize = options.providers.some(
|
||||
(p) => p.type === "credentials" && !p.authorize
|
||||
)
|
||||
if (credentialsNoAuthorize) {
|
||||
return new MissingAuthorize(
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEmail && !options.adapter) {
|
||||
return new MissingAdapter("E-mail login requires an adapter.")
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export default async function callbackHandler(params: {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
session: { jwt: useJwtSession },
|
||||
session: { strategy: sessionStrategy },
|
||||
} = options
|
||||
|
||||
// If no adapter is configured then we don't have a database and cannot
|
||||
@@ -61,6 +61,8 @@ export default async function callbackHandler(params: {
|
||||
let user: AdapterUser | null = null
|
||||
let isNewUser = false
|
||||
|
||||
const useJwtSession = sessionStrategy === "jwt"
|
||||
|
||||
if (sessionToken) {
|
||||
if (useJwtSession) {
|
||||
try {
|
||||
|
||||
@@ -1,148 +1,46 @@
|
||||
// REVIEW: Is there any way to defer two types of strings?
|
||||
import type { IncomingHttpHeaders } from "http"
|
||||
import type { CookiesOptions } from "../.."
|
||||
import type { CookieOption, LoggerInstance, SessionStrategy } from "../types"
|
||||
|
||||
import { CookiesOptions } from "../.."
|
||||
import { CookieOption } from "../types"
|
||||
// Uncomment to recalculate the estimated size
|
||||
// of an empty session cookie
|
||||
// import { serialize } from "cookie"
|
||||
// console.log(
|
||||
// "Cookie estimated to be ",
|
||||
// serialize(`__Secure.next-auth.session-token.0`, "", {
|
||||
// expires: new Date(),
|
||||
// httpOnly: true,
|
||||
// maxAge: Number.MAX_SAFE_INTEGER,
|
||||
// path: "/",
|
||||
// sameSite: "strict",
|
||||
// secure: true,
|
||||
// domain: "example.com",
|
||||
// }).length,
|
||||
// " bytes"
|
||||
// )
|
||||
|
||||
const ALLOWED_COOKIE_SIZE = 4096
|
||||
// Based on commented out section above
|
||||
const ESTIMATED_EMPTY_COOKIE_SIZE = 163
|
||||
const CHUNK_SIZE = ALLOWED_COOKIE_SIZE - ESTIMATED_EMPTY_COOKIE_SIZE
|
||||
|
||||
// REVIEW: Is there any way to defer two types of strings?
|
||||
|
||||
/** Stringified form of `JWT`. Extract the content with `jwt.decode` */
|
||||
export type JWTString = string
|
||||
|
||||
/** If `options.session.jwt` is set to `true`, this is a stringified `JWT`. In case of a database persisted session, this is the `sessionToken` of the session in the database.. */
|
||||
export type SessionToken<T extends "jwt" | "db" = "jwt"> = T extends "jwt"
|
||||
? JWTString
|
||||
: string
|
||||
export type SetCookieOptions = Partial<CookieOption["options"]> & {
|
||||
expires?: Date | string
|
||||
encode?: (val: unknown) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to set cookies server side
|
||||
*
|
||||
* Credit to @huv1k and @jshttp contributors for the code which this is based on (MIT License).
|
||||
* * https://github.com/jshttp/cookie/blob/master/index.js
|
||||
* * https://github.com/zeit/next.js/blob/master/examples/api-routes-middleware/utils/cookies.js
|
||||
*
|
||||
* As only partial functionlity is required, only the code we need has been incorporated here
|
||||
* (with fixes for specific issues) to keep dependancy size down.
|
||||
* If `options.session.strategy` is set to `jwt`, this is a stringified `JWT`.
|
||||
* In case of `strategy: "database"`, this is the `sessionToken` of the session in the database.
|
||||
*/
|
||||
export function set(
|
||||
res,
|
||||
name,
|
||||
value,
|
||||
options: {
|
||||
expires?: Date
|
||||
maxAge?: number
|
||||
} = {}
|
||||
) {
|
||||
const stringValue =
|
||||
typeof value === "object" ? "j:" + JSON.stringify(value) : String(value)
|
||||
|
||||
if ("maxAge" in options) {
|
||||
options.expires = new Date(Date.now() + (options.maxAge ?? 0))
|
||||
options.maxAge = (options.maxAge ?? 0) / 1000
|
||||
}
|
||||
|
||||
// Preserve any existing cookies that have already been set in the same session
|
||||
let setCookieHeader = res.getHeader("Set-Cookie") || []
|
||||
// If not an array (i.e. a string with a single cookie) convert it into an array
|
||||
if (!Array.isArray(setCookieHeader)) {
|
||||
setCookieHeader = [setCookieHeader]
|
||||
}
|
||||
setCookieHeader.push(_serialize(name, String(stringValue), options))
|
||||
res.setHeader("Set-Cookie", setCookieHeader)
|
||||
}
|
||||
|
||||
function _serialize(name, val, options) {
|
||||
const fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
|
||||
|
||||
const opt = options || {}
|
||||
const enc = opt.encode || encodeURIComponent
|
||||
|
||||
if (typeof enc !== "function") {
|
||||
throw new TypeError("option encode is invalid")
|
||||
}
|
||||
|
||||
if (!fieldContentRegExp.test(name)) {
|
||||
throw new TypeError("argument name is invalid")
|
||||
}
|
||||
|
||||
const value = enc(val)
|
||||
|
||||
if (value && !fieldContentRegExp.test(value)) {
|
||||
throw new TypeError("argument val is invalid")
|
||||
}
|
||||
|
||||
let str = `${name}=${value}`
|
||||
|
||||
if (opt.maxAge != null) {
|
||||
const maxAge = opt.maxAge - 0
|
||||
|
||||
if (isNaN(maxAge) || !isFinite(maxAge)) {
|
||||
throw new TypeError("option maxAge is invalid")
|
||||
}
|
||||
|
||||
str += `; Max-Age=${Math.floor(maxAge)}`
|
||||
}
|
||||
|
||||
if (opt.domain) {
|
||||
if (!fieldContentRegExp.test(opt.domain)) {
|
||||
throw new TypeError("option domain is invalid")
|
||||
}
|
||||
|
||||
str += `; Domain=${opt.domain}`
|
||||
}
|
||||
|
||||
if (opt.path) {
|
||||
if (!fieldContentRegExp.test(opt.path)) {
|
||||
throw new TypeError("option path is invalid")
|
||||
}
|
||||
|
||||
str += `; Path=${opt.path}`
|
||||
} else {
|
||||
str += "; Path=/"
|
||||
}
|
||||
|
||||
if (opt.expires) {
|
||||
let expires = opt.expires
|
||||
if (typeof opt.expires.toUTCString === "function") {
|
||||
expires = opt.expires.toUTCString()
|
||||
} else {
|
||||
const dateExpires = new Date(opt.expires)
|
||||
expires = dateExpires.toUTCString()
|
||||
}
|
||||
str += `; Expires=${expires}`
|
||||
}
|
||||
|
||||
if (opt.httpOnly) {
|
||||
str += "; HttpOnly"
|
||||
}
|
||||
|
||||
if (opt.secure) {
|
||||
str += "; Secure"
|
||||
}
|
||||
|
||||
if (opt.sameSite) {
|
||||
const sameSite =
|
||||
typeof opt.sameSite === "string"
|
||||
? opt.sameSite.toLowerCase()
|
||||
: opt.sameSite
|
||||
|
||||
switch (sameSite) {
|
||||
case true:
|
||||
str += "; SameSite=Strict"
|
||||
break
|
||||
case "lax":
|
||||
str += "; SameSite=Lax"
|
||||
break
|
||||
case "strict":
|
||||
str += "; SameSite=Strict"
|
||||
break
|
||||
case "none":
|
||||
str += "; SameSite=None"
|
||||
break
|
||||
default:
|
||||
throw new TypeError("option sameSite is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
export type SessionToken<T extends SessionStrategy = "jwt"> = T extends "jwt"
|
||||
? JWTString
|
||||
: string
|
||||
|
||||
/**
|
||||
* Use secure cookies if the site uses HTTPS
|
||||
@@ -154,7 +52,7 @@ function _serialize(name, val, options) {
|
||||
*
|
||||
* @TODO Review cookie settings (names, options)
|
||||
*/
|
||||
export function defaultCookies(useSecureCookies): CookiesOptions {
|
||||
export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
const cookiePrefix = useSecureCookies ? "__Secure-" : ""
|
||||
return {
|
||||
// default cookie options
|
||||
@@ -195,9 +93,120 @@ export function defaultCookies(useSecureCookies): CookiesOptions {
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
name: `${cookiePrefix}next-auth.state`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: useSecureCookies,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export interface Cookie extends CookieOption {
|
||||
value: string
|
||||
}
|
||||
|
||||
type Chunks = Record<string, string>
|
||||
|
||||
export class SessionStore {
|
||||
#chunks: Chunks = {}
|
||||
#option: CookieOption
|
||||
#logger: LoggerInstance | Console
|
||||
|
||||
constructor(
|
||||
option: CookieOption,
|
||||
req: {
|
||||
cookies?: Record<string, string>
|
||||
headers?: Record<string, string> | IncomingHttpHeaders
|
||||
},
|
||||
logger: LoggerInstance | Console
|
||||
) {
|
||||
this.#logger = logger
|
||||
this.#option = option
|
||||
|
||||
if (!req) return
|
||||
|
||||
for (const name in req.cookies) {
|
||||
if (name.startsWith(option.name)) {
|
||||
this.#chunks[name] = req.cookies[name]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return Object.values(this.#chunks)?.join("")
|
||||
}
|
||||
|
||||
/** Given a cookie, return a list of cookies, chunked to fit the allowed cookie size. */
|
||||
#chunk(cookie: Cookie): Cookie[] {
|
||||
const chunkCount = Math.ceil(cookie.value.length / CHUNK_SIZE)
|
||||
|
||||
if (chunkCount === 1) {
|
||||
this.#chunks[cookie.name] = cookie.value
|
||||
return [cookie]
|
||||
}
|
||||
|
||||
const cookies: Cookie[] = []
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const name = `${cookie.name}.${i}`
|
||||
const value = cookie.value.substr(i * CHUNK_SIZE, CHUNK_SIZE)
|
||||
cookies.push({ ...cookie, name, value })
|
||||
this.#chunks[name] = value
|
||||
}
|
||||
|
||||
this.#logger.debug("CHUNKING_SESSION_COOKIE", {
|
||||
message: `Session cookie exceeds allowed ${ALLOWED_COOKIE_SIZE} bytes.`,
|
||||
emptyCookieSize: ESTIMATED_EMPTY_COOKIE_SIZE,
|
||||
valueSize: cookie.value.length,
|
||||
chunks: cookies.map((c) => c.value.length + ESTIMATED_EMPTY_COOKIE_SIZE),
|
||||
})
|
||||
|
||||
return cookies
|
||||
}
|
||||
|
||||
/** Returns cleaned cookie chunks. */
|
||||
#clean(): Record<string, Cookie> {
|
||||
const cleanedChunks: Record<string, Cookie> = {}
|
||||
for (const name in this.#chunks) {
|
||||
delete this.#chunks?.[name]
|
||||
cleanedChunks[name] = {
|
||||
name,
|
||||
value: "",
|
||||
options: { ...this.#option.options, maxAge: 0 },
|
||||
}
|
||||
}
|
||||
return cleanedChunks
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a cookie value, return new cookies, chunked, to fit the allowed cookie size.
|
||||
* If the cookie has changed from chunked to unchunked or vice versa,
|
||||
* it deletes the old cookies as well.
|
||||
*/
|
||||
chunk(value: string, options: Partial<Cookie["options"]>): Cookie[] {
|
||||
// Assume all cookies should be cleaned by default
|
||||
const cookies: Record<string, Cookie> = this.#clean()
|
||||
|
||||
// Calculate new chunks
|
||||
const chunked = this.#chunk({
|
||||
name: this.#option.name,
|
||||
value,
|
||||
options: { ...this.#option.options, ...options },
|
||||
})
|
||||
|
||||
// Update stored chunks / cookies
|
||||
for (const chunk of chunked) {
|
||||
cookies[chunk.name] = chunk
|
||||
}
|
||||
|
||||
return Object.values(cookies)
|
||||
}
|
||||
|
||||
/** Returns a list of cookies that should be cleaned. */
|
||||
clean(): Cookie[] {
|
||||
return Object.values(this.#clean())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export const defaultCallbacks: CallbacksOptions = {
|
||||
},
|
||||
redirect({ url, baseUrl }) {
|
||||
if (url.startsWith(baseUrl)) return url
|
||||
else if (url.startsWith("/")) return new URL(url, baseUrl).toString()
|
||||
return baseUrl
|
||||
},
|
||||
session({ session }) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function email(
|
||||
logger.error("SEND_VERIFICATION_EMAIL_ERROR", {
|
||||
identifier,
|
||||
url,
|
||||
error,
|
||||
error: error as Error,
|
||||
})
|
||||
throw new Error("SEND_VERIFICATION_EMAIL_ERROR")
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ import { openidClient } from "./client"
|
||||
import { oAuth1Client } from "./client-legacy"
|
||||
import { createState } from "./state-handler"
|
||||
import { createPKCE } from "./pkce-handler"
|
||||
import { InternalOptions } from "../../../lib/types"
|
||||
import { IncomingRequest } from "../.."
|
||||
import { Cookie } from "../cookie"
|
||||
|
||||
import type { AuthorizationParameters } from "openid-client"
|
||||
import type { InternalOptions } from "../../../lib/types"
|
||||
import type { IncomingRequest } from "../.."
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -48,26 +50,30 @@ export default async function getAuthorizationUrl(params: {
|
||||
return { redirect: url }
|
||||
}
|
||||
|
||||
const cookies: Cookie[] = []
|
||||
const client = await openidClient(options)
|
||||
|
||||
const authorizationParams: AuthorizationParameters = params
|
||||
const cookies: Cookie[] = []
|
||||
|
||||
const state = await createState(options)
|
||||
if (state) {
|
||||
authorizationParams.state = state.value
|
||||
cookies.push(state.cookie)
|
||||
}
|
||||
|
||||
const pkce = await createPKCE(options)
|
||||
if (pkce?.cookie) {
|
||||
if (pkce) {
|
||||
authorizationParams.code_challenge = pkce.code_challenge
|
||||
authorizationParams.code_challenge_method = pkce.code_challenge_method
|
||||
cookies.push(pkce.cookie)
|
||||
}
|
||||
|
||||
const url = client.authorizationUrl({
|
||||
...params,
|
||||
...pkce,
|
||||
state: createState(options),
|
||||
})
|
||||
const url = client.authorizationUrl(authorizationParams)
|
||||
|
||||
logger.debug("GET_AUTHORIZATION_URL", { url })
|
||||
return {
|
||||
redirect: url,
|
||||
cookies,
|
||||
}
|
||||
logger.debug("GET_AUTHORIZATION_URL", { url, cookies })
|
||||
return { redirect: url, cookies }
|
||||
} catch (error) {
|
||||
logger.error("GET_AUTHORIZATION_URL_ERROR", error)
|
||||
logger.error("GET_AUTHORIZATION_URL_ERROR", error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { TokenSet } from "openid-client"
|
||||
import { CallbackParamsType, TokenSet } from "openid-client"
|
||||
import { openidClient } from "./client"
|
||||
import { oAuth1Client } from "./client-legacy"
|
||||
import { getState } from "./state-handler"
|
||||
import { useState } from "./state-handler"
|
||||
import { usePKCECodeVerifier } from "./pkce-handler"
|
||||
import { OAuthCallbackError } from "../../errors"
|
||||
import { Account, LoggerInstance, Profile } from "../../.."
|
||||
import { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||
import { InternalOptions } from "../../../lib/types"
|
||||
import { IncomingRequest, OutgoingResponse } from "../.."
|
||||
|
||||
import type { Account, LoggerInstance, Profile } from "../../.."
|
||||
import type { OAuthChecks, OAuthConfig } from "../../../providers"
|
||||
import type { InternalOptions } from "../../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from "../.."
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
export default async function oAuthCallback(params: {
|
||||
options: InternalOptions<"oauth">
|
||||
query: IncomingRequest["query"]
|
||||
body: IncomingRequest["body"]
|
||||
method: IncomingRequest["method"]
|
||||
codeVerifier?: string
|
||||
method: Required<IncomingRequest>["method"]
|
||||
cookies: IncomingRequest["cookies"]
|
||||
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
|
||||
const { options, query, body, method, codeVerifier } = params
|
||||
const { options, query, body, method, cookies } = params
|
||||
const { logger, provider } = options
|
||||
|
||||
const errorMessage = body?.error ?? query?.error
|
||||
@@ -38,14 +40,14 @@ export default async function oAuthCallback(params: {
|
||||
const { oauth_token, oauth_verifier } = query ?? {}
|
||||
// @ts-expect-error
|
||||
const tokens: TokenSet = await client.getOAuthAccessToken(
|
||||
oauth_token,
|
||||
oauth_token as string,
|
||||
// @ts-expect-error
|
||||
null,
|
||||
oauth_verifier
|
||||
)
|
||||
// @ts-expect-error
|
||||
let profile: Profile = await client.get(
|
||||
provider.profileUrl,
|
||||
(provider as any).profileUrl,
|
||||
tokens.oauth_token,
|
||||
tokens.oauth_token_secret
|
||||
)
|
||||
@@ -56,7 +58,7 @@ export default async function oAuthCallback(params: {
|
||||
|
||||
return await getProfile({ profile, tokens, provider, logger })
|
||||
} catch (error) {
|
||||
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error)
|
||||
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -66,15 +68,24 @@ export default async function oAuthCallback(params: {
|
||||
|
||||
let tokens: TokenSet
|
||||
|
||||
const pkce = await usePKCECodeVerifier({
|
||||
options,
|
||||
codeVerifier,
|
||||
})
|
||||
const checks: OAuthChecks = {
|
||||
code_verifier: pkce?.codeVerifier,
|
||||
state: getState(options),
|
||||
const checks: OAuthChecks = {}
|
||||
const resCookies: Cookie[] = []
|
||||
|
||||
const state = await useState(cookies?.[options.cookies.state.name], options)
|
||||
|
||||
if (state) {
|
||||
checks.state = state.value
|
||||
resCookies.push(state.cookie)
|
||||
}
|
||||
const params = {
|
||||
|
||||
const codeVerifier = cookies?.[options.cookies.pkceCodeVerifier.name]
|
||||
const pkce = await usePKCECodeVerifier(codeVerifier, options)
|
||||
if (pkce) {
|
||||
checks.code_verifier = pkce.codeVerifier
|
||||
resCookies.push(pkce.cookie)
|
||||
}
|
||||
|
||||
const params: CallbackParamsType = {
|
||||
...client.callbackParams({
|
||||
url: `http://n?${new URLSearchParams(query)}`,
|
||||
// TODO: Ask to allow object to be passed upstream:
|
||||
@@ -126,23 +137,19 @@ export default async function oAuthCallback(params: {
|
||||
})
|
||||
}
|
||||
|
||||
// If a user object is supplied (e.g. Apple provider) add it to the profile object
|
||||
// TODO: Remove/extract to Apple provider?
|
||||
profile.user = JSON.parse(body?.user ?? query?.user ?? null)
|
||||
|
||||
const profileResult = await getProfile({
|
||||
profile,
|
||||
provider,
|
||||
tokens,
|
||||
logger,
|
||||
})
|
||||
return {
|
||||
...profileResult,
|
||||
cookies: pkce?.cookie ? [pkce.cookie] : undefined,
|
||||
}
|
||||
return { ...profileResult, cookies: resCookies }
|
||||
} catch (error) {
|
||||
logger.error("OAUTH_CALLBACK_ERROR", { error, providerId: provider.id })
|
||||
throw new OAuthCallbackError(error)
|
||||
logger.error("OAUTH_CALLBACK_ERROR", {
|
||||
error: error as Error,
|
||||
providerId: provider.id,
|
||||
})
|
||||
throw new OAuthCallbackError(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +198,10 @@ async function getProfile({
|
||||
// all providers, so we return an empty object; the user should then be
|
||||
// redirected back to the sign up page. We log the error to help developers
|
||||
// who might be trying to debug this when configuring a new provider.
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", { error, OAuthProfile })
|
||||
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
|
||||
error: error as Error,
|
||||
OAuthProfile,
|
||||
})
|
||||
return {
|
||||
profile: null,
|
||||
account: null,
|
||||
|
||||
@@ -40,11 +40,11 @@ export function oAuth1Client(options: InternalOptions<"oauth">) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
originalGetOAuth1AccessToken(
|
||||
...args,
|
||||
(error, oauth_token, oauth_token_secret) => {
|
||||
(error: any, oauth_token: any, oauth_token_secret: any) => {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve({ oauth_token, oauth_token_secret })
|
||||
resolve({ oauth_token, oauth_token_secret } as any)
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -60,7 +60,7 @@ export function oAuth1Client(options: InternalOptions<"oauth">) {
|
||||
if (error) {
|
||||
return reject(error)
|
||||
}
|
||||
resolve({ oauth_token, oauth_token_secret, params })
|
||||
resolve({ oauth_token, oauth_token_secret, params } as any)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Issuer, Client } from "openid-client"
|
||||
import { Issuer, Client, custom } from "openid-client"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@ export async function openidClient(
|
||||
): Promise<Client> {
|
||||
const provider = options.provider
|
||||
|
||||
let issuer
|
||||
let issuer: Issuer
|
||||
if (provider.wellKnown) {
|
||||
issuer = await Issuer.discover(provider.wellKnown)
|
||||
} else {
|
||||
@@ -31,13 +31,20 @@ export async function openidClient(
|
||||
|
||||
const client = new issuer.Client(
|
||||
{
|
||||
client_id: provider.clientId,
|
||||
client_secret: provider.clientSecret,
|
||||
client_id: provider.clientId as string,
|
||||
client_secret: provider.clientSecret as string,
|
||||
redirect_uris: [provider.callbackUrl],
|
||||
...provider.client,
|
||||
},
|
||||
provider.jwks
|
||||
)
|
||||
|
||||
// allow a 10 second skew
|
||||
// See https://github.com/nextauthjs/next-auth/issues/3032
|
||||
// and https://github.com/nextauthjs/next-auth/issues/3067
|
||||
client[custom.clock_tolerance] = 10
|
||||
|
||||
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as cookie from "../cookie"
|
||||
import * as jwt from "../../../jwt"
|
||||
import { generators } from "openid-client"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
import type { InternalOptions } from "src/lib/types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
const PKCE_CODE_CHALLENGE_METHOD = "S256"
|
||||
const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
@@ -9,93 +9,76 @@ const PKCE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
/**
|
||||
* Returns `code_challenge` and `code_challenge_method`
|
||||
* and saves them in a cookie.
|
||||
* @type {import("src/lib/types").InternalOptions}
|
||||
* @returns {Promise<undefined | {
|
||||
* code_challenge: string
|
||||
* code_challenge_method: "S256"
|
||||
* cookie: import("../cookie").Cookie
|
||||
* }>
|
||||
*/
|
||||
export async function createPKCE(options) {
|
||||
const { cookies, logger } = options
|
||||
/** @type {import("src/providers").OAuthConfig} */
|
||||
const provider = options.provider
|
||||
export async function createPKCE(options: InternalOptions<"oauth">): Promise<
|
||||
| undefined
|
||||
| {
|
||||
code_challenge: string
|
||||
code_challenge_method: "S256"
|
||||
cookie: Cookie
|
||||
}
|
||||
> {
|
||||
const { cookies, logger, provider } = options
|
||||
if (!provider.checks?.includes("pkce")) {
|
||||
// Provider does not support PKCE, return nothing.
|
||||
return
|
||||
}
|
||||
const codeVerifier = generators.codeVerifier()
|
||||
const codeChallenge = generators.codeChallenge(codeVerifier)
|
||||
const code_verifier = generators.codeVerifier()
|
||||
const code_challenge = generators.codeChallenge(code_verifier)
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + PKCE_MAX_AGE * 1000)
|
||||
|
||||
// Encrypt code_verifier and save it to an encrypted cookie
|
||||
const encryptedCodeVerifier = await jwt.encode({
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
...options.jwt,
|
||||
token: { code_verifier: codeVerifier },
|
||||
maxAge: PKCE_MAX_AGE,
|
||||
token: { code_verifier },
|
||||
})
|
||||
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + PKCE_MAX_AGE * 1000)
|
||||
|
||||
logger.debug("CREATE_PKCE_CHALLENGE_VERIFIER", {
|
||||
pkce: {
|
||||
code_challenge: codeChallenge,
|
||||
code_verifier: codeVerifier,
|
||||
},
|
||||
method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
code_verifier,
|
||||
PKCE_MAX_AGE,
|
||||
})
|
||||
|
||||
return {
|
||||
code_challenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
cookie: {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: encryptedCodeVerifier,
|
||||
options: {
|
||||
expires: cookieExpires.toISOString(),
|
||||
...cookies.pkceCodeVerifier.options,
|
||||
},
|
||||
options: { ...cookies.pkceCodeVerifier.options, expires },
|
||||
},
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: PKCE_CODE_CHALLENGE_METHOD,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns code_verifier if provider uses PKCE,
|
||||
* and clears the cookie afterwards.
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export async function usePKCECodeVerifier(params: {
|
||||
export async function usePKCECodeVerifier(
|
||||
codeVerifier: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
codeVerifier?: string
|
||||
}): Promise<
|
||||
| {
|
||||
codeVerifier?: string
|
||||
cookie?: cookie.Cookie
|
||||
}
|
||||
| undefined
|
||||
> {
|
||||
const { options, codeVerifier } = params
|
||||
): Promise<{ codeVerifier: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider } = options
|
||||
|
||||
if (!provider?.checks?.includes("pkce") || !codeVerifier) {
|
||||
return
|
||||
}
|
||||
|
||||
const pkce = await jwt.decode({
|
||||
const pkce = (await jwt.decode({
|
||||
...options.jwt,
|
||||
token: codeVerifier,
|
||||
})
|
||||
|
||||
// remove PKCE cookie after it has been used up
|
||||
const cookie: cookie.Cookie = {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: "",
|
||||
options: {
|
||||
...cookies.pkceCodeVerifier.options,
|
||||
maxAge: 0,
|
||||
},
|
||||
}
|
||||
})) as any
|
||||
|
||||
return {
|
||||
codeVerifier: (pkce?.code_verifier as any) ?? undefined,
|
||||
cookie,
|
||||
codeVerifier: pkce?.code_verifier ?? undefined,
|
||||
cookie: {
|
||||
name: cookies.pkceCodeVerifier.name,
|
||||
value: "",
|
||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,63 @@
|
||||
import { createHash } from "crypto"
|
||||
import { InternalOptions } from "src/lib/types"
|
||||
import { generators } from "openid-client"
|
||||
|
||||
/** Returns state if provider supports it */
|
||||
export function createState(options: InternalOptions<"oauth">) {
|
||||
const { csrfToken, logger, provider } = options
|
||||
import type { InternalOptions } from "src/lib/types"
|
||||
import type { Cookie } from "../cookie"
|
||||
|
||||
const STATE_MAX_AGE = 60 * 15 // 15 minutes in seconds
|
||||
|
||||
/** Returns state if the provider supports it */
|
||||
export async function createState(
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ cookie: Cookie; value: string } | undefined> {
|
||||
const { logger, provider, jwt, cookies } = options
|
||||
|
||||
if (!provider.checks?.includes("state")) {
|
||||
// Provider does not support state, return nothing
|
||||
return
|
||||
}
|
||||
|
||||
if (!csrfToken) {
|
||||
logger.warn("NO_CSRF_TOKEN")
|
||||
return
|
||||
const state = generators.state()
|
||||
|
||||
const encodedState = await jwt.encode({
|
||||
...jwt,
|
||||
maxAge: STATE_MAX_AGE,
|
||||
token: { state },
|
||||
})
|
||||
|
||||
logger.debug("CREATE_STATE", { state, maxAge: STATE_MAX_AGE })
|
||||
|
||||
const expires = new Date()
|
||||
expires.setTime(expires.getTime() + STATE_MAX_AGE * 1000)
|
||||
return {
|
||||
value: state,
|
||||
cookie: {
|
||||
name: cookies.state.name,
|
||||
value: encodedState,
|
||||
options: { ...cookies.state.options, expires },
|
||||
},
|
||||
}
|
||||
|
||||
// A hash of the NextAuth.js CSRF token is used as the state
|
||||
const state = createHash("sha256").update(csrfToken).digest("hex")
|
||||
|
||||
logger.debug("OAUTH_CALLBACK_PROTECTION", { state, csrfToken })
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* Consistently recreate state from the csrfToken
|
||||
* if `provider.checks` supports `"state"`.
|
||||
* Returns state from if the provider supports states,
|
||||
* and clears the container cookie afterwards.
|
||||
*/
|
||||
export function getState({ provider, csrfToken }: InternalOptions<"oauth">) {
|
||||
if (provider?.checks?.includes("state") && csrfToken) {
|
||||
return createHash("sha256").update(csrfToken).digest("hex")
|
||||
export async function useState(
|
||||
state: string | undefined,
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<{ value: string; cookie: Cookie } | undefined> {
|
||||
const { cookies, provider, jwt } = options
|
||||
|
||||
if (!provider.checks?.includes("state") || !state) return
|
||||
|
||||
const value = (await jwt.decode({ ...options.jwt, token: state })) as any
|
||||
|
||||
return {
|
||||
value: value?.state ?? undefined,
|
||||
cookie: {
|
||||
name: cookies.state.name,
|
||||
value: "",
|
||||
options: { ...cookies.pkceCodeVerifier.options, maxAge: 0 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,25 +36,25 @@ export default function parseProviders(params: {
|
||||
function normalizeProvider(provider?: Provider) {
|
||||
if (!provider) return
|
||||
|
||||
const normalizedProvider: any = Object.entries(provider).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (
|
||||
["authorization", "token", "userinfo"].includes(key) &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
const url = new URL(value)
|
||||
acc[key] = {
|
||||
url: `${url.origin}${url.pathname}`,
|
||||
params: Object.fromEntries(url.searchParams ?? []),
|
||||
}
|
||||
} else {
|
||||
acc[key] = value
|
||||
const normalizedProvider: InternalProvider = Object.entries(
|
||||
provider
|
||||
).reduce<InternalProvider>((acc, [key, value]) => {
|
||||
if (
|
||||
["authorization", "token", "userinfo"].includes(key) &&
|
||||
typeof value === "string"
|
||||
) {
|
||||
const url = new URL(value)
|
||||
;(acc as any)[key] = {
|
||||
url: `${url.origin}${url.pathname}`,
|
||||
params: Object.fromEntries(url.searchParams ?? []),
|
||||
}
|
||||
} else {
|
||||
acc[key as keyof InternalProvider] = value
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
return acc
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
|
||||
}, {} as InternalProvider)
|
||||
|
||||
// Checks only work on OAuth 2.x + OIDC providers
|
||||
if (
|
||||
@@ -62,7 +62,7 @@ function normalizeProvider(provider?: Provider) {
|
||||
!provider.version?.startsWith("1.") &&
|
||||
!provider.checks
|
||||
) {
|
||||
normalizedProvider.checks = ["state"]
|
||||
;(normalizedProvider as InternalProvider<"oauth">).checks = ["state"]
|
||||
}
|
||||
return normalizedProvider as InternalProvider
|
||||
return normalizedProvider
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { InternalUrl } from "../../lib/parse-url"
|
||||
* Optionally takes a second date parameter. In that case
|
||||
* the date in the future will be calculated from that date instead of now.
|
||||
*/
|
||||
export function fromDate(time, date = Date.now()) {
|
||||
export function fromDate(time: number, date = Date.now()) {
|
||||
return new Date(date + time * 1000)
|
||||
}
|
||||
|
||||
@@ -25,9 +25,8 @@ export function hashToken(token: string, options: InternalOptions<"email">) {
|
||||
/**
|
||||
* Secret used salt cookies and tokens (e.g. for CSRF protection).
|
||||
* If no secret option is specified then it creates one on the fly
|
||||
* based on options passed here. A options contains unique data, such as
|
||||
* OAuth provider secrets and database credentials it should be sufficent.
|
||||
*/
|
||||
* based on options passed here. If options contains unique data, such as
|
||||
* OAuth provider secrets and database credentials it should be sufficent. If no secret provided in production, we throw an error. */
|
||||
export default function createSecret(params: {
|
||||
userOptions: NextAuthOptions
|
||||
url: InternalUrl
|
||||
|
||||
@@ -2,24 +2,37 @@ import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
export interface ErrorProps {
|
||||
url: InternalUrl
|
||||
theme: Theme
|
||||
url?: InternalUrl
|
||||
theme?: Theme
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ErrorView {
|
||||
status: number
|
||||
heading: string
|
||||
message: JSX.Element
|
||||
signin?: JSX.Element
|
||||
}
|
||||
|
||||
export type ErrorType =
|
||||
| "default"
|
||||
| "configuration"
|
||||
| "accessdenied"
|
||||
| "verification"
|
||||
|
||||
/** Renders an error page. */
|
||||
export default function ErrorPage(props: ErrorProps) {
|
||||
const { url, error = "default", theme } = props
|
||||
const signinPageUrl = `${url}/signin`
|
||||
|
||||
const errors = {
|
||||
const errors: Record<ErrorType, ErrorView> = {
|
||||
default: {
|
||||
status: 200,
|
||||
heading: "Error",
|
||||
message: (
|
||||
<p>
|
||||
<a className="site" href={url.origin}>
|
||||
{url.host}
|
||||
<a className="site" href={url?.origin}>
|
||||
{url?.host}
|
||||
</a>
|
||||
</p>
|
||||
),
|
||||
@@ -74,16 +87,16 @@ export default function ErrorPage(props: ErrorProps) {
|
||||
status,
|
||||
html: (
|
||||
<div className="error">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
{ theme?.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--brand-color: ${theme.brandColor}
|
||||
--brand-color: ${theme?.brandColor}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
}}
|
||||
/> }
|
||||
{theme?.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
<h1>{heading}</h1>
|
||||
<div className="message">{message}</div>
|
||||
|
||||
@@ -4,21 +4,28 @@ import SignoutPage from "./signout"
|
||||
import VerifyRequestPage from "./verify-request"
|
||||
import ErrorPage from "./error"
|
||||
import css from "../../css"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { IncomingRequest, OutgoingResponse } from ".."
|
||||
import { Cookie } from "../lib/cookie"
|
||||
|
||||
/** Takes a request and response, and gives renderable pages */
|
||||
export default function renderPage({
|
||||
options,
|
||||
query,
|
||||
cookies,
|
||||
}: {
|
||||
options: InternalOptions
|
||||
query: IncomingRequest["query"]
|
||||
cookies: Cookie[]
|
||||
}) {
|
||||
const { url, callbackUrl, csrfToken, providers, theme } = options
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from ".."
|
||||
import type { Cookie } from "../lib/cookie"
|
||||
import type { ErrorType } from "./error"
|
||||
|
||||
type RenderPageParams = {
|
||||
query?: IncomingRequest["query"]
|
||||
cookies?: Cookie[]
|
||||
} & Partial<
|
||||
Pick<
|
||||
InternalOptions,
|
||||
"url" | "callbackUrl" | "csrfToken" | "providers" | "theme"
|
||||
>
|
||||
>
|
||||
|
||||
/**
|
||||
* Unless the user defines their [own pages](https://next-auth.js.org/configuration/pages),
|
||||
* we render a set of default ones, using Preact SSR.
|
||||
*/
|
||||
export default function renderPage(params: RenderPageParams) {
|
||||
const { url, theme, query, cookies } = params
|
||||
|
||||
function send({ html, title, status }: any): OutgoingResponse {
|
||||
return {
|
||||
@@ -26,7 +33,7 @@ export default function renderPage({
|
||||
status,
|
||||
headers: [{ key: "Content-Type", value: "text/html" }],
|
||||
body: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>${css()}</style><title>${title}</title></head><body class="__next-auth-theme-${
|
||||
theme.colorScheme
|
||||
theme?.colorScheme ?? "auto"
|
||||
}"><div class="page">${renderToString(html)}</div></body></html>`,
|
||||
}
|
||||
}
|
||||
@@ -35,9 +42,9 @@ export default function renderPage({
|
||||
signin(props?: any) {
|
||||
return send({
|
||||
html: SigninPage({
|
||||
csrfToken,
|
||||
providers,
|
||||
callbackUrl,
|
||||
csrfToken: params.csrfToken,
|
||||
providers: params.providers,
|
||||
callbackUrl: params.callbackUrl,
|
||||
theme,
|
||||
...query,
|
||||
...props,
|
||||
@@ -47,7 +54,12 @@ export default function renderPage({
|
||||
},
|
||||
signout(props?: any) {
|
||||
return send({
|
||||
html: SignoutPage({ csrfToken, url, theme, ...props }),
|
||||
html: SignoutPage({
|
||||
csrfToken: params.csrfToken,
|
||||
url,
|
||||
theme,
|
||||
...props,
|
||||
}),
|
||||
title: "Sign Out",
|
||||
})
|
||||
},
|
||||
@@ -57,7 +69,7 @@ export default function renderPage({
|
||||
title: "Verify Request",
|
||||
})
|
||||
},
|
||||
error(props) {
|
||||
error(props?: { error?: ErrorType }) {
|
||||
return send({
|
||||
...ErrorPage({ url, theme, ...props }),
|
||||
title: "Error",
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
export default function SigninPage(props) {
|
||||
import { Theme } from "../.."
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
|
||||
export interface SignInServerPageParams {
|
||||
csrfToken: string
|
||||
providers: InternalProvider[]
|
||||
callbackUrl: string
|
||||
email: string
|
||||
error: string
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export default function SigninPage(props: SignInServerPageParams) {
|
||||
const {
|
||||
csrfToken,
|
||||
providers,
|
||||
@@ -20,14 +32,14 @@ export default function SigninPage(props) {
|
||||
return false
|
||||
})
|
||||
|
||||
if (typeof document !== "undefined") {
|
||||
if (typeof document !== "undefined" && theme.brandColor) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--brand-color",
|
||||
theme.brandColor
|
||||
)
|
||||
}
|
||||
|
||||
const errors = {
|
||||
const errors: Record<string, string> = {
|
||||
Signin: "Try signing in with a different account.",
|
||||
OAuthSignin: "Try signing in with a different account.",
|
||||
OAuthCallback: "Try signing in with a different account.",
|
||||
@@ -39,6 +51,7 @@ export default function SigninPage(props) {
|
||||
EmailSignin: "Check your email inbox.",
|
||||
CredentialsSignin:
|
||||
"Sign in failed. Check the details you provided are correct.",
|
||||
SessionRequired: "Please sign in to access this page.",
|
||||
default: "Unable to sign in.",
|
||||
}
|
||||
|
||||
@@ -46,7 +59,8 @@ export default function SigninPage(props) {
|
||||
|
||||
return (
|
||||
<div className="signin">
|
||||
<style
|
||||
|
||||
{ theme.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
@@ -54,7 +68,7 @@ export default function SigninPage(props) {
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
/> }
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
{error && (
|
||||
@@ -109,14 +123,14 @@ export default function SigninPage(props) {
|
||||
className="section-header"
|
||||
htmlFor={`input-${credential}-for-${provider.id}-provider`}
|
||||
>
|
||||
{provider.credentials[credential].label || credential}
|
||||
{provider.credentials[credential].label ?? credential}
|
||||
</label>
|
||||
<input
|
||||
name={credential}
|
||||
id={`input-${credential}-for-${provider.id}-provider`}
|
||||
type={provider.credentials[credential].type || "text"}
|
||||
type={provider.credentials[credential].type ?? "text"}
|
||||
placeholder={
|
||||
provider.credentials[credential].placeholder ||
|
||||
provider.credentials[credential].placeholder ??
|
||||
"Password"
|
||||
}
|
||||
{...provider.credentials[credential]}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function SignoutPage(props: SignoutProps) {
|
||||
|
||||
return (
|
||||
<div className="signout">
|
||||
<style
|
||||
{ theme.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
@@ -20,7 +20,7 @@ export default function SignoutPage(props: SignoutProps) {
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
/> }
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
<h1>Signout</h1>
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
|
||||
|
||||
return (
|
||||
<div className="verify-request">
|
||||
<style
|
||||
{ theme.brandColor && <style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
@@ -19,7 +19,7 @@ export default function VerifyRequestPage(props: VerifyRequestPageProps) {
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
/> }
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
<h1>Check your email</h1>
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import oAuthCallback from "../lib/oauth/callback"
|
||||
import callbackHandler from "../lib/callback-handler"
|
||||
import * as cookie from "../lib/cookie"
|
||||
import { hashToken } from "../lib/utils"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { IncomingRequest, OutgoingResponse } from ".."
|
||||
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { IncomingRequest, OutgoingResponse } from ".."
|
||||
import type { Cookie, SessionStore } from "../lib/cookie"
|
||||
import type { User } from "../.."
|
||||
|
||||
/** Handle callbacks from login services */
|
||||
export default async function callback(params: {
|
||||
options: InternalOptions<"oauth" | "credentials" | "email">
|
||||
query: IncomingRequest["query"]
|
||||
method: IncomingRequest["method"]
|
||||
method: Required<IncomingRequest>["method"]
|
||||
body: IncomingRequest["body"]
|
||||
headers: IncomingRequest["headers"]
|
||||
sessionToken?: string
|
||||
codeVerifier?: string
|
||||
cookies: IncomingRequest["cookies"]
|
||||
sessionStore: SessionStore
|
||||
}): Promise<OutgoingResponse> {
|
||||
const { options, query, body, method, headers, sessionToken, codeVerifier } =
|
||||
params
|
||||
const { options, query, body, method, headers, sessionStore } = params
|
||||
const {
|
||||
provider,
|
||||
adapter,
|
||||
@@ -26,11 +27,13 @@ export default async function callback(params: {
|
||||
jwt,
|
||||
events,
|
||||
callbacks,
|
||||
session: { jwt: useJwtSession, maxAge: sessionMaxAge },
|
||||
session: { strategy: sessionStrategy, maxAge: sessionMaxAge },
|
||||
logger,
|
||||
} = options
|
||||
|
||||
const cookies: cookie.Cookie[] = []
|
||||
const cookies: Cookie[] = []
|
||||
|
||||
const useJwtSession = sessionStrategy === "jwt"
|
||||
|
||||
if (provider.type === "oauth") {
|
||||
try {
|
||||
@@ -44,7 +47,7 @@ export default async function callback(params: {
|
||||
body,
|
||||
method,
|
||||
options,
|
||||
codeVerifier,
|
||||
cookies: params.cookies,
|
||||
})
|
||||
|
||||
if (oauthCookies) cookies.push(...oauthCookies)
|
||||
@@ -99,7 +102,9 @@ export default async function callback(params: {
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: `${url}/error?error=${encodeURIComponent(error.message)}`,
|
||||
redirect: `${url}/error?error=${encodeURIComponent(
|
||||
(error as Error).message
|
||||
)}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
@@ -107,7 +112,7 @@ export default async function callback(params: {
|
||||
// Sign user in
|
||||
// @ts-expect-error
|
||||
const { user, session, isNewUser } = await callbackHandler({
|
||||
sessionToken,
|
||||
sessionToken: sessionStore.value,
|
||||
profile,
|
||||
// @ts-expect-error
|
||||
account,
|
||||
@@ -130,29 +135,25 @@ export default async function callback(params: {
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: newEncodedJwt,
|
||||
options: {
|
||||
expires: cookieExpires,
|
||||
...options.cookies.sessionToken.options,
|
||||
},
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: cookieExpires,
|
||||
})
|
||||
cookies.push(...sessionCookies)
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: session.sessionToken,
|
||||
options: {
|
||||
expires: session.expires,
|
||||
...options.cookies.sessionToken.options,
|
||||
expires: session.expires,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -175,37 +176,31 @@ export default async function callback(params: {
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return { redirect: callbackUrl, cookies }
|
||||
} catch (error) {
|
||||
if (error.name === "AccountNotLinkedError") {
|
||||
if ((error as Error).name === "AccountNotLinkedError") {
|
||||
// If the email on the account is already linked, but not with this OAuth account
|
||||
return {
|
||||
redirect: `${url}/error?error=OAuthAccountNotLinked`,
|
||||
cookies,
|
||||
}
|
||||
} else if (error.name === "CreateUserError") {
|
||||
} else if ((error as Error).name === "CreateUserError") {
|
||||
return { redirect: `${url}/error?error=OAuthCreateAccount`, cookies }
|
||||
}
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error)
|
||||
logger.error("OAUTH_CALLBACK_HANDLER_ERROR", error as Error)
|
||||
return { redirect: `${url}/error?error=Callback`, cookies }
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "OAuthCallbackError") {
|
||||
logger.error("CALLBACK_OAUTH_ERROR", error)
|
||||
if ((error as Error).name === "OAuthCallbackError") {
|
||||
logger.error("CALLBACK_OAUTH_ERROR", error as Error)
|
||||
return { redirect: `${url}/error?error=OAuthCallback`, cookies }
|
||||
}
|
||||
logger.error("OAUTH_CALLBACK_ERROR", error)
|
||||
logger.error("OAUTH_CALLBACK_ERROR", error as Error)
|
||||
return { redirect: `${url}/error?error=Callback`, cookies }
|
||||
}
|
||||
} else if (provider.type === "email") {
|
||||
try {
|
||||
if (!adapter) {
|
||||
logger.error(
|
||||
"EMAIL_REQUIRES_ADAPTER_ERROR",
|
||||
new Error("E-mail login requires an adapter but it was undefined")
|
||||
)
|
||||
return { redirect: `${url}/error?error=Configuration`, cookies }
|
||||
}
|
||||
|
||||
const { useVerificationToken, getUserByEmail } = adapter
|
||||
// Verified in `assertConfig`
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const { useVerificationToken, getUserByEmail } = adapter!
|
||||
|
||||
const token = query?.token
|
||||
const identifier = query?.email
|
||||
@@ -251,7 +246,9 @@ export default async function callback(params: {
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: `${url}/error?error=${encodeURIComponent(error.message)}`,
|
||||
redirect: `${url}/error?error=${encodeURIComponent(
|
||||
(error as Error).message
|
||||
)}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
@@ -259,7 +256,7 @@ export default async function callback(params: {
|
||||
// Sign user in
|
||||
// @ts-expect-error
|
||||
const { user, session, isNewUser } = await callbackHandler({
|
||||
sessionToken,
|
||||
sessionToken: sessionStore.value,
|
||||
// @ts-expect-error
|
||||
profile,
|
||||
// @ts-expect-error
|
||||
@@ -282,29 +279,25 @@ export default async function callback(params: {
|
||||
isNewUser,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: newEncodedJwt,
|
||||
options: {
|
||||
expires: cookieExpires,
|
||||
...options.cookies.sessionToken.options,
|
||||
},
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: cookieExpires,
|
||||
})
|
||||
cookies.push(...sessionCookies)
|
||||
} else {
|
||||
// Save Session Token in cookie
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: session.sessionToken,
|
||||
options: {
|
||||
expires: session.expires,
|
||||
...options.cookies.sessionToken.options,
|
||||
expires: session.expires,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -327,51 +320,23 @@ export default async function callback(params: {
|
||||
// Callback URL is already verified at this point, so safe to use if specified
|
||||
return { redirect: callbackUrl, cookies }
|
||||
} catch (error) {
|
||||
if (error.name === "CreateUserError") {
|
||||
if ((error as Error).name === "CreateUserError") {
|
||||
return { redirect: `${url}/error?error=EmailCreateAccount`, cookies }
|
||||
}
|
||||
logger.error("CALLBACK_EMAIL_ERROR", error)
|
||||
logger.error("CALLBACK_EMAIL_ERROR", error as Error)
|
||||
return { redirect: `${url}/error?error=Callback`, cookies }
|
||||
}
|
||||
} else if (provider.type === "credentials" && method === "POST") {
|
||||
if (!useJwtSession) {
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_JWT_ERROR",
|
||||
new Error(
|
||||
"Signin in with credentials is only supported if JSON Web Tokens are enabled"
|
||||
)
|
||||
)
|
||||
return {
|
||||
status: 500,
|
||||
redirect: `${url}/error?error=Configuration`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
|
||||
if (!provider.authorize) {
|
||||
logger.error(
|
||||
"CALLBACK_CREDENTIALS_HANDLER_ERROR",
|
||||
new Error(
|
||||
"Must define an authorize() handler to use credentials authentication provider"
|
||||
)
|
||||
)
|
||||
return {
|
||||
status: 500,
|
||||
redirect: `${url}/error?error=Configuration`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
|
||||
const credentials = body
|
||||
|
||||
let user
|
||||
let user: User
|
||||
try {
|
||||
user = await provider.authorize(credentials, {
|
||||
user = (await provider.authorize(credentials, {
|
||||
query,
|
||||
body,
|
||||
headers,
|
||||
method,
|
||||
})
|
||||
})) as User
|
||||
if (!user) {
|
||||
return {
|
||||
status: 401,
|
||||
@@ -384,7 +349,9 @@ export default async function callback(params: {
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: `${url}/error?error=${encodeURIComponent(error.message)}`,
|
||||
redirect: `${url}/error?error=${encodeURIComponent(
|
||||
(error as Error).message
|
||||
)}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
@@ -414,7 +381,9 @@ export default async function callback(params: {
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: `${url}/error?error=${encodeURIComponent(error.message)}`,
|
||||
redirect: `${url}/error?error=${encodeURIComponent(
|
||||
(error as Error).message
|
||||
)}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
@@ -434,22 +403,19 @@ export default async function callback(params: {
|
||||
isNewUser: false,
|
||||
})
|
||||
|
||||
// Sign and encrypt token
|
||||
const newEncodedJwt = await jwt.encode({ ...jwt, token })
|
||||
// Encode token
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
|
||||
// Set cookie expiry date
|
||||
const cookieExpires = new Date()
|
||||
cookieExpires.setTime(cookieExpires.getTime() + sessionMaxAge * 1000)
|
||||
|
||||
cookies.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: newEncodedJwt,
|
||||
options: {
|
||||
expires: cookieExpires,
|
||||
...options.cookies.sessionToken.options,
|
||||
},
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: cookieExpires,
|
||||
})
|
||||
|
||||
cookies.push(...sessionCookies)
|
||||
|
||||
// @ts-expect-error
|
||||
await events.signIn?.({ user, account })
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function providers(
|
||||
): OutgoingResponse<Record<string, PublicProvider>> {
|
||||
return {
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
body: providers.reduce(
|
||||
body: providers.reduce<Record<string, PublicProvider>>(
|
||||
(acc, { id, name, type, signinUrl, callbackUrl }) => {
|
||||
acc[id] = { id, name, type, signinUrl, callbackUrl }
|
||||
return acc
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Adapter } from "../../adapters"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { OutgoingResponse } from ".."
|
||||
import { Session } from "../.."
|
||||
import { fromDate } from "../lib/utils"
|
||||
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { OutgoingResponse } from ".."
|
||||
import type { Session } from "../.."
|
||||
import type { SessionStore } from "../lib/cookie"
|
||||
|
||||
interface SessionParams {
|
||||
options: InternalOptions
|
||||
sessionToken?: string
|
||||
sessionStore: SessionStore
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,10 +19,15 @@ interface SessionParams {
|
||||
export default async function session(
|
||||
params: SessionParams
|
||||
): Promise<OutgoingResponse<Session | {}>> {
|
||||
const { options, sessionToken } = params
|
||||
const { adapter, jwt, events, callbacks, logger } = options
|
||||
const useJwtSession = options.session.jwt
|
||||
const sessionMaxAge = options.session.maxAge
|
||||
const { options, sessionStore } = params
|
||||
const {
|
||||
adapter,
|
||||
jwt,
|
||||
events,
|
||||
callbacks,
|
||||
logger,
|
||||
session: { strategy: sessionStrategy, maxAge: sessionMaxAge },
|
||||
} = options
|
||||
|
||||
const response: OutgoingResponse<Session | {}> = {
|
||||
body: {},
|
||||
@@ -28,19 +35,22 @@ export default async function session(
|
||||
cookies: [],
|
||||
}
|
||||
|
||||
const sessionToken = sessionStore.value
|
||||
|
||||
if (!sessionToken) return response
|
||||
|
||||
if (useJwtSession) {
|
||||
if (sessionStrategy === "jwt") {
|
||||
try {
|
||||
// Decrypt and verify token
|
||||
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
const decodedToken = await jwt.decode({
|
||||
...jwt,
|
||||
token: sessionToken,
|
||||
})
|
||||
|
||||
// Generate new session expiry date
|
||||
const newExpires = fromDate(sessionMaxAge)
|
||||
|
||||
// By default, only exposes a limited subset of information to the client
|
||||
// as needed for presentation purposes (e.g. "you are logged in as...").
|
||||
const defaultSession = {
|
||||
const session = {
|
||||
user: {
|
||||
name: decodedToken?.name,
|
||||
email: decodedToken?.email,
|
||||
@@ -49,41 +59,34 @@ export default async function session(
|
||||
expires: newExpires.toISOString(),
|
||||
}
|
||||
|
||||
// Pass Session and JSON Web Token through to the session callback
|
||||
// @ts-expect-error
|
||||
const token = await callbacks.jwt({ token: decodedToken })
|
||||
// @ts-expect-error
|
||||
const session = await callbacks.session({
|
||||
session: defaultSession,
|
||||
token,
|
||||
})
|
||||
const newSession = await callbacks.session({ session, token })
|
||||
|
||||
// Return session payload as response
|
||||
response.body = session
|
||||
response.body = newSession
|
||||
|
||||
// Refresh JWT expiry by re-signing it, with an updated expiry date
|
||||
const newToken = await jwt.encode({ ...jwt, token })
|
||||
const newToken = await jwt.encode({
|
||||
...jwt,
|
||||
token,
|
||||
maxAge: options.session.maxAge,
|
||||
})
|
||||
|
||||
// Set cookie, to also update expiry date on cookie
|
||||
response.cookies?.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: newToken,
|
||||
options: {
|
||||
expires: newExpires,
|
||||
...options.cookies.sessionToken.options,
|
||||
},
|
||||
const sessionCookies = sessionStore.chunk(newToken, {
|
||||
expires: newExpires,
|
||||
})
|
||||
|
||||
await events.session?.({ session, token })
|
||||
response.cookies?.push(...sessionCookies)
|
||||
|
||||
await events.session?.({ session: newSession, token })
|
||||
} catch (error) {
|
||||
// If JWT not verifiable, make sure the cookie for it is removed and return empty object
|
||||
logger.error("JWT_SESSION_ERROR", error)
|
||||
logger.error("JWT_SESSION_ERROR", error as Error)
|
||||
|
||||
response.cookies?.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: "",
|
||||
options: { ...options.cookies.sessionToken.options, maxAge: 0 },
|
||||
})
|
||||
response.cookies?.push(...sessionStore.clean())
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -143,24 +146,20 @@ export default async function session(
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: sessionToken,
|
||||
options: {
|
||||
expires: newExpires,
|
||||
...options.cookies.sessionToken.options,
|
||||
expires: newExpires,
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error
|
||||
await events.session?.({ session: sessionPayload })
|
||||
} else if (sessionToken) {
|
||||
// If sessionToken was found set but it's not valid for a session then
|
||||
// If `sessionToken` was found set but it's not valid for a session then
|
||||
// remove the sessionToken cookie from browser.
|
||||
response.cookies?.push({
|
||||
name: options.cookies.sessionToken.name,
|
||||
value: "",
|
||||
options: { ...options.cookies.sessionToken.options, maxAge: 0 },
|
||||
})
|
||||
response.cookies?.push(...sessionStore.clean())
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("SESSION_ERROR", error)
|
||||
logger.error("SESSION_ERROR", error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,18 +26,10 @@ export default async function signin(params: {
|
||||
const response = await getAuthorizationUrl({ options, query })
|
||||
return response
|
||||
} catch (error) {
|
||||
logger.error("SIGNIN_OAUTH_ERROR", { error, provider })
|
||||
logger.error("SIGNIN_OAUTH_ERROR", { error: error as Error, provider })
|
||||
return { redirect: `${url}/error?error=OAuthSignin` }
|
||||
}
|
||||
} else if (provider.type === "email") {
|
||||
if (!adapter) {
|
||||
logger.error(
|
||||
"EMAIL_REQUIRES_ADAPTER_ERROR",
|
||||
new Error("E-mail login requires an adapter but it was undefined")
|
||||
)
|
||||
return { redirect: `${url}/error?error=Configuration` }
|
||||
}
|
||||
|
||||
// Note: Technically the part of the email address local mailbox element
|
||||
// (everything before the @ symbol) should be treated as 'case sensitive'
|
||||
// according to RFC 2821, but in practice this causes more problems than
|
||||
@@ -45,7 +37,9 @@ export default async function signin(params: {
|
||||
// complains about this we can make strict RFC 2821 compliance an option.
|
||||
const email = body?.email?.toLowerCase() ?? null
|
||||
|
||||
const { getUserByEmail } = adapter
|
||||
// Verified in `assertConfig`
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const { getUserByEmail } = adapter!
|
||||
// If is an existing user return a user object (otherwise use placeholder)
|
||||
const user: User = (email ? await getUserByEmail(email) : null) ?? {
|
||||
email,
|
||||
@@ -73,13 +67,17 @@ export default async function signin(params: {
|
||||
return { redirect: signInCallbackResponse }
|
||||
}
|
||||
} catch (error) {
|
||||
return { redirect: `${url}/error?${new URLSearchParams({ error })}}` }
|
||||
return {
|
||||
redirect: `${url}/error?${new URLSearchParams({
|
||||
error: error as string,
|
||||
})}}`,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await emailSignin(email, options)
|
||||
} catch (error) {
|
||||
logger.error("SIGNIN_EMAIL_ERROR", error)
|
||||
logger.error("SIGNIN_EMAIL_ERROR", error as Error)
|
||||
return { redirect: `${url}/error?error=EmailSignin` }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { Adapter } from "src/adapters"
|
||||
import { InternalOptions } from "../../lib/types"
|
||||
import { OutgoingResponse } from ".."
|
||||
import { Cookie } from "../lib/cookie"
|
||||
import type { Adapter } from "../../adapters"
|
||||
import type { InternalOptions } from "../../lib/types"
|
||||
import type { OutgoingResponse } from ".."
|
||||
import type { SessionStore } from "../lib/cookie"
|
||||
|
||||
/** Handle requests to /api/auth/signout */
|
||||
export default async function signout(params: {
|
||||
options: InternalOptions
|
||||
sessionToken?: string
|
||||
sessionStore: SessionStore
|
||||
}): Promise<OutgoingResponse> {
|
||||
const { options, sessionToken } = params
|
||||
const { adapter, cookies, events, jwt, callbackUrl, logger } = options
|
||||
const { options, sessionStore } = params
|
||||
const { adapter, events, jwt, callbackUrl, logger, session } = options
|
||||
|
||||
const sessionToken = sessionStore?.value
|
||||
if (!sessionToken) {
|
||||
return { redirect: callbackUrl }
|
||||
}
|
||||
|
||||
const useJwtSession = options.session.jwt
|
||||
|
||||
if (useJwtSession) {
|
||||
if (session.strategy === "jwt") {
|
||||
// Dispatch signout event
|
||||
try {
|
||||
const decodedJwt = await jwt.decode({ ...jwt, token: sessionToken })
|
||||
@@ -25,6 +24,7 @@ export default async function signout(params: {
|
||||
await events.signOut?.({ token: decodedJwt })
|
||||
} catch (error) {
|
||||
// Do nothing if decoding the JWT fails
|
||||
logger.error("SIGNOUT_ERROR", error)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -34,16 +34,12 @@ export default async function signout(params: {
|
||||
await events.signOut?.({ session })
|
||||
} catch (error) {
|
||||
// If error, log it but continue
|
||||
logger.error("SIGNOUT_ERROR", error)
|
||||
logger.error("SIGNOUT_ERROR", error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove Session Token
|
||||
const sessionCookie: Cookie = {
|
||||
name: cookies.sessionToken.name,
|
||||
value: "",
|
||||
options: { ...cookies.sessionToken.options, maxAge: 0 },
|
||||
}
|
||||
const sessionCookies = sessionStore.clean()
|
||||
|
||||
return { redirect: callbackUrl, cookies: [sessionCookie] }
|
||||
return { redirect: callbackUrl, cookies: sessionCookies }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Adapter } from "../adapters"
|
||||
import { Provider, CredentialInput, ProviderType } from "../providers"
|
||||
import type { Adapter } from "../adapters"
|
||||
import type { Provider, CredentialInput, ProviderType } from "../providers"
|
||||
import type { TokenSetParameters } from "openid-client"
|
||||
import { JWT, JWTOptions } from "../jwt"
|
||||
import { LoggerInstance } from "../lib/logger"
|
||||
import type { JWT, JWTOptions } from "../jwt"
|
||||
import type { LoggerInstance } from "../lib/logger"
|
||||
import type { CookieSerializeOptions } from "cookie"
|
||||
|
||||
export type Awaitable<T> = T | PromiseLike<T>
|
||||
|
||||
@@ -338,15 +339,7 @@ export interface CallbacksOptions<
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
export interface CookieOption {
|
||||
name: string
|
||||
options: {
|
||||
httpOnly?: boolean
|
||||
sameSite: true | "strict" | "lax" | "none"
|
||||
path?: string
|
||||
secure: boolean
|
||||
maxAge?: number
|
||||
domain?: string
|
||||
expires?: Date
|
||||
}
|
||||
options: CookieSerializeOptions
|
||||
}
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#cookies) */
|
||||
@@ -355,6 +348,7 @@ export interface CookiesOptions {
|
||||
callbackUrl: CookieOption
|
||||
csrfToken: CookieOption
|
||||
pkceCodeVerifier: CookieOption
|
||||
state: CookieOption
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,9 +423,23 @@ export interface DefaultSession extends Record<string, unknown> {
|
||||
*/
|
||||
export interface Session extends Record<string, unknown>, DefaultSession {}
|
||||
|
||||
export type SessionStrategy = "jwt" | "database"
|
||||
|
||||
/** [Documentation](https://next-auth.js.org/configuration/options#session) */
|
||||
export interface SessionOptions {
|
||||
jwt: boolean
|
||||
/**
|
||||
* Choose how you want to save the user session.
|
||||
* The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie.
|
||||
*
|
||||
* If you use an `adapter` however, we default it to `"database"` instead.
|
||||
* You can still force a JWT session by explicitly defining `"jwt"`.
|
||||
*
|
||||
* When using `"database"`, the session cookie will only contain a `sessionToken` value,
|
||||
* which is used to look up the session in the database.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#session) | [Adapter](https://next-auth.js.org/configuration/options#adapter) | [About JSON Web Tokens](https://next-auth.js.org/faq#json-web-tokens)
|
||||
*/
|
||||
strategy: SessionStrategy
|
||||
/**
|
||||
* Relative time from now in seconds when to expire the session
|
||||
* @default 2592000 // 30 days
|
||||
@@ -463,13 +471,3 @@ export interface DefaultUser {
|
||||
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
|
||||
*/
|
||||
export interface User extends Record<string, unknown>, DefaultUser {}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXTAUTH_URL?: string
|
||||
VERCEL_URL?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ label {
|
||||
text-align: left;
|
||||
margin-bottom: 0.25rem;
|
||||
display: block;
|
||||
color: #666;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
input[type] {
|
||||
@@ -258,5 +258,5 @@ a.site {
|
||||
}
|
||||
|
||||
.section-header {
|
||||
color: var(--brand-color);
|
||||
color: var(--brand-color, var(--color-text));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { EncryptJWT, jwtDecrypt } from "jose"
|
||||
import hkdf from '@panva/hkdf'
|
||||
import hkdf from "@panva/hkdf"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { NextApiRequest } from "next"
|
||||
import type { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "./types"
|
||||
import { SessionStore } from "../core/lib/cookie"
|
||||
import type { NextApiRequest } from "next"
|
||||
import type { JWT, JWTDecodeParams, JWTEncodeParams } from "./types"
|
||||
import type { LoggerInstance } from ".."
|
||||
|
||||
export * from "./types"
|
||||
|
||||
@@ -38,7 +40,7 @@ export async function decode({
|
||||
return payload
|
||||
}
|
||||
|
||||
export type GetTokenParams<R extends boolean = false> = {
|
||||
export interface GetTokenParams<R extends boolean = false> {
|
||||
/** The request containing the JWT either in the cookies or in the `Authorization` header. */
|
||||
req: NextApiRequest
|
||||
/**
|
||||
@@ -53,7 +55,10 @@ export type GetTokenParams<R extends boolean = false> = {
|
||||
* @default false
|
||||
*/
|
||||
raw?: R
|
||||
} & Pick<JWTOptions, "decode" | "secret">
|
||||
secret: string
|
||||
decode?: typeof decode
|
||||
logger?: LoggerInstance | Console
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a NextAuth.js request (`req`) and returns either the NextAuth.js issued JWT's payload,
|
||||
@@ -74,21 +79,23 @@ export async function getToken<R extends boolean = false>(
|
||||
: "next-auth.session-token",
|
||||
raw,
|
||||
decode: _decode = decode,
|
||||
logger = console,
|
||||
} = params ?? {}
|
||||
|
||||
if (!req) throw new Error("Must pass `req` to JWT getToken()")
|
||||
|
||||
let token = req.cookies[cookieName]
|
||||
const sessionStore = new SessionStore(
|
||||
{ name: cookieName, options: { secure: secureCookie } },
|
||||
{ cookies: req.cookies, headers: req.headers },
|
||||
logger
|
||||
)
|
||||
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
const token = sessionStore.value
|
||||
// @ts-expect-error
|
||||
if (!token) return null
|
||||
|
||||
if (raw) {
|
||||
// @ts-expect-error
|
||||
return token
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (raw) return token
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
@@ -99,9 +106,9 @@ export async function getToken<R extends boolean = false>(
|
||||
}
|
||||
}
|
||||
|
||||
async function getDerivedEncryptionKey(secret) {
|
||||
async function getDerivedEncryptionKey(secret: string | Buffer) {
|
||||
return await hkdf(
|
||||
'sha256',
|
||||
"sha256",
|
||||
secret,
|
||||
"",
|
||||
"NextAuth.js Generated Encryption Key",
|
||||
|
||||
@@ -46,3 +46,5 @@ export interface JWTOptions {
|
||||
/** Override this method to control the NextAuth.js issued JWT decoding. */
|
||||
decode: typeof decode
|
||||
}
|
||||
|
||||
export type Secret = string | Buffer
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { UnknownError } from "../core/errors"
|
||||
|
||||
// TODO: better typing
|
||||
/** Makes sure that error is always serializable */
|
||||
function formatError(o) {
|
||||
function formatError(o: unknown): unknown {
|
||||
if (o instanceof Error && !(o instanceof UnknownError)) {
|
||||
return { message: o.message, stack: o.stack, name: o.name }
|
||||
}
|
||||
if (o?.error) {
|
||||
o.error = formatError(o.error)
|
||||
if (hasErrorProperty(o)) {
|
||||
o.error = formatError(o.error) as Error
|
||||
o.message = o.message ?? o.error.message
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
function hasErrorProperty(
|
||||
x: unknown
|
||||
): x is { error: Error; [key: string]: unknown } {
|
||||
return !!(x as any)?.error
|
||||
}
|
||||
|
||||
export type WarningCode = "NEXTAUTH_URL" | "NO_SECRET"
|
||||
|
||||
/**
|
||||
* Override any of the methods, and the rest will use the default logger.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/options#logger)
|
||||
*/
|
||||
export interface LoggerInstance {
|
||||
warn: (
|
||||
code:
|
||||
| "JWT_AUTO_GENERATED_SIGNING_KEY"
|
||||
| "JWT_AUTO_GENERATED_ENCRYPTION_KEY"
|
||||
| "NEXTAUTH_URL"
|
||||
| "NO_CSRF_TOKEN"
|
||||
) => void
|
||||
export interface LoggerInstance extends Record<string, Function> {
|
||||
warn: (code: WarningCode) => void
|
||||
error: (
|
||||
code: string,
|
||||
/**
|
||||
@@ -39,7 +42,7 @@ export interface LoggerInstance {
|
||||
|
||||
const _logger: LoggerInstance = {
|
||||
error(code, metadata) {
|
||||
metadata = formatError(metadata)
|
||||
metadata = formatError(metadata) as Error
|
||||
console.error(
|
||||
`[next-auth][error][${code}]`,
|
||||
`\nhttps://next-auth.js.org/errors#${code.toLowerCase()}`,
|
||||
@@ -54,16 +57,21 @@ const _logger: LoggerInstance = {
|
||||
)
|
||||
},
|
||||
debug(code, metadata) {
|
||||
if (!process?.env?._NEXTAUTH_DEBUG) return
|
||||
console.log(`[next-auth][debug][${code}]`, metadata)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the built-in logger.
|
||||
* Override the built-in logger with user's implementation.
|
||||
* Any `undefined` level will use the default logger.
|
||||
*/
|
||||
export function setLogger(newLogger: Partial<LoggerInstance> = {}) {
|
||||
export function setLogger(
|
||||
newLogger: Partial<LoggerInstance> = {},
|
||||
debug?: boolean
|
||||
) {
|
||||
// Turn off debug logging if `debug` isn't set to `true`
|
||||
if (!debug) _logger.debug = () => {}
|
||||
|
||||
if (newLogger.error) _logger.error = newLogger.error
|
||||
if (newLogger.warn) _logger.warn = newLogger.warn
|
||||
if (newLogger.debug) _logger.debug = newLogger.debug
|
||||
@@ -81,15 +89,15 @@ export function proxyLogger(
|
||||
return logger
|
||||
}
|
||||
|
||||
const clientLogger = {}
|
||||
const clientLogger: Record<string, unknown> = {}
|
||||
for (const level in logger) {
|
||||
clientLogger[level] = (code, metadata) => {
|
||||
clientLogger[level] = (code: string, metadata: Error) => {
|
||||
_logger[level](code, metadata) // Logs to console
|
||||
|
||||
if (level === "error") {
|
||||
metadata = formatError(metadata)
|
||||
metadata = formatError(metadata) as Error
|
||||
}
|
||||
metadata.client = true
|
||||
;(metadata as any).client = true
|
||||
const url = `${basePath}/_log`
|
||||
const body = new URLSearchParams({ level, code, ...metadata })
|
||||
if (navigator.sendBeacon) {
|
||||
@@ -98,7 +106,7 @@ export function proxyLogger(
|
||||
return fetch(url, { method: "POST", body, keepalive: true })
|
||||
}
|
||||
}
|
||||
return clientLogger as LoggerInstance
|
||||
return clientLogger as unknown as LoggerInstance
|
||||
} catch {
|
||||
return _logger
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ function isObject(item: any): boolean {
|
||||
}
|
||||
|
||||
/** Deep merge two objects */
|
||||
export function merge(target: any, ...sources: any[]) {
|
||||
export function merge(target: any, ...sources: any[]): any {
|
||||
if (!sources.length) return target
|
||||
const source = sources.shift()
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ export interface InternalUrl {
|
||||
|
||||
export default function parseUrl(url?: string): InternalUrl {
|
||||
const defaultUrl = new URL("http://localhost:3000/api/auth")
|
||||
|
||||
if (url && !url.startsWith("http")) {
|
||||
url = `https://${url}`
|
||||
}
|
||||
|
||||
const _url = new URL(url ?? defaultUrl)
|
||||
const path = (_url.pathname === "/" ? defaultUrl.pathname : _url.pathname)
|
||||
// Remove trailing slash
|
||||
|
||||
15
src/next/cookie.ts
Normal file
15
src/next/cookie.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { serialize } from "cookie"
|
||||
import { Cookie } from "../core/lib/cookie"
|
||||
|
||||
export function setCookie(res, cookie: Cookie) {
|
||||
// Preserve any existing cookies that have already been set in the same session
|
||||
let setCookieHeader = res.getHeader("Set-Cookie") ?? []
|
||||
// If not an array (i.e. a string with a single cookie) convert it into an array
|
||||
if (!Array.isArray(setCookieHeader)) {
|
||||
setCookieHeader = [setCookieHeader]
|
||||
}
|
||||
const { name, value, options } = cookie
|
||||
const cookieHeader = serialize(name, value, options)
|
||||
setCookieHeader.push(cookieHeader)
|
||||
res.setHeader("Set-Cookie", setCookieHeader)
|
||||
}
|
||||
@@ -1,76 +1,58 @@
|
||||
import {
|
||||
import { NextAuthHandler } from "../core"
|
||||
import { setCookie } from "./cookie"
|
||||
|
||||
import type {
|
||||
GetServerSidePropsContext,
|
||||
NextApiRequest,
|
||||
NextApiResponse,
|
||||
} from "next"
|
||||
import { NextAuthOptions, Session } from ".."
|
||||
import { NextAuthHandler } from "../core"
|
||||
import { NextAuthAction } from "../lib/types"
|
||||
import { set as setCookie } from "../core/lib/cookie"
|
||||
import logger, { setLogger } from "../lib/logger"
|
||||
import type { NextAuthOptions, Session } from ".."
|
||||
import type {
|
||||
NextAuthAction,
|
||||
NextAuthRequest,
|
||||
NextAuthResponse,
|
||||
} from "../lib/types"
|
||||
|
||||
async function NextAuthNextHandler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
options: NextAuthOptions
|
||||
) {
|
||||
setLogger(options.logger)
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = new Error(
|
||||
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
|
||||
)
|
||||
|
||||
logger.error("MISSING_NEXTAUTH_API_ROUTE_ERROR", error)
|
||||
return res.status(500).send(error.message)
|
||||
}
|
||||
|
||||
const host = process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL
|
||||
if (!host) logger.warn("NEXTAUTH_URL")
|
||||
|
||||
const {
|
||||
body,
|
||||
redirect,
|
||||
cookies,
|
||||
headers,
|
||||
status = 200,
|
||||
} = await NextAuthHandler({
|
||||
const { nextauth, ...query } = req.query
|
||||
const handler = await NextAuthHandler({
|
||||
req: {
|
||||
host,
|
||||
host: (process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL) as string,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
query,
|
||||
cookies: req.cookies,
|
||||
headers: req.headers,
|
||||
method: req.method ?? "GET",
|
||||
action: req.query.nextauth[0] as NextAuthAction,
|
||||
providerId: req.query.nextauth[1],
|
||||
error: req.query.nextauth[1],
|
||||
method: req.method,
|
||||
action: nextauth?.[0] as NextAuthAction,
|
||||
providerId: nextauth?.[1],
|
||||
error: (req.query.error as string | undefined) ?? nextauth?.[1],
|
||||
},
|
||||
options,
|
||||
})
|
||||
|
||||
res.status(status)
|
||||
res.status(handler.status ?? 200)
|
||||
|
||||
cookies?.forEach((cookie) => {
|
||||
setCookie(res, cookie.name, cookie.value, cookie.options)
|
||||
})
|
||||
headers?.forEach((header) => {
|
||||
res.setHeader(header.key, header.value)
|
||||
})
|
||||
handler.cookies?.forEach((cookie) => setCookie(res, cookie))
|
||||
|
||||
if (redirect) {
|
||||
handler.headers?.forEach((h) => res.setHeader(h.key, h.value))
|
||||
|
||||
if (handler.redirect) {
|
||||
// If the request expects a return URL, send it as JSON
|
||||
// instead of doing an actual redirect.
|
||||
if (req.body?.json !== "true") {
|
||||
// Could chain. .end() when lowest target is Node 14
|
||||
// https://github.com/nodejs/node/issues/33148
|
||||
res.status(302).setHeader("Location", redirect)
|
||||
res.status(302).setHeader("Location", handler.redirect)
|
||||
return res.end()
|
||||
}
|
||||
return res.json({ url: redirect })
|
||||
return res.json({ url: handler.redirect })
|
||||
}
|
||||
|
||||
return res.send(body)
|
||||
return res.send(handler.body)
|
||||
}
|
||||
|
||||
function NextAuth(options: NextAuthOptions): any
|
||||
@@ -81,9 +63,14 @@ function NextAuth(
|
||||
): any
|
||||
|
||||
/** Tha main entry point to next-auth */
|
||||
function NextAuth(...args) {
|
||||
function NextAuth(
|
||||
...args:
|
||||
| [NextAuthOptions]
|
||||
| [NextApiRequest, NextApiResponse, NextAuthOptions]
|
||||
) {
|
||||
if (args.length === 1) {
|
||||
return async (req, res) => await NextAuthNextHandler(req, res, args[0])
|
||||
return async (req: NextAuthRequest, res: NextAuthResponse) =>
|
||||
await NextAuthNextHandler(req, res, args[0])
|
||||
}
|
||||
|
||||
return NextAuthNextHandler(args[0], args[1], args[2])
|
||||
@@ -100,6 +87,7 @@ export async function getServerSession(
|
||||
const session = await NextAuthHandler<Session | {}>({
|
||||
options,
|
||||
req: {
|
||||
host: (process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL) as string,
|
||||
action: "session",
|
||||
method: "GET",
|
||||
cookies: context.req.cookies,
|
||||
@@ -109,10 +97,18 @@ export async function getServerSession(
|
||||
|
||||
const { body, cookies } = session
|
||||
|
||||
cookies?.forEach((cookie) => {
|
||||
setCookie(context.res, cookie.name, cookie.value, cookie.options)
|
||||
})
|
||||
cookies?.forEach((cookie) => setCookie(context.res, cookie))
|
||||
|
||||
if (body && Object.keys(body).length) return body as Session
|
||||
return null
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NEXTAUTH_URL?: string
|
||||
VERCEL_URL?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
179
src/providers/42-school.ts
Normal file
179
src/providers/42-school.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface UserData {
|
||||
id: number
|
||||
email: string
|
||||
login: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
usual_full_name: null | string
|
||||
usual_first_name: null | string
|
||||
url: string
|
||||
phone: "hidden" | string | null
|
||||
displayname: string
|
||||
image_url: string | null
|
||||
"staff?": boolean
|
||||
correction_point: number
|
||||
pool_month: string | null
|
||||
pool_year: string | null
|
||||
location: string | null
|
||||
wallet: number
|
||||
anonymize_date: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
alumni: boolean
|
||||
"is_launched?": boolean
|
||||
}
|
||||
|
||||
export interface CursusUser {
|
||||
grade: string | null
|
||||
level: number
|
||||
skills: Array<{ id: number; name: string; level: number }>
|
||||
blackholed_at: string | null
|
||||
id: number
|
||||
begin_at: string | null
|
||||
end_at: string | null
|
||||
cursus_id: number
|
||||
has_coalition: boolean
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
user: UserData
|
||||
cursus: { id: number; created_at: string; name: string; slug: string }
|
||||
}
|
||||
|
||||
export interface ProjectUser {
|
||||
id: number
|
||||
occurrence: number
|
||||
final_mark: number | null
|
||||
status: "in_progress" | "finished"
|
||||
"validated?": boolean | null
|
||||
current_team_id: number
|
||||
project: {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
parent_id: number | null
|
||||
}
|
||||
cursus_ids: number[]
|
||||
marked_at: string | null
|
||||
marked: boolean
|
||||
retriable_at: string | null
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface Achievement {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
tier: "none" | "easy" | "medium" | "hard" | "challenge"
|
||||
kind: "scolarity" | "project" | "pedagogy" | "scolarity"
|
||||
visible: boolean
|
||||
image: string | null
|
||||
nbr_of_success: number | null
|
||||
users_url: string
|
||||
}
|
||||
|
||||
export interface LanguagesUser {
|
||||
id: number
|
||||
language_id: number
|
||||
user_id: number
|
||||
position: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface TitlesUser {
|
||||
id: number
|
||||
user_id: number
|
||||
title_id: number
|
||||
selected: boolean
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface ExpertisesUser {
|
||||
id: number
|
||||
expertise_id: number
|
||||
interested: boolean
|
||||
value: number
|
||||
contact_me: boolean
|
||||
created_at: string
|
||||
user_id: number
|
||||
}
|
||||
|
||||
export interface Campus {
|
||||
id: number
|
||||
name: string
|
||||
time_zone: string
|
||||
language: {
|
||||
id: number
|
||||
name: string
|
||||
identifier: string
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
users_count: number
|
||||
vogsphere_id: number
|
||||
country: string
|
||||
address: string
|
||||
zip: string
|
||||
city: string
|
||||
website: string
|
||||
facebook: string
|
||||
twitter: string
|
||||
active: boolean
|
||||
email_extension: string
|
||||
default_hidden_phone: boolean
|
||||
}
|
||||
|
||||
export interface CampusUser {
|
||||
id: number
|
||||
user_id: number
|
||||
campus_id: number
|
||||
is_primary: boolean
|
||||
created_at: string
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface FortyTwoProfile extends UserData {
|
||||
groups: Array<{ id: string; name: string }>
|
||||
cursus_users: CursusUser[]
|
||||
projects_users: ProjectUser[]
|
||||
languages_users: LanguagesUser[]
|
||||
achievements: Achievement[]
|
||||
titles: Array<{ id: string; name: string }>
|
||||
titles_users: TitlesUser[]
|
||||
partnerships: any[]
|
||||
patroned: any[]
|
||||
patroning: any[]
|
||||
expertises_users: ExpertisesUser[]
|
||||
roles: Array<{ id: string; name: string }>
|
||||
campus: Campus[]
|
||||
campus_users: CampusUser[]
|
||||
user: any | null
|
||||
}
|
||||
|
||||
export default function FortyTwo<
|
||||
P extends Record<string, any> = FortyTwoProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "42-school",
|
||||
name: "42 School",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://api.intra.42.fr/oauth/authorize",
|
||||
params: { scope: "public" },
|
||||
},
|
||||
token: "https://api.intra.42.fr/oauth/token",
|
||||
userinfo: "https://api.intra.42.fr/v2/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.usual_full_name,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function FortyTwo(options) {
|
||||
return {
|
||||
id: "42-school",
|
||||
name: "42 School",
|
||||
type: "oauth",
|
||||
authorization: "https://api.intra.42.fr/oauth/authorize",
|
||||
token: "https://api.intra.42.fr/oauth/token",
|
||||
userinfo: "https://api.intra.42.fr/v2/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.usual_full_name,
|
||||
email: profile.email,
|
||||
image: profile.image_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Apple(options) {
|
||||
return {
|
||||
id: "apple",
|
||||
name: "Apple",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://appleid.apple.com/auth/authorize",
|
||||
params: {
|
||||
scope: "name email",
|
||||
response_type: "code",
|
||||
id_token: "",
|
||||
response_mode: "form_post",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: "https://appleid.apple.com/auth/token",
|
||||
idToken: true,
|
||||
},
|
||||
jwks_endpoint: "https://appleid.apple.com/auth/keys",
|
||||
profile(profile) {
|
||||
// The name of the user will only be returned on first login
|
||||
const name = profile.user
|
||||
? profile.user.name.firstName + " " + profile.user.name.lastName
|
||||
: null
|
||||
|
||||
return {
|
||||
id: profile.sub,
|
||||
name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
checks: ["none"], // REVIEW: Apple does not support state, as far as I know. Can we use "pkce" then?
|
||||
options,
|
||||
}
|
||||
}
|
||||
122
src/providers/apple.ts
Normal file
122
src/providers/apple.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
/**
|
||||
* See more at:
|
||||
* [Retrieve the User's Information from Apple ID Servers
|
||||
](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple#3383773)
|
||||
*/
|
||||
export interface AppleProfile {
|
||||
/**
|
||||
* The issuer registered claim identifies the principal that issued the identity token.
|
||||
* Since Apple generates the token, the value is `https://appleid.apple.com`.
|
||||
*/
|
||||
iss: "https://appleid.apple.com"
|
||||
/**
|
||||
* The audience registered claim identifies the recipient for which the identity token is intended.
|
||||
* Since the token is meant for your application, the value is the `client_id` from your developer account.
|
||||
*/
|
||||
aud: string
|
||||
/**
|
||||
* The issued at registered claim indicates the time at which Apple issued the identity token,
|
||||
* in terms of the number of seconds since Epoch, in UTC.
|
||||
*/
|
||||
iat: number
|
||||
|
||||
/**
|
||||
* The expiration time registered identifies the time on or after which the identity token expires,
|
||||
* in terms of number of seconds since Epoch, in UTC.
|
||||
* The value must be greater than the current date/time when verifying the token.
|
||||
*/
|
||||
exp: number
|
||||
/**
|
||||
* The subject registered claim identifies the principal that's the subject of the identity token.
|
||||
* Since this token is meant for your application, the value is the unique identifier for the user.
|
||||
*/
|
||||
sub: string
|
||||
/**
|
||||
* A String value used to associate a client session and the identity token.
|
||||
* This value mitigates replay attacks and is present only if passed during the authorization request.
|
||||
*/
|
||||
nonce: string
|
||||
|
||||
/**
|
||||
* A Boolean value that indicates whether the transaction is on a nonce-supported platform.
|
||||
* If you sent a nonce in the authorization request but don't see the nonce claim in the identity token,
|
||||
* check this claim to determine how to proceed.
|
||||
* If this claim returns true, you should treat nonce as mandatory and fail the transaction;
|
||||
* otherwise, you can proceed treating the nonce as options.
|
||||
*/
|
||||
nonce_supported: boolean
|
||||
|
||||
/**
|
||||
* A String value representing the user's email address.
|
||||
* The email address is either the user's real email address or the proxy address,
|
||||
* depending on their status private email relay service.
|
||||
*/
|
||||
email: string
|
||||
|
||||
/**
|
||||
* A String or Boolean value that indicates whether the service has verified the email.
|
||||
* The value of this claim is always true, because the servers only return verified email addresses.
|
||||
* The value can either be a String (`"true"`) or a Boolean (`true`).
|
||||
*/
|
||||
email_verified: "true" | true
|
||||
|
||||
/**
|
||||
* A String or Boolean value that indicates whether the email shared by the user is the proxy address.
|
||||
* The value can either be a String (`"true"` or `"false"`) or a Boolean (`true` or `false`).
|
||||
*/
|
||||
is_private_email: boolean | "true" | "false"
|
||||
|
||||
/**
|
||||
* An Integer value that indicates whether the user appears to be a real person.
|
||||
* Use the value of this claim to mitigate fraud. The possible values are: 0 (or Unsupported), 1 (or Unknown), 2 (or LikelyReal).
|
||||
* For more information, see [`ASUserDetectionStatus`](https://developer.apple.com/documentation/authenticationservices/asuserdetectionstatus).
|
||||
* This claim is present only on iOS 14 and later, macOS 11 and later, watchOS 7 and later, tvOS 14 and later;
|
||||
* the claim isn't present or supported for web-based apps.
|
||||
*/
|
||||
real_user_status: 0 | 1 | 2
|
||||
|
||||
/**
|
||||
* A String value representing the transfer identifier used to migrate users to your team.
|
||||
* This claim is present only during the 60-day transfer period after an you transfer an app.
|
||||
* For more information, see [Bringing New Apps and Users into Your Team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team).
|
||||
*/
|
||||
transfer_sub: string
|
||||
at_hash: string
|
||||
auth_time: number
|
||||
}
|
||||
|
||||
export default function Apple<P extends Record<string, any> = AppleProfile>(
|
||||
options: Omit<OAuthUserConfig<P>, "clientSecret"> & {
|
||||
/**
|
||||
* Apple requires the client secret to be a JWT. You can generate one using the following script:
|
||||
* https://bal.so/apple-gen-secret
|
||||
*
|
||||
* Read more: [Creating the Client Secret
|
||||
](https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens#3262048)
|
||||
*/
|
||||
clientSecret: string
|
||||
}
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "apple",
|
||||
name: "Apple",
|
||||
type: "oauth",
|
||||
wellKnown: "https://appleid.apple.com/.well-known/openid-configuration",
|
||||
authorization: {
|
||||
params: { scope: "name email", response_mode: "form_post" },
|
||||
},
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
checks: ["pkce"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Atlassian(options) {
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
interface AtlassianProfile {
|
||||
account_id: string
|
||||
name: string
|
||||
email: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
export default function Atlassian<P extends AtlassianProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "atlassian",
|
||||
name: "Atlassian",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://auth.atlassian.com/oauth/authorize",
|
||||
url: "https://auth.atlassian.com/authorize",
|
||||
params: {
|
||||
audience: "api.atlassian.com",
|
||||
prompt: "consent",
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface Auth0Profile {
|
||||
sub: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface AzureB2CProfile {
|
||||
exp: number
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface AzureADProfile {
|
||||
sub: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface CognitoProfile {
|
||||
sub: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IncomingRequest } from "src/core"
|
||||
import { CommonProviderOptions } from "."
|
||||
import { User, Awaitable } from ".."
|
||||
import type { IncomingRequest } from "../core"
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { User, Awaitable } from ".."
|
||||
|
||||
export interface CredentialInput {
|
||||
label?: string
|
||||
@@ -10,7 +10,7 @@ export interface CredentialInput {
|
||||
}
|
||||
|
||||
export interface CredentialsConfig<
|
||||
C extends Record<string, CredentialInput> = {}
|
||||
C extends Record<string, CredentialInput> = Record<string, CredentialInput>
|
||||
> extends CommonProviderOptions {
|
||||
type: "credentials"
|
||||
credentials: C
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createTransport } from "nodemailer"
|
||||
|
||||
import { CommonProviderOptions } from "."
|
||||
import { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection"
|
||||
import { Awaitable } from ".."
|
||||
import type { CommonProviderOptions } from "."
|
||||
import type { Options as SMTPConnectionOptions } from "nodemailer/lib/smtp-connection"
|
||||
import type { Awaitable } from ".."
|
||||
|
||||
export interface EmailConfig extends CommonProviderOptions {
|
||||
type: "email"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function EVEOnline(options) {
|
||||
return {
|
||||
id: "eveonline",
|
||||
name: "EVE Online",
|
||||
type: "oauth",
|
||||
authorization: "https://login.eveonline.com/oauth/authorize",
|
||||
token: "https://login.eveonline.com/oauth/token",
|
||||
userinfo: "https://login.eveonline.com/oauth/verify",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
email: null,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
38
src/providers/eveonline.ts
Normal file
38
src/providers/eveonline.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface EVEOnlineProfile {
|
||||
CharacterID: number
|
||||
CharacterName: string
|
||||
ExpiresOn: string
|
||||
Scopes: string
|
||||
TokenType: string
|
||||
CharacterOwnerHash: string
|
||||
IntellectualProperty: string
|
||||
}
|
||||
|
||||
export default function EVEOnline<
|
||||
P extends Record<string, any> = EVEOnlineProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "eveonline",
|
||||
name: "EVE Online",
|
||||
type: "oauth",
|
||||
wellKnown:
|
||||
"https://login.eveonline.com/.well-known/oauth-authorization-server",
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "publicData",
|
||||
},
|
||||
},
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.CharacterID,
|
||||
name: profile.CharacterName,
|
||||
email: null,
|
||||
image: `https://image.eveonline.com/Character/${profile.CharacterID}_128.jpg`,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Profile } from ".."
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface FacebookProfile extends Profile {
|
||||
export interface FacebookProfile {
|
||||
id: string
|
||||
picture: { data: { url: string } }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,31 @@ export default function GitHub(options) {
|
||||
type: "oauth",
|
||||
authorization: "https://github.com/login/oauth/authorize?scope=read:user+user:email",
|
||||
token: "https://github.com/login/oauth/access_token",
|
||||
userinfo: "https://api.github.com/user",
|
||||
userinfo: {
|
||||
url: "https://api.github.com/user",
|
||||
async request({ client, tokens }) {
|
||||
// Get base profile
|
||||
const profile = await client.userinfo(tokens)
|
||||
|
||||
// If user has email hidden, get their primary email from the GitHub API
|
||||
if (!profile.email) {
|
||||
const emails = await (
|
||||
await fetch("https://api.github.com/user/emails", {
|
||||
headers: { Authorization: `token ${tokens.access_token}` },
|
||||
})
|
||||
).json()
|
||||
|
||||
if (emails?.length > 0) {
|
||||
// Get primary email
|
||||
profile.email = emails.find(email => email.primary)?.email;
|
||||
// And if for some reason it doesn't exist, just use the first
|
||||
if (!profile.email) profile.email = emails[0].email;
|
||||
}
|
||||
}
|
||||
|
||||
return profile
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id.toString(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface GoogleProfile {
|
||||
sub: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { OAuthConfig, OAuthProvider, OAuthProviderType } from "./oauth"
|
||||
import type { OAuthConfig, OAuthProvider, OAuthProviderType } from "./oauth"
|
||||
|
||||
import { EmailConfig, EmailProvider, EmailProviderType } from "./email"
|
||||
import type { EmailConfig, EmailProvider, EmailProviderType } from "./email"
|
||||
|
||||
import {
|
||||
import type {
|
||||
CredentialsConfig,
|
||||
CredentialsProvider,
|
||||
CredentialsProviderType,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface KeycloakProfile {
|
||||
exp: number
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface LineProfile {
|
||||
iss: string;
|
||||
sub: string;
|
||||
aud: string;
|
||||
exp: number;
|
||||
iat: number;
|
||||
amr: string[];
|
||||
name: string;
|
||||
picture: string;
|
||||
user: any;
|
||||
iss: string
|
||||
sub: string
|
||||
aud: string
|
||||
exp: number
|
||||
iat: number
|
||||
amr: string[]
|
||||
name: string
|
||||
picture: string
|
||||
user: any
|
||||
}
|
||||
|
||||
export default function LINE<
|
||||
P extends Record<string, any> = LineProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
export default function LINE<P extends Record<string, any> = LineProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "line",
|
||||
name: "LINE",
|
||||
@@ -33,6 +33,6 @@ export default function LINE<
|
||||
client: {
|
||||
id_token_signed_response_alg: "HS256",
|
||||
},
|
||||
options
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
interface Identifier {
|
||||
identifier: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonProviderOptions } from "../providers"
|
||||
import { Profile, TokenSet, User, Awaitable } from ".."
|
||||
import type { CommonProviderOptions } from "../providers"
|
||||
import type { Profile, TokenSet, User, Awaitable } from ".."
|
||||
|
||||
import type {
|
||||
AuthorizationParameters,
|
||||
@@ -9,10 +9,11 @@ import type {
|
||||
IssuerMetadata,
|
||||
OAuthCallbackChecks,
|
||||
OpenIDCallbackChecks,
|
||||
HttpOptions,
|
||||
} from "openid-client"
|
||||
import type { JWK } from "jose"
|
||||
|
||||
type Client = InstanceType<Issuer['Client']>;
|
||||
type Client = InstanceType<Issuer["Client"]>
|
||||
|
||||
export type { OAuthProviderType } from "./oauth-types"
|
||||
|
||||
@@ -114,10 +115,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
client?: Partial<ClientMetadata>
|
||||
jwks?: { keys: JWK[] }
|
||||
clientId?: string
|
||||
clientSecret?:
|
||||
| string
|
||||
// TODO: only allow for Apple
|
||||
| Record<"appleId" | "teamId" | "privateKey" | "keyId", string>
|
||||
clientSecret?: string
|
||||
/**
|
||||
* If set to `true`, the user information will be extracted
|
||||
* from the `id_token` claims, instead of
|
||||
@@ -132,6 +130,9 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
|
||||
region?: string
|
||||
// TODO: only allow for some
|
||||
issuer?: string
|
||||
/** Read more at: https://github.com/panva/node-openid-client/tree/main/docs#customizing-http-requests */
|
||||
httpOptions?: HttpOptions
|
||||
|
||||
/**
|
||||
* The options provided by the user.
|
||||
* We will perform a deep-merge of these values
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface OktaProfile {
|
||||
iss: string
|
||||
|
||||
77
src/providers/osu.ts
Normal file
77
src/providers/osu.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface OsuUserCompact {
|
||||
avatar_url: string
|
||||
country_code: string
|
||||
default_group: string
|
||||
id: string
|
||||
is_active: boolean
|
||||
is_bot: boolean
|
||||
is_deleted: boolean
|
||||
is_online: boolean
|
||||
is_supporter: boolean
|
||||
last_visit: Date | null
|
||||
pm_friends_only: boolean
|
||||
profile_colour: string | null
|
||||
username: string
|
||||
}
|
||||
|
||||
export interface OsuProfile extends OsuUserCompact {
|
||||
discord: string | null
|
||||
has_supported: boolean
|
||||
interests: string | null
|
||||
join_date: Date
|
||||
kudosu: {
|
||||
available: number
|
||||
total: number
|
||||
}
|
||||
location: string | null
|
||||
max_blocks: number
|
||||
max_friends: number
|
||||
occupation: string | null
|
||||
playmode: string
|
||||
playstyle: string[]
|
||||
post_count: number
|
||||
profile_order: string[]
|
||||
title: string | null
|
||||
title_url: string | null
|
||||
twitter: string | null
|
||||
website: string | null
|
||||
country: {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
cover: {
|
||||
custom_url: string | null
|
||||
url: string
|
||||
id: number | null
|
||||
}
|
||||
is_restricted: boolean
|
||||
}
|
||||
|
||||
export default function Osu<P extends Record<string, any> = OsuProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "osu",
|
||||
name: "Osu!",
|
||||
type: "oauth",
|
||||
token: "https://osu.ppy.sh/oauth/token",
|
||||
authorization: {
|
||||
url: "https://osu.ppy.sh/oauth/authorize",
|
||||
params: {
|
||||
scope: "identify",
|
||||
},
|
||||
},
|
||||
userinfo: "https://osu.ppy.sh/api/v2/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
email: null,
|
||||
name: profile.username,
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
59
src/providers/pipedrive.ts
Normal file
59
src/providers/pipedrive.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface PipedriveProfile {
|
||||
success: boolean
|
||||
data: {
|
||||
id: number
|
||||
name: string
|
||||
default_currency?: string
|
||||
locale?: string
|
||||
lang?: number
|
||||
email: string
|
||||
phone?: string
|
||||
activated?: boolean
|
||||
last_login?: Date
|
||||
created?: Date
|
||||
modified?: Date
|
||||
signup_flow_variation?: string
|
||||
has_created_company?: boolean
|
||||
is_admin?: number
|
||||
active_flag?: boolean
|
||||
timezone_name?: string
|
||||
timezone_offset?: string
|
||||
role_id?: number
|
||||
icon_url?: string
|
||||
is_you?: boolean
|
||||
company_id?: number
|
||||
company_name?: string
|
||||
company_domain?: string
|
||||
company_country?: string
|
||||
company_industry?: string
|
||||
language?: {
|
||||
language_code?: string
|
||||
country_code?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function Pipedrive<
|
||||
P extends Record<string, any> = PipedriveProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "pipedrive",
|
||||
name: "Pipedrive",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
authorization: "https://oauth.pipedrive.com/oauth/authorize",
|
||||
token: "https://oauth.pipedrive.com/oauth/token",
|
||||
userinfo: "https://api.pipedrive.com/users/me",
|
||||
profile: ({ data: profile }) => {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.icon_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface SlackProfile {
|
||||
ok: boolean
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface SpotifyImage {
|
||||
url: string
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface TwitchProfile {
|
||||
sub: string
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Twitter(options) {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter",
|
||||
type: "oauth",
|
||||
version: "1.0A",
|
||||
authorization: "https://api.twitter.com/oauth/authenticate",
|
||||
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
|
||||
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
|
||||
profileUrl:
|
||||
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(
|
||||
/_normal\.(jpg|png|gif)$/,
|
||||
".$1"
|
||||
),
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
124
src/providers/twitter.ts
Normal file
124
src/providers/twitter.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface TwitterProfile {
|
||||
id: number
|
||||
id_str: string
|
||||
name: string
|
||||
screen_name: string
|
||||
location: string
|
||||
description: string
|
||||
url: string
|
||||
entities: {
|
||||
url: {
|
||||
urls: Array<{
|
||||
url: string
|
||||
expanded_url: string
|
||||
display_url: string
|
||||
indices: number[]
|
||||
}>
|
||||
}
|
||||
description: {
|
||||
urls: any[]
|
||||
}
|
||||
}
|
||||
protected: boolean
|
||||
followers_count: number
|
||||
friends_count: number
|
||||
listed_count: number
|
||||
created_at: string
|
||||
favourites_count: number
|
||||
utc_offset?: any
|
||||
time_zone?: any
|
||||
geo_enabled: boolean
|
||||
verified: boolean
|
||||
statuses_count: number
|
||||
lang?: any
|
||||
status: {
|
||||
created_at: string
|
||||
id: number
|
||||
id_str: string
|
||||
text: string
|
||||
truncated: boolean
|
||||
entities: {
|
||||
hashtags: any[]
|
||||
symbols: any[]
|
||||
user_mentions: Array<{
|
||||
screen_name: string
|
||||
name: string
|
||||
id: number
|
||||
id_str: string
|
||||
indices: number[]
|
||||
}>
|
||||
urls: any[]
|
||||
}
|
||||
source: string
|
||||
in_reply_to_status_id: number
|
||||
in_reply_to_status_id_str: string
|
||||
in_reply_to_user_id: number
|
||||
in_reply_to_user_id_str: string
|
||||
in_reply_to_screen_name: string
|
||||
geo?: any
|
||||
coordinates?: any
|
||||
place?: any
|
||||
contributors?: any
|
||||
is_quote_status: boolean
|
||||
retweet_count: number
|
||||
favorite_count: number
|
||||
favorited: boolean
|
||||
retweeted: boolean
|
||||
lang: string
|
||||
}
|
||||
contributors_enabled: boolean
|
||||
is_translator: boolean
|
||||
is_translation_enabled: boolean
|
||||
profile_background_color: string
|
||||
profile_background_image_url: string
|
||||
profile_background_image_url_https: string
|
||||
profile_background_tile: boolean
|
||||
profile_image_url: string
|
||||
profile_image_url_https: string
|
||||
profile_banner_url: string
|
||||
profile_link_color: string
|
||||
profile_sidebar_border_color: string
|
||||
profile_sidebar_fill_color: string
|
||||
profile_text_color: string
|
||||
profile_use_background_image: boolean
|
||||
has_extended_profile: boolean
|
||||
default_profile: boolean
|
||||
default_profile_image: boolean
|
||||
following: boolean
|
||||
follow_request_sent: boolean
|
||||
notifications: boolean
|
||||
translator_type: string
|
||||
withheld_in_countries: any[]
|
||||
suspended: boolean
|
||||
needs_phone_verification: boolean
|
||||
}
|
||||
|
||||
export default function Twitter<P extends Record<string, any> = TwitterProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter",
|
||||
type: "oauth",
|
||||
version: "1.0A",
|
||||
authorization: "https://api.twitter.com/oauth/authenticate",
|
||||
accessTokenUrl: "https://api.twitter.com/oauth/access_token",
|
||||
requestTokenUrl: "https://api.twitter.com/oauth/request_token",
|
||||
profileUrl:
|
||||
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id_str,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.profile_image_url_https.replace(
|
||||
/_normal\.(jpg|png|gif)$/,
|
||||
".$1"
|
||||
),
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,9 @@ export default function Yandex(options) {
|
||||
id: profile.id,
|
||||
name: profile.real_name,
|
||||
email: profile.default_email,
|
||||
image: profile.is_avatar_empty ? null : `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
|
||||
image: profile.is_avatar_empty
|
||||
? null
|
||||
: `https://avatars.yandex.net/get-yapic/${profile.default_avatar_id}/islands-200`,
|
||||
}
|
||||
},
|
||||
options,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Zoom(options) {
|
||||
return {
|
||||
id: "zoom",
|
||||
name: "Zoom",
|
||||
type: "oauth",
|
||||
authorization: "https://zoom.us/oauth/authorize",
|
||||
token: "https://zoom.us/oauth/token",
|
||||
userinfo: "https://api.zoom.us/v2/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
email: profile.email,
|
||||
image: null,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
52
src/providers/zoom.ts
Normal file
52
src/providers/zoom.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface ZoomProfile {
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
email: string
|
||||
type: number
|
||||
role_name: string
|
||||
pmi: number
|
||||
use_pmi: boolean
|
||||
vanity_url: string
|
||||
personal_meeting_url: string
|
||||
timezone: string
|
||||
verified: number
|
||||
dept: string
|
||||
created_at: string
|
||||
last_login_time: string
|
||||
last_client_version: string
|
||||
pic_url: string
|
||||
host_key: string
|
||||
jid: string
|
||||
group_ids: string[]
|
||||
im_group_ids: string[]
|
||||
account_id: string
|
||||
language: string
|
||||
phone_country: string
|
||||
phone_number: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export default function Zoom<P extends Record<string, any> = ZoomProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "zoom",
|
||||
name: "Zoom",
|
||||
type: "oauth",
|
||||
authorization: "https://zoom.us/oauth/authorize?scope",
|
||||
token: "https://zoom.us/oauth/token",
|
||||
userinfo: "https://api.zoom.us/v2/users/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: `${profile.first_name} ${profile.last_name}`,
|
||||
email: profile.email,
|
||||
image: profile.pic_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ export function SessionProvider(props: SessionProviderProps) {
|
||||
__NEXTAUTH._session = await getSession()
|
||||
setSession(__NEXTAUTH._session)
|
||||
} catch (error) {
|
||||
logger.error("CLIENT_SESSION_ERROR", error)
|
||||
logger.error("CLIENT_SESSION_ERROR", error as Error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -16,14 +16,9 @@
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"declaration": true,
|
||||
"stripInternal": true
|
||||
"stripInternal": true,
|
||||
"skipLibCheck": true,
|
||||
"skipDefaultLibCheck": true
|
||||
},
|
||||
"exclude": [
|
||||
"./*.js",
|
||||
"./*.d.ts",
|
||||
"app",
|
||||
"**/tests",
|
||||
"**/__tests__",
|
||||
"config"
|
||||
]
|
||||
"exclude": ["./*.js", "./*.d.ts", "app", "**/tests", "**/__tests__", "config"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user