Compare commits

...

30 Commits

Author SHA1 Message Date
Thang Vu
777b7b2f23 chore(release): bump package version(s) [skip ci] 2022-10-09 22:52:53 +07:00
Balázs Orbán
6132c3fa75 fix(ts): match TS types better with implementation (#4953)
* refactor(ts): export `AdapterAccount` from `next-auth/adapters`

* chore: run linter, remove prisma warning

* fix(ts): match TS with implementation closer

* remove unused import

* rename error

* add missing dev dependency

* fix type

* fix type

* fix more types and tests

* remove unused `id`

* skip upstash tests in CI

* revert some changes

* fix type

* revert some change

* revert some change

* revert some change

* revert some changes

* update lock file

* revert line change

* revert some change

* improve adapter & oauth typing

* fix test, revert

* apply review suggestion

* Add test for new rejection logics

* Update assert.test.ts

* fix: Hubspot config

* restore some ts-expect-error

* fix: tests in mirko-orm

* fix: remove redundant id: string

* fix: use ts-expect-errors

* fix: simplify provider type

* fix: normalize user options

* restore ts-expect-errors

Co-authored-by: Thang Vu <hi@thvu.dev>
2022-10-09 21:54:01 +07:00
Usman Sabuwala (Max Programming)
94beef77e6 docs: Remove extra space in code (#5515) 2022-10-09 13:40:38 +02:00
Philipp
490d59dd17 fix(middleware): improve handling of custom Next.js basePath (#5109)
* fix(middleware): improve handling of custom nextjs basePath

* fix(middleware): improve extraction of nextjs base path from req.nextUrl

* adapt to req.nextUrl.basePath

* Fix indent

* Add middleware test for custom-base and simplified code a little bit

* Fix indent

* Add another test

* Rename basePath and nextJsBasePath

* Fix lint error
2022-10-09 11:31:28 +07:00
Thang Vu
26a8c5fc6d chore: lint in apps (#5507) 2022-10-06 22:14:12 +01:00
Colby Fayock
e26ec74720 docs(tutorial): Creating a Custom Adapter (#5506)
* Add new item to tutorials page

* Update tutorials.md

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-10-06 22:12:20 +01:00
Max Peintner
d13997e140 feat(providers): ZITADEL provider (#5479)
* feat: zitadel provider

* Update packages/next-auth/src/providers/zitadel.ts

Co-authored-by: Balázs Orbán <info@balazsorban.com>

* Update packages/next-auth/src/providers/zitadel.ts

Co-authored-by: Balázs Orbán <info@balazsorban.com>

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-10-06 09:44:21 +02:00
Balázs Orbán
d6efda077d chore(release): bump package version(s) [skip ci] 2022-10-05 19:30:28 +02:00
Eric Carboni
0a4b99de3b chore(docs): update middleware documentation link (#5492)
closes #5489
2022-10-04 19:25:56 +02:00
Daniel
2d2dfecc9d docs(core): update documentation callbacks to include user id as example (#5465)
* Add user id to `session` and `jwt` callback

* Minor changes

- Notes on why the id is not exposed by default is already documented in the `session` section.

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-10-03 16:03:33 +02:00
Thang Vu
2a2c3d7a45 chore: add security guidelines to PR & issue template (#5470)
* chore: add security guidelines to pr & issue template

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-10-03 15:59:19 +02:00
kesoji
82786ac440 chore: remove duplicate key in pacakges/tsconfig/package.json (#5469)
fix: remove duplicate key
2022-10-02 20:51:17 +02:00
Vedant Nandwana
dfe3e02132 docs(adapters): Add TS type to prisma client (#5463)
* docs(adapters): Add prisma client docs for typescript users

Add documentation for connecting prisma client w/ prisma adapter for typescript users.

* docs(adapters): remove prismadb.js for prismadb.ts

remove prismadb.js as it is identical to the prismadb.ts

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-10-01 22:17:41 +02:00
Itunu Lamina
92b38ed740 docs: fix 'JWKKeySupport' typo (#5452)
update 'JWKKeySupport' typo error
2022-09-29 15:34:23 +02:00
Tom Freudenberg
97feae7916 fix(types): export SessionContext #5437 (#5438)
Co-authored-by: Lluis Agusti <hi@llu.lu>

Fixes #5437
2022-09-28 18:48:42 +02:00
Balázs Orbán
24945895e9 chore(release): bump package version(s) [skip ci] 2022-09-28 18:10:38 +02:00
Balázs Orbán
6deccf610f fix(core): return JSON for non-HTML server route errors (#5442)
* fix(core): return JSON for non-HTML server route errors

* refactor: throw in `unstable_getServerSession`

* test: expect `unstable_getServerSession` to throw

* refactor: destructure

* fix unrelated test formatting

* catch error page
2022-09-28 17:01:39 +01:00
Etienne Martin
f770b90219 fix(react): safe use of localStorage API (#5444)
fix: safe use of localstorage

Co-authored-by: Etienne <>
2022-09-28 16:54:07 +01:00
Balázs Orbán
87f4786917 chore: bump release package 2022-09-28 13:51:41 +02:00
Balázs Orbán
191ef06471 chore(release): bump package version(s) [skip ci] 2022-09-28 13:00:32 +02:00
Philip
75e6d8f0aa docs(adapters): Update prisma.md (#5366)
* Update prisma.md

The referenced official doc page describes how to fix the `warn(prisma-client) There are already 10 instances of Prisma Client actively running.` error in development mode.

* Update prisma.md

Implemented best practice for Prisma Client creation.

* Fixed typo in Prisma db filename.
2022-09-28 11:15:55 +01:00
Yixuan Xu
17999edd30 chore(example): fix hydrate problem in react18 (#5439) 2022-09-28 10:50:40 +02:00
Tom Freudenberg
54b1845e58 fix(core): don't lock next in peerDependencies #5427 (#5430)
* Update peerDependencies #5427

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-27 00:04:50 +01:00
Tomas Pozo
879faf9fab docs(middleware): add tip on additional matcher patterns (#5404)
* docs(middleware): add tip on additional matcher patterns

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-26 13:39:32 +02:00
Balázs Orbán
3e3c36891e docs(example): use generic type in AppProps
closes #5401
2022-09-25 10:57:44 +01:00
Balázs Orbán
ac5d8a9795 chore(release): bump package version(s) [skip ci] 2022-09-25 11:42:17 +02:00
Matt Oliver
965c6267e2 feat(core): make session token with DB session strategy customizable (#5328)
* Add option for custom generateSessionToken

* Apply suggestions from code review

* Apply suggestions from code review

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-25 10:26:59 +01:00
Sébastien Vanvelthem
bfc429d20b fix: update jose to fix nextjs edge error with middleware (#5372)
fix: update jose to fix nextjs edge error
2022-09-25 15:46:02 +07:00
Balázs Orbán
2d8e910a19 chore(release): bump package version(s) [skip ci] 2022-09-25 10:29:56 +02:00
voinik
d16e04848e fix(adapters): check token during email verification in Upstash Adapter (#5377)
* Check token during email verification

* Undo accidental linter fix

* Update index.ts

Co-authored-by: Balázs Orbán <info@balazsorban.com>
2022-09-25 09:10:55 +01:00
82 changed files with 786 additions and 407 deletions

View File

@@ -3,10 +3,27 @@ const path = require("path")
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
project: [path.resolve(__dirname, "./packages/**/tsconfig.eslint.json")],
},
extends: ["standard-with-typescript", "prettier"],
overrides: [
{
files: ["*.ts", "*.tsx"],
extends: ["standard-with-typescript", "prettier"],
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
parserOptions: {
project: [
path.resolve(__dirname, "./packages/**/tsconfig.eslint.json"),
path.resolve(__dirname, "./apps/**/tsconfig.json"),
],
},
},
],
extends: ["prettier"],
globals: {
localStorage: "readonly",
location: "readonly",
@@ -14,10 +31,6 @@ module.exports = {
},
rules: {
camelcase: "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/strict-boolean-expressions": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/restrict-template-expressions": "off",
},
plugins: ["jest"],
env: {

View File

@@ -5,6 +5,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this issue after reading/searching through the [documentation](https://next-auth.js.org) first!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -5,6 +5,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this [Provider](https://next-auth.js.org/providers/overview) related issue!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -5,6 +5,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thanks for taking the time to fill out this [Adapter](https://next-auth.js.org/adapters/overview) related issue!
Is this your first time contributing? Check out this video: https://www.youtube.com/watch?v=cuoNzXFLitc

View File

@@ -9,6 +9,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Thank you very much for reaching out to us regarding the awesome feature that you believe should be included in the NextAuth.js library.
_NOTE: Feature requests are converted to [discussions (Ideas 💡)](https://github.com/nextauthjs/next-auth/discussions/categories/ideas). Make sure your idea hasn't been asked yet, and upvote the existing one before opening a new instead._

View File

@@ -17,6 +17,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js

View File

@@ -9,6 +9,7 @@ body:
- type: markdown
attributes:
value: |
**NOTE:** Issues that are potentially security related should be reported to us by following the [Security guidelines](https://next-auth.js.org/security) rather than on GitHub.
We are glad that you have a question about this library. Please provide the following information:
- type: textarea

View File

@@ -5,9 +5,14 @@ Please fill out the information below to expedite the review and (hopefully)
merge of your pull request!
-->
> _NOTE_:
>
> - It's a good idea to open an issue first to discuss potential changes.
> - Please make sure that you are _NOT_ opening a PR to fix a potential security vulnerability. Instead, please follow the [Security guidelines](../Security.md) to disclose the issue to us confidentially.
## ☕️ Reasoning
What changes are being made? What feature/bug is being fixed here?
<!-- What changes are being made? What feature/bug is being fixed here? -->
## 🧢 Checklist
@@ -23,6 +28,7 @@ Fixes: INSERT_ISSUE_LINK_HERE
## 📌 Resources
- [Contributing guidelines](./CONTRIBUTING.md)
- [Code of conduct](./CODE_OF_CONDUCT.md)
- [Security guidelines](../Security.md)
- [Contributing guidelines](../CONTRIBUTING.md)
- [Code of conduct](../CODE_OF_CONDUCT.md)
- [Contributing to Open Source](https://kcd.im/pull-request)

View File

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com.
reported by contacting hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com.
All complaints will be reviewed and investigated and will result in a response
that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an

View File

@@ -13,7 +13,7 @@ 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.
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, 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.)
The best way to report an issue is by contacting us via email at hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com, 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 in the future) it is appropriate to submit these publicly as bug reports or feature requests or to raise a question to open a discussion around them.

View File

@@ -6,6 +6,7 @@
"scripts": {
"clean": "rm -rf .next",
"dev": "next dev",
"lint": "next lint",
"build": "next build",
"start": "next start",
"email": "fake-smtp-server",

View File

@@ -18,6 +18,7 @@ import Freshbooks from "next-auth/providers/freshbooks"
import GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google"
import Hubspot from "next-auth/providers/hubspot"
import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak"
@@ -35,6 +36,7 @@ import Twitter, { TwitterLegacy } from "next-auth/providers/twitter"
import Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos"
import Zitadel from "next-auth/providers/zitadel"
// Adapters
import { PrismaClient } from "@prisma/client"
@@ -102,6 +104,7 @@ export const authOptions: NextAuthOptions = {
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
Hubspot({ clientId: process.env.HUBSPOT_ID, clientSecret: process.env.HUBSPOT_SECRET }),
IDS4({ clientId: process.env.IDS4_ID, clientSecret: process.env.IDS4_SECRET, issuer: process.env.IDS4_ISSUER }),
Instagram({ clientId: process.env.INSTAGRAM_ID, clientSecret: process.env.INSTAGRAM_SECRET }),
Keycloak({ clientId: process.env.KEYCLOAK_ID, clientSecret: process.env.KEYCLOAK_SECRET, issuer: process.env.KEYCLOAK_ISSUER }),
@@ -120,6 +123,7 @@ export const authOptions: NextAuthOptions = {
Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_SECRET }),
Zitadel({ issuer: process.env.ZITADEL_ISSUER, clientId: process.env.ZITADEL_CLIENT_ID, clientSecret: process.env.ZITADEL_CLIENT_SECRET }),
],
}

View File

@@ -2,12 +2,16 @@ import { SessionProvider } from "next-auth/react"
import "./styles.css"
import type { AppProps } from "next/app"
import type { Session } from "next-auth"
// Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object.
export default function App({ Component, pageProps }: AppProps) {
export default function App({
Component,
pageProps: { session, ...pageProps },
}: AppProps<{ session: Session }>) {
return (
<SessionProvider session={pageProps.session} refetchInterval={0}>
<SessionProvider session={session}>
<Component {...pageProps} />
</SessionProvider>
)

View File

@@ -4,8 +4,7 @@ import Layout from "../components/layout"
import AccessDenied from "../components/access-denied"
export default function ProtectedPage() {
const { data: session, status } = useSession()
const loading = status === "loading"
const { data: session } = useSession()
const [content, setContent] = useState()
// Fetch content from protected route
@@ -19,9 +18,7 @@ export default function ProtectedPage() {
}
fetchData()
}, [session])
// When rendering client side don't display anything until loading is complete
if (typeof window !== "undefined" && loading) return null
// If no session exists, display access denied message
if (!session) {

View File

@@ -12,15 +12,28 @@ npm install next-auth @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev
```
Create a file with your Prisma Client:
```typescript title="lib/prismadb.ts"
import { PrismaClient } from "@prisma/client"
declare global {
var prisma: PrismaClient | undefined
}
const client = globalThis.prisma || new PrismaClient()
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
export default client
```
Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
import prisma from "../../../lib/prismadb"
export default NextAuth({
adapter: PrismaAdapter(prisma),

View File

@@ -112,15 +112,16 @@ Requests to `/api/auth/signin`, `/api/auth/session` and calls to `getSession()`,
- As with database persisted session expiry times, token expiry time is extended whenever a session is active.
- The arguments _user_, _account_, _profile_ and _isNewUser_ are only passed the first time this callback is called on a new session, after the user signs in. In subsequent calls, only `token` will be available.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and on if you are using a database or not. You can persist data such as User ID, OAuth Access Token in this token. To make it available in the browser, check out the [`session()` callback](#session-callback) as well.
The contents _user_, _account_, _profile_ and _isNewUser_ will vary depending on the provider and if you are using a database. You can persist data such as User ID, OAuth Access Token in this token, see the example below for `access_token` and `user.id`. To expose it on the client side, check out the [`session()` callback](#session-callback) as well.
```js title="pages/api/auth/[...nextauth].js"
...
callbacks: {
async jwt({ token, account }) {
// Persist the OAuth access_token to the token right after signin
async jwt({ token, account, profile }) {
// Persist the OAuth access_token and or the user id to the token right after signin
if (account) {
token.accessToken = account.access_token
token.id = profile.id
}
return token
}
@@ -134,7 +135,7 @@ Use an if branch to check for the existence of parameters (apart from `token`).
## Session callback
The session callback is called whenever a session is checked. By default, only a subset of the token is returned for increased security. If you want to make something available you added to the token through the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
The session callback is called whenever a session is checked. By default, **only a subset of the token is returned for increased security**. If you want to make something available you added to the token (like `access_token` and `user.id` from above) via the `jwt()` callback, you have to explicitly forward it here to make it available to the client.
e.g. `getSession()`, `useSession()`, `/api/auth/session`
@@ -145,8 +146,10 @@ e.g. `getSession()`, `useSession()`, `/api/auth/session`
...
callbacks: {
async session({ session, token, user }) {
// Send properties to the client, like an access_token from a provider.
// Send properties to the client, like an access_token and user id from a provider.
session.accessToken = token.accessToken
session.user.id = token.id
return session
}
}
@@ -155,7 +158,7 @@ callbacks: {
:::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the
JSON Web Token will be immediately available in the session callback, like for example an `access_token` from a provider.
JSON Web Token will be immediately available in the session callback, like for example an `access_token` or `id` from a provider.
:::
:::warning

View File

@@ -114,6 +114,12 @@ session: {
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours
// The session token is usually either a random UUID or string, however if you
// need a more customized session token string, you can define your own generate function.
generateSessionToken: () => {
return randomUUID?.() ?? randomBytes(32).toString("hex")
}
}
```

View File

@@ -156,7 +156,7 @@ interface OAuthConfig {
*/
id: string
version: string
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }>
profile(profile: P, tokens: TokenSet): Awaitable<User>
checks?: ChecksType | ChecksType[]
clientId: string
clientSecret: string

View File

@@ -136,7 +136,7 @@ The `callbackUrl` provided was either invalid or not defined. See [specifying a
#### JWT_SESSION_ERROR
JWKKeySupport: the key does not support HS512 verify algorithm
JWTKeySupport: the key does not support HS512 verify algorithm
The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using

View File

@@ -50,7 +50,7 @@ providers: [
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" }
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Add logic here to look up the user from the credentials supplied

View File

@@ -0,0 +1,87 @@
---
id: zitadel
title: Zitadel
---
## Documentation
https://docs.zitadel.com/docs/apis/openidoauth/endpoints
## Configuration
https://docs.zitadel.com/docs/guides/integrate/oauth-recommended-flows
The Redirect URIs used when creating the credentials must include your full domain and end in the callback path. For example:
- For production: `https://{YOUR_DOMAIN}/api/auth/callback/zitadel`
- For development: `http://localhost:3000/api/auth/callback/zitadel`
Make sure to enable **dev mode** in ZITADEL console to allow redirects for local development.
## Options
The **ZITADEL Provider** comes with a set of default options:
- [ZITADEL Provider options](https://github.com/nextauthjs/next-auth/blob/main/packages/next-auth/src/providers/zitadel.ts)
You can override any of the options to suit your own use case.
## Example
```js
import ZitadelProvider from "next-auth/providers/zitadel";
...
providers: [
ZitadelProvider({
issuer: process.env.ZITADEL_ISSUER,
clientId: process.env.ZITADEL_CLIENT_ID,
clientSecret: process.env.ZITADEL_CLIENT_SECRET,
})
]
...
```
If you need access to ZITADEL APIs or need additional information, make sure to add the corresponding scopes.
To get the full list of supported claims take a look [here](https://docs.zitadel.com/docs/apis/openidoauth/endpoints).
```js
const options = {
...
providers: [
ZitadelProvider({
clientId: process.env.ZITADEL_CLIENT_ID,
authorization: {
params: {
scope: `openid email profile urn:zitadel:iam:org:project:id:${process.env.ZITADEL_PROJECT_ID}:aud`
}
}
})
],
...
}
```
:::
:::tip
ZITADEL also returns a `email_verified` boolean property in the profile.
You can use this property to restrict access to people with verified accounts.
```js
const options = {
...
callbacks: {
async signIn({ account, profile }) {
if (account.provider === "zitadel") {
return profile.email_verified;
}
return true; // Do different verification for other providers that don't have `email_verified`
},
}
...
}
```
:::

View File

@@ -16,7 +16,7 @@ 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.
The best way to report an issue is by contacting us via email at info@balazsorban.com, yo@ndo.dev, thvu@hey.com and me@iaincollins.com, 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.)
The best way to report an issue is by contacting us via email at hi@thvu.dev, info@balazsorban.com, yo@ndo.dev and me@iaincollins.com, 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.)
:::note
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.

View File

@@ -105,6 +105,11 @@ This tutorial covers:
## Database
#### [Create a NextAuth.js Custom Adapter with HarperDB & Next.js](https://spacejelly.dev/posts/how-to-create-a-nextauth-js-custom-adapter-with-harperdb-next-js/) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- Use a custom database in a Custom Adapter for persisted NextAuth.js sessions using HarperDB as an example.
- Video tutorial also available: <https://www.youtube.com/watch?v=pu7xBv7sZ8s>
#### [Using NextAuth.js with Prisma and PlanetScale serverless databases](https://github.com/planetscale/nextjs-planetscale-starter) <svg xmlns="http://www.w3.org/2000/svg" style={{ marginLeft: '5px', marginBottom:'-6px'}} height="20" width="20" fill="none" viewBox="0 0 24 24" stroke="currentColor"><title>External</title> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> </svg>
- How to set up a PlanetScale database to fetch and store user / account data with the Prisma adapter.

View File

@@ -42,7 +42,7 @@ export default function Page() {
### Next.js (Middleware)
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `_middleware.js` file in your root `pages` directory which looks like this:
With NextAuth.js 4.2.0 and Next.js 12, you can now protect your pages via the middleware pattern more easily. If you would like to protect all pages, you can create a `middleware.js` file in your root `pages` directory which looks like this:
```js title="/middleware.js"
export { default } from "next-auth/middleware"
@@ -60,6 +60,12 @@ For the time being, the `withAuth` middleware only supports `"jwt"` as [session
More details can be found [here](https://next-auth.js.org/configuration/nextjs#middleware).
:::tip
To inclue all `dashboard` nested routes (sub pages like `/dashboard/settings`, `/dashboard/profile`) you can pass `matcher: "/dashboard/:path*"` to `config`.
For other patterns check out the [Next.js Middleware documentation](https://nextjs.org/docs/advanced-features/middleware#matcher).
:::
### Server Side
You can protect server side rendered pages using the `unstable_getServerSession` method. This is different from the old `getSession()` method, in that it does not do an extra fetch out over the internet to confirm data from itself, increasing performance significantly.

View File

@@ -18,7 +18,7 @@
},
"devDependencies": {
"@actions/core": "^1.6.0",
"@balazsorban/monorepo-release": "0.0.4",
"@balazsorban/monorepo-release": "0.0.5",
"@types/jest": "^28.1.3",
"@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.10.2",

View File

@@ -1,7 +1,7 @@
{
"name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.4",
"version": "1.0.5",
"description": "AWS DynamoDB adapter for next-auth.",
"keywords": [
"next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -4,10 +4,10 @@ import type {
BatchWriteCommandInput,
DynamoDBDocument,
} from "@aws-sdk/lib-dynamodb"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterSession,
AdapterAccount,
AdapterUser,
VerificationToken,
} from "next-auth/adapters"
@@ -86,7 +86,7 @@ export function DynamoDBAdapter(
})
if (!data.Items?.length) return null
const accounts = data.Items[0] as Account
const accounts = data.Items[0] as AdapterAccount
const res = await client.get({
TableName,
Key: {
@@ -174,7 +174,7 @@ export function DynamoDBAdapter(
":gsi1sk": `ACCOUNT#${providerAccountId}`,
},
})
const account = format.from<Account>(data.Items?.[0])
const account = format.from<AdapterAccount>(data.Items?.[0])
if (!account) return
await client.delete({
TableName,

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/firebase-adapter",
"version": "1.0.1",
"version": "1.0.2",
"description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -43,4 +43,4 @@
"jest": "^27.4.3",
"next-auth": "workspace:*"
}
}
}

View File

@@ -15,17 +15,18 @@ import {
where,
connectFirestoreEmulator,
} from "firebase/firestore"
import type { Account } from "next-auth"
import type {
Adapter,
AdapterSession,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
import { getConverter } from "./converter"
type IndexableObject = Record<string, unknown>
export type IndexableObject = Record<string, unknown>
export interface FirestoreAdapterOptions {
emulator?: {
@@ -50,13 +51,13 @@ export function FirestoreAdapter({
}
const Users = collection(db, "users").withConverter(
getConverter<AdapterUser>()
getConverter<AdapterUser & IndexableObject>()
)
const Sessions = collection(db, "sessions").withConverter(
getConverter<AdapterSession & IndexableObject>()
)
const Accounts = collection(db, "accounts").withConverter(
getConverter<Account>()
getConverter<AdapterAccount>()
)
const VerificationTokens = collection(db, "verificationTokens").withConverter(
getConverter<VerificationToken & IndexableObject>({ excludeId: true })

View File

@@ -14,7 +14,7 @@ connectFirestoreEmulator(firestore, 'localhost', 8080);
type IndexableObject = Record<string, unknown>;
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser>());
const Users = collection(firestore, 'users').withConverter(getConverter<AdapterUser & IndexableObject>());
const Sessions = collection(firestore, 'sessions').withConverter(getConverter<AdapterSession & IndexableObject>());
const Accounts = collection(firestore, 'accounts').withConverter(getConverter<Account>());
const VerificationTokens = collection(firestore, 'verificationTokens').withConverter(getConverter<VerificationToken & IndexableObject>({ excludeId: true }));

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mikro-orm-adapter",
"version": "3.0.0",
"version": "3.0.1",
"description": "MikroORM adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -5,17 +5,16 @@ import {
Unique,
PrimaryKey,
Entity,
Enum,
OneToMany,
Collection,
ManyToOne,
types,
} from "@mikro-orm/core"
import type { DefaultAccount } from "next-auth"
import type {
AdapterSession,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken as AdapterVerificationToken,
} from "next-auth/adapters"
import type { ProviderType } from "next-auth/providers"
@@ -35,7 +34,7 @@ export class User implements RemoveIndex<AdapterUser> {
@Property({ type: types.string, nullable: true })
@Unique()
email?: string
email: string = ""
@Property({ type: types.datetime, nullable: true })
emailVerified: Date | null = null
@@ -44,7 +43,7 @@ export class User implements RemoveIndex<AdapterUser> {
image?: string
@OneToMany({
entity: 'Session',
entity: "Session",
mappedBy: (session: Session) => session.user,
hidden: true,
orphanRemoval: true,
@@ -52,7 +51,7 @@ export class User implements RemoveIndex<AdapterUser> {
sessions = new Collection<Session, object>(this)
@OneToMany({
entity: 'Account',
entity: "Account",
mappedBy: (account: Account) => account.user,
hidden: true,
orphanRemoval: true,
@@ -67,7 +66,7 @@ export class Session implements AdapterSession {
id: string = randomUUID()
@ManyToOne({
entity: 'User',
entity: "User",
hidden: true,
onDelete: "cascade",
})
@@ -76,7 +75,7 @@ export class Session implements AdapterSession {
@Property({ type: types.string, persist: false })
userId!: string
@Property({ type: 'Date' })
@Property({ type: "Date" })
expires!: Date
@Property({ type: types.string })
@@ -86,13 +85,13 @@ export class Session implements AdapterSession {
@Entity()
@Unique({ properties: ["provider", "providerAccountId"] })
export class Account implements RemoveIndex<DefaultAccount> {
export class Account implements RemoveIndex<AdapterAccount> {
@PrimaryKey()
@Property({ type: types.string })
id: string = randomUUID()
@ManyToOne({
entity: 'User',
entity: "User",
hidden: true,
onDelete: "cascade",
})
@@ -139,7 +138,7 @@ export class VerificationToken implements AdapterVerificationToken {
@Property({ type: types.string })
token!: string
@Property({ type: 'Date' })
@Property({ type: "Date" })
expires!: Date
@Property({ type: types.string })

View File

@@ -1,7 +1,4 @@
import { Options, types } from "@mikro-orm/core"
import type { SqliteDriver } from "@mikro-orm/sqlite"
import { MikroORM, wrap } from "@mikro-orm/core"
import { runBasicTests } from "@next-auth/adapter-test"
import { MikroOrmAdapter, defaultEntities } from "../src"
import {
Cascade,
@@ -11,8 +8,12 @@ import {
PrimaryKey,
Property,
Unique,
MikroORM,
wrap,
Options,
types,
} from "@mikro-orm/core"
import { randomUUID } from "@next-auth/adapter-test"
import { randomUUID, runBasicTests } from "@next-auth/adapter-test"
@Entity()
export class User implements defaultEntities.User {
@@ -25,16 +26,16 @@ export class User implements defaultEntities.User {
@Property({ type: types.string, nullable: true })
@Unique()
email?: string
email: string = ""
@Property({ type: 'Date', nullable: true })
@Property({ type: "Date", nullable: true })
emailVerified: Date | null = null
@Property({ type: types.string, nullable: true })
image?: string
@OneToMany({
entity: 'Session',
entity: "Session",
mappedBy: (session: defaultEntities.Session) => session.user,
hidden: true,
orphanRemoval: true,
@@ -43,7 +44,7 @@ export class User implements defaultEntities.User {
sessions = new Collection<defaultEntities.Session>(this)
@OneToMany({
entity: 'Account',
entity: "Account",
mappedBy: (account: defaultEntities.Account) => account.user,
hidden: true,
orphanRemoval: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/mongodb-adapter",
"version": "1.1.0",
"version": "1.1.1",
"description": "mongoDB adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -44,4 +44,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -3,12 +3,12 @@ import { ObjectId } from "mongodb"
import type {
Adapter,
AdapterSession,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
import type { MongoClient } from "mongodb"
import type { Account } from "next-auth"
export interface MongoDBAdapterOptions {
collections?: {
@@ -56,7 +56,7 @@ export const format = {
else if (key === "id") continue
else newObject[key] = value
}
return newObject as T
return newObject as T & { _id: ObjectId }
},
}
@@ -78,7 +78,7 @@ export function MongoDBAdapter(
const c = { ...defaultCollections, ...collections }
return {
U: _db.collection<AdapterUser>(c.Users),
A: _db.collection<Account>(c.Accounts),
A: _db.collection<AdapterAccount>(c.Accounts),
S: _db.collection<AdapterSession>(c.Sessions),
V: _db.collection<VerificationToken>(c?.VerificationTokens),
}
@@ -128,7 +128,7 @@ export function MongoDBAdapter(
])
},
linkAccount: async (data) => {
const account = to<Account>(data)
const account = to<AdapterAccount>(data)
await (await db).A.insertOne(account)
return account
},
@@ -136,7 +136,7 @@ export function MongoDBAdapter(
const { value: account } = await (
await db
).A.findOneAndDelete(provider_providerAccountId)
return from<Account>(account!)
return from<AdapterAccount>(account!)
},
async getSessionAndUser(sessionToken) {
const session = await (await db).S.findOne({ sessionToken })
@@ -156,7 +156,6 @@ export function MongoDBAdapter(
return from<AdapterSession>(session)
},
async updateSession(data) {
// @ts-expect-error
const { _id, ...session } = to<AdapterSession>(data)
const result = await (

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/neo4j-adapter",
"version": "1.0.4",
"version": "1.0.5",
"description": "neo4j adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -50,4 +50,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -87,8 +87,6 @@ export function Neo4jAdapter(session: Session): Adapter {
)
},
// @ts-expect-error Property 'id' is missing in type
// We never use `session.id` anywhere in the core, so this is fine.
async createSession(data) {
const { userId, ...s } = format.to(data)
await write(

View File

@@ -38,7 +38,7 @@ runBasicTests({
return format.from(result?.records[0]?.get("u")?.properties)
},
async session(sessionToken: any) {
async session(sessionToken: string) {
const result = await neo4jSession.readTransaction((tx) =>
tx.run(
`MATCH (u:User)-[:HAS_SESSION]->(s:Session)

View File

@@ -1,6 +1,5 @@
#!/usr/bin/env bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
NEO4J_USER=neo4j
NEO4J_PASS=password
CONTAINER_NAME=next-auth-neo4j-test-e
@@ -29,7 +28,7 @@ neo4j:4.2.0
# -e NEO4J_ACCEPT_LICENSE_AGREEMENT=yes \
# neo4j:4.2.0-enterprise
echo "Waiting 5 sec for db to start..." && sleep 5
echo "Waiting 10 sec for db to start..." && sleep 10
if $JEST_WATCH; then
# Run jest in watch mode

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/prisma-adapter",
"version": "1.0.4",
"version": "1.0.5",
"description": "Prisma adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -52,4 +52,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -20,7 +20,6 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -35,11 +34,10 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@id([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -51,5 +49,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@unique([identifier, token])
@@id([identifier, token])
}

View File

@@ -4,8 +4,7 @@ datasource db {
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["mongoDb"]
provider = "prisma-client-js"
}
model Account {

View File

@@ -10,7 +10,7 @@ generator client {
model User {
id String @id @default(cuid())
name String?
email String? @unique
email String @unique
emailVerified DateTime?
image String?
accounts Account[]
@@ -18,7 +18,6 @@ model User {
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
@@ -33,11 +32,10 @@ model Account {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@id([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
@@ -49,5 +47,5 @@ model VerificationToken {
token String @unique
expires DateTime
@@unique([identifier, token])
@@id([identifier, token])
}

View File

@@ -1,5 +1,5 @@
import type { PrismaClient, Prisma } from "@prisma/client"
import type { Adapter } from "next-auth/adapters"
import type { Adapter, AdapterAccount } from "next-auth/adapters"
export function PrismaAdapter(p: PrismaClient): Adapter {
return {
@@ -15,9 +15,12 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
},
updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }),
deleteUser: (id) => p.user.delete({ where: { id } }),
linkAccount: (data) => p.account.create({ data }) as any,
linkAccount: (data) =>
p.account.create({ data }) as unknown as AdapterAccount,
unlinkAccount: (provider_providerAccountId) =>
p.account.delete({ where: { provider_providerAccountId } }) as any,
p.account.delete({
where: { provider_providerAccountId },
}) as unknown as AdapterAccount,
async getSessionAndUser(sessionToken) {
const userAndSession = await p.session.findUnique({
where: { sessionToken },
@@ -33,17 +36,18 @@ export function PrismaAdapter(p: PrismaClient): Adapter {
deleteSession: (sessionToken) =>
p.session.delete({ where: { sessionToken } }),
async createVerificationToken(data) {
// @ts-ignore
const { id: _, ...verificationToken } = await p.verificationToken.create({
data,
})
const verificationToken = await p.verificationToken.create({ data })
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
return verificationToken
},
async useVerificationToken(identifier_token) {
try {
// @ts-ignore
const { id: _, ...verificationToken } =
await p.verificationToken.delete({ where: { identifier_token } })
const verificationToken = await p.verificationToken.delete({
where: { identifier_token },
})
// @ts-expect-errors // MongoDB needs an ID, but we don't
if (verificationToken.id) delete verificationToken.id
return verificationToken
} catch (error) {
// If token already used/deleted, just return null

View File

@@ -40,9 +40,9 @@ runBasicTests({
where: { identifier_token },
})
if (!result) return null
// @ts-ignore
const { id: _, ...verificationToken } = result
return verificationToken
// @ts-ignore // MongoDB needs an ID, but we don't
delete result.id
return result
},
},
})

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/sequelize-adapter",
"version": "1.0.5",
"version": "1.0.6",
"description": "Sequelize adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/typeorm-legacy-adapter",
"version": "2.0.0",
"version": "2.0.1",
"description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",

View File

@@ -1,6 +1,10 @@
import type { Adapter, AdapterSession, AdapterUser } from "next-auth/adapters"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
} from "next-auth/adapters"
import { DataSourceOptions, DataSource, EntityManager } from "typeorm"
import type { Account } from "next-auth"
import * as defaultEntities from "./entities"
import { parseDataSourceConfig, updateConnectionEntities } from "./utils"
@@ -87,7 +91,7 @@ export function TypeORMLegacyAdapter(
},
async getUserByAccount(provider_providerAccountId) {
const m = await getManager(c)
const account = await m.findOne<Account & { user: AdapterUser }>(
const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
"AccountEntity",
{ where: provider_providerAccountId, relations: ["user"] }
)
@@ -115,9 +119,8 @@ export function TypeORMLegacyAdapter(
},
async unlinkAccount(providerAccountId) {
const m = await getManager(c)
await m.delete<Account>("AccountEntity", providerAccountId)
await m.delete<AdapterAccount>("AccountEntity", providerAccountId)
},
// @ts-expect-error
async createSession(data) {
const m = await getManager(c)
const session = await m.save("SessionEntity", data)

View File

@@ -1,6 +1,6 @@
{
"name": "@next-auth/upstash-redis-adapter",
"version": "3.0.1",
"version": "3.0.3",
"description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth",
@@ -49,4 +49,4 @@
"jest": {
"preset": "@next-auth/adapter-test/jest"
}
}
}

View File

@@ -1,7 +1,7 @@
import type { Account as AdapterAccount } from "next-auth"
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters"
@@ -117,7 +117,6 @@ export function UpstashRedisAdapter(
const id = uuid()
// TypeScript thinks the emailVerified field is missing
// but all fields are copied directly from user, so it's there
// @ts-expect-error
return await setUser(id, { ...user, id })
},
getUser,
@@ -144,10 +143,7 @@ export function UpstashRedisAdapter(
const id = `${account.provider}:${account.providerAccountId}`
return await setAccount(id, { ...account, id })
},
async createSession(session) {
const id = session.sessionToken
return await setSession(id, { ...session, id })
},
createSession: (session) => setSession(session.sessionToken, session),
async getSessionAndUser(sessionToken) {
const session = await getSession(sessionToken)
if (!session) return null
@@ -165,13 +161,20 @@ export function UpstashRedisAdapter(
},
async createVerificationToken(verificationToken) {
await setObjectAsJson(
verificationTokenKeyPrefix + verificationToken.identifier,
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token,
verificationToken
)
return verificationToken
},
async useVerificationToken(verificationToken) {
const tokenKey = verificationTokenKeyPrefix + verificationToken.identifier
const tokenKey =
verificationTokenKeyPrefix +
verificationToken.identifier +
":" +
verificationToken.token
const token = await client.get<VerificationToken>(tokenKey)
if (!token) return null

View File

@@ -11,6 +11,14 @@ if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_KEY) {
process.exit(0)
}
if (process.env.CI) {
// TODO: Fix this
test('Skipping UpstashRedisAdapter tests in CI because of "Request failed" errors. Should revisit', () => {
expect(true).toBe(true)
})
process.exit(0)
}
const client = new Redis({
url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY,

View File

@@ -1,6 +1,6 @@
{
"name": "next-auth",
"version": "4.11.0",
"version": "4.13.0",
"description": "Authentication for Next.js",
"homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth.git",
@@ -70,7 +70,7 @@
"@babel/runtime": "^7.16.3",
"@panva/hkdf": "^1.0.1",
"cookie": "^0.5.0",
"jose": "^4.3.7",
"jose": "^4.9.3",
"oauth": "^0.9.15",
"openid-client": "^5.1.0",
"preact": "^10.6.3",
@@ -78,7 +78,7 @@
"uuid": "^8.3.2"
},
"peerDependencies": {
"next": "12.2.5",
"next": "^12.2.5",
"nodemailer": "^6.6.5",
"react": "^17.0.2 || ^18",
"react-dom": "^17.0.2 || ^18"
@@ -130,4 +130,4 @@
"engines": {
"node": "^12.19.0 || ^14.15.0 || ^16.13.0"
}
}
}

View File

@@ -2,11 +2,15 @@ import { Account, User, Awaitable } from "."
export interface AdapterUser extends User {
id: string
email: string
emailVerified: Date | null
}
export interface AdapterAccount extends Account {
userId: string
}
export interface AdapterSession {
id: string
/** A randomly generated value that is used to get hold of the session. */
sessionToken: string
/** Used to connect the session to a particular user */
@@ -55,13 +59,30 @@ export interface VerificationToken {
* [Adapters Overview](https://next-auth.js.org/adapters/overview) |
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter)
*/
export interface Adapter {
export type Adapter<WithVerificationToken = boolean> = DefaultAdapter &
(WithVerificationToken extends true
? {
createVerificationToken: (
verificationToken: VerificationToken
) => Awaitable<VerificationToken | null | undefined>
/**
* Return verification token from the database
* and delete it so it cannot be used again.
*/
useVerificationToken: (params: {
identifier: string
token: string
}) => Awaitable<VerificationToken | null>
}
: {})
export interface DefaultAdapter {
createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
getUser: (id: string) => Awaitable<AdapterUser | null>
getUserByEmail: (email: string) => Awaitable<AdapterUser | null>
/** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount: (
providerAccountId: Pick<Account, "provider" | "providerAccountId">
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
/** @todo Implement */
@@ -69,12 +90,12 @@ export interface Adapter {
userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: (
account: Account
) => Promise<void> | Awaitable<Account | null | undefined>
account: AdapterAccount
) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
/** @todo Implement */
unlinkAccount?: (
providerAccountId: Pick<Account, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<Account | undefined>
providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<AdapterAccount | undefined>
/** Creates a session for the user and returns it. */
createSession: (session: {
sessionToken: string

View File

@@ -94,10 +94,18 @@ export function BroadcastChannel(name = "nextauth.message") {
/** Notify other tabs/windows. */
post(message: Record<string, unknown>) {
if (typeof window === "undefined") return
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
try {
localStorage.setItem(
name,
JSON.stringify({ ...message, timestamp: now() })
)
} catch {
/**
* The localStorage API isn't always available.
* It won't work in private mode prior to Safari 11 for example.
* Notifications are simply dropped if an error is encountered.
*/
}
},
}
}

View File

@@ -1,5 +1,4 @@
import type { EventCallbacks, LoggerInstance } from ".."
import type { Adapter } from "../adapters"
/**
* Same as the default `Error`, but it is JSON serializable.
@@ -58,6 +57,11 @@ export class MissingAdapter extends UnknownError {
code = "EMAIL_REQUIRES_ADAPTER_ERROR"
}
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR"
@@ -99,10 +103,10 @@ export function eventsErrorHandler(
}
/** Handles adapter induced errors. */
export function adapterErrorHandler(
adapter: Adapter | undefined,
export function adapterErrorHandler<TAdapter>(
adapter: TAdapter | undefined,
logger: LoggerInstance
): Adapter | undefined {
): TAdapter | undefined {
if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => {

View File

@@ -94,13 +94,21 @@ export async function NextAuthHandler<
assertionResult.forEach(logger.warn)
} 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)
const htmlPages = ["signin", "signout", "error", "verify-request"]
if (!htmlPages.includes(req.action) || req.method !== "GET") {
const message = `There is a problem with the server configuration. Check the server logs for more information.`
return {
status: 500,
headers: [{ key: "Content-Type", value: "application/json" }],
body: { message } as any,
}
}
const { pages, theme } = userOptions
const authOnErrorPage =
pages?.error &&
req.action === "signin" &&
req.query?.callbackUrl.startsWith(pages.error)
pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) {

View File

@@ -1,3 +1,4 @@
import { randomBytes, randomUUID } from "crypto"
import { NextAuthOptions } from ".."
import logger from "../utils/logger"
import parseUrl from "../utils/parse-url"
@@ -70,6 +71,7 @@ export async function init({
// and are request-specific.
url,
action,
// @ts-expect-errors
provider,
cookies: {
...cookie.defaultCookies(
@@ -86,6 +88,10 @@ export async function init({
strategy: userOptions.adapter ? "database" : "jwt",
maxAge,
updateAge: 24 * 60 * 60,
generateSessionToken: () => {
// Use `randomUUID` if available. (Node 15.6+)
return randomUUID?.() ?? randomBytes(32).toString("hex")
},
...userOptions.session,
},
// JWT options

View File

@@ -5,6 +5,7 @@ import {
MissingSecret,
UnsupportedStrategy,
InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors"
import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie"
@@ -120,8 +121,23 @@ export function assertConfig(params: {
}
}
if (hasEmail && !options.adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an adapter.")
}
const missingMethods = [
"createVerificationToken",
"useVerificationToken",
"getUserByEmail",
].filter((method) => !adapter[method])
if (missingMethods.length) {
return new MissingAdapterMethods(
`Required adapter methods were missing: ${missingMethods.join(", ")}`
)
}
}
if (!warned) {

View File

@@ -1,4 +1,3 @@
import { randomBytes, randomUUID } from "crypto"
import { AccountNotLinkedError } from "../errors"
import { fromDate } from "./utils"
@@ -22,11 +21,11 @@ import type { SessionToken } from "./cookie"
*/
export default async function callbackHandler(params: {
sessionToken?: SessionToken
profile: User
account: Account
profile: User | AdapterUser | { email: string }
account: Account | null
options: InternalOptions
}) {
const { sessionToken, profile, account, options } = params
const { sessionToken, profile: _profile, account, options } = params
// Input validation
if (!account?.providerAccountId || !account.type)
throw new Error("Missing or invalid provider account")
@@ -37,15 +36,17 @@ export default async function callbackHandler(params: {
adapter,
jwt,
events,
session: { strategy: sessionStrategy },
session: { strategy: sessionStrategy, generateSessionToken },
} = options
// If no adapter is configured then we don't have a database and cannot
// persist data; in this mode we just return a dummy session object.
if (!adapter) {
return { user: profile, account, session: {} }
return { user: _profile as User, account }
}
const profile = _profile as AdapterUser
const {
createUser,
updateUser,
@@ -85,9 +86,7 @@ export default async function callbackHandler(params: {
if (account.type === "email") {
// If signing in with an email, check if an account with the same email address exists already
const userByEmail = profile.email
? await getUserByEmail(profile.email)
: null
const userByEmail = await getUserByEmail(profile.email)
if (userByEmail) {
// If they are not already signed in as the same user, this flow will
// sign them out of the current session and sign them in as the new user
@@ -102,8 +101,7 @@ export default async function callbackHandler(params: {
user = await updateUser({ id: userByEmail.id, emailVerified: new Date() })
await events.updateUser?.({ user })
} else {
const newUser = { ...profile, emailVerified: new Date() }
delete (newUser as Omit<AdapterUser, "id">).id
const { id: _, ...newUser } = { ...profile, emailVerified: new Date() }
// Create user account if there isn't one for the email address already
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -199,8 +197,7 @@ export default async function callbackHandler(params: {
// If no account matching the same [provider].id or .email exists, we can
// create a new account for the user, link it to the OAuth acccount and
// create a new session for them so they are signed in with it.
const newUser = { ...profile, emailVerified: null }
delete (newUser as Omit<AdapterUser, "id">).id
const { id: _, ...newUser } = { ...profile, emailVerified: null }
user = await createUser(newUser)
await events.createUser?.({ user })
@@ -218,9 +215,6 @@ export default async function callbackHandler(params: {
return { session, user, isNewUser: true }
}
}
}
function generateSessionToken() {
// Use `randomUUID` if available. (Node 15.6++)
return randomUUID?.() ?? randomBytes(32).toString("hex")
throw new Error("Unsupported account type")
}

View File

@@ -0,0 +1,19 @@
import type { InternalOptions } from "../../types"
export default async function getUserFromEmail({
email,
adapter,
withId = false,
}: {
email: string
adapter: InternalOptions<"email">["adapter"]
withId: boolean
}) {
const { getUserByEmail } = adapter
// If is an existing user return a user object (otherwise use placeholder)
return (email ? await getUserByEmail(email) : null) ?? withId
? { id: email, email }
: {
email,
}
}

View File

@@ -36,7 +36,6 @@ export default async function email(
theme,
}),
// Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({
identifier,
token: hashToken(token, options),

View File

@@ -39,10 +39,7 @@ export default async function getAuthorizationUrl({
if (provider.version?.startsWith("1.")) {
const client = oAuth1Client(options)
const tokens = (await client.getOAuthRequestToken(params)) as any
const url = `${
// @ts-expect-error
provider.authorization?.url ?? provider.authorization
}?${new URLSearchParams({
const url = `${provider.authorization?.url}?${new URLSearchParams({
oauth_token: tokens.oauth_token,
oauth_token_secret: tokens.oauth_token_secret,
...tokens.params,
@@ -68,7 +65,7 @@ export default async function getAuthorizationUrl({
authorizationParams.nonce = nonce.value
cookies.push(nonce.cookie)
}
const pkce = await createPKCE(options)
if (pkce) {
authorizationParams.code_challenge = pkce.code_challenge

View File

@@ -7,10 +7,10 @@ import { useNonce } from "./nonce-handler"
import { OAuthCallbackError } from "../../errors"
import type { CallbackParamsType, OpenIDCallbackChecks } from "openid-client"
import type { Account, LoggerInstance, Profile } from "../../.."
import type { LoggerInstance, Profile } from "../../.."
import type { OAuthChecks, OAuthConfig } from "../../../providers"
import type { InternalOptions } from "../../types"
import type { RequestInternal, OutgoingResponse } from "../.."
import type { RequestInternal } from "../.."
import type { Cookie } from "../cookie"
export default async function oAuthCallback(params: {
@@ -19,7 +19,7 @@ export default async function oAuthCallback(params: {
body: RequestInternal["body"]
method: Required<RequestInternal>["method"]
cookies: RequestInternal["cookies"]
}): Promise<GetProfileResult & { cookies?: OutgoingResponse["cookies"] }> {
}) {
const { options, query, body, method, cookies } = params
const { logger, provider } = options
@@ -34,23 +34,19 @@ export default async function oAuthCallback(params: {
logger.debug("OAUTH_CALLBACK_HANDLER_ERROR", { body })
throw error
}
if (provider.version?.startsWith("1.")) {
try {
const client = await oAuth1Client(options)
// Handle OAuth v1.x
const { oauth_token, oauth_verifier } = query ?? {}
// @ts-expect-error
const tokens: TokenSet = await client.getOAuthAccessToken(
oauth_token as string,
// @ts-expect-error
const tokens = (await (client as any).getOAuthAccessToken(
oauth_token,
null,
oauth_verifier
)
// @ts-expect-error
let profile: Profile = await client.get(
(provider as any).profileUrl,
)) as TokenSet
let profile: Profile = await (client as any).get(
provider.profileUrl,
tokens.oauth_token,
tokens.oauth_token_secret
)
@@ -59,7 +55,8 @@ export default async function oAuthCallback(params: {
profile = JSON.parse(profile)
}
return await getProfile({ profile, tokens, provider, logger })
const newProfile = await getProfile({ profile, tokens, provider, logger })
return { ...newProfile, cookies: [] }
} catch (error) {
logger.error("OAUTH_V1_GET_ACCESS_TOKEN_ERROR", error as Error)
throw error
@@ -82,7 +79,7 @@ export default async function oAuthCallback(params: {
const nonce = await useNonce(cookies?.[options.cookies.nonce.name], options)
if (nonce && provider.idToken) {
(checks as OpenIDCallbackChecks).nonce = nonce.value
;(checks as OpenIDCallbackChecks).nonce = nonce.value
resCookies.push(nonce.cookie)
}
@@ -102,13 +99,10 @@ export default async function oAuthCallback(params: {
body,
method,
}),
// @ts-expect-error
...provider.token?.params,
}
// @ts-expect-error
if (provider.token?.request) {
// @ts-expect-error
const response = await provider.token.request({
provider,
params,
@@ -128,9 +122,7 @@ export default async function oAuthCallback(params: {
}
let profile: Profile
// @ts-expect-error
if (provider.userinfo?.request) {
// @ts-expect-error
profile = await provider.userinfo.request({
provider,
tokens,
@@ -140,7 +132,6 @@ export default async function oAuthCallback(params: {
profile = tokens.claims()
} else {
profile = await client.userinfo(tokens, {
// @ts-expect-error
params: provider.userinfo?.params,
})
}
@@ -164,25 +155,22 @@ export interface GetProfileParams {
logger: LoggerInstance
}
export interface GetProfileResult {
// @ts-expect-error
profile: ReturnType<OAuthConfig["profile"]> | null
account: Omit<Account, "userId"> | null
OAuthProfile: Profile
}
/** Returns profile, raw profile and auth provider details */
async function getProfile({
profile: OAuthProfile,
tokens,
provider,
logger,
}: GetProfileParams): Promise<GetProfileResult> {
}: GetProfileParams) {
try {
logger.debug("PROFILE_DATA", { OAuthProfile })
// @ts-expect-error
const profile = await provider.profile(OAuthProfile, tokens)
profile.email = profile.email?.toLowerCase()
if (!profile.id)
throw new TypeError(
`Profile id is missing in ${provider.name} OAuth profile response`
)
// Return profile, raw profile and auth provider details
return {
profile,
@@ -202,11 +190,9 @@ 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 as Error)
return {
profile: null,
account: null,
logger.error("OAUTH_PARSE_PROFILE_ERROR", {
error: error as Error,
OAuthProfile,
}
})
}
}

View File

@@ -22,13 +22,9 @@ export async function openidClient(
} else {
issuer = new Issuer({
issuer: provider.issuer as string,
authorization_endpoint:
// @ts-expect-error
provider.authorization?.url ?? provider.authorization,
// @ts-expect-error
token_endpoint: provider.token?.url ?? provider.token,
// @ts-expect-error
userinfo_endpoint: provider.userinfo?.url ?? provider.userinfo,
authorization_endpoint: provider.authorization?.url,
token_endpoint: provider.token?.url,
userinfo_endpoint: provider.userinfo?.url,
})
}

View File

@@ -1,7 +1,11 @@
import { merge } from "../../utils/merge"
import type { InternalProvider } from "../types"
import type { Provider } from "../../providers"
import type {
InternalOAuthConfig,
OAuthConfig,
Provider,
} from "../../providers"
import type { InternalUrl } from "../../utils/parse-url"
/**
@@ -18,52 +22,72 @@ export default function parseProviders(params: {
} {
const { url, providerId } = params
const providers = params.providers.map(({ options, ...rest }) => {
const defaultOptions = normalizeProvider(rest as Provider)
const userOptions = normalizeProvider(options as Provider)
const providers = params.providers.map<InternalProvider>(
({ options: userOptions, ...rest }) => {
if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
const normalizedUserOptions = normalizeOAuthOptions(userOptions, true)
return merge(normalizedOptions, {
...normalizedUserOptions,
signinUrl: `${url}/signin/${normalizedUserOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${
normalizedUserOptions?.id ?? rest.id
}`,
})
}
return merge(rest, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
}
)
return merge(defaultOptions, {
...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
})
})
const provider = providers.find(({ id }) => id === providerId)
return { providers, provider }
return {
providers,
provider: providers.find(({ id }) => id === providerId),
}
}
function normalizeProvider(provider?: Provider) {
if (!provider) return
/**
* Transform OAuth options `authorization`, `token` and `profile` strings to `{ url: string; params: Record<string, string> }`
*/
function normalizeOAuthOptions(
oauthOptions?: Partial<OAuthConfig<any>> | Record<string, unknown>,
isUserOptions = false
) {
if (!oauthOptions) return
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 ?? []),
const normalized = Object.entries(oauthOptions).reduce<
InternalOAuthConfig<Record<string, unknown>>
>(
(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
}
} else {
acc[key] = value
}
return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions
}, {} as any)
return acc
},
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
{} as any
)
if (normalized.type === "oauth" && !normalized.version?.startsWith("1.")) {
if (!isUserOptions && !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`
// Only do this if this function is not called with user options to avoid overriding in later stage.
normalized.idToken = Boolean(
normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ??
// @ts-expect-error
normalized.authorization?.params?.scope?.includes("openid")
)

View File

@@ -1,15 +1,17 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import { hashToken } from "../lib/utils"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { InternalOptions } from "../types"
import type { RequestInternal, OutgoingResponse } from ".."
import type { Cookie, SessionStore } from "../lib/cookie"
import type { User } from "../.."
import type { AdapterSession } from "../../adapters"
/** Handle callbacks from login services */
export default async function callback(params: {
options: InternalOptions<"oauth" | "credentials" | "email">
options: InternalOptions
query: RequestInternal["query"]
method: Required<RequestInternal>["method"]
body: RequestInternal["body"]
@@ -50,7 +52,7 @@ export default async function callback(params: {
cookies: params.cookies,
})
if (oauthCookies) cookies.push(...oauthCookies)
if (oauthCookies.length) cookies.push(...oauthCookies)
try {
// Make it easier to debug when adding a new provider
@@ -68,7 +70,7 @@ export default async function callback(params: {
// Note: In oAuthCallback an error is logged with debug info, so it
// should at least be visible to developers what happened if it is an
// error with the provider.
if (!profile) {
if (!profile || !account || !OAuthProfile) {
return { redirect: `${url}/signin`, cookies }
}
@@ -80,7 +82,6 @@ export default async function callback(params: {
if (adapter) {
const { getUserByAccount } = adapter
const userByAccount = await getUserByAccount({
// @ts-expect-error
providerAccountId: account.providerAccountId,
provider: provider.id,
})
@@ -91,7 +92,6 @@ export default async function callback(params: {
try {
const isAllowed = await callbacks.signIn({
user: userOrProfile,
// @ts-expect-error
account,
profile: OAuthProfile,
})
@@ -110,11 +110,9 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
profile,
// @ts-expect-error
account,
options,
})
@@ -129,7 +127,6 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
profile: OAuthProfile,
isNewUser,
@@ -150,10 +147,10 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: session.sessionToken,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: session.expires,
expires: (session as AdapterSession).expires,
},
})
}
@@ -201,14 +198,16 @@ export default async function callback(params: {
}
} else if (provider.type === "email") {
try {
// Verified in `assertConfig`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { useVerificationToken, getUserByEmail } = adapter!
const token = query?.token as string | undefined
const identifier = query?.email as string | undefined
const token = query?.token
const identifier = query?.email
// If these are missing, the sign-in URL was manually opened without these params or the `sendVerificationRequest` method did not send the link correctly in the email.
if (!token || !identifier) {
return { redirect: `${url}/error?error=configuration`, cookies }
}
const invite = await useVerificationToken?.({
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
const invite = await adapter.useVerificationToken({
identifier,
token: hashToken(token, options),
})
@@ -218,29 +217,23 @@ export default async function callback(params: {
return { redirect: `${url}/error?error=Verification`, cookies }
}
// If it is an existing user, use that, otherwise use a placeholder
const profile = (identifier
? await getUserByEmail(identifier)
: null) ?? {
const profile = await getUserFromEmail({
email: identifier,
}
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter,
})
/** @type {import("src").Account} */
const account = {
providerAccountId: profile.email,
type: "email",
type: "email" as const,
provider: provider.id,
}
// Check if user is allowed to sign in
try {
const signInCallbackResponse = await callbacks.signIn({
// @ts-expect-error
user: profile,
// @ts-expect-error
account,
// @ts-expect-error
email: { email: identifier },
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
@@ -257,12 +250,9 @@ export default async function callback(params: {
}
// Sign user in
// @ts-expect-error
const { user, session, isNewUser } = await callbackHandler({
sessionToken: sessionStore.value,
// @ts-expect-error
profile,
// @ts-expect-error
account,
options,
})
@@ -277,7 +267,6 @@ export default async function callback(params: {
const token = await callbacks.jwt({
token: defaultToken,
user,
// @ts-expect-error
account,
isNewUser,
})
@@ -297,15 +286,14 @@ export default async function callback(params: {
// Save Session Token in cookie
cookies.push({
name: options.cookies.sessionToken.name,
value: session.sessionToken,
value: (session as AdapterSession).sessionToken,
options: {
...options.cookies.sessionToken.options,
expires: session.expires,
expires: (session as AdapterSession).expires,
},
})
}
// @ts-expect-error
await events.signIn?.({ user, account, isNewUser })
// Handle first logins on new accounts

View File

@@ -1,8 +1,9 @@
import getAuthorizationUrl from "../lib/oauth/authorization-url"
import emailSignin from "../lib/email/signin"
import getUserFromEmail from "../lib/email/getUserFromEmail"
import type { RequestInternal, OutgoingResponse } from ".."
import type { InternalOptions } from "../types"
import type { Account, User } from "../.."
import type { Account } from "../.."
/** Handle requests to /api/auth/signin */
export default async function signin(params: {
@@ -11,7 +12,7 @@ export default async function signin(params: {
body: RequestInternal["body"]
}): Promise<OutgoingResponse> {
const { options, query, body } = params
const { url, adapter, callbacks, logger, provider } = options
const { url, callbacks, logger, provider } = options
if (!provider.type) {
return {
@@ -54,14 +55,12 @@ export default async function signin(params: {
return { redirect: `${url}/error?error=EmailSignin` }
}
// 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) ?? {
const user = await getUserFromEmail({
email,
id: email,
}
// @ts-expect-error -- Verified in `assertConfig`. adapter: Adapter<true>
adapter: options.adapter,
withId: true,
})
const account: Account = {
providerAccountId: email,
@@ -72,7 +71,6 @@ export default async function signin(params: {
// Check if user is allowed to sign in
try {
// @ts-expect-error
const signInCallbackResponse = await callbacks.signIn({
user,
account,

View File

@@ -1,11 +1,11 @@
import type { Adapter } from "../adapters"
import type { Adapter, AdapterUser } from "../adapters"
import type {
Provider,
CredentialInput,
ProviderType,
OAuthConfig,
EmailConfig,
CredentialsConfig,
InternalOAuthConfig,
} from "../providers"
import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt"
@@ -231,7 +231,7 @@ export type TokenSet = TokenSetParameters
* Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers.
*/
export interface DefaultAccount extends Partial<TokenSet> {
export interface Account extends Partial<TokenSet> {
/**
* This value depends on the type of the provider being used to create the account.
* - oauth: The OAuth account's id, returned from the `profile()` callback.
@@ -240,30 +240,23 @@ export interface DefaultAccount extends Partial<TokenSet> {
*/
providerAccountId: string
/** id of the user this account belongs to. */
userId: string
userId?: string
/** id of the provider used for this account */
provider: string
/** Provider's type for this account */
type: ProviderType
}
export interface Account extends Record<string, unknown>, DefaultAccount {}
export interface DefaultProfile {
/** The OAuth profile returned from your provider */
export interface Profile {
sub?: string
name?: string
email?: string
image?: string
}
/** The OAuth profile returned from your provider */
export interface Profile extends Record<string, unknown>, DefaultProfile {}
/** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions<
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
export interface CallbacksOptions<P = Profile, A = Account> {
/**
* Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow.
@@ -272,13 +265,13 @@ export interface CallbacksOptions<
* [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/
signIn: (params: {
user: User
account: A
user: User | { email: string }
account: A | null
/**
* If OAuth provider is used, it contains the full
* OAuth profile returned by your provider.
*/
profile: P & Record<string, unknown>
profile?: P
/**
* If Email provider is used, on the first call, it contains a
* `verificationRequest: true` property to indicate it is being triggered in the verification request flow.
@@ -287,7 +280,7 @@ export interface CallbacksOptions<
* to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list.
*/
email: {
email?: {
verificationRequest?: boolean
}
/** If Credentials provider is used, it contains the user credentials */
@@ -341,8 +334,8 @@ export interface CallbacksOptions<
*/
jwt: (params: {
token: JWT
user?: User
account?: A
user?: User | AdapterUser
account?: A | null
profile?: P
isNewUser?: boolean
}) => Awaitable<JWT>
@@ -378,7 +371,7 @@ export interface EventCallbacks {
*/
signIn: (message: {
user: User
account: Account
account: Account | null
profile?: Profile
isNewUser?: boolean
}) => Awaitable<void>
@@ -392,9 +385,9 @@ export interface EventCallbacks {
createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: {
user: User
user: User | AdapterUser | { email: string }
account: Account
profile: User
profile: User | AdapterUser | { email: string }
}) => Awaitable<void>
/**
* The message object will contain one of these depending on
@@ -420,7 +413,7 @@ export interface PagesOptions {
export type ISODateString = string
export interface DefaultSession extends Record<string, unknown> {
export interface DefaultSession {
user?: {
name?: string | null
email?: string | null
@@ -438,7 +431,7 @@ export interface DefaultSession extends Record<string, unknown> {
* [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback)
*/
export interface Session extends Record<string, unknown>, DefaultSession {}
export interface Session extends DefaultSession {}
export type SessionStrategy = "jwt" | "database"
@@ -468,6 +461,13 @@ export interface SessionOptions {
* @default 86400 // 1 day
*/
updateAge: number
/**
* Generate a custom session token for database-based sessions.
* By default, a random UUID or string is generated depending on the Node.js version.
* However, you can specify your own custom string (such as CUID) to be used.
* @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version
*/
generateSessionToken: () => string
}
export interface DefaultUser {
@@ -487,13 +487,13 @@ export interface DefaultUser {
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) |
* [`profile` OAuth provider callback](https://next-auth.js.org/configuration/providers#using-a-custom-provider)
*/
export interface User extends Record<string, unknown>, DefaultUser {}
export interface User extends DefaultUser {}
// Below are types that are only supposed be used by next-auth internally
/** @internal */
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth"
? OAuthConfig<any>
export type InternalProvider<T = ProviderType> = (T extends "oauth"
? InternalOAuthConfig<any>
: T extends "email"
? EmailConfig
: T extends "credentials"
@@ -515,7 +515,10 @@ export type NextAuthAction =
| "_log"
/** @internal */
export interface InternalOptions<T extends ProviderType = any> {
export interface InternalOptions<
TProviderType = ProviderType,
WithVerificationToken = TProviderType extends "email" ? true : false
> {
providers: InternalProvider[]
/**
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
@@ -523,9 +526,7 @@ export interface InternalOptions<T extends ProviderType = any> {
*/
url: InternalUrl
action: NextAuthAction
provider: T extends string
? InternalProvider<T>
: InternalProvider<T> | undefined
provider: InternalProvider<TProviderType>
csrfToken?: string
csrfTokenVerified?: boolean
secret: string
@@ -536,7 +537,9 @@ export interface InternalOptions<T extends ProviderType = any> {
pages: Partial<PagesOptions>
jwt: JWTOptions
events: Partial<EventCallbacks>
adapter?: Adapter
adapter: WithVerificationToken extends true
? Adapter<WithVerificationToken>
: Adapter<WithVerificationToken> | undefined
callbacks: CallbacksOptions
cookies: CookiesOptions
callbackUrl: string

View File

@@ -118,12 +118,14 @@ export async function unstable_getServerSession(
},
})
const { body, cookies } = session
const { body, cookies, status = 200 } = session
cookies?.forEach((cookie) => setCookie(res, cookie))
if (body && typeof body !== "string" && Object.keys(body).length)
return body as Session
if (body && typeof body !== "string" && Object.keys(body).length) {
if (status === 200) return body as Session
throw new Error((body as any).message)
}
return null
}

View File

@@ -80,7 +80,7 @@ export interface NextAuthMiddlewareOptions {
* ```
*
* ---
* [Documentation](https://next-auth.js.org/getting-started/nextjs/middleware#api) | [`signIn` callback](configuration/callbacks#sign-in-callback)
* [Documentation](https://next-auth.js.org/configuration/nextjs#middleware) | [`signIn` callback](configuration/callbacks#sign-in-callback)
*/
authorized?: AuthorizedCallback
}
@@ -101,17 +101,17 @@ async function handleMiddleware(
options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult>
) {
const { pathname, search, origin } = req.nextUrl
const { pathname, search, origin, basePath } = req.nextUrl
const signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error"
const basePath = parseUrl(process.env.NEXTAUTH_URL).path
const authPath = parseUrl(process.env.NEXTAUTH_URL).path
const publicPaths = ["/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response
// on paths that never require authentication
if (
pathname.startsWith(basePath) ||
`${basePath}${pathname}`.startsWith(authPath) ||
[signInPage, errorPage].includes(pathname) ||
publicPaths.some((p) => pathname.startsWith(p))
) {
@@ -125,7 +125,7 @@ async function handleMiddleware(
`\nhttps://next-auth.js.org/errors#no_secret`
)
const errorUrl = new URL(errorPage, origin)
const errorUrl = new URL(`${basePath}${errorPage}`, origin)
errorUrl.searchParams.append("error", "Configuration")
return NextResponse.redirect(errorUrl)
@@ -145,8 +145,8 @@ async function handleMiddleware(
if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(signInPage, origin)
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`)
const signInUrl = new URL(`${basePath}${signInPage}`, origin)
signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
return NextResponse.redirect(signInUrl)
}

View File

@@ -1,28 +1,25 @@
import type { OAuthConfig, OAuthUserConfig } from "."
interface HubSpotProfile extends Record<string, any> {
// TODO: figure out additional fields, for now using
// TODO: figure out additional fields, for now using
// https://legacydocs.hubspot.com/docs/methods/oauth2/get-access-token-information
user: string,
user_id: string,
user: string
user_id: string
hub_domain: string,
hub_id: string,
hub_domain: string
hub_id: string
}
const HubSpotConfig = {
authorizationUrl: "https://app.hubspot.com/oauth/authorize",
tokenUrl: "https://api.hubapi.com/oauth/v1/token",
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens"
profileUrl: "https://api.hubapi.com/oauth/v1/access-tokens",
}
export default function HubSpot<P extends HubSpotProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
return {
id: "hubspot",
name: "HubSpot",
@@ -36,7 +33,6 @@ export default function HubSpot<P extends HubSpotProfile>(
scope: "oauth",
client_id: options.clientId,
},
},
client: {
token_endpoint_auth_method: "client_secret_post",
@@ -45,33 +41,27 @@ export default function HubSpot<P extends HubSpotProfile>(
userinfo: {
url: HubSpotConfig.profileUrl,
async request(context) {
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`;
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`
const response = await fetch(url, {
headers: {
"Content-Type": "application/json",
},
method: "GET",
});
})
const userInfo = await response.json();
return { userInfo }
}
return await response.json()
},
},
profile(profile) {
const { userInfo } = profile
return {
id: userInfo.user_id,
name: userInfo.user,
email: userInfo.user,
id: profile.user_id,
name: profile.user,
email: profile.user,
// TODO: get image from profile once it's available
// TODO: get image from profile once it's available
// Details available https://community.hubspot.com/t5/APIs-Integrations/Profile-photo-is-not-retrieved-with-User-API/m-p/325521
image: null
image: null,
}
},
options,

View File

@@ -110,7 +110,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
userinfo?: string | UserinfoEndpointHandler
type: "oauth"
version?: string
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }>
profile: (profile: P, tokens: TokenSet) => Awaitable<User>
checks?: ChecksType | ChecksType[]
client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] }
@@ -147,6 +147,14 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
encoding?: string
}
/** @internal */
export interface InternalOAuthConfig<P>
extends Omit<OAuthConfig<P>, "authorization" | "token" | "userinfo"> {
authorization?: AuthorizationEndpointHandler
token?: TokenEndpointHandler
userinfo?: UserinfoEndpointHandler
}
export type OAuthUserConfig<P> = Omit<
Partial<OAuthConfig<P>>,
"options" | "type"

View File

@@ -0,0 +1,51 @@
import type { OAuthConfig, OAuthUserConfig } from "."
export interface ZitadelProfile extends Record<string, any> {
amr: string // Authentication Method References as defined in RFC8176
aud: string // The audience of the token, by default all client id's and the project id are included
auth_time: number // Unix time of the authentication
azp: string // Client id of the client who requested the token
email: string // Email Address of the subject
email_verified: boolean // if the email was verified by ZITADEL
exp: number // Time the token expires (as unix time)
family_name: string // The subjects family name
given_name: string // Given name of the subject
gender: string // Gender of the subject
iat: number // Time of the token was issued at (as unix time)
iss: string // Issuing domain of a token
jti: string // Unique id of the token
locale: string // Language from the subject
name: string // The subjects full name
nbf: number // Time the token must not be used before (as unix time)
picture: string // The subjects profile picture
phone: string // Phone number provided by the user
phone_verified: boolean // if the phonenumber was verified by ZITADEL
preferred_username: string // ZITADEL's login name of the user. Consist of username@primarydomain
sub: string // Subject ID of the user
}
export default function Zitadel<P extends ZitadelProfile>(
options: OAuthUserConfig<P>
): OAuthConfig<P> {
const { issuer } = options
return {
id: "zitadel",
name: "ZITADEL",
type: "oauth",
version: "2",
wellKnown: `${issuer}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
checks: ["pkce", "state"],
async profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
}
},
options,
}
}

View File

@@ -74,7 +74,7 @@ export type SessionContextValue<R extends boolean = false> = R extends true
| { data: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" }
const SessionContext = React.createContext<SessionContextValue | undefined>(
export const SessionContext = React.createContext<SessionContextValue | undefined>(
undefined
)

View File

@@ -1,5 +1,11 @@
import { InvalidCallbackUrl, MissingSecret } from "../src/core/errors"
import {
InvalidCallbackUrl,
MissingAdapter,
MissingAdapterMethods,
MissingSecret,
} from "../src/core/errors"
import { handler } from "./lib"
import EmailProvider from "../src/providers/email"
it("Show error page if secret is not defined", async () => {
const { res, log } = await handler(
@@ -14,6 +20,48 @@ it("Show error page if secret is not defined", async () => {
expect(log.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Show error page if adapter is missing functions when using with email", async () => {
const sendVerificationRequest = jest.fn()
const missingFunctionAdapter: any = {}
const { res, log } = await handler(
{
adapter: missingFunctionAdapter,
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"MISSING_ADAPTER_METHODS_ERROR",
expect.any(MissingAdapterMethods)
)
})
it("Show error page if adapter is not configured when using with email", async () => {
const sendVerificationRequest = jest.fn()
const { res, log } = await handler(
{
providers: [EmailProvider({ sendVerificationRequest })],
secret: "secret",
},
{ prod: true }
)
expect(res.status).toBe(500)
expect(res.html).toMatch(/there is a problem with the server configuration./i)
expect(res.html).toMatch(/check the server logs for more information./i)
expect(log.error).toBeCalledWith(
"EMAIL_REQUIRES_ADAPTER_ERROR",
expect.any(MissingAdapter)
)
})
it("Should show configuration error page on invalid `callbackUrl`", async () => {
const { res, log } = await handler(
{ providers: [] },

View File

@@ -156,6 +156,7 @@ it("Redirect to error page if multiple addresses aren't allowed", async () => {
expect(signIn).toBeCalledTimes(0)
expect(sendVerificationRequest).toBeCalledTimes(0)
// @ts-expect-error
expect(log.error.mock.calls[0]).toEqual([
"SIGNIN_EMAIL_ERROR",
{ error, providerId: "email" },

View File

@@ -47,17 +47,19 @@ describe("Treat secret correctly", () => {
})
it("Error if missing NEXTAUTH_SECRET and secret", async () => {
const session = await unstable_getServerSession(req, res, {
providers: [],
logger,
})
const configError = new Error(
"There is a problem with the server configuration. Check the server logs for more information."
)
await expect(
unstable_getServerSession(req, res, { providers: [], logger })
).rejects.toThrowError(configError)
expect(session).toEqual(null)
expect(logger.error).toBeCalledTimes(1)
expect(logger.error).toBeCalledWith("NO_SECRET", expect.any(MissingSecret))
})
it("Only logs warning once and in development", async () => {
process.env.NEXTAUTH_SECRET = "secret"
// Expect console.warn to NOT be called due to NODE_ENV=production
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(0)
@@ -71,6 +73,7 @@ describe("Treat secret correctly", () => {
// Expect console.warn to be still only be called ONCE
await unstable_getServerSession(req, res, { providers: [], logger })
expect(console.warn).toBeCalledTimes(1)
delete process.env.NEXTAUTH_SECRET
})
})

View File

@@ -59,10 +59,10 @@ export function createCSRF() {
}
export function mockAdapter(): Adapter {
// @ts-expect-error
const adapter: Adapter = {
createVerificationToken: jest.fn(() => {}),
useVerificationToken: jest.fn(() => {}),
getUserByEmail: jest.fn(() => {}),
}
return adapter;
return adapter
}

View File

@@ -1,40 +1,95 @@
import { NextMiddleware } from "next/server"
import { NextAuthMiddlewareOptions, withAuth } from "../next/middleware"
import { NextAuthMiddlewareOptions, withAuth } from "../src/next/middleware"
it("should not match pages as public paths", async () => {
const options: NextAuthMiddlewareOptions = {
pages: {
signIn: "/",
error: "/"
error: "/",
},
secret: "secret"
secret: "secret",
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1"
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null)
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res.status).toBe(307)
expect(res?.status).toBe(307)
})
it("should not redirect on public paths", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret"
secret: "secret",
}
const nextUrl: any = {
pathname: "/_next/foo",
search: "",
origin: "http://127.0.0.1"
origin: "http://127.0.0.1",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null)
const res = await handleMiddleware(req, null as any)
expect(res).toBeUndefined()
})
it("should redirect according to nextUrl basePath", async () => {
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const nextUrl: any = {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path",
}
const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null as any)
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get('location')).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
})
it("should redirect according to nextUrl basePath", async () => {
// given
const options: NextAuthMiddlewareOptions = {
secret: "secret"
}
const handleMiddleware = withAuth(options) as NextMiddleware
// when
const res = await handleMiddleware({
nextUrl: {
pathname: "/protected/pathA",
search: "",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then
expect(res).toBeDefined()
expect(res.status).toEqual(307)
expect(res.headers.get("location")).toContain("http://127.0.0.1/custom-base-path/api/auth/signin?callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA")
// and when follow redirect
const resFromRedirectedUrl = await handleMiddleware({
nextUrl: {
pathname: "/api/auth/signin",
search: "callbackUrl=%2Fcustom-base-path%2Fprotected%2FpathA",
origin: "http://127.0.0.1",
basePath: "/custom-base-path"
}, headers: { authorization: "" }
} as any, null as any)
// then return sign in page
expect(resFromRedirectedUrl).toBeUndefined()
})

View File

@@ -1,5 +1,4 @@
{
"private": true,
"name": "@next-auth/tsconfig",
"private": true,
"version": "0.0.0",

38
pnpm-lock.yaml generated
View File

@@ -5,7 +5,7 @@ importers:
.:
specifiers:
'@actions/core': ^1.6.0
'@balazsorban/monorepo-release': 0.0.4
'@balazsorban/monorepo-release': 0.0.5
'@types/jest': ^28.1.3
'@types/node': ^17.0.25
'@typescript-eslint/eslint-plugin': ^5.10.2
@@ -27,7 +27,7 @@ importers:
typescript: 4.7.4
devDependencies:
'@actions/core': 1.9.0
'@balazsorban/monorepo-release': 0.0.4
'@balazsorban/monorepo-release': 0.0.5
'@types/jest': 28.1.3
'@types/node': 17.0.45
'@typescript-eslint/eslint-plugin': 5.29.0_3ekaj7j3owlolnuhj3ykrb7u7i
@@ -433,7 +433,7 @@ importers:
jest: ^28.1.1
jest-environment-jsdom: ^28.1.1
jest-watch-typeahead: ^1.1.0
jose: ^4.3.7
jose: ^4.9.3
msw: ^0.42.3
next: 12.2.5
oauth: ^0.9.15
@@ -451,7 +451,7 @@ importers:
'@babel/runtime': 7.18.3
'@panva/hkdf': 1.0.2
cookie: 0.5.0
jose: 4.8.1
jose: 4.9.3
oauth: 0.9.15
openid-client: 5.1.6
preact: 10.8.2
@@ -3638,8 +3638,8 @@ packages:
'@babel/helper-validator-identifier': 7.16.7
to-fast-properties: 2.0.0
/@balazsorban/monorepo-release/0.0.4:
resolution: {integrity: sha512-jjYc05vcRueT+nC7BD7C0D2JjE+H8xDdAIfwjtlbMHTnTwPx2KYXrbWohbL7bGVN8ZbhJDmXkXOQjppSrZCQBw==}
/@balazsorban/monorepo-release/0.0.5:
resolution: {integrity: sha512-IeLswLrG7a+us5cQVxb1w8hbfgYYLIoIuodU6yDTo4Ln0qzS6AZGnwiL9ykAxewirFYCEjBGa0tqOymOpEvLtA==}
engines: {node: '>=16.16.0'}
hasBin: true
dependencies:
@@ -7919,10 +7919,8 @@ packages:
clean-stack: 2.2.0
indent-string: 4.0.0
/ajv-formats/2.1.1_ajv@8.11.0:
/ajv-formats/2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
@@ -9531,8 +9529,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
dependencies:
JSONStream: 1.3.5
is-text-path: 1.0.1
JSONStream: 1.3.5
lodash: 4.17.21
meow: 8.1.2
split2: 3.2.2
@@ -11709,7 +11707,7 @@ packages:
dependencies:
'@apidevtools/json-schema-ref-parser': 9.0.9
ajv: 8.11.0
ajv-formats: 2.1.1_ajv@8.11.0
ajv-formats: 2.1.1
body-parser: 1.20.0
content-type: 1.0.4
deep-freeze: 0.0.1
@@ -12618,7 +12616,7 @@ packages:
dev: true
/git-log-parser/1.2.0:
resolution: {integrity: sha1-LmpMGxP8AAKCB7p5WnrDFme5/Uo=}
resolution: {integrity: sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==}
dependencies:
argv-formatter: 1.0.0
spawn-error-forwarder: 1.0.0
@@ -15426,8 +15424,8 @@ packages:
valid-url: 1.0.9
dev: true
/jose/4.8.1:
resolution: {integrity: sha512-+/hpTbRcCw9YC0TOfN1W47pej4a9lRmltdOVdRLz5FP5UvUq3CenhXjQK7u/8NdMIIShMXYAh9VLPhc7TjhvFw==}
/jose/4.9.3:
resolution: {integrity: sha512-f8E/z+T3Q0kA9txzH2DKvH/ds2uggcw0m3vVPSB9HrSkrQ7mojjifvS7aR8cw+lQl2Fcmx9npwaHpM/M3GD8UQ==}
dev: false
/js-beautify/1.14.4:
@@ -17339,7 +17337,7 @@ packages:
resolution: {integrity: sha512-HTFaXWdUHvLFw4GaEMgC0jXYBgpjgzQQNHW1pZsSqJorSgrXzxJ+4u/LWCGaClDEse5HLjXRV+zU5Bn3OefiZw==}
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0}
dependencies:
jose: 4.8.1
jose: 4.9.3
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.0.1
@@ -18833,12 +18831,6 @@ packages:
/react-dev-utils/12.0.1_webpack@5.73.0:
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=2.7'
webpack: '>=4'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@babel/code-frame': 7.16.7
address: 1.2.0
@@ -18868,7 +18860,9 @@ packages:
transitivePeerDependencies:
- eslint
- supports-color
- typescript
- vue-template-compiler
- webpack
dev: false
/react-dom/18.2.0_react@18.2.0:
@@ -19570,7 +19564,7 @@ packages:
dependencies:
'@types/json-schema': 7.0.11
ajv: 8.11.0
ajv-formats: 2.1.1_ajv@8.11.0
ajv-formats: 2.1.1
ajv-keywords: 5.1.0_ajv@8.11.0
dev: false