authentication - Next.js 15 Supabase SSR auth: I can't secure the reset password route - Stack Overflow

admin2025-05-01  1

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?

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?

Share Improve this question asked Jan 2 at 15:17 Detla888Detla888 213 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 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);
}
转载请注明原文地址:http://www.anycun.com/QandA/1746110532a91812.html