supabase / ssr

Supabase clients for use in server-side rendering frameworks.
MIT License
52 stars 6 forks source link

Reset Password Flow not working #46

Closed limegorilla closed 1 week ago

limegorilla commented 1 month ago

Bug report

Describe the bug

I've been running around this quite a bit.

I'm using NextJs 14 and trying to get the password reset flow to work properly. I'll note now that the MagicLink flow works perfectly.

When the user is redirected to my confirm route, I should get their session - which as per my logs, I do. However, with only the reset password flow, this doesn't actually update on the Supabase client.

To Reproduce

Done as per the docs

  1. User is at the login page (auth/v2/login)
  2. They click on the "I forgot my password button" which redirects to auth/v2/forgot-password
  3. User enters in their password and clicks "Send email":
    const { error, data } = await supbase.auth.resetPasswordForEmail(
        values.email,
      );
  4. Email is sent to user with following link in the email: href="{{ .SiteURL }}/auth/v2/confirm?token_hash={{ .TokenHash }}&type=recovery&next=/auth/v2/recovery"
  5. This directs to my confirm API route:
    
    import type { EmailOtpType } from "@supabase/supabase-js";
    import type { NextRequest } from "next/server";
    import { revalidatePath } from "next/cache";
    import { NextResponse } from "next/server";
    import * as Sentry from "@sentry/nextjs";
    import { createClient } from "@company_name_removed/supabase/server";

export async function GET(request: NextRequest) { const { searchParams, origin } = new URL(request.url); const token_hash = searchParams.get("token_hash"); const type = searchParams.get("type") as EmailOtpType | null; const next = type === "recovery" ? "/app/settings/account" : "/app"; // Edited here so I didn't have to keep re-rendering my email. Have tried sending this round the app just in case it's a problem with caching etc. const redirectTo = request.nextUrl.clone(); redirectTo.pathname = next;

if (token_hash && type) { const supabase = createClient();

const { error, data } = await supabase.auth.verifyOtp({
  type,
  token_hash,
});

if (!error) {
  console.info(">>> AUTH: OTP Verified for user", data.user?.email);

  revalidatePath("/");

  return NextResponse.redirect(redirectTo);
} else {
  console.warn(`>>> AUTH: OTP Verification failed! ${error.message}`);

  const seid = Sentry.captureException(error, {
    data: {
      ...request,
    },
  });

  return NextResponse.redirect(
    `${origin}/auth/v2/error?name=${error.name}&message=${error.message}&code=${error.code}&sentry_digest=${seid}`,
  );
}

}

const er = new Error("Missing token", { cause: "auth", });

const seid = Sentry.captureException(er, { data: { ...request }, });

// Missing code return NextResponse.redirect( ${origin}/auth/v2/error?name=${er.name}&message=${er.message}&code=500&sentry_digest=${seid}, ); }

6. User is caught by my middleware and redirected to login screen:
```ts
import type { CookieOptions } from "@supabase/ssr";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value),
          );
          supabaseResponse = NextResponse.next({
            request,
          });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options),
          );
        },
      },
    },
  );

  // refreshing the auth token
  const { data: user } = await supabase.auth.getUser();

  if (
    !user &&
    // Old route
    !request.nextUrl.pathname.startsWith("/login") &&
    // Should stop this running on anything that starts with /auth
    !request.nextUrl.pathname.startsWith("/auth") &&
    // Don't want this for API routes. API has it's own middleware
    !request.nextUrl.pathname.startsWith("/api") &&
    // Don't block the homepage
    request.nextUrl.pathname !== "/"
  ) {
    const url = request.nextUrl.clone();
    const path = url.pathname;
    url.pathname = "/auth/v2/login?redirectTo=" + path;

    console.log(
      `>>> [MIDDLEWARE] Someone tried to access protected route (${path}) without being logged in. Redirecting to ${url.pathname}`,
    );

    return NextResponse.redirect(url);
  }

  return supabaseResponse;
}

I get the following logs

>>> AUTH: OTP Verified for user admin@workforce.com # From the auth/v2/confirm route
GET /auth/v2/confirm?token_hash=pkce_f2d850fd7b161ec21e6b3d21894e5c4359374744cb8dba8c8e0ccc0a&type=recovery&next=/auth/v2/recovery 307 in 55ms
>>> tRPC Request from rsc by undefined # My TRPC route. When using magic links, this would have a user here
[AUTH]: User is not logged in - redirecting to login # From a layout on /app/*

Expected behavior

User's session should save to supabase client

System information

j4w8n commented 1 month ago

Upgrading to ssr 0.4.1 should resolve this. You'll either have to use the specific version number or the @patched tag instead of @latest.

silentworks commented 3 weeks ago

Latest is now 0.5.0 which should contain the fix for this issue too.

J0 commented 1 week ago

Hey thanks for flagging, going to close the issue for now but let us know if upgrading does not resolve the issue.