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

View File

@@ -5,6 +5,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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! 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 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 - type: markdown
attributes: attributes:
value: | 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! 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 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 - type: markdown
attributes: attributes:
value: | 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! 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 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 - type: markdown
attributes: attributes:
value: | 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. 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._ _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 - type: markdown
attributes: attributes:
value: | 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: Make sure you [link]() to external documentation if necessary and provide inline code examples like so:
```js ```js

View File

@@ -9,6 +9,7 @@ body:
- type: markdown - type: markdown
attributes: attributes:
value: | 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: We are glad that you have a question about this library. Please provide the following information:
- type: textarea - 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! 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 ## ☕️ 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 ## 🧢 Checklist
@@ -23,6 +28,7 @@ Fixes: INSERT_ISSUE_LINK_HERE
## 📌 Resources ## 📌 Resources
- [Contributing guidelines](./CONTRIBUTING.md) - [Security guidelines](../Security.md)
- [Code of conduct](./CODE_OF_CONDUCT.md) - [Contributing guidelines](../CONTRIBUTING.md)
- [Code of conduct](../CODE_OF_CONDUCT.md)
- [Contributing to Open Source](https://kcd.im/pull-request) - [Contributing to Open Source](https://kcd.im/pull-request)

View File

@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement ## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be 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 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 that is deemed necessary and appropriate to the circumstances. The project team
is obligated to maintain confidentiality with regard to the reporter of an 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. - 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. - 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. > 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": { "scripts": {
"clean": "rm -rf .next", "clean": "rm -rf .next",
"dev": "next dev", "dev": "next dev",
"lint": "next lint",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"email": "fake-smtp-server", "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 GitHub from "next-auth/providers/github"
import Gitlab from "next-auth/providers/gitlab" import Gitlab from "next-auth/providers/gitlab"
import Google from "next-auth/providers/google" import Google from "next-auth/providers/google"
import Hubspot from "next-auth/providers/hubspot"
import IDS4 from "next-auth/providers/identity-server4" import IDS4 from "next-auth/providers/identity-server4"
import Instagram from "next-auth/providers/instagram" import Instagram from "next-auth/providers/instagram"
import Keycloak from "next-auth/providers/keycloak" 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 Vk from "next-auth/providers/vk"
import Wikimedia from "next-auth/providers/wikimedia" import Wikimedia from "next-auth/providers/wikimedia"
import WorkOS from "next-auth/providers/workos" import WorkOS from "next-auth/providers/workos"
import Zitadel from "next-auth/providers/zitadel"
// Adapters // Adapters
import { PrismaClient } from "@prisma/client" import { PrismaClient } from "@prisma/client"
@@ -102,6 +104,7 @@ export const authOptions: NextAuthOptions = {
GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }), GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }), Gitlab({ clientId: process.env.GITLAB_ID, clientSecret: process.env.GITLAB_SECRET }),
Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_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 }), 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 }), 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 }), 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 }), Vk({ clientId: process.env.VK_ID, clientSecret: process.env.VK_SECRET }),
Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }), Wikimedia({ clientId: process.env.WIKIMEDIA_ID, clientSecret: process.env.WIKIMEDIA_SECRET }),
WorkOS({ clientId: process.env.WORKOS_ID, clientSecret: process.env.WORKOS_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 "./styles.css"
import type { AppProps } from "next/app" import type { AppProps } from "next/app"
import type { Session } from "next-auth"
// Use of the <SessionProvider> is mandatory to allow components that call // Use of the <SessionProvider> is mandatory to allow components that call
// `useSession()` anywhere in your application to access the `session` object. // `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 ( return (
<SessionProvider session={pageProps.session} refetchInterval={0}> <SessionProvider session={session}>
<Component {...pageProps} /> <Component {...pageProps} />
</SessionProvider> </SessionProvider>
) )

View File

@@ -4,8 +4,7 @@ import Layout from "../components/layout"
import AccessDenied from "../components/access-denied" import AccessDenied from "../components/access-denied"
export default function ProtectedPage() { export default function ProtectedPage() {
const { data: session, status } = useSession() const { data: session } = useSession()
const loading = status === "loading"
const [content, setContent] = useState() const [content, setContent] = useState()
// Fetch content from protected route // Fetch content from protected route
@@ -20,8 +19,6 @@ export default function ProtectedPage() {
fetchData() fetchData()
}, [session]) }, [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 no session exists, display access denied message
if (!session) { if (!session) {

View File

@@ -12,15 +12,28 @@ npm install next-auth @prisma/client @next-auth/prisma-adapter
npm install prisma --save-dev 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: Configure your NextAuth.js to use the Prisma Adapter:
```javascript title="pages/api/auth/[...nextauth].js" ```javascript title="pages/api/auth/[...nextauth].js"
import NextAuth from "next-auth" import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google" import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter" import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { PrismaClient } from "@prisma/client" import prisma from "../../../lib/prismadb"
const prisma = new PrismaClient()
export default NextAuth({ export default NextAuth({
adapter: PrismaAdapter(prisma), 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. - 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 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" ```js title="pages/api/auth/[...nextauth].js"
... ...
callbacks: { callbacks: {
async jwt({ token, account }) { async jwt({ token, account, profile }) {
// Persist the OAuth access_token to the token right after signin // Persist the OAuth access_token and or the user id to the token right after signin
if (account) { if (account) {
token.accessToken = account.access_token token.accessToken = account.access_token
token.id = profile.id
} }
return token return token
} }
@@ -134,7 +135,7 @@ Use an if branch to check for the existence of parameters (apart from `token`).
## Session callback ## 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` e.g. `getSession()`, `useSession()`, `/api/auth/session`
@@ -145,8 +146,10 @@ e.g. `getSession()`, `useSession()`, `/api/auth/session`
... ...
callbacks: { callbacks: {
async session({ session, token, user }) { 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.accessToken = token.accessToken
session.user.id = token.id
return session return session
} }
} }
@@ -155,7 +158,7 @@ callbacks: {
:::tip :::tip
When using JSON Web Tokens the `jwt()` callback is invoked before the `session()` callback, so anything you add to the 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 :::warning

View File

@@ -114,6 +114,12 @@ session: {
// Use it to limit write operations. Set to 0 to always update the database. // Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens // Note: This option is ignored if using JSON Web Tokens
updateAge: 24 * 60 * 60, // 24 hours 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 id: string
version: string version: string
profile(profile: P, tokens: TokenSet): Awaitable<User & { id: string }> profile(profile: P, tokens: TokenSet): Awaitable<User>
checks?: ChecksType | ChecksType[] checks?: ChecksType | ChecksType[]
clientId: string clientId: string
clientSecret: string clientSecret: string

View File

@@ -136,7 +136,7 @@ The `callbackUrl` provided was either invalid or not defined. See [specifying a
#### JWT_SESSION_ERROR #### 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 The algorithm used for generating your key isn't listed as supported. You can generate a HS512 key using

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. - 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. - 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 :::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. 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 ## 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> #### [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. - 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) ### 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" ```js title="/middleware.js"
export { default } from "next-auth/middleware" 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). 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 ### 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. 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": { "devDependencies": {
"@actions/core": "^1.6.0", "@actions/core": "^1.6.0",
"@balazsorban/monorepo-release": "0.0.4", "@balazsorban/monorepo-release": "0.0.5",
"@types/jest": "^28.1.3", "@types/jest": "^28.1.3",
"@types/node": "^17.0.25", "@types/node": "^17.0.25",
"@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/eslint-plugin": "^5.10.2",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@next-auth/dynamodb-adapter", "name": "@next-auth/dynamodb-adapter",
"repository": "https://github.com/nextauthjs/next-auth", "repository": "https://github.com/nextauthjs/next-auth",
"version": "1.0.4", "version": "1.0.5",
"description": "AWS DynamoDB adapter for next-auth.", "description": "AWS DynamoDB adapter for next-auth.",
"keywords": [ "keywords": [
"next-auth", "next-auth",

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-auth/firebase-adapter", "name": "@next-auth/firebase-adapter",
"version": "1.0.1", "version": "1.0.2",
"description": "Firebase adapter for next-auth.", "description": "Firebase adapter for next-auth.",
"homepage": "https://next-auth.js.org", "homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth", "repository": "https://github.com/nextauthjs/next-auth",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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) { async createSession(data) {
const { userId, ...s } = format.to(data) const { userId, ...s } = format.to(data)
await write( await write(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-auth/typeorm-legacy-adapter", "name": "@next-auth/typeorm-legacy-adapter",
"version": "2.0.0", "version": "2.0.1",
"description": "TypeORM (legacy) adapter for next-auth.", "description": "TypeORM (legacy) adapter for next-auth.",
"homepage": "https://next-auth.js.org", "homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth", "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 { DataSourceOptions, DataSource, EntityManager } from "typeorm"
import type { Account } from "next-auth"
import * as defaultEntities from "./entities" import * as defaultEntities from "./entities"
import { parseDataSourceConfig, updateConnectionEntities } from "./utils" import { parseDataSourceConfig, updateConnectionEntities } from "./utils"
@@ -87,7 +91,7 @@ export function TypeORMLegacyAdapter(
}, },
async getUserByAccount(provider_providerAccountId) { async getUserByAccount(provider_providerAccountId) {
const m = await getManager(c) const m = await getManager(c)
const account = await m.findOne<Account & { user: AdapterUser }>( const account = await m.findOne<AdapterAccount & { user: AdapterUser }>(
"AccountEntity", "AccountEntity",
{ where: provider_providerAccountId, relations: ["user"] } { where: provider_providerAccountId, relations: ["user"] }
) )
@@ -115,9 +119,8 @@ export function TypeORMLegacyAdapter(
}, },
async unlinkAccount(providerAccountId) { async unlinkAccount(providerAccountId) {
const m = await getManager(c) const m = await getManager(c)
await m.delete<Account>("AccountEntity", providerAccountId) await m.delete<AdapterAccount>("AccountEntity", providerAccountId)
}, },
// @ts-expect-error
async createSession(data) { async createSession(data) {
const m = await getManager(c) const m = await getManager(c)
const session = await m.save("SessionEntity", data) const session = await m.save("SessionEntity", data)

View File

@@ -1,6 +1,6 @@
{ {
"name": "@next-auth/upstash-redis-adapter", "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.", "description": "Upstash adapter for next-auth. It uses Upstash's connectionless (HTTP based) Redis client.",
"homepage": "https://next-auth.js.org", "homepage": "https://next-auth.js.org",
"repository": "https://github.com/nextauthjs/next-auth", "repository": "https://github.com/nextauthjs/next-auth",

View File

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

View File

@@ -11,6 +11,14 @@ if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_KEY) {
process.exit(0) 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({ const client = new Redis({
url: process.env.UPSTASH_REDIS_URL, url: process.env.UPSTASH_REDIS_URL,
token: process.env.UPSTASH_REDIS_KEY, token: process.env.UPSTASH_REDIS_KEY,

View File

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

View File

@@ -2,11 +2,15 @@ import { Account, User, Awaitable } from "."
export interface AdapterUser extends User { export interface AdapterUser extends User {
id: string id: string
email: string
emailVerified: Date | null emailVerified: Date | null
} }
export interface AdapterAccount extends Account {
userId: string
}
export interface AdapterSession { export interface AdapterSession {
id: string
/** A randomly generated value that is used to get hold of the session. */ /** A randomly generated value that is used to get hold of the session. */
sessionToken: string sessionToken: string
/** Used to connect the session to a particular user */ /** 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) | * [Adapters Overview](https://next-auth.js.org/adapters/overview) |
* [Create a custom adapter](https://next-auth.js.org/tutorials/creating-a-database-adapter) * [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> createUser: (user: Omit<AdapterUser, "id">) => Awaitable<AdapterUser>
getUser: (id: string) => Awaitable<AdapterUser | null> getUser: (id: string) => Awaitable<AdapterUser | null>
getUserByEmail: (email: 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. */ /** Using the provider id and the id of the user for a specific account, get the user. */
getUserByAccount: ( getUserByAccount: (
providerAccountId: Pick<Account, "provider" | "providerAccountId"> providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Awaitable<AdapterUser | null> ) => Awaitable<AdapterUser | null>
updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser> updateUser: (user: Partial<AdapterUser>) => Awaitable<AdapterUser>
/** @todo Implement */ /** @todo Implement */
@@ -69,12 +90,12 @@ export interface Adapter {
userId: string userId: string
) => Promise<void> | Awaitable<AdapterUser | null | undefined> ) => Promise<void> | Awaitable<AdapterUser | null | undefined>
linkAccount: ( linkAccount: (
account: Account account: AdapterAccount
) => Promise<void> | Awaitable<Account | null | undefined> ) => Promise<void> | Awaitable<AdapterAccount | null | undefined>
/** @todo Implement */ /** @todo Implement */
unlinkAccount?: ( unlinkAccount?: (
providerAccountId: Pick<Account, "provider" | "providerAccountId"> providerAccountId: Pick<AdapterAccount, "provider" | "providerAccountId">
) => Promise<void> | Awaitable<Account | undefined> ) => Promise<void> | Awaitable<AdapterAccount | undefined>
/** Creates a session for the user and returns it. */ /** Creates a session for the user and returns it. */
createSession: (session: { createSession: (session: {
sessionToken: string sessionToken: string

View File

@@ -94,10 +94,18 @@ export function BroadcastChannel(name = "nextauth.message") {
/** Notify other tabs/windows. */ /** Notify other tabs/windows. */
post(message: Record<string, unknown>) { post(message: Record<string, unknown>) {
if (typeof window === "undefined") return if (typeof window === "undefined") return
try {
localStorage.setItem( localStorage.setItem(
name, name,
JSON.stringify({ ...message, timestamp: now() }) 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 { EventCallbacks, LoggerInstance } from ".."
import type { Adapter } from "../adapters"
/** /**
* Same as the default `Error`, but it is JSON serializable. * Same as the default `Error`, but it is JSON serializable.
@@ -58,6 +57,11 @@ export class MissingAdapter extends UnknownError {
code = "EMAIL_REQUIRES_ADAPTER_ERROR" code = "EMAIL_REQUIRES_ADAPTER_ERROR"
} }
export class MissingAdapterMethods extends UnknownError {
name = "MissingAdapterMethodsError"
code = "MISSING_ADAPTER_METHODS_ERROR"
}
export class UnsupportedStrategy extends UnknownError { export class UnsupportedStrategy extends UnknownError {
name = "UnsupportedStrategyError" name = "UnsupportedStrategyError"
code = "CALLBACK_CREDENTIALS_JWT_ERROR" code = "CALLBACK_CREDENTIALS_JWT_ERROR"
@@ -99,10 +103,10 @@ export function eventsErrorHandler(
} }
/** Handles adapter induced errors. */ /** Handles adapter induced errors. */
export function adapterErrorHandler( export function adapterErrorHandler<TAdapter>(
adapter: Adapter | undefined, adapter: TAdapter | undefined,
logger: LoggerInstance logger: LoggerInstance
): Adapter | undefined { ): TAdapter | undefined {
if (!adapter) return if (!adapter) return
return Object.keys(adapter).reduce<any>((acc, name) => { return Object.keys(adapter).reduce<any>((acc, name) => {

View File

@@ -94,13 +94,21 @@ export async function NextAuthHandler<
assertionResult.forEach(logger.warn) assertionResult.forEach(logger.warn)
} else if (assertionResult instanceof Error) { } else if (assertionResult instanceof Error) {
// Bail out early if there's an error in the user config // Bail out early if there's an error in the user config
const { pages, theme } = userOptions
logger.error(assertionResult.code, assertionResult) 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 = const authOnErrorPage =
pages?.error && pages?.error && req.query?.callbackUrl?.startsWith(pages.error)
req.action === "signin" &&
req.query?.callbackUrl.startsWith(pages.error)
if (!pages?.error || authOnErrorPage) { if (!pages?.error || authOnErrorPage) {
if (authOnErrorPage) { if (authOnErrorPage) {

View File

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

View File

@@ -5,6 +5,7 @@ import {
MissingSecret, MissingSecret,
UnsupportedStrategy, UnsupportedStrategy,
InvalidCallbackUrl, InvalidCallbackUrl,
MissingAdapterMethods,
} from "../errors" } from "../errors"
import parseUrl from "../../utils/parse-url" import parseUrl from "../../utils/parse-url"
import { defaultCookies } from "./cookie" import { defaultCookies } from "./cookie"
@@ -120,10 +121,25 @@ export function assertConfig(params: {
} }
} }
if (hasEmail && !options.adapter) { if (hasEmail) {
const { adapter } = options
if (!adapter) {
return new MissingAdapter("E-mail login requires an 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) { if (!warned) {
if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA") if (hasTwitterOAuth2) warnings.push("TWITTER_OAUTH_2_BETA")
warned = true warned = true

View File

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

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, theme,
}), }),
// Save in database // Save in database
// @ts-expect-error // verified in `assertConfig`
adapter.createVerificationToken({ adapter.createVerificationToken({
identifier, identifier,
token: hashToken(token, options), token: hashToken(token, options),

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,11 @@
import { merge } from "../../utils/merge" import { merge } from "../../utils/merge"
import type { InternalProvider } from "../types" import type { InternalProvider } from "../types"
import type { Provider } from "../../providers" import type {
InternalOAuthConfig,
OAuthConfig,
Provider,
} from "../../providers"
import type { InternalUrl } from "../../utils/parse-url" import type { InternalUrl } from "../../utils/parse-url"
/** /**
@@ -18,28 +22,46 @@ export default function parseProviders(params: {
} { } {
const { url, providerId } = params const { url, providerId } = params
const providers = params.providers.map(({ options, ...rest }) => { const providers = params.providers.map<InternalProvider>(
const defaultOptions = normalizeProvider(rest as Provider) ({ options: userOptions, ...rest }) => {
const userOptions = normalizeProvider(options as Provider) if (rest.type === "oauth") {
const normalizedOptions = normalizeOAuthOptions(rest)
return merge(defaultOptions, { 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, ...userOptions,
signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`, signinUrl: `${url}/signin/${userOptions?.id ?? rest.id}`,
callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`, callbackUrl: `${url}/callback/${userOptions?.id ?? rest.id}`,
}) })
}) }
)
const provider = providers.find(({ id }) => id === providerId) return {
providers,
return { providers, provider } 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( const normalized = Object.entries(oauthOptions).reduce<
provider InternalOAuthConfig<Record<string, unknown>>
).reduce<InternalProvider>((acc, [key, value]) => { >(
(acc, [key, value]) => {
if ( if (
["authorization", "token", "userinfo"].includes(key) && ["authorization", "token", "userinfo"].includes(key) &&
typeof value === "string" typeof value === "string"
@@ -54,16 +76,18 @@ function normalizeProvider(provider?: Provider) {
} }
return acc return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter, @typescript-eslint/consistent-type-assertions },
}, {} as any) // 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 // 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` // 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 = Boolean(
normalized.idToken ?? normalized.idToken ??
normalized.wellKnown?.includes("openid-configuration") ?? normalized.wellKnown?.includes("openid-configuration") ??
// @ts-expect-error
normalized.authorization?.params?.scope?.includes("openid") normalized.authorization?.params?.scope?.includes("openid")
) )

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
import type { Adapter } from "../adapters" import type { Adapter, AdapterUser } from "../adapters"
import type { import type {
Provider, Provider,
CredentialInput, CredentialInput,
ProviderType, ProviderType,
OAuthConfig,
EmailConfig, EmailConfig,
CredentialsConfig, CredentialsConfig,
InternalOAuthConfig,
} from "../providers" } from "../providers"
import type { TokenSetParameters } from "openid-client" import type { TokenSetParameters } from "openid-client"
import type { JWT, JWTOptions } from "../jwt" import type { JWT, JWTOptions } from "../jwt"
@@ -231,7 +231,7 @@ export type TokenSet = TokenSetParameters
* Usually contains information about the provider being used * Usually contains information about the provider being used
* and also extends `TokenSet`, which is different tokens returned by OAuth Providers. * 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. * 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. * - oauth: The OAuth account's id, returned from the `profile()` callback.
@@ -240,30 +240,23 @@ export interface DefaultAccount extends Partial<TokenSet> {
*/ */
providerAccountId: string providerAccountId: string
/** id of the user this account belongs to. */ /** id of the user this account belongs to. */
userId: string userId?: string
/** id of the provider used for this account */ /** id of the provider used for this account */
provider: string provider: string
/** Provider's type for this account */ /** Provider's type for this account */
type: ProviderType type: ProviderType
} }
export interface Account extends Record<string, unknown>, DefaultAccount {} /** The OAuth profile returned from your provider */
export interface Profile {
export interface DefaultProfile {
sub?: string sub?: string
name?: string name?: string
email?: string email?: string
image?: 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) */ /** [Documentation](https://next-auth.js.org/configuration/callbacks) */
export interface CallbacksOptions< export interface CallbacksOptions<P = Profile, A = Account> {
P extends Record<string, unknown> = Profile,
A extends Record<string, unknown> = Account
> {
/** /**
* Use this callback to control if a user is allowed to sign in. * Use this callback to control if a user is allowed to sign in.
* Returning true will continue the sign-in flow. * 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) * [Documentation](https://next-auth.js.org/configuration/callbacks#sign-in-callback)
*/ */
signIn: (params: { signIn: (params: {
user: User user: User | { email: string }
account: A account: A | null
/** /**
* If OAuth provider is used, it contains the full * If OAuth provider is used, it contains the full
* OAuth profile returned by your provider. * 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 * 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. * `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 * to avoid sending emails to addresses or domains on a blocklist or to only explicitly generate them
* for email address in an allow list. * for email address in an allow list.
*/ */
email: { email?: {
verificationRequest?: boolean verificationRequest?: boolean
} }
/** If Credentials provider is used, it contains the user credentials */ /** If Credentials provider is used, it contains the user credentials */
@@ -341,8 +334,8 @@ export interface CallbacksOptions<
*/ */
jwt: (params: { jwt: (params: {
token: JWT token: JWT
user?: User user?: User | AdapterUser
account?: A account?: A | null
profile?: P profile?: P
isNewUser?: boolean isNewUser?: boolean
}) => Awaitable<JWT> }) => Awaitable<JWT>
@@ -378,7 +371,7 @@ export interface EventCallbacks {
*/ */
signIn: (message: { signIn: (message: {
user: User user: User
account: Account account: Account | null
profile?: Profile profile?: Profile
isNewUser?: boolean isNewUser?: boolean
}) => Awaitable<void> }) => Awaitable<void>
@@ -392,9 +385,9 @@ export interface EventCallbacks {
createUser: (message: { user: User }) => Awaitable<void> createUser: (message: { user: User }) => Awaitable<void>
updateUser: (message: { user: User }) => Awaitable<void> updateUser: (message: { user: User }) => Awaitable<void>
linkAccount: (message: { linkAccount: (message: {
user: User user: User | AdapterUser | { email: string }
account: Account account: Account
profile: User profile: User | AdapterUser | { email: string }
}) => Awaitable<void> }) => Awaitable<void>
/** /**
* The message object will contain one of these depending on * The message object will contain one of these depending on
@@ -420,7 +413,7 @@ export interface PagesOptions {
export type ISODateString = string export type ISODateString = string
export interface DefaultSession extends Record<string, unknown> { export interface DefaultSession {
user?: { user?: {
name?: string | null name?: string | null
email?: 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) | * [`SessionProvider`](https://next-auth.js.org/getting-started/client#sessionprovider) |
* [`session` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) * [`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" export type SessionStrategy = "jwt" | "database"
@@ -468,6 +461,13 @@ export interface SessionOptions {
* @default 86400 // 1 day * @default 86400 // 1 day
*/ */
updateAge: number 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 { export interface DefaultUser {
@@ -487,13 +487,13 @@ export interface DefaultUser {
* [`jwt` callback](https://next-auth.js.org/configuration/callbacks#jwt-callback) | * [`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) * [`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 // Below are types that are only supposed be used by next-auth internally
/** @internal */ /** @internal */
export type InternalProvider<T extends ProviderType = any> = (T extends "oauth" export type InternalProvider<T = ProviderType> = (T extends "oauth"
? OAuthConfig<any> ? InternalOAuthConfig<any>
: T extends "email" : T extends "email"
? EmailConfig ? EmailConfig
: T extends "credentials" : T extends "credentials"
@@ -515,7 +515,10 @@ export type NextAuthAction =
| "_log" | "_log"
/** @internal */ /** @internal */
export interface InternalOptions<T extends ProviderType = any> { export interface InternalOptions<
TProviderType = ProviderType,
WithVerificationToken = TProviderType extends "email" ? true : false
> {
providers: InternalProvider[] providers: InternalProvider[]
/** /**
* Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel. * Parsed from `NEXTAUTH_URL` or `x-forwarded-host` on Vercel.
@@ -523,9 +526,7 @@ export interface InternalOptions<T extends ProviderType = any> {
*/ */
url: InternalUrl url: InternalUrl
action: NextAuthAction action: NextAuthAction
provider: T extends string provider: InternalProvider<TProviderType>
? InternalProvider<T>
: InternalProvider<T> | undefined
csrfToken?: string csrfToken?: string
csrfTokenVerified?: boolean csrfTokenVerified?: boolean
secret: string secret: string
@@ -536,7 +537,9 @@ export interface InternalOptions<T extends ProviderType = any> {
pages: Partial<PagesOptions> pages: Partial<PagesOptions>
jwt: JWTOptions jwt: JWTOptions
events: Partial<EventCallbacks> events: Partial<EventCallbacks>
adapter?: Adapter adapter: WithVerificationToken extends true
? Adapter<WithVerificationToken>
: Adapter<WithVerificationToken> | undefined
callbacks: CallbacksOptions callbacks: CallbacksOptions
cookies: CookiesOptions cookies: CookiesOptions
callbackUrl: string 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)) cookies?.forEach((cookie) => setCookie(res, cookie))
if (body && typeof body !== "string" && Object.keys(body).length) if (body && typeof body !== "string" && Object.keys(body).length) {
return body as Session if (status === 200) return body as Session
throw new Error((body as any).message)
}
return null 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 authorized?: AuthorizedCallback
} }
@@ -101,17 +101,17 @@ async function handleMiddleware(
options: NextAuthMiddlewareOptions | undefined, options: NextAuthMiddlewareOptions | undefined,
onSuccess?: (token: JWT | null) => Promise<NextMiddlewareResult> 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 signInPage = options?.pages?.signIn ?? "/api/auth/signin"
const errorPage = options?.pages?.error ?? "/api/auth/error" 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"] const publicPaths = ["/_next", "/favicon.ico"]
// Avoid infinite redirects/invalid response // Avoid infinite redirects/invalid response
// on paths that never require authentication // on paths that never require authentication
if ( if (
pathname.startsWith(basePath) || `${basePath}${pathname}`.startsWith(authPath) ||
[signInPage, errorPage].includes(pathname) || [signInPage, errorPage].includes(pathname) ||
publicPaths.some((p) => pathname.startsWith(p)) publicPaths.some((p) => pathname.startsWith(p))
) { ) {
@@ -125,7 +125,7 @@ async function handleMiddleware(
`\nhttps://next-auth.js.org/errors#no_secret` `\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") errorUrl.searchParams.append("error", "Configuration")
return NextResponse.redirect(errorUrl) return NextResponse.redirect(errorUrl)
@@ -145,8 +145,8 @@ async function handleMiddleware(
if (isAuthorized) return await onSuccess?.(token) if (isAuthorized) return await onSuccess?.(token)
// the user is not logged in, redirect to the sign-in page // the user is not logged in, redirect to the sign-in page
const signInUrl = new URL(signInPage, origin) const signInUrl = new URL(`${basePath}${signInPage}`, origin)
signInUrl.searchParams.append("callbackUrl", `${pathname}${search}`) signInUrl.searchParams.append("callbackUrl", `${basePath}${pathname}${search}`)
return NextResponse.redirect(signInUrl) return NextResponse.redirect(signInUrl)
} }

View File

@@ -1,28 +1,25 @@
import type { OAuthConfig, OAuthUserConfig } from "." import type { OAuthConfig, OAuthUserConfig } from "."
interface HubSpotProfile extends Record<string, any> { 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 // https://legacydocs.hubspot.com/docs/methods/oauth2/get-access-token-information
user: string, user: string
user_id: string, user_id: string
hub_domain: string, hub_domain: string
hub_id: string, hub_id: string
} }
const HubSpotConfig = { const HubSpotConfig = {
authorizationUrl: "https://app.hubspot.com/oauth/authorize", authorizationUrl: "https://app.hubspot.com/oauth/authorize",
tokenUrl: "https://api.hubapi.com/oauth/v1/token", 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>( export default function HubSpot<P extends HubSpotProfile>(
options: OAuthUserConfig<P> options: OAuthUserConfig<P>
): OAuthConfig<P> { ): OAuthConfig<P> {
return { return {
id: "hubspot", id: "hubspot",
name: "HubSpot", name: "HubSpot",
@@ -36,7 +33,6 @@ export default function HubSpot<P extends HubSpotProfile>(
scope: "oauth", scope: "oauth",
client_id: options.clientId, client_id: options.clientId,
}, },
}, },
client: { client: {
token_endpoint_auth_method: "client_secret_post", token_endpoint_auth_method: "client_secret_post",
@@ -45,33 +41,27 @@ export default function HubSpot<P extends HubSpotProfile>(
userinfo: { userinfo: {
url: HubSpotConfig.profileUrl, url: HubSpotConfig.profileUrl,
async request(context) { async request(context) {
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`
const url = `${HubSpotConfig.profileUrl}/${context.tokens.access_token}`;
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
method: "GET", method: "GET",
}); })
const userInfo = await response.json(); return await response.json()
},
return { userInfo }
}
}, },
profile(profile) { profile(profile) {
const { userInfo } = profile
return { return {
id: userInfo.user_id, id: profile.user_id,
name: userInfo.user, name: profile.user,
email: userInfo.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 // 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, options,

View File

@@ -110,7 +110,7 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
userinfo?: string | UserinfoEndpointHandler userinfo?: string | UserinfoEndpointHandler
type: "oauth" type: "oauth"
version?: string version?: string
profile?: (profile: P, tokens: TokenSet) => Awaitable<User & { id: string }> profile: (profile: P, tokens: TokenSet) => Awaitable<User>
checks?: ChecksType | ChecksType[] checks?: ChecksType | ChecksType[]
client?: Partial<ClientMetadata> client?: Partial<ClientMetadata>
jwks?: { keys: JWK[] } jwks?: { keys: JWK[] }
@@ -147,6 +147,14 @@ export interface OAuthConfig<P> extends CommonProviderOptions, PartialIssuer {
encoding?: string 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< export type OAuthUserConfig<P> = Omit<
Partial<OAuthConfig<P>>, Partial<OAuthConfig<P>>,
"options" | "type" "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: Session; status: "authenticated" }
| { data: null; status: "unauthenticated" | "loading" } | { data: null; status: "unauthenticated" | "loading" }
const SessionContext = React.createContext<SessionContextValue | undefined>( export const SessionContext = React.createContext<SessionContextValue | undefined>(
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 { handler } from "./lib"
import EmailProvider from "../src/providers/email"
it("Show error page if secret is not defined", async () => { it("Show error page if secret is not defined", async () => {
const { res, log } = await handler( 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)) 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 () => { it("Should show configuration error page on invalid `callbackUrl`", async () => {
const { res, log } = await handler( const { res, log } = await handler(
{ providers: [] }, { providers: [] },

View File

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

View File

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

View File

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

View File

@@ -1,40 +1,95 @@
import { NextMiddleware } from "next/server" 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 () => { it("should not match pages as public paths", async () => {
const options: NextAuthMiddlewareOptions = { const options: NextAuthMiddlewareOptions = {
pages: { pages: {
signIn: "/", signIn: "/",
error: "/" error: "/",
}, },
secret: "secret" secret: "secret",
} }
const nextUrl: any = { const nextUrl: any = {
pathname: "/protected/pathA", pathname: "/protected/pathA",
search: "", search: "",
origin: "http://127.0.0.1" origin: "http://127.0.0.1",
} }
const req: any = { nextUrl, headers: { authorization: "" } } const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware 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).toBeDefined()
expect(res.status).toBe(307) expect(res?.status).toBe(307)
}) })
it("should not redirect on public paths", async () => { it("should not redirect on public paths", async () => {
const options: NextAuthMiddlewareOptions = { const options: NextAuthMiddlewareOptions = {
secret: "secret" secret: "secret",
} }
const nextUrl: any = { const nextUrl: any = {
pathname: "/_next/foo", pathname: "/_next/foo",
search: "", search: "",
origin: "http://127.0.0.1" origin: "http://127.0.0.1",
} }
const req: any = { nextUrl, headers: { authorization: "" } } const req: any = { nextUrl, headers: { authorization: "" } }
const handleMiddleware = withAuth(options) as NextMiddleware const handleMiddleware = withAuth(options) as NextMiddleware
const res = await handleMiddleware(req, null) const res = await handleMiddleware(req, null as any)
expect(res).toBeUndefined() 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", "name": "@next-auth/tsconfig",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",

38
pnpm-lock.yaml generated
View File

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