clerk / javascript

Official JavaScript repository for Clerk authentication
https://clerk.com
MIT License
1.18k stars 270 forks source link

Next.js authMiddleware requires an 'afterAuth' function to redirect to user page, after Google social login #1477

Closed jstlaurent closed 12 months ago

jstlaurent commented 1 year ago

Package + Version

Dependencies + versions

"dependencies": {
    "@clerk/nextjs": "^4.21.14",
    "next": "^13.4.5",
    "react": "^18.2.0",
  },

Browser/OS

Firefox 115.0.1

Description

Context

I've configured my Clerk app to use Google as a social login provider.

I've setup a simple Next.js app, using the new App Router, with a user page I've configured the auth middleware like this:

import { authMiddleware } from "@clerk/nextjs";

export default authMiddleware({
  publicRoutes: ["/", "/api"],
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

I've created a user page (mapped to /user), like so:

import { UserProfile } from "@clerk/nextjs";

export default function UserPage() {
  return <UserProfile />;
}

I have a header component that uses Clerk's own SignInButton component, that looks like this:

"use client";
import { SignedOut, SignInButton, UserButton } from "@clerk/nextjs";
import Navigation from "@/components/Navigation";

export function Header() {
  return (
    <header className="container z-40 bg-background">
      <div className="flex h-20 items-center justify-between py-6">
        <Navigation />
        <nav>
          <SignedOut>
            <SignInButton
              mode="modal"
              afterSignInUrl="/user"
              afterSignUpUrl="/user"
            >
                Sign In
            </SignInButton>
          </SignedOut>
          <UserButton
            afterSignOutUrl="/"
            showName={true}
            userProfileMode="navigation"
            userProfileUrl="/user"
          />
        </nav>
      </div>
    </header>
  );
}

Problem

Running locally, if I click on the SignInButton in my header, then pick Google to login, I go through a successful OAuth login flow and get redirected to https://localhost:3000/user, but the following warning is displayed in the console:

INFO: Clerk: The request to /user is being redirected because there is no signed-in user, and the path is not included in `ignoredRoutes` or `publicRoutes`. To prevent this behavior, choose one of:

1. To make the route accessible to both signed in and signed out users, add "/user" to the `publicRoutes` array passed to authMiddleware
2. To prevent Clerk authentication from running at all, pass `ignoredRoutes: ["/((?!api|trpc))(_next|.+\..+)(.*)", "/user"]` to authMiddleware
3. Pass a custom `afterAuth` to authMiddleware, and replace Clerk's default behavior of redirecting unless a route is included in publicRoutes

However, I can modify the authMiddleware usage to be this:

export default authMiddleware({
  afterAuth: (auth) => {
    console.log("afterAuth", auth);
  },
  publicRoutes: ["/", "/api"],
});

And in that case, the redirect functions correctly, sending me to the user page, displaying Clerk's UserProfile component.

The strangest part, to me at least, is that the auth object logged to the console appears to be empty:

afterAuth {
  sessionClaims: null,
  sessionId: null,
  session: null,
  userId: null,
  user: null,
  actor: null,
  orgId: null,
  orgRole: null,
  orgSlug: null,
  organization: null,
  getToken: [Function: getToken],
  debug: [Function],
  isPublicRoute: false,
  isApiRoute: false
}

I can live with a bogus function in the middleware definition, if that fixes it, but it feels like I'm missing something here.

clerk-cookie commented 1 year ago

This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days.

jstlaurent commented 1 year ago

I'll update to latest and see if the issue persists.

jescalan commented 1 year ago

@jstlaurent is there any chance you'd be willing to put together a minimal reproduction for this? This would help us to look into it - so sorry for the slow response!

MichelML commented 1 year ago

+1 for this

dimkl commented 1 year ago

Kind reminder, to use the latest version of @clerk/nextjs, as it may resolve the issue.

jstlaurent commented 1 year ago

Hi @jescalan! Sorry for the delay. I've created a repro case in this repo: https://github.com/jstlaurent/issue-1477

I initialized a basic Next.js app (using npx create-next-app@latest, picking default options), and then followed the steps outlined in Clerk's documentation here.

It's using a Clerk project created here, with Google as an SSO provider and all default options.

My only change to the Clerk quickstart was to add a /user path, and configure a post-signup, post-signin redirect to that page. I defined that redirect both in the .env.local env variables, but also on Clerk's <SignIn/> and <SignUp/> components.

I'm running Node 18.16.0, and I'm running the locally app with npm run dev.

My expectations would be that, after signing up through Google, I would be redirected to the user page.

The result is that I'm being returned to the root page.

Let me know if I can be of further assistance in clarifying what's happening.

freightcrossings commented 1 year ago

Hi,

I'm also experiencing a partially empty auth object in authMiddleware(). My user creates an organization using the following:

// SomeComponent.tsx
import { useOrganizations } from "@clerk/nextjs";
...
const { createOrganization } = useOrganizations();
...
const org = await createOrganization({ name: values.name, slug: values.slug });

Then in my middleware I check for this org via:

if (auth.userId && !auth.orgId && req.url !== `${getURL()}/setup`) {
    //some redirect
}

The result is the following:

Screenshot 2023-09-07 at 11 08 42 AM

I've looked online, but I can't find any solution, but I have been seeing more people here posting the same issue. This is a need for my application. I would like to upgrade to premium, but not before this is addressed. Looking forward to a fix

freightcrossings commented 1 year ago

Also, I should mention that the organization is created in the Clerk dashboard and the user that creates it is listed as an Admin member.

freightcrossings commented 1 year ago

I did a bit of digging and unfortunately all roads lead to @clerk/backend which I can't find and I'm assuming is a private repo. But it does seem as though there must be some bug in this package as I have tried both getAuth using withMiddleware and authMiddleware's auth object, and both return an empty set of org values for an org that is set in Clerk's dashboard.

Figured my search might help a bit with debugging.

withMiddleware:

authMiddleware:

authenticateRequest:

clerkClient:

freightcrossings commented 1 year ago

Okay,

I found a workaround. It is as follows:

async afterAuth(auth, req, evt) {
        const orgList = await clerkClient.users.getOrganizationMembershipList({ userId: auth.userId ?? '' });
        const orgId = orgList.find(org => org?.publicUserData?.userId === auth.userId)?.organization?.id;
        ...
}

However, this further indicates that something isn't right with the auth object on the authMiddleware. If when I query the organization via the clerkClient.getOrganizationMembershipList() and it exists, but it doesn't exist on the auth object, that means that there is indeed an org that isn't being passed via the auth object.

freightcrossings commented 1 year ago

The other issue with the authMiddleware is that it passes back an "interstitial state" in debug mode, and throws the following error:

[Error: A middleware can not alter response's body. Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware] 

This is leading me to believe that authMiddleware is altering the response body in the internal implementation and that isn't compatible with NextJs 13+.

Looking forward to some feedback

jescalan commented 1 year ago

Hey @freightcrossings! I'm so sorry you have been running into these issues, and really appreciate the digging you have done to try to solve this. In this case, it would be better if you could open a new issue for your use case here, since this is a different issue, despite it seeming to be related. Including a reproduction as this issue's author has done would also help us much more to investigate.

clerk-cookie commented 1 year ago

Hello 👋

We currently close issues after 40 days of inactivity. It's been 30 days since the last update here. If we missed this issue, please reply here. Otherwise, we'll close this issue in 10 days.

As a friendly reminder: The best way to see an issue fixed is to open a pull request. If you're not sure how to do that, please check out our contributing guide.

Thanks for being a part of the Clerk community! 🙏

jstlaurent commented 1 year ago

I submitted a repro case in my last message above.

AyushChamoli961 commented 1 year ago

Facing exactly same issue with "next": "13.4.19" and clerk "@clerk/nextjs": "^4.25.3". Please update if any solution found.

The problem only occurs on macos , I tried setting up clerk auth on my linux machine and it works fine but on macos I face the same problem as stated above.

waldothedeveloper commented 1 year ago

This is happening to me as well, I just added the code afterAuth: (auth) => { console.log("afterAuth", auth); }, to the middleware as @jstlaurent suggested, and my error is now gone!

jstlaurent commented 1 year ago

This is happening to me as well, I just added the code afterAuth: (auth) => { console.log("afterAuth", auth); }, to the middleware as @jstlaurent suggested, and my error is now gone!

@waldothedeveloper: I discovered afterwards that doing that seems to break the check that prevents non-authenticated users from accessing the protected pages of you application. I ended up removing the function and living with the failed redirect in local development.

aakash19here commented 1 year ago
import { authMiddleware } from "@clerk/nextjs";
import { NextResponse } from "next/server";

export const runtime = "nodejs";

export default authMiddleware({
  publicRoutes: [
    "/",
    "/sign-in(.*)",
    "/sign-up(.*)",
    "/api(.*)",
  ],
  async afterAuth(auth, req) {
    if (auth.isPublicRoute) {
      //  For public routes, we don't need to do anything
      return NextResponse.next();
    }

    const url = new URL(req.nextUrl.origin);

    if (!auth.userId) {
      //  If user tries to access a private route without being authenticated,
      //  redirect them to the sign in page
      url.pathname = "/sign-in";
      return NextResponse.redirect(url);
    }
  },
});

export const config = {
  matcher: ["/((?!.*\\..*|_next).*)"],
};

Hey @jstlaurent this approach worked for me

jstlaurent commented 1 year ago

@aakash19here : Thanks for that! I feel the documentation could be clearer that you have to handle all cases when you choose to implement afterAuth. That tripped me.

BaDo2001 commented 1 year ago

@aakash19here Huge kudos, the runtime export was missing in my case.

jescalan commented 1 year ago

Sorry for the slow response here friends, we will discuss this one in the next couple days!

merveillevaneck commented 1 year ago

I agree with a lot of the engineers who commented above. Above having searched through this thread, implementing a custom after auth function seems to make Next 14 + TRPC + clerk work normally. While this may not be something that HAS to be fixed, what would be awesome is just mentioning in the docs that it might be necessary for TRPC users to add that implementation. in most of the above cases I believe that the following snippet will work:

export default authMiddleware({
  publicRoutes: ["/"],
  afterAuth: (auth, req) => auth.isPublicRoute ? NextReponse.next() : undefined
  })

This essentially just makes clerks middleware respond appropriately. However, the error that is communicated by the clerk lib on the server side seems to suggest that only adding a path to the publicRoutes array should do the trick. but it does not.

LekoArts commented 12 months ago

Hello @jstlaurent,

I tried your provided reproduction and with some changes I applied it works for me:

I added a .env.local file as following:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your-key
CLERK_SECRET_KEY=your-key
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up

Then changed these files:

diff --git a/package.json b/package.json
index dfd7730..9f3988f 100644
--- a/package.json
+++ b/package.json
@@ -9,14 +9,14 @@
     "lint": "next lint"
   },
   "dependencies": {
-    "@clerk/nextjs": "^4.23.5",
+    "@clerk/nextjs": "^4.27.4",
     "@types/node": "20.5.9",
     "@types/react": "18.2.21",
     "@types/react-dom": "18.2.7",
     "autoprefixer": "10.4.15",
     "eslint": "8.48.0",
-    "eslint-config-next": "13.4.19",
-    "next": "13.4.19",
+    "eslint-config-next": "^14.0.3",
+    "next": "^14.0.3",
     "postcss": "8.4.29",
     "react": "18.2.0",
     "react-dom": "18.2.0",
diff --git a/src/middleware.ts b/src/middleware.ts
index 89801cf..7ff0010 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -3,9 +3,11 @@ import { authMiddleware } from "@clerk/nextjs";
 // This example protects all routes including api/trpc routes
 // Please edit this to allow other routes to be public as needed.
 // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your middleware
-export default authMiddleware({});
+export default authMiddleware({
+  publicRoutes: ['/', '/sign-in', '/sign-up']
+});

 export const config = {
-  matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
+  matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
 };

I don't see any warnings in my console nor that it doesn't work. Can you please verify on your end if with my updates it works for you?

jstlaurent commented 12 months ago

@LekoArts : Thanks for taking the time to look into this! I'll be honest, our login flow has changed since I filed this issue so this is no longer a problem for us even if it was still happening. Maybe something got fixed in a library update along the way. I'll close the issue.