0

I’m building authentication with Supabase in a Next.js 15 application. I have a /update-password route that should only be accessible via a reset password link containing a token hash. However, despite using middleware.ts for SSR auth (following Supabase's instructions), I am still able to access /update-password even when logged out. Other protected routes behave correctly and redirect unauthenticated users as expected.

middleware.ts

import { type NextRequest } from 'next/server';
import { updateSession } from '@/utils/supabase/middleware';

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

utils/supabase/middleware.ts

import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

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)
          );
        },
      },
    }
  );

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user && request.nextUrl.pathname.startsWith('/update-password')) {
    const url = request.nextUrl.clone();
    url.pathname = '/login';
    return NextResponse.redirect(url);
  }

  return supabaseResponse;
}

/auth/confirm/route.ts

import { type EmailOtpType } from '@supabase/supabase-js';
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const token_hash = searchParams.get('token_hash');
  const type = searchParams.get('type') as EmailOtpType | null;
  const next = searchParams.get('next') ?? '/';

  const baseURL = process.env.NEXT_PUBLIC_SITE_URL || request.nextUrl.origin;
  const redirectTo = new URL(next, baseURL);

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

    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    });
    if (!error) {
      return NextResponse.redirect(redirectTo);
    }
  }

  return NextResponse.redirect(new URL('/error', baseURL));
}

/update-password/actions.ts

'use server';

import { createClient } from '@/utils/supabase/server';

export const updatePassword = async (
  email: string,
  password: string
): Promise<{ message: string }> => {
  const supabase = await createClient();
  try {
    const { error } = await supabase.auth.updateUser({
      email,
      password,
    });
    if (error) {
      console.error(error);
      return { message: 'Error updating password' };
    }
    return { message: 'Password updated successfully' };
  } catch (error) {
    console.error(error);
    return { message: 'Error updating password' };
  }
};

/update-password/page.tsx

import { updatePassword } from './actions';
import { createClient } from '@/utils/supabase/server';

export default async function UpdatePassword() {
  const supabase = await createClient();
  const { data } = await supabase.auth.getUser();

  const handleSubmit = async (formData: FormData) => {
    'use server';
    const password = formData.get('password');
    const { message } = await updatePassword(
      data?.user?.email as string,
      password as string
    );
    console.log(message);
  };

  return (
    <form action={handleSubmit}>
      <input type="password" name="password" />
      <button type="submit">Update password</button>
    </form>
  );
}

Even when logged out, I can access /update-password. Other routes protected by the middleware redirect unauthenticated users correctly.

Why is /update-password still accessible without being logged in, and how can I ensure it is properly protected?

1 Answer 1

0

I cant say exactly what's going on there but I made a new nextjs project and added the same middleware you have and it works for me. When i'm logged out i get redirected to /login.

Just out of curiosity, can you add a console.log to the middleware just above checking for the pathname? It shouldn't be passing this part when logged out.

console.log(user)
if (!user && request.nextUrl.pathname.startsWith("/update-password")) {
  const url = request.nextUrl.clone();
  url.pathname = "/login";
  return NextResponse.redirect(url);
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.