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