kinde-oss / kinde-auth-nextjs

Kinde NextJS SDK - authentication for server rendered apps
https://kinde.com/docs/developer-tools/nextjs-sdk/
MIT License
149 stars 18 forks source link

Bug: api/auth/setup firing 90 times on page load #197

Closed johnpwilkinson closed 1 month ago

johnpwilkinson commented 1 month ago

Prerequisites

Describe the issue

I am seeing the api/auth/setup endpoint being hit 90 times on page loads after I successfully sign a user in to my Nextjs app.

I did follow the docs and instruction.

I am getting the desired effect of being able to log a user in and authenticate that they are a user.

Here is my /app/api/[kindeAuth]/route.js

import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server";
export const GET = handleAuth();

There are 5 server components in my code that call getUser from getKindeServerSession in the same pattern

export default async function Page() {
  const { getUser } = getKindeServerSession();

  const user = await getUser();
  ...

and one instance where this is being called from the client side with useKindeBrowserClient

    const { getUser } = useKindeBrowserClient();
  const pathname = usePathname();

Thats it.... that is the extent of my employment of the Kinde SDK in this project.

This is happening in local dev and in the vercel deployed app.

I have tried to comment out / delete these invocations to see if somehow they were all being triggered with each page load.... did nothing... and furthermore, the more I look at the issue, the more I doubt that caling getUser has anything to do with it.

Looking at the console as I log a user in and the initial page loads.... I see a series of actions:

  1. temporary redirect - /api/auth/login - 307
  2. temporary redirect - /api/auth/kinde_callback - 307 ...then, 90 times, I see:
  3. /api/auth/setup - 200... they all resolve with 200s

    This cannot be correct, right?

    and I have been racking my brain to figure out what is causing this.... like I said, getUser doesnt seem to be the culprit, as these all take place immediately after logging in.

    I do not seem to be missing anything from the documentations set up steps...and the product is working n terms of auth and user management,

    Here is a screenshot of some of them:

Screenshot 2024-08-14 at 11 06 10 AM

Library URL

https://github.com/kinde-oss/kinde-auth-react

Library version

2.3.5

Operating system(s)

macOS

Operating system version(s)

13.6

Further environment details

this is happening in local dev and in my vercel deploy app.

here is my package.json

  "dependencies": {
    "@adobe/react-spectrum": "^3.36.1",
    "@dnd-kit/core": "^6.1.0",
    "@hookform/resolvers": "^3.9.0",
    "@kinde-oss/kinde-auth-nextjs": "^2.3.5",
    "@million/lint": "^1.0.0-rc.84",
    "@prisma/client": "^5.16.2",
    "@radix-ui/react-dialog": "^1.1.1",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-icons": "^1.3.0",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-popover": "^1.1.1",
    "@radix-ui/react-select": "^2.1.1",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.1.2",
    "@uidotdev/usehooks": "^2.4.1",
    "@vercel/postgres": "^0.9.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "cuid": "^3.0.0",
    "date-fns": "^3.6.0",
    "framer-motion": "^11.3.17",
    "html-to-image": "^1.11.11",
    "jsonwebtoken": "^9.0.2",
    "jwks-rsa": "^3.1.0",
    "lodash.debounce": "^4.0.8",
    "lucide-react": "^0.408.0",
    "next": "14.2.5",
    "next-themes": "^0.3.0",
    "react": "^18",
    "react-aria": "^3.34.1",
    "react-aria-components": "^1.2.1",
    "react-day-picker": "^8.10.1",
    "react-dom": "^18",
    "react-gauge-component": "^1.2.21",
    "react-hook-form": "^7.52.1",
    "react-stately": "^3.32.1",
    "recharts": "^2.12.7",
    "tailwind-merge": "^2.4.0",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.1",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@tailwindcss/typography": "^0.5.13",
    "@types/react": "18.3.3",
    "daisyui": "^4.12.10",
    "eslint": "^8",
    "eslint-config-next": "14.2.5",
    "postcss": "^8",
    "prisma": "^5.16.2",
    "tailwindcss": "^3.4.1"
  }

My env variables should be correct... and the core functionality of Kinde does work as expected, so i do not think there is any issue there.

Using the devtools from chrome, the initiator is always code from the Kinde SDK, and I am only invoking the SDK with the <LoginLink />, <LogoutLink />, and the getUser(). The clues from the network/devtools, do not really elucidate much more than that.

Reproducible test case URL

No response

Additional information

No response

DanielRivers commented 1 month ago

Hi @johnpwilkinson,

Thanks for raising, I have not seen this before. Is there a change that the component which is using Kinde is being re-rendered for some reason in your project?

johnpwilkinson commented 1 month ago

edit: On further review, this does not happen when navigating to /profile... but does happen when navigating to my two other "main" sections of the app. The thing these two have in common is the rendering of a Table component that I made from scratch. The only references to Kinde that these tables have is at the top (server components), they both call getUser and then take the users id and pass it in to functions that generate the data for the table. That data is passed into the table and the table is rendered.

The table, is made up of a TableHeader and one to N TableRow components.... Each TableRow has an actions columns and this action column, is a server component called ActionIcons that rendered 3 icons that serve as buttons for said row of data... (details, edit, delete)...

const ActionIcons = React.memo(({ mode, row }) => (
  <div className="flex items-center justify-center gap-1 w-full h-full">
    {mode !== "protocolDetail" && (
      <div className="flex items-center justify-center">
        <Link href={handleDetailsNav(mode, row.id)}>
          <Search className="h-5 w-5" />
        </Link>
      </div>
    )}

    <div className="flex items-center justify-center">
      <Modal
        mode="edit"
        data={row}
        formFor={mode}
        icon={<Pencil1Icon className="h-5 w-5" />}
      >
        <CustomForm />
      </Modal>
    </div>

    <div className="flex items-center justify-center">
      <Modal
        formFor={mode}
        data={row}
        mode="delete"
        icon={<Cross1Icon className="h-5 w-5" />}
      >
        <CardDialog />
      </Modal>
    </div>
  </div>
));
ActionIcons.displayName = "ActionIcons";

export default ActionIcons;

you will notice that this component, uses the Modal component so that when a user clicks on one of these action icons, a modal (or drawer depending on screen size) opens with the desired action or form...

lastly, and thank you for baring with me, Modal is a client component that does call getUser

"use client";
import { useState, cloneElement, useEffect } from "react";
import { usePathname } from "next/navigation";
import { formData as myFormData } from "../lib/FormData";
import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
  DialogTrigger,
} from "@/components/ui/dialog";
import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer";

export default function Modal({ children, open, icon, mode, data, formFor }) {
  const [isOpen, setIsOpen] = useState(open);
  const [isDesktop, setIsDesktop] = useState(false);
  const { getUser } = useKindeBrowserClient();
  const pathname = usePathname();

  useEffect(() => {
    if (typeof window !== "undefined") {
      const mediaQuery = window.matchMedia("(min-width: 768px)");
      setIsDesktop(mediaQuery.matches);
      const handleMediaQueryChange = (e) => setIsDesktop(e.matches);
      mediaQuery.addEventListener("change", handleMediaQueryChange);

      return () => {
        mediaQuery.removeEventListener("change", handleMediaQueryChange);
      };
    }
  }, []);

  const user = getUser();

  const handleClose = () => {
    setIsOpen(false);
  };

  const handleOpen = () => {
    setIsOpen(true);
  };

  const setFormFor = () => {
    const [section, detail] = pathname.split("/").slice(1, 3);
    if (section === "protocol") {
      return {
        section: detail === "overview" ? "protocolOverview" : "protocolDetail",
        id: detail === "details" ? pathname.split("/")[3] : undefined,
      };
    } else if (section === "bloodwork") {
      return {
        section:
          detail === "overview" ? "bloodworkOverview" : "bloodworkDetail",
        id: detail === "details" ? pathname.split("/")[3] : undefined,
      };
    }
    return { section: "" };
  };
  const recordId = data?.id;
  const generateForm = (mode) => {
    const routeInfo = setFormFor();
    if (mode === "add") {
      return {
        content: { title: "add", id: routeInfo.id || "" },
        formData: myFormData[routeInfo.section],
        myFormFor: routeInfo.section,
      };
    } else if (mode === "edit") {
      return {
        content: { title: "edit" },
        formData: myFormData[routeInfo.section],
        myFormFor: formFor,
      };
    } else if (mode === "delete") {
      return {
        content: { title: "delete" },
        formData: myFormData[routeInfo.section],
        myFormFor: formFor,
      };
    }
    return { content: {} };
  };

  const { content, formData, myFormFor } = generateForm(mode);

  return isDesktop ? (
    <Dialog>
      <DialogTrigger>{icon}</DialogTrigger>

      <DialogContent className="p-0 w-fit">
        <DialogHeader>
          <DialogTitle className="sr-only">{content.title}</DialogTitle>
          <DialogDescription className="sr-only">
            {content.id}
          </DialogDescription>
        </DialogHeader>
        {cloneElement(children, {
          handleClose,
          mode,
          data,
          content,
          formData,
          formFor: myFormFor,
          recordId,
          user,
        })}
      </DialogContent>
    </Dialog>
  ) : (
    <Drawer open={isOpen} onOpenChange={setIsOpen} className="">
      <DrawerTrigger>{icon}</DrawerTrigger>
      <DrawerContent className="">
        {cloneElement(children, {
          handleClose,
          mode,
          data,
          content,
          formData,
          formFor: myFormFor,
          user,
        })}
      </DrawerContent>
    </Drawer>
  );
}

This is the only thing I can think of that would differentiate these two "main components" from the Profile component...

But would calling getUser trigger the api/auth/setup endpoint? Just doesnt make sense if so...

Hey @DanielRivers

thanks for the quick response.

Is there a change that the component which is using Kinde is being re-rendered for some reason in your project?

This is the thing I have been spending my time thinking about.

with the exception of the one client component, every other component that remotely uses kinde is a server component. All of the code I have written, client or server, only ever uses the respective getUser method from the Kinde SDK. (as noted above)

I guess, to fully elucidate if that is even a concern, I would ask the following....

Does calling getUser from either the client or server side, in turn, make a GET request to app/auth/setup ?

or a better question, What triggers the app/auth/setup route to be hit in the first place

I did not see any info that granular in the docs and I have not seen this issue mentioned elsewhere... not in youtube vids, tutorials, Nextjs docs/templates/repos.... and asking chatGPT about it is essentially a waste of time... as its canned responses are irrelevant to this particular situation.

With a better idea around what actually triggers the api/auth/setup in the first place, perhaps I could then pinpoint where in my code the disconnect is happening.

A naive take, would be that the setup endpoint is only hit when the user first logs in? If so, this is more interesting, as I see this request being made on every subsequent page load after they login.

another note, I had a protection strategy in place where I placed all protected routes into a (routeGroup) called (protected) and in this I have a layout.jsx:

import { LoginLink } from "@kinde-oss/kinde-auth-nextjs/components";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";

export default async function RootLayout({ children }) {
  const { isAuthenticated } = getKindeServerSession();

  return (await isAuthenticated()) ? (
    <div className="">{children}</div>
  ) : (
    <div>
      This page is , please <LoginLink>Login</LoginLink> to view it
    </div>
  );
}

this works as far as protecting all of the children routes... I sorta cooked this idea up whilst learning about Next and using Kinde for the first time.

Earlier today, I removed the check on this component, rendered all children and then implemented the middleware to perform the authenticated check.... and this works great too, in terms of protecting routes... but either way, the mass firing of the setup endpoint is still taking place.

My hail mary idea was that for whatever reason, Kinde did not like that these routes were sitting behind a route group in the first place.... but I havent given that thought too much weight, as it just doesnt seem correct.

Here is the general folder structure of my project, maybe I am just missing something. Used Section to obfuscate actual names of core parts of the app. Anything stick out as problematic?

.
├── README.md
├── app
│   ├── (protected)
│   │   ├── Section
│   │   │   ├── details
│   │   │   │   ├── [id]
│   │   │   │   │   └── page.jsx
│   │   │   │   └── page.jsx
│   │   │   ├── layout.jsx
│   │   │   ├── Section
│   │   │   │   ├── [id]
│   │   │   │   │   └── page.jsx
│   │   │   │   └── page.jsx
│   │   │   ├── overview
│   │   │   │   └── page.jsx
│   │   │   └── page.jsx
│   │   ├── Section
│   │   │   ├── Section
│   │   │   │   ├── details
│   │   │   │   │   └── page.jsx
│   │   │   │   └── overview
│   │   │   │       └── page.jsx
│   │   │   ├── Section
│   │   │   │   ├── details
│   │   │   │   │   └── page.jsx
│   │   │   │   └── overview
│   │   │   │       └── page.jsx
│   │   │   └── page.jsx
│   │   ├── Section
│   │   │   └── page.jsx
│   │   ├── layout.jsx
│   │   ├── lib
│   │   │   ├── FormData.jsx
│   │   │   ├── actions.js
│   │   │   ├── constants.js
│   │   │   ├── context
│   │   │   │   └── SomeContext.jsx
│   │   │   ├── data.js
│   │   │   ├── headers.js
│   │   │   ├── hooks
│   │   │   ├── placeholder-data.js
│   │   │   ├── prisma.js
│   │   │   └── utils.js
│   │   ├── profile
│   │   │   └── page.jsx
│   │   ├── Section
│   │   │   ├── details
│   │   │   │   ├── [id]
│   │   │   │   │   └── page.jsx
│   │   │   │   └── page.jsx
│   │   │   ├── layout.jsx
│   │   │   ├── overview
│   │   │   │   └── page.jsx
│   │   │   └── page.jsx
│   │   └── ui
│   │       ├── UIStuff.jsx
│   ├── api
│   │   ├── auth
│   │   │   ├── [kindeAuth]
│   │   │   │   └── route.js
│   │   │   └── addNewUser
│   │   │       └── route.js
│   │   └── webhook
│   │       └── route.js
│   ├── favicon.ico
│   ├── globals.css
│   ├── hooks
│   │   ├── useDevice.jsx
│   │   └── useScreenshot.jsx
│   ├── layout.jsx
│   ├── page.jsx
│   └── tools
│       └── page.jsx
├── components
│   ├── theme-provider.jsx
│   └── ui
│       ├── breadcrumb.jsx
│       ├── button.jsx
│       ├── calendar.jsx
│       ├── card.jsx
│       ├── chart.jsx
│       ├── dialog.jsx
│       ├── drawer.jsx
│       ├── dropdown-menu.jsx
│       ├── form.jsx
│       ├── input.jsx
│       ├── label.jsx
│       ├── popover.jsx
│       ├── select.jsx
│       ├── sheet.jsx
│       ├── slider.jsx
│       ├── switch.jsx
│       ├── tabs.jsx
│       └── tooltip.jsx
├── components.json
├── env.local
├── jsconfig.json
├── lib
│   ├── data.js
│   └── utils.js
├── middleware.js
├── next-env.d.ts
├── next.config.mjs
├── next.config.mjs.temp
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── prisma
│   ├── migrations
│   │   ├── 20240714173919_add_optional_end_date
│   │   │   └── migration.sql
│   │   ├── 20240714225049_
│   │   │   └── migration.sql
│   │   ├── 20240715194636_bloodwork_add
│   │   │   └── migration.sql
│   │   └── migration_lock.toml
│   ├── schema.prisma
│   └── seed.js
├── public
│   ├── Logo.svg
│   ├── next.svg
│   └── vercel.svg
└── tailwind.config.js
johnpwilkinson commented 1 month ago

@DanielRivers ... I think that just the act of responding to your comment and then tracing everything out mentally made all the difference...

After my response, it occurred to me that I shouldn't be calling getUser inside of the client component Modal regardless, when I could just pass the userId from the top level server component.

Making this change has completely fixed the issue.

Thanks for being the proverbial rubber ducky and thanks for the swift response and the ultimate resolution.

Kinde is fucking awesome!

DanielRivers commented 1 month ago

Great that its all solved for you, pleased was able to solve your issue 🎉