mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
Compare commits
148 Commits
v4.0.0-bet
...
v4.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3be5e87f6 | ||
|
|
844c9b147c | ||
|
|
c9e16fb71e | ||
|
|
a7d34f97c8 | ||
|
|
f20d6790c8 | ||
|
|
53baf6d67d | ||
|
|
255c822dfb | ||
|
|
31c03c96d1 | ||
|
|
74df39a678 | ||
|
|
714d80a4f5 | ||
|
|
3d5c669a05 | ||
|
|
29977f108f | ||
|
|
7d2e16a6bb | ||
|
|
af157dac07 | ||
|
|
1bf56a218e | ||
|
|
4824f8c02a | ||
|
|
a4d831d1b9 | ||
|
|
59985264a2 | ||
|
|
c844296982 | ||
|
|
d1aa2a1a8e | ||
|
|
8139126f29 | ||
|
|
aa0e8200b3 | ||
|
|
82447f8e3e | ||
|
|
a0b3814c81 | ||
|
|
90c7d535c0 | ||
|
|
0510c9b1ba | ||
|
|
49e4af17e2 | ||
|
|
db65afe5ab | ||
|
|
36ca1f99e3 | ||
|
|
9bec96784f | ||
|
|
227ff2259f | ||
|
|
c71cb8457d | ||
|
|
a09a75be53 | ||
|
|
c4936991e5 | ||
|
|
e2add6a597 | ||
|
|
0e8be0c7d2 | ||
|
|
d1d2d977fe | ||
|
|
48749d7320 | ||
|
|
87d0beb70c | ||
|
|
978e2eeb08 | ||
|
|
8ab057ea33 | ||
|
|
2c269a6a81 | ||
|
|
8b9a109255 | ||
|
|
ac35d9f739 | ||
|
|
30a0fc6bc0 | ||
|
|
b0f6175cec | ||
|
|
1c7fe57edb | ||
|
|
59797bbdef | ||
|
|
9eb78a9de9 | ||
|
|
2670bbb28f | ||
|
|
0431c2a334 | ||
|
|
5ac688cc18 | ||
|
|
8ea75f0c1c | ||
|
|
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 |
15
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
15
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -10,7 +10,18 @@ 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!
|
||||
Is this your first time contributing? See this video https://www.youtube.com/watch?v=cuoNzXFLitc
|
||||
|
||||
**Providing incorrect/insufficient information or skipping steps to reproduce the bug may result in closing the issue or converting to discussion without further explanation.**
|
||||
|
||||
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:
|
||||
@@ -44,8 +55,6 @@ body:
|
||||
We encourage you to use one of the templates set up on **CodeSandbox** to reproduce your issue:
|
||||
- [`next-auth-example`](https://codesandbox.io/s/next-auth-example-1kktb)
|
||||
- [`next-auth-typescript-example`](https://codesandbox.io/s/next-auth-typescript-example-se32w)
|
||||
|
||||
🚧 – _If you don't provide any way to reproduce the bug, the issue is at risk of being closed._
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
|
||||
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 💚
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -36,6 +36,8 @@ node_modules
|
||||
/index.d.ts
|
||||
/index.js
|
||||
/next
|
||||
/middleware.d.ts
|
||||
/middleware.js
|
||||
|
||||
# Development app
|
||||
app/src/css
|
||||
@@ -43,6 +45,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 +54,9 @@ app/prisma/dev.db*
|
||||
/.vs
|
||||
.vscode
|
||||
|
||||
# Jetbrains
|
||||
.idea
|
||||
|
||||
# GitHub Actions runner
|
||||
/actions-runner
|
||||
/_work
|
||||
|
||||
35
README.md
35
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
|
||||
|
||||
```
|
||||
@@ -49,7 +54,7 @@ See [next-auth.js.org](https://next-auth.js.org) for more information and docume
|
||||
### Flexible and easy to use
|
||||
|
||||
- Designed to work with any OAuth service, it supports OAuth 1.0, 1.0A and 2.0
|
||||
- Built-in support for [many popular sign-in services](https://next-auth.js.org/configuration/providers)
|
||||
- Built-in support for [many popular sign-in services](https://next-auth.js.org/providers/overview)
|
||||
- Supports email / passwordless authentication
|
||||
- Supports stateless authentication with any backend (Active Directory, LDAP, etc)
|
||||
- Supports both JSON Web Tokens and database sessions
|
||||
@@ -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>",
|
||||
}),
|
||||
@@ -140,7 +149,7 @@ export default function Component() {
|
||||
|
||||
### Share/configure session state
|
||||
|
||||
Use the `<SessionProvider>` to allows instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
|
||||
Use the `<SessionProvider>` to allow instances of `useSession()` to share the session object across components. It also takes care of keeping the session updated and synced between tabs/windows.
|
||||
|
||||
```jsx title="pages/_app.js"
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
@@ -170,7 +179,7 @@ export default function App({
|
||||
|
||||
### Support
|
||||
|
||||
We're happy to announce we've recently created an [OpenCollective](https://opencollective.org/nextauth) for individuals and companies looking to contribute financially to the project!
|
||||
We're happy to announce we've recently created an [OpenCollective](https://opencollective.com/nextauth) for individuals and companies looking to contribute financially to the project!
|
||||
|
||||
<!--sponsors start-->
|
||||
<table>
|
||||
@@ -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/main/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
16
SECURITY.md
16
SECURITY.md
@@ -2,12 +2,6 @@
|
||||
|
||||
NextAuth.js practices responsible disclosure.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are only released for the current version.
|
||||
|
||||
Old releases are not maintained and do not receive updates.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We request that you contact us directly to report serious issues that might impact the security of sites using NextAuth.js.
|
||||
@@ -19,6 +13,12 @@ If you contact us regarding a serious issue:
|
||||
- We will disclose the issue (and credit you, with your consent) once a fix to resolve the issue has been released.
|
||||
- If 90 days has elapsed and we still don't have a fix, we will disclose the issue publicly.
|
||||
|
||||
Currently, the best way to report an issue is by contacting us via email at me@iaincollins.com or info@balazsorban.com and yo@ndo.dev.
|
||||
The best way to report an issue is by contacting us via email at info@balazsorban.com or me@iaincollins.com and yo@ndo.dev, or raise a public issue requesting someone get in touch with you via whatever means you prefer for more details. (Please do not disclose sensitive details publicly at this stage.)
|
||||
|
||||
For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem future or default behaviour / options) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.
|
||||
> For less serious issues (e.g. RFC compliance for unsupported flows or potential issues that may cause a problem in the future) it is appropriate to submit these these publically as bug reports or feature requests or to raise a question to open a discussion around them.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are only released for the current version.
|
||||
|
||||
Old releases are not maintained and do not receive updates.
|
||||
|
||||
@@ -7,7 +7,7 @@ NEXTAUTH_URL=http://localhost:3000
|
||||
# https://generate-secret.vercel.app/32 to generate a secret.
|
||||
# Note: Changing a secret may invalidate existing sessions
|
||||
# and/or verification tokens.
|
||||
SECRET=secret
|
||||
NEXTAUTH_SECRET=secret
|
||||
|
||||
AUTH0_ID=
|
||||
AUTH0_SECRET=
|
||||
@@ -33,6 +33,9 @@ TWITTER_SECRET=
|
||||
LINE_ID=
|
||||
LINE_SECRET=
|
||||
|
||||
TRAKT_ID=
|
||||
TRAKT_SECRET=
|
||||
|
||||
# Example configuration for a Gmail account (will need SMTP enabled)
|
||||
EMAIL_SERVER=smtps://user@gmail.com:password@smtp.gmail.com:465
|
||||
EMAIL_FROM=user@gmail.com
|
||||
|
||||
@@ -103,6 +103,11 @@ export default function Header() {
|
||||
<a>Email</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className={styles.navItem}>
|
||||
<Link href="/middleware-protected">
|
||||
<a>Middleware protected</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
1
app/next-env.d.ts
vendored
1
app/next-env.d.ts
vendored
@@ -1,5 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
|
||||
@@ -2,6 +2,10 @@ const path = require("path")
|
||||
|
||||
module.exports = {
|
||||
webpack(config) {
|
||||
config.experiments = {
|
||||
...config.experiments,
|
||||
topLevelAwait: true,
|
||||
}
|
||||
config.resolve = {
|
||||
...config.resolve,
|
||||
alias: {
|
||||
@@ -18,6 +22,9 @@ module.exports = {
|
||||
|
||||
return config
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
experimental: {
|
||||
externalDir: true,
|
||||
},
|
||||
|
||||
@@ -5,29 +5,31 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean": "rm -rf .next",
|
||||
"postinstall": "rm -rf node_modules/next-auth",
|
||||
"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",
|
||||
"email": "npx fake-smtp-server",
|
||||
"start:email": "email"
|
||||
"start:email": "npm run email"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@next-auth/fauna-adapter": "0.2.2-next.4",
|
||||
"@next-auth/prisma-adapter": "0.5.2-next.5",
|
||||
"@prisma/client": "^2.29.1",
|
||||
"@next-auth/fauna-adapter": "^1.0.1",
|
||||
"@next-auth/prisma-adapter": "^1.0.1",
|
||||
"@prisma/client": "^3.7.0",
|
||||
"fake-smtp-server": "^0.8.0",
|
||||
"faunadb": "^4.3.0",
|
||||
"next": "^11.1.0",
|
||||
"nodemailer": "^6.6.3",
|
||||
"faunadb": "^4.4.1",
|
||||
"next": "^12.0.8",
|
||||
"nodemailer": "^6.7.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prisma": "^2.29.1"
|
||||
"prisma": "^3.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth"
|
||||
import EmailProvider from "next-auth/providers/email"
|
||||
// import EmailProvider from "next-auth/providers/email"
|
||||
import GitHubProvider from "next-auth/providers/github"
|
||||
import Auth0Provider from "next-auth/providers/auth0"
|
||||
import KeycloakProvider from "next-auth/providers/keycloak"
|
||||
import TwitterProvider from "next-auth/providers/twitter"
|
||||
import TwitterProvider, {
|
||||
TwitterLegacy as TwitterLegacyProvider,
|
||||
} from "next-auth/providers/twitter"
|
||||
import CredentialsProvider from "next-auth/providers/credentials"
|
||||
import IDS4Provider from "next-auth/providers/identity-server4"
|
||||
import Twitch from "next-auth/providers/twitch"
|
||||
@@ -23,6 +25,10 @@ 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 PatreonProvider from "next-auth/providers/patreon"
|
||||
import TraktProvider from "next-auth/providers/trakt"
|
||||
|
||||
// import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||
// import { PrismaClient } from "@prisma/client"
|
||||
@@ -42,15 +48,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",
|
||||
@@ -69,11 +75,17 @@ export const authOptions: NextAuthOptions = {
|
||||
},
|
||||
}),
|
||||
// OAuth 1
|
||||
// TwitterLegacyProvider({
|
||||
// clientId: process.env.TWITTER_LEGACY_ID,
|
||||
// clientSecret: process.env.TWITTER_LEGACY_SECRET,
|
||||
// }),
|
||||
// OAuth 2 / OIDC
|
||||
TwitterProvider({
|
||||
// Opt-in to the new Twitter API for now. Should be default in the future.
|
||||
version: "2.0",
|
||||
clientId: process.env.TWITTER_ID,
|
||||
clientSecret: process.env.TWITTER_SECRET,
|
||||
}),
|
||||
// OAuth 2 / OIDC
|
||||
GitHubProvider({
|
||||
clientId: process.env.GITHUB_ID,
|
||||
clientSecret: process.env.GITHUB_SECRET,
|
||||
@@ -167,11 +179,23 @@ 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,
|
||||
}),
|
||||
PatreonProvider({
|
||||
clientId: process.env.PATREON_ID,
|
||||
clientSecret: process.env.PATREON_SECRET,
|
||||
}),
|
||||
TraktProvider({
|
||||
clientId: process.env.TRAKT_ID,
|
||||
clientSecret: process.env.TRAKT_SECRET,
|
||||
}),
|
||||
],
|
||||
jwt: {
|
||||
encryption: true,
|
||||
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))
|
||||
}
|
||||
|
||||
44
app/pages/middleware-protected/_middleware.js
Normal file
44
app/pages/middleware-protected/_middleware.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export { default } from "next-auth/middleware"
|
||||
|
||||
// Other ways to use this middleware
|
||||
|
||||
// import withAuth from "next-auth/middleware"
|
||||
// import { withAuth } from "next-auth/middleware"
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req)
|
||||
// }
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req, ev)
|
||||
// }
|
||||
|
||||
// export function middleware(req, ev) {
|
||||
// return withAuth(req, {
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => !!token,
|
||||
// },
|
||||
// })
|
||||
// }
|
||||
|
||||
// export default withAuth(function middleware(req, ev) {
|
||||
// console.log(req.nextauth.token)
|
||||
// })
|
||||
|
||||
// export default withAuth(
|
||||
// function middleware(req, ev) {
|
||||
// console.log(req, ev)
|
||||
// return undefined // NOTE: `NextMiddleware` should allow returning `void`
|
||||
// },
|
||||
// {
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => token.name === "Balázs Orbán",
|
||||
// }
|
||||
// }
|
||||
// )
|
||||
|
||||
// export default withAuth({
|
||||
// callbacks: {
|
||||
// authorized: ({ token }) => !!token,
|
||||
// },
|
||||
// })
|
||||
9
app/pages/middleware-protected/index.js
Normal file
9
app/pages/middleware-protected/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import Layout from "components/layout"
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Layout>
|
||||
<h1>Page protected by Middleware</h1>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ body {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
color: #333;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
li,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"incremental": true,
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
||||
@@ -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" }],
|
||||
|
||||
10668
package-lock.json
generated
10668
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
89
package.json
89
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",
|
||||
@@ -26,12 +31,13 @@
|
||||
"./react": "./react/index.js",
|
||||
"./core": "./core/index.js",
|
||||
"./next": "./next/index.js",
|
||||
"./middleware": "./middleware.js",
|
||||
"./client/_utils": "./client/_utils.js",
|
||||
"./providers/*": "./providers/*.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:js && npm run build:css",
|
||||
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts",
|
||||
"clean": "rm -rf client css lib providers core jwt react next index.d.ts index.js adapters.d.ts middleware.d.ts middleware.js",
|
||||
"build:js": "npm run clean && npm run generate-providers && tsc && babel --config-file ./config/babel.config.js src --out-dir . --extensions \".tsx,.ts,.js,.jsx\"",
|
||||
"build:css": "postcss --config config/postcss.config.js src/**/*.css --base src --dir . && node config/wrap-css.js",
|
||||
"dev:setup": "npm i && npm run generate-providers && npm run build:css && cd app && npm i",
|
||||
@@ -56,23 +62,26 @@
|
||||
"core",
|
||||
"index.d.ts",
|
||||
"index.js",
|
||||
"adapters.d.ts"
|
||||
"adapters.d.ts",
|
||||
"middleware.d.ts",
|
||||
"middleware.js"
|
||||
],
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@panva/hkdf": "^1.0.0",
|
||||
"jose": "^4.1.2",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@panva/hkdf": "^1.0.1",
|
||||
"cookie": "^0.4.1",
|
||||
"jose": "^4.3.7",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.0.1",
|
||||
"preact": "^10.5.14",
|
||||
"openid-client": "^5.1.0",
|
||||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"nodemailer": "^6.6.5",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
"react": "^17.0.2 || ^18.0.0-0",
|
||||
"react-dom": "^17.0.2 || ^18.0.0-0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"nodemailer": {
|
||||
@@ -81,50 +90,47 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/core": "^1.6.0",
|
||||
"@babel/cli": "^7.15.7",
|
||||
"@babel/core": "^7.15.5",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.14.5",
|
||||
"@babel/plugin-transform-runtime": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.6",
|
||||
"@babel/preset-react": "^7.14.5",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@babel/cli": "^7.16.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-proposal-optional-catch-binding": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^12.1.2",
|
||||
"@testing-library/react-hooks": "^7.0.2",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"@types/node": "^16.11.6",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/node": "^16.11.12",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/oauth": "^0.9.1",
|
||||
"@types/react": "^17.0.27",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"autoprefixer": "^10.3.7",
|
||||
"babel-jest": "^27.3.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"babel-jest": "^27.4.2",
|
||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||
"babel-preset-preact": "^2.0.0",
|
||||
"conventional-changelog-conventionalcommits": "4.6.1",
|
||||
"cssnano": "^5.0.8",
|
||||
"cssnano": "^5.0.12",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard-with-typescript": "^21.0.1",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jest": "^25.2.2",
|
||||
"eslint-plugin-jest": "^25.3.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"husky": "^7.0.2",
|
||||
"jest": "^27.3.0",
|
||||
"husky": "^7.0.4",
|
||||
"jest": "^27.4.3",
|
||||
"jest-watch-typeahead": "^1.0.0",
|
||||
"msw": "^0.35.0",
|
||||
"next": "v11.1.3-canary.0",
|
||||
"postcss-cli": "^9.0.1",
|
||||
"msw": "^0.36.3",
|
||||
"next": "12.0.9",
|
||||
"postcss-cli": "^9.0.2",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"prettier": "^2.4.1",
|
||||
"pretty-quick": "^3.1.1",
|
||||
"prettier": "2.4.1",
|
||||
"pretty-quick": "^3.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"typescript": "^4.4.3",
|
||||
"typescript": "^4.5.2",
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -136,7 +142,7 @@
|
||||
"eslintConfig": {
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
"project": "./tsconfig.eslint.json"
|
||||
},
|
||||
"extends": [
|
||||
"standard-with-typescript",
|
||||
@@ -180,6 +186,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"./*.d.ts",
|
||||
"**/tests",
|
||||
"**/__tests__"
|
||||
],
|
||||
"release": {
|
||||
"branches": [
|
||||
"+([0-9])?(.{+([0-9]),x}).x",
|
||||
@@ -210,6 +221,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>
|
||||
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,26 @@ 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
|
||||
case "session": {
|
||||
const session = await routes.session({ options, sessionStore })
|
||||
if (session.cookies) cookies.push(...session.cookies)
|
||||
return { ...session, cookies } as any
|
||||
}
|
||||
case "csrf":
|
||||
return {
|
||||
headers: [{ key: "Content-Type", value: "application/json" }],
|
||||
@@ -96,11 +122,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 }
|
||||
@@ -112,15 +138,6 @@ export async function NextAuthHandler<
|
||||
}
|
||||
return render.verifyRequest()
|
||||
case "error":
|
||||
if (pages.error) {
|
||||
return {
|
||||
redirect: `${pages.error}${
|
||||
pages.error.includes("?") ? "&" : "?"
|
||||
}error=${error}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
|
||||
// These error messages are displayed in line on the sign in page
|
||||
if (
|
||||
[
|
||||
@@ -139,10 +156,19 @@ export async function NextAuthHandler<
|
||||
return { redirect: `${options.url}/signin?error=${error}`, cookies }
|
||||
}
|
||||
|
||||
return render.error({ error })
|
||||
if (pages.error) {
|
||||
return {
|
||||
redirect: `${pages.error}${
|
||||
pages.error.includes("?") ? "&" : "?"
|
||||
}error=${error}`,
|
||||
cookies,
|
||||
}
|
||||
}
|
||||
|
||||
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 +186,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 +204,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 +221,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 +231,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,
|
||||
@@ -104,7 +100,7 @@ export async function init({
|
||||
// Callback functions
|
||||
callbacks: { ...defaultCallbacks, ...userOptions.callbacks },
|
||||
logger,
|
||||
callbackUrl: process.env.NEXTAUTH_URL ?? "http://localhost:3000",
|
||||
callbackUrl: url.origin,
|
||||
}
|
||||
|
||||
// Init cookies
|
||||
|
||||
87
src/core/lib/assert.ts
Normal file
87
src/core/lib/assert.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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
|
||||
|
||||
let twitterWarned = false
|
||||
|
||||
/**
|
||||
* 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
|
||||
let hasTwitterProvider
|
||||
|
||||
for (const provider of options.providers) {
|
||||
if (provider.type === "credentials") hasCredentials = true
|
||||
else if (provider.type === "email") hasEmail = true
|
||||
else if (provider.id === "twitter") hasTwitterProvider = 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.")
|
||||
}
|
||||
|
||||
if (!twitterWarned && hasTwitterProvider) {
|
||||
twitterWarned = true
|
||||
return "TWITTER_OAUTH_2_BETA"
|
||||
}
|
||||
}
|
||||
@@ -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,25 @@
|
||||
import { 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 { CallbackParamsType } from "openid-client"
|
||||
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 +41,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 +59,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 +69,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 +138,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 +199,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"
|
||||
|
||||
/**
|
||||
@@ -12,8 +12,10 @@ export async function openidClient(
|
||||
options: InternalOptions<"oauth">
|
||||
): Promise<Client> {
|
||||
const provider = options.provider
|
||||
|
||||
let issuer
|
||||
|
||||
if (provider.httpOptions) custom.setHttpOptionsDefaults(provider.httpOptions)
|
||||
|
||||
let issuer: Issuer
|
||||
if (provider.wellKnown) {
|
||||
issuer = await Issuer.discover(provider.wellKnown)
|
||||
} else {
|
||||
@@ -31,13 +33,18 @@ 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
|
||||
|
||||
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 },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
import { Provider } from "../../providers"
|
||||
import { merge } from "../../lib/merge"
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
import type { InternalProvider } from "../../lib/types"
|
||||
import type { Provider } from "../../providers"
|
||||
import type { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
/**
|
||||
* Adds `signinUrl` and `callbackUrl` to each provider
|
||||
@@ -36,33 +37,37 @@ 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 normalized: InternalProvider = Object.entries(
|
||||
provider
|
||||
).reduce<InternalProvider>((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
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
return acc
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
|
||||
}, {} as any)
|
||||
|
||||
// Checks only work on OAuth 2.x + OIDC providers
|
||||
if (
|
||||
provider.type === "oauth" &&
|
||||
!provider.version?.startsWith("1.") &&
|
||||
!provider.checks
|
||||
) {
|
||||
normalizedProvider.checks = ["state"]
|
||||
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
|
||||
// If provider has as an "openid-configuration" well-known endpoint
|
||||
// or an "openid" scope request, it will also likely be able to receive an `id_token`
|
||||
normalized.idToken = Boolean(
|
||||
normalized.idToken ??
|
||||
normalized.wellKnown?.includes("openid-configuration") ??
|
||||
// @ts-expect-error
|
||||
normalized.authorization?.params?.scope?.includes("openid")
|
||||
)
|
||||
|
||||
if (!normalized.checks) normalized.checks = ["state"]
|
||||
}
|
||||
return normalizedProvider as InternalProvider
|
||||
return normalized
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
import { Theme } from "../.."
|
||||
import { InternalUrl } from "../../lib/parse-url"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden error page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages#error-page) */
|
||||
export type ErrorType =
|
||||
| "default"
|
||||
| "configuration"
|
||||
| "accessdenied"
|
||||
| "verification"
|
||||
|
||||
export interface ErrorProps {
|
||||
url: InternalUrl
|
||||
theme: Theme
|
||||
error?: string
|
||||
url?: InternalUrl
|
||||
theme?: Theme
|
||||
error?: ErrorType
|
||||
}
|
||||
|
||||
interface ErrorView {
|
||||
status: number
|
||||
heading: string
|
||||
message: JSX.Element
|
||||
signin?: JSX.Element
|
||||
}
|
||||
|
||||
/** Renders an error page. */
|
||||
@@ -12,14 +29,14 @@ 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 +91,18 @@ 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,33 @@
|
||||
export default function SigninPage(props) {
|
||||
import { Theme } from "../.."
|
||||
import { InternalProvider } from "../../lib/types"
|
||||
|
||||
/**
|
||||
* The following errors are passed as error query parameters to the default or overridden sign-in page.
|
||||
*
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages#sign-in-page) */
|
||||
export type SignInErrorTypes =
|
||||
| "Signin"
|
||||
| "OAuthSignin"
|
||||
| "OAuthCallback"
|
||||
| "OAuthCreateAccount"
|
||||
| "EmailCreateAccount"
|
||||
| "Callback"
|
||||
| "OAuthAccountNotLinked"
|
||||
| "EmailSignin"
|
||||
| "CredentialsSignin"
|
||||
| "SessionRequired"
|
||||
| "default"
|
||||
|
||||
export interface SignInServerPageParams {
|
||||
csrfToken: string
|
||||
providers: InternalProvider[]
|
||||
callbackUrl: string
|
||||
email: string
|
||||
error: SignInErrorTypes
|
||||
theme: Theme
|
||||
}
|
||||
|
||||
export default function SigninPage(props: SignInServerPageParams) {
|
||||
const {
|
||||
csrfToken,
|
||||
providers,
|
||||
@@ -20,14 +49,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<SignInErrorTypes, 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.",
|
||||
@@ -36,9 +65,10 @@ export default function SigninPage(props) {
|
||||
Callback: "Try signing in with a different account.",
|
||||
OAuthAccountNotLinked:
|
||||
"To confirm your identity, sign in with the same account you used originally.",
|
||||
EmailSignin: "Check your email inbox.",
|
||||
EmailSignin: "The e-mail could not be sent.",
|
||||
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,15 +76,17 @@ export default function SigninPage(props) {
|
||||
|
||||
return (
|
||||
<div className="signin">
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
{theme.brandColor && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
:root {
|
||||
--brand-color: ${theme.brandColor}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{theme.logo && <img src={theme.logo} alt="Logo" className="logo" />}
|
||||
<div className="card">
|
||||
{error && (
|
||||
@@ -109,15 +141,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 ||
|
||||
"Password"
|
||||
provider.credentials[credential].placeholder ?? ""
|
||||
}
|
||||
{...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>
|
||||
|
||||
@@ -26,9 +27,10 @@ export interface NextAuthOptions {
|
||||
providers: Provider[]
|
||||
/**
|
||||
* A random string used to hash tokens, sign cookies and generate cryptographic keys.
|
||||
* If not specified is uses a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
* The default behavior is volatile, and **it is strongly recommended** you explicitly specify a value
|
||||
* to avoid invalidating end user sessions when configuration changes are deployed.
|
||||
* If not specified, it falls back to `jwt.secret` or `NEXTAUTH_SECRET` from environment vairables.
|
||||
* Otherwise it will use a hash of all configuration options, including Client ID / Secrets for entropy.
|
||||
*
|
||||
* NOTE: The last behavior is extrmely volatile, and will throw an error in production.
|
||||
* * **Default value**: `string` (SHA hash of the "options" object)
|
||||
* * **Required**: No - **but strongly recommended**!
|
||||
*
|
||||
@@ -297,7 +299,7 @@ export interface CallbacksOptions<
|
||||
* This callback is called whenever a session is checked.
|
||||
* (Eg.: invoking the `/api/session` endpoint, using `useSession` or `getSession`)
|
||||
*
|
||||
* ⚠ By default, only a subset (email, name, imgage)
|
||||
* ⚠ By default, only a subset (email, name, image)
|
||||
* of the token is returned for increased security.
|
||||
*
|
||||
* If you want to make something available you added to the token through the `jwt` callback,
|
||||
@@ -338,15 +340,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 +349,7 @@ export interface CookiesOptions {
|
||||
callbackUrl: CookieOption
|
||||
csrfToken: CookieOption
|
||||
pkceCodeVerifier: CookieOption
|
||||
state: CookieOption
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,9 +424,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 +472,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 { SessionStore } from "../core/lib/cookie"
|
||||
import type { NextApiRequest } from "next"
|
||||
import type { JWT, JWTDecodeParams, JWTEncodeParams, JWTOptions } from "./types"
|
||||
import type { LoggerInstance } from ".."
|
||||
|
||||
export * from "./types"
|
||||
|
||||
@@ -11,11 +13,8 @@ const DEFAULT_MAX_AGE = 30 * 24 * 60 * 60 // 30 days
|
||||
const now = () => (Date.now() / 1000) | 0
|
||||
|
||||
/** Issues a JWT. By default, the JWT is encrypted using "A256GCM". */
|
||||
export async function encode({
|
||||
token = {},
|
||||
secret,
|
||||
maxAge = DEFAULT_MAX_AGE,
|
||||
}: JWTEncodeParams) {
|
||||
export async function encode(params: JWTEncodeParams) {
|
||||
const { token = {}, secret, maxAge = DEFAULT_MAX_AGE } = params
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret)
|
||||
return await new EncryptJWT(token)
|
||||
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
|
||||
@@ -26,10 +25,8 @@ export async function encode({
|
||||
}
|
||||
|
||||
/** Decodes a NextAuth.js issued JWT. */
|
||||
export async function decode({
|
||||
token,
|
||||
secret,
|
||||
}: JWTDecodeParams): Promise<JWT | null> {
|
||||
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
|
||||
const { token, secret } = params
|
||||
if (!token) return null
|
||||
const encryptionSecret = await getDerivedEncryptionKey(secret)
|
||||
const { payload } = await jwtDecrypt(token, encryptionSecret, {
|
||||
@@ -38,9 +35,9 @@ 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
|
||||
req: NextApiRequest | Pick<NextApiRequest, "cookies" | "headers">
|
||||
/**
|
||||
* Use secure prefix for cookie name, unless URL in `NEXTAUTH_URL` is http://
|
||||
* or not set (e.g. development or test instance) case use unprefixed name
|
||||
@@ -53,7 +50,14 @@ export type GetTokenParams<R extends boolean = false> = {
|
||||
* @default false
|
||||
*/
|
||||
raw?: R
|
||||
} & Pick<JWTOptions, "decode" | "secret">
|
||||
/**
|
||||
* The same `secret` used in the `NextAuth` configuration.
|
||||
* Defaults to the `NEXTAUTH_SECRET` environment variable.
|
||||
*/
|
||||
secret?: string
|
||||
decode?: JWTOptions["decode"]
|
||||
logger?: LoggerInstance | Console
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a NextAuth.js request (`req`) and returns either the NextAuth.js issued JWT's payload,
|
||||
@@ -65,43 +69,50 @@ export async function getToken<R extends boolean = false>(
|
||||
): Promise<R extends true ? string : JWT | null> {
|
||||
const {
|
||||
req,
|
||||
secureCookie = !(
|
||||
!process.env.NEXTAUTH_URL ||
|
||||
process.env.NEXTAUTH_URL.startsWith("http://")
|
||||
),
|
||||
secureCookie = process.env.NEXTAUTH_URL?.startsWith("https://") ??
|
||||
!!process.env.VERCEL,
|
||||
cookieName = secureCookie
|
||||
? "__Secure-next-auth.session-token"
|
||||
: "next-auth.session-token",
|
||||
raw,
|
||||
decode: _decode = decode,
|
||||
logger = console,
|
||||
secret = process.env.NEXTAUTH_SECRET,
|
||||
} = 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
|
||||
)
|
||||
|
||||
let token = sessionStore.value
|
||||
|
||||
if (!token && req.headers.authorization?.split(" ")[0] === "Bearer") {
|
||||
const urlEncodedToken = req.headers.authorization.split(" ")[1]
|
||||
token = decodeURIComponent(urlEncodedToken)
|
||||
}
|
||||
|
||||
if (raw) {
|
||||
// @ts-expect-error
|
||||
return token
|
||||
}
|
||||
// @ts-expect-error
|
||||
if (!token) return null
|
||||
|
||||
// @ts-expect-error
|
||||
if (raw) return token
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
return await _decode({ token, ...params })
|
||||
return await _decode({ token, secret })
|
||||
} catch {
|
||||
// @ts-expect-error
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function getDerivedEncryptionKey(secret) {
|
||||
async function getDerivedEncryptionKey(secret: string | Buffer) {
|
||||
return await hkdf(
|
||||
'sha256',
|
||||
"sha256",
|
||||
secret,
|
||||
"",
|
||||
"NextAuth.js Generated Encryption Key",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decode, encode } from "."
|
||||
import type { Awaitable } from ".."
|
||||
|
||||
export interface DefaultJWT extends Record<string, unknown> {
|
||||
name?: string | null
|
||||
@@ -34,7 +34,11 @@ export interface JWTDecodeParams {
|
||||
}
|
||||
|
||||
export interface JWTOptions {
|
||||
/** The secret used to encode/decode the NextAuth.js issued JWT. */
|
||||
/**
|
||||
* The secret used to encode/decode the NextAuth.js issued JWT.
|
||||
* @deprecated Set the `NEXTAUTH_SECRET` environment vairable or
|
||||
* use the top-level `secret` option instead
|
||||
*/
|
||||
secret: string
|
||||
/**
|
||||
* The maximum age of the NextAuth.js issued JWT in seconds.
|
||||
@@ -42,7 +46,9 @@ export interface JWTOptions {
|
||||
*/
|
||||
maxAge: number
|
||||
/** Override this method to control the NextAuth.js issued JWT encoding. */
|
||||
encode: typeof encode
|
||||
encode: (params: JWTEncodeParams) => Awaitable<string>
|
||||
/** Override this method to control the NextAuth.js issued JWT decoding. */
|
||||
decode: typeof decode
|
||||
decode: (params: JWTDecodeParams) => Awaitable<JWT | null>
|
||||
}
|
||||
|
||||
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" | "TWITTER_OAUTH_2_BETA"
|
||||
|
||||
/**
|
||||
* 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()
|
||||
|
||||
|
||||
@@ -11,8 +11,14 @@ export interface InternalUrl {
|
||||
toString: () => string
|
||||
}
|
||||
|
||||
/** Returns an `URL` like object to make requests/redirects from server-side */
|
||||
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
|
||||
|
||||
@@ -50,7 +50,7 @@ export type NextAuthAction =
|
||||
export interface InternalOptions<T extends ProviderType = any> {
|
||||
providers: InternalProvider[]
|
||||
/**
|
||||
* Parsed from `NEXTAUTH_URL` or `VERCEL_URL`.
|
||||
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
|
||||
* @default "http://localhost:3000/api/auth"
|
||||
*/
|
||||
url: InternalUrl
|
||||
|
||||
2
src/middleware.ts
Normal file
2
src/middleware.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./next/middleware"
|
||||
export * from "./next/middleware"
|
||||
@@ -1,76 +1,62 @@
|
||||
import {
|
||||
import { NextAuthHandler } from "../core"
|
||||
import { setCookie, detectHost } from "./utils"
|
||||
|
||||
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)
|
||||
const { nextauth, ...query } = req.query
|
||||
|
||||
if (!req.query.nextauth) {
|
||||
const error = new Error(
|
||||
"Cannot find [...nextauth].js in pages/api/auth. Make sure the filename is written correctly."
|
||||
)
|
||||
options.secret =
|
||||
options.secret ?? options.jwt?.secret ?? process.env.NEXTAUTH_SECRET
|
||||
|
||||
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 handler = await NextAuthHandler({
|
||||
req: {
|
||||
host,
|
||||
host: detectHost(req.headers["x-forwarded-host"]),
|
||||
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 +67,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 +91,7 @@ export async function getServerSession(
|
||||
const session = await NextAuthHandler<Session | {}>({
|
||||
options,
|
||||
req: {
|
||||
host: detectHost(context.req.headers["x-forwarded-host"]),
|
||||
action: "session",
|
||||
method: "GET",
|
||||
cookies: context.req.cookies,
|
||||
@@ -109,10 +101,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?: "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
141
src/next/middleware.ts
Normal file
141
src/next/middleware.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import type { NextMiddleware, NextFetchEvent } from "next/server"
|
||||
import type { Awaitable, NextAuthOptions } from ".."
|
||||
import type { JWT } from "../jwt"
|
||||
|
||||
import { NextResponse, NextRequest } from "next/server"
|
||||
|
||||
import { getToken } from "../jwt"
|
||||
import parseUrl from "../lib/parse-url"
|
||||
|
||||
type AuthorizedCallback = (params: {
|
||||
token: JWT | null
|
||||
req: NextRequest
|
||||
}) => Awaitable<boolean>
|
||||
|
||||
export interface NextAuthMiddlewareOptions {
|
||||
/**
|
||||
* Where to redirect the user in case of an error if they weren't logged in.
|
||||
* Similar to `pages` in `NextAuth`.
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/configuration/pages)
|
||||
*/
|
||||
pages?: NextAuthOptions["pages"]
|
||||
callbacks?: {
|
||||
/**
|
||||
* Callback that receives the user's JWT payload
|
||||
* and returns `true` to allow the user to continue.
|
||||
*
|
||||
* This is similar to the `signIn` callback in `NextAuthOptions`.
|
||||
*
|
||||
* If it returns `false`, the user is redirected to the sign-in page instead
|
||||
*
|
||||
* The default is to let the user continue if they have a valid JWT (basic authentication).
|
||||
*
|
||||
* How to restrict a page and all of it's subpages for admins-only:
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // `pages/admin/_middleware.js`
|
||||
* import { withAuth } from "next-auth/middleware"
|
||||
*
|
||||
* export default withAuth({
|
||||
* callbacks: {
|
||||
* authorized: ({ token }) => token?.user.isAdmin
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
|
||||
*/
|
||||
authorized?: AuthorizedCallback
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMiddleware(
|
||||
req: NextRequest,
|
||||
options: NextAuthMiddlewareOptions | undefined,
|
||||
onSuccess?: (token: JWT | null) => Promise<any>
|
||||
) {
|
||||
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
|
||||
const errorPage = options?.pages?.error ?? "/api/auth/error"
|
||||
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
|
||||
// Avoid infinite redirect loop
|
||||
if (
|
||||
req.nextUrl.pathname.startsWith(basePath) ||
|
||||
[signInPage, errorPage].includes(req.nextUrl.pathname)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!process.env.NEXTAUTH_SECRET) {
|
||||
console.error(
|
||||
`[next-auth][error][NO_SECRET]`,
|
||||
`\nhttps://next-auth.js.org/errors#no_secret`
|
||||
)
|
||||
|
||||
return {
|
||||
redirect: NextResponse.redirect(`${errorPage}?error=Configuration`),
|
||||
}
|
||||
}
|
||||
|
||||
const token = await getToken({ req: req as any })
|
||||
|
||||
const isAuthorized =
|
||||
(await options?.callbacks?.authorized?.({ req, token })) ?? !!token
|
||||
|
||||
// the user is authorized, let the middleware handle the rest
|
||||
if (isAuthorized) return await onSuccess?.(token)
|
||||
|
||||
// the user is not logged in, re-direct to the sign-in page
|
||||
return NextResponse.redirect(
|
||||
`${signInPage}?${new URLSearchParams({ callbackUrl: req.url })}`
|
||||
)
|
||||
}
|
||||
|
||||
export type WithAuthArgs =
|
||||
| [NextRequest]
|
||||
| [NextRequest, NextFetchEvent]
|
||||
| [NextRequest, NextAuthMiddlewareOptions]
|
||||
| [NextMiddleware]
|
||||
| [NextMiddleware, NextAuthMiddlewareOptions]
|
||||
| [NextAuthMiddlewareOptions]
|
||||
|
||||
/**
|
||||
* Middleware that checks if the user is authenticated/authorized.
|
||||
* If if they aren't, they will be redirected to the login page.
|
||||
* Otherwise, continue.
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* ```js
|
||||
* // `pages/_middleware.js`
|
||||
* export { default } from "next-auth/middleware"
|
||||
* ```
|
||||
*
|
||||
* ---
|
||||
* [Documentation](https://next-auth.js.org/getting-started/middleware)
|
||||
*/
|
||||
export function withAuth(...args: WithAuthArgs) {
|
||||
if (args[0] instanceof NextRequest) {
|
||||
// @ts-expect-error
|
||||
return handleMiddleware(...args)
|
||||
}
|
||||
|
||||
if (typeof args[0] === "function") {
|
||||
const middleware = args[0]
|
||||
const options = args[1] as NextAuthMiddlewareOptions | undefined
|
||||
return async (...args: Parameters<NextMiddleware>) =>
|
||||
await handleMiddleware(args[0], options, async (token) => {
|
||||
;(args[0] as any).nextauth = { token }
|
||||
return await middleware(...args)
|
||||
})
|
||||
}
|
||||
|
||||
const options = args[0]
|
||||
return async (...args: Parameters<NextMiddleware>) =>
|
||||
await handleMiddleware(args[0], options)
|
||||
}
|
||||
|
||||
export default withAuth
|
||||
23
src/next/utils.ts
Normal file
23
src/next/utils.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
|
||||
/** Extract the host from the environment */
|
||||
export function detectHost(forwardedHost: any) {
|
||||
// If we detect a Vercel environment, we can trust the host
|
||||
if (process.env.VERCEL) return forwardedHost
|
||||
// If `NEXTAUTH_URL` is `undefined` we fall back to "http://localhost:3000"
|
||||
return process.env.NEXTAUTH_URL
|
||||
}
|
||||
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.toString(),
|
||||
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,8 +1,8 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface Auth0Profile {
|
||||
sub: string
|
||||
nicname: string
|
||||
nickname: string
|
||||
email: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
44
src/providers/authentik.ts
Normal file
44
src/providers/authentik.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface AuthentikProfile {
|
||||
iss: string,
|
||||
sub: string,
|
||||
aud: string,
|
||||
exp: number,
|
||||
iat: number,
|
||||
auth_time: number,
|
||||
acr: string,
|
||||
c_hash: string,
|
||||
nonce: string,
|
||||
at_hash: string,
|
||||
email: string,
|
||||
email_verified: boolean,
|
||||
name: string,
|
||||
given_name: string,
|
||||
family_name: string,
|
||||
preferred_username: string,
|
||||
nickname: string,
|
||||
groups: string[]
|
||||
}
|
||||
|
||||
export default function Authentik<
|
||||
P extends Record<string, any> = AuthentikProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "authentik",
|
||||
name: "Authentik",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
type: "oauth",
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
checks: ["pkce", "state"],
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name ?? profile.preferred_username,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -28,7 +28,7 @@ export default function AzureAD<P extends Record<string, any> = AzureADProfile>(
|
||||
wellKnown: `https://login.microsoftonline.com/${tenant}/v2.0/.well-known/openid-configuration`,
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "User.Read",
|
||||
scope: "openid profile email",
|
||||
},
|
||||
},
|
||||
async profile(profile, tokens) {
|
||||
@@ -41,13 +41,23 @@ export default function AzureAD<P extends Record<string, any> = AzureADProfile>(
|
||||
},
|
||||
}
|
||||
)
|
||||
const pictureBuffer = await profilePicture.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: `data:image/jpeg;base64, ${pictureBase64}`,
|
||||
|
||||
// Confirm that profile photo was returned
|
||||
if (profilePicture.ok) {
|
||||
const pictureBuffer = await profilePicture.arrayBuffer()
|
||||
const pictureBase64 = Buffer.from(pictureBuffer).toString("base64")
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: `data:image/jpeg;base64, ${pictureBase64}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
}
|
||||
}
|
||||
},
|
||||
options,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "."
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface CognitoProfile {
|
||||
sub: string
|
||||
@@ -15,6 +15,7 @@ export default function Cognito<P extends Record<string, any> = CognitoProfile>(
|
||||
name: "Cognito",
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
|
||||
@@ -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 } }
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function FusionAuth(options) {
|
||||
return {
|
||||
id: "fusionauth",
|
||||
name: "FusionAuth",
|
||||
type: "oauth",
|
||||
authorization: `${options.issuer}oauth2/authorize`,
|
||||
token: `${options.issuer}oauth2/token`,
|
||||
userinfo: `${options.issuer}oauth2/userinfo`,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
name: profile.name,
|
||||
email: profile.email,
|
||||
image: profile.picture,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
52
src/providers/fusionauth.ts
Normal file
52
src/providers/fusionauth.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
|
||||
/** This is the default openid signature returned from FusionAuth
|
||||
* it can be customized using [lambda functions](https://fusionauth.io/docs/v1/tech/lambdas)
|
||||
*/
|
||||
export interface FusionAuthProfile {
|
||||
aud: string
|
||||
exp: number
|
||||
iat: number
|
||||
iss: string
|
||||
sub: string
|
||||
jti: string
|
||||
authenticationType: string
|
||||
email: string
|
||||
email_verified: boolean
|
||||
preferred_username: string
|
||||
at_hash: string
|
||||
c_hash: string
|
||||
scope: string
|
||||
sid: string
|
||||
}
|
||||
|
||||
export default function FusionAuth<
|
||||
P extends Record<string, any> = FusionAuthProfile
|
||||
>(
|
||||
// tenantId only needed if there is more than one tenant configured on the server
|
||||
options: OAuthUserConfig<P> & { tenantId?: string }
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "fusionauth",
|
||||
name: "FusionAuth",
|
||||
type: "oauth",
|
||||
wellKnown: options?.tenantId
|
||||
? `${options.issuer}/.well-known/openid-configuration?tenantId=${options.tenantId}`
|
||||
: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: {
|
||||
params: {
|
||||
scope: "openid offline_access",
|
||||
...(options?.tenantId && { tenantId: options.tenantId }),
|
||||
},
|
||||
},
|
||||
checks: ["pkce", "state"],
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
email: profile.email,
|
||||
name: profile?.preferred_username,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -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,10 +1,21 @@
|
||||
import { OAuthConfig, OAuthUserConfig } from "./oauth"
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface GoogleProfile {
|
||||
sub: string
|
||||
name: string
|
||||
aud: string
|
||||
azp: string
|
||||
email: string
|
||||
email_verified: boolean
|
||||
exp: number
|
||||
family_name: string
|
||||
given_name: string
|
||||
hd: string
|
||||
iat: number
|
||||
iss: string
|
||||
jti: string
|
||||
name: string
|
||||
nbf: number
|
||||
picture: string
|
||||
sub: string
|
||||
}
|
||||
|
||||
export default function Google<P extends Record<string, any> = GoogleProfile>(
|
||||
|
||||
@@ -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,20 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Kakao(options) {
|
||||
return {
|
||||
id: "kakao",
|
||||
name: "Kakao",
|
||||
type: "oauth",
|
||||
authorization: "https://kauth.kakao.com/oauth/authorize",
|
||||
token: "https://kauth.kakao.com/oauth/token",
|
||||
userinfo: "https://kapi.kakao.com/v2/user/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.kakao_account?.profile.nickname,
|
||||
email: profile.kakao_account?.email,
|
||||
image: profile.kakao_account?.profile.profile_image_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
92
src/providers/kakao.ts
Normal file
92
src/providers/kakao.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export type DateTime = string
|
||||
export type Gender = "female" | "male"
|
||||
export type AgeRange =
|
||||
| "1-9"
|
||||
| "10-14"
|
||||
| "15-19"
|
||||
| "20-29"
|
||||
| "30-39"
|
||||
| "40-49"
|
||||
| "50-59"
|
||||
| "60-69"
|
||||
| "70-79"
|
||||
| "80-89"
|
||||
| "90-"
|
||||
|
||||
/**
|
||||
* https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info
|
||||
* type from : https://gist.github.com/ziponia/cdce1ebd88f979b2a6f3f53416b56a77
|
||||
*/
|
||||
export interface KakaoProfile {
|
||||
id: number
|
||||
has_signed_up?: boolean
|
||||
connected_at?: DateTime
|
||||
synched_at?: DateTime
|
||||
properties?: {
|
||||
id?: string
|
||||
status?: string
|
||||
registered_at?: DateTime
|
||||
msg_blocked?: boolean
|
||||
nickname?: string
|
||||
profile_image?: string
|
||||
thumbnail_image?: string
|
||||
}
|
||||
kakao_account?: {
|
||||
profile_needs_agreement?: boolean
|
||||
profile_nickname_needs_agreement?: boolean
|
||||
profile_image_needs_agreement?: boolean
|
||||
profile?: {
|
||||
nickname?: string
|
||||
thumbnail_image_url?: string
|
||||
profile_image_url?: string
|
||||
is_default_image?: boolean
|
||||
}
|
||||
name_needs_agreement?: boolean
|
||||
name?: string
|
||||
email_needs_agreement?: boolean
|
||||
is_email_valid?: boolean
|
||||
is_email_verified?: boolean
|
||||
email?: string
|
||||
age_range_needs_agreement?: boolean
|
||||
age_range?: AgeRange
|
||||
birthyear_needs_agreement?: boolean
|
||||
birthyear?: string
|
||||
birthday_needs_agreement?: boolean
|
||||
birthday?: string
|
||||
birthday_type?: string
|
||||
gender_needs_agreement?: boolean
|
||||
gender?: Gender
|
||||
phone_number_needs_agreement?: boolean
|
||||
phone_number?: string
|
||||
ci_needs_agreement?: boolean
|
||||
ci?: string
|
||||
ci_authenticated_at?: DateTime
|
||||
}
|
||||
}
|
||||
|
||||
export default function Kakao<P extends Record<string, any> = KakaoProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "kakao",
|
||||
name: "Kakao",
|
||||
type: "oauth",
|
||||
authorization: "https://kauth.kakao.com/oauth/authorize?scope",
|
||||
token: "https://kauth.kakao.com/oauth/token",
|
||||
userinfo: "https://kapi.kakao.com/v2/user/me",
|
||||
client: {
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
name: profile.kakao_account?.profile.nickname,
|
||||
email: profile.kakao_account?.email,
|
||||
image: profile.kakao_account?.profile.profile_image_url,
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -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,18 +0,0 @@
|
||||
/** @type {import(".").OAuthProvider} */
|
||||
export default function Naver(options) {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
type: "oauth",
|
||||
authorization: "https://nid.naver.com/oauth2.0/authorize",
|
||||
token: "https://nid.naver.com/oauth2.0/token",
|
||||
userinfo: "https://openapi.naver.com/v1/nid/me",
|
||||
profile(profile) {
|
||||
// REVIEW: By default, we only want to expose the
|
||||
// "id", "name", "email" and "image" fields.
|
||||
return profile.response
|
||||
},
|
||||
checks: ["state"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
42
src/providers/naver.ts
Normal file
42
src/providers/naver.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
/** https://developers.naver.com/docs/login/profile/profile.md */
|
||||
export interface NaverProfile {
|
||||
resultcode: string
|
||||
message: string
|
||||
response: {
|
||||
id: string
|
||||
nickname?: string
|
||||
name?: string
|
||||
email?: string
|
||||
gender?: "F" | "M" | "U"
|
||||
age?: string
|
||||
birthday?: string
|
||||
profile_image?: string
|
||||
birthyear?: string
|
||||
mobile?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function Naver<P extends Record<string, any> = NaverProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
type: "oauth",
|
||||
authorization: "https://nid.naver.com/oauth2.0/authorize",
|
||||
token: "https://nid.naver.com/oauth2.0/token",
|
||||
userinfo: "https://openapi.naver.com/v1/nid/me",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.response.id,
|
||||
name: profile.response.name,
|
||||
email: profile.response.email,
|
||||
image: profile.response.profile_image,
|
||||
}
|
||||
},
|
||||
checks: ["state"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -43,6 +43,7 @@ export default function Okta<P extends Record<string, any> = OktaProfile>(
|
||||
type: "oauth",
|
||||
wellKnown: `${options.issuer}/.well-known/openid-configuration`,
|
||||
authorization: { params: { scope: "openid email profile" } },
|
||||
idToken: true,
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.sub,
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
34
src/providers/patreon.ts
Normal file
34
src/providers/patreon.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface PatreonProfile {
|
||||
sub: string
|
||||
nickname: string
|
||||
email: string
|
||||
picture: string
|
||||
}
|
||||
|
||||
export default function Patreon<P extends Record<string, any> = PatreonProfile>(
|
||||
options: OAuthUserConfig<P>
|
||||
): OAuthConfig<P> {
|
||||
return {
|
||||
id: "patreon",
|
||||
name: "Patreon",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
authorization: {
|
||||
url: "https://www.patreon.com/oauth2/authorize",
|
||||
params: { scope: "identity identity[email]" },
|
||||
},
|
||||
token: "https://www.patreon.com/api/oauth2/token",
|
||||
userinfo: "https://www.patreon.com/api/oauth2/api/current_user",
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.data.id,
|
||||
name: profile.data.attributes.full_name,
|
||||
email: profile.data.attributes.email,
|
||||
image: profile.data.attributes.image_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
|
||||
|
||||
@@ -4,9 +4,23 @@ export default function Strava(options) {
|
||||
id: "strava",
|
||||
name: "Strava",
|
||||
type: "oauth",
|
||||
authorization: "https://www.strava.com/api/v3/oauth/authorize?scope=read",
|
||||
token: "https://www.strava.com/api/v3/oauth/token",
|
||||
authorization: {
|
||||
url: "https://www.strava.com/api/v3/oauth/authorize",
|
||||
params: {
|
||||
scope: "read",
|
||||
approval_prompt: "auto",
|
||||
response_type: "code",
|
||||
redirect_uri: "http://localhost:3000/api/auth/callback/strava",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: "https://www.strava.com/api/v3/oauth/token",
|
||||
},
|
||||
userinfo: "https://www.strava.com/api/v3/athlete",
|
||||
client: {
|
||||
token_endpoint_auth_method: "client_secret_post",
|
||||
},
|
||||
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id,
|
||||
|
||||
56
src/providers/trakt.ts
Normal file
56
src/providers/trakt.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface TraktUser {
|
||||
username: string
|
||||
private: boolean
|
||||
name: string
|
||||
vip: boolean
|
||||
vip_ep: boolean
|
||||
ids: { slug: string }
|
||||
joined_at: string
|
||||
location: string | null
|
||||
about: string | null
|
||||
gender: string | null
|
||||
age: number | null
|
||||
images: { avatar: { full: string } }
|
||||
}
|
||||
|
||||
export default function Trakt<
|
||||
P extends Record<string, any> = TraktUser
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "trakt",
|
||||
name: "Trakt",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://trakt.tv/oauth/authorize",
|
||||
params: { scope: "" }, // when default, trakt returns auth error
|
||||
},
|
||||
token: "https://api.trakt.tv/oauth/token",
|
||||
|
||||
userinfo: {
|
||||
async request(context) {
|
||||
const res = await fetch("https://api.trakt.tv/users/me?extended=full", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${context.tokens.access_token}`,
|
||||
"trakt-api-version": "2",
|
||||
"trakt-api-key": context.provider.clientId as string,
|
||||
},
|
||||
})
|
||||
|
||||
if (res.ok) return await res.json()
|
||||
|
||||
throw new Error("Expected 200 OK from the userinfo endpoint")
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.ids.slug,
|
||||
name: profile.name,
|
||||
email: null, // trakt does not provide user emails
|
||||
image: profile.images.avatar.full, // trakt does not allow hotlinking
|
||||
}
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
214
src/providers/twitter.ts
Normal file
214
src/providers/twitter.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "."
|
||||
|
||||
export interface TwitterLegacyProfile {
|
||||
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 function TwitterLegacy<
|
||||
P extends Record<string, any> = TwitterLegacyProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter (Legacy)",
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Documentation](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me)
|
||||
*/
|
||||
export interface TwitterProfile {
|
||||
data: {
|
||||
id: string
|
||||
name: string
|
||||
username: string
|
||||
location?: string
|
||||
entities?: {
|
||||
url: {
|
||||
urls: Array<{
|
||||
start: number
|
||||
end: number
|
||||
url: string
|
||||
expanded_url: string
|
||||
display_url: string
|
||||
}>
|
||||
}
|
||||
description: {
|
||||
hashtags: Array<{
|
||||
start: number
|
||||
end: number
|
||||
tag: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
verified?: boolean
|
||||
description?: string
|
||||
url?: string
|
||||
profile_image_url?: string
|
||||
protected?: boolean
|
||||
pinned_tweet_id?: string
|
||||
created_at?: string
|
||||
}
|
||||
includes?: {
|
||||
tweets?: Array<{
|
||||
id: string
|
||||
text: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export default function Twitter<
|
||||
P extends Record<string, any> = TwitterLegacyProfile | TwitterProfile
|
||||
>(options: OAuthUserConfig<P>): OAuthConfig<P> {
|
||||
if (options.version === "2.0") {
|
||||
return {
|
||||
id: "twitter",
|
||||
name: "Twitter",
|
||||
version: "2.0",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: "https://twitter.com/i/oauth2/authorize",
|
||||
params: { scope: "users.read tweet.read offline.access" },
|
||||
},
|
||||
token: {
|
||||
url: "https://api.twitter.com/2/oauth2/token",
|
||||
// TODO: Remove this
|
||||
async request({ client, params, checks, provider }) {
|
||||
const response = await client.oauthCallback(
|
||||
provider.callbackUrl,
|
||||
params,
|
||||
checks,
|
||||
{ exchangeBody: { client_id: options.clientId } }
|
||||
)
|
||||
return { tokens: response }
|
||||
},
|
||||
},
|
||||
userinfo: {
|
||||
url: "https://api.twitter.com/2/users/me",
|
||||
params: { "user.fields": "profile_image_url" },
|
||||
},
|
||||
profile({ data }) {
|
||||
return {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
// NOTE: E-mail is currently unsupported by OAuth 2 Twitter.
|
||||
email: null,
|
||||
image: data.profile_image_url,
|
||||
}
|
||||
},
|
||||
checks: ["pkce", "state"],
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
return TwitterLegacy(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,
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user