supabase
blockpassword-based-auth-nextjssource: ok · refreshed
Password Based Auth flow for Nextjs and Supabase
Password Based Auth flow for Nextjs and Supabase
npx shadcn add "https://supabase.com/ui/r/password-based-auth-nextjs.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@supabase/ssr@latest@supabase/supabase-js@latest
Registry dependencies
buttoncardinputlabelhttps://supabase.com/library/r/safe-next-path.json
Files17 · source cached 10/8/2026, 7:41:45 AM
registry/default/blocks/password-based-auth-nextjs/app/auth/login/page.tsx
import { LoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/login-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/auth/error/page.tsx
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
export default async function Page({ searchParams }: { searchParams: Promise<{ error: string }> }) {
const params = await searchParams
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Sorry, something went wrong.
</CardTitle>
</CardHeader>
<CardContent>
{params?.error ? (
<p className="text-sm text-muted-foreground">Code error: {params.error}</p>
) : (
<p className="text-sm text-muted-foreground">An unspecified error occurred.</p>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx
import { redirect } from 'next/navigation'
import { LogoutButton } from '@/registry/default/blocks/password-based-auth-nextjs/components/logout-button'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getClaims()
if (error || !data?.claims) {
redirect('/auth/login')
}
return (
<div className="flex h-svh w-full items-center justify-center gap-2">
<p>
Hello <span>{data.claims.email}</span>
</p>
<LogoutButton />
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/auth/confirm/route.ts
import { type EmailOtpType } from '@supabase/supabase-js'
import { redirect } from 'next/navigation'
import { type NextRequest } from 'next/server'
import { createClient } from '@/registry/default/clients/nextjs/lib/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 next = _next?.startsWith('/') ? _next : '/'
if (token_hash && type) {
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
// redirect user to specified redirect URL or root of app
redirect(next)
} else {
// redirect the user to an error page with some instructions
redirect(`/auth/error?error=${error?.message}`)
}
}
// redirect the user to an error page with some instructions
redirect(`/auth/error?error=No token hash or type`)
}
registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
const next = new URLSearchParams(window.location.search).get('next')
router.push(safeNextPath(next, '/protected'))
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Sign in</CardTitle>
<CardDescription>Enter your email below to sign in to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
href="/auth/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign in'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don't have an account?{' '}
<Link href="/auth/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
registry/default/blocks/password-based-auth-nextjs/app/auth/sign-up/page.tsx
import { SignUpForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/sign-up-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<SignUpForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/auth/sign-up-success/page.tsx
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Thank you for signing up!
</CardTitle>
<CardDescription>Check your email to confirm</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
You've successfully signed up. Please check your email to confirm your account
before signing in.
</p>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/components/sign-up-form.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [repeatPassword, setRepeatPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
if (password !== repeatPassword) {
setError('Passwords do not match')
setIsLoading(false)
return
}
try {
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/protected`,
},
})
if (error) throw error
router.push('/auth/sign-up-success')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSignUp}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input
id="repeat-password"
type="password"
required
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link href="/auth/login" className="underline underline-offset-4">
Sign in
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/auth/forgot-password/page.tsx
import { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<ForgotPasswordForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/app/auth/update-password/page.tsx
import { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/update-password-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<UpdatePasswordForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx
'use client'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/update-password`,
})
if (error) throw error
setSuccess(true)
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
{success ? (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Check Your Email</CardTitle>
<CardDescription>Password reset instructions sent</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
If you registered using your email and password, you will receive a password reset
email.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Reset Your Password
</CardTitle>
<CardDescription>
Type in your email and we'll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send reset email'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link href="/auth/login" className="underline underline-offset-4">
Sign in
</Link>
</div>
</form>
</CardContent>
</Card>
)}
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/components/update-password-form.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/protected')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Reset Your Password</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
placeholder="New password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save new password'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-nextjs/components/logout-button.tsx
'use client'
import { useRouter } from 'next/navigation'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
export function LogoutButton() {
const router = useRouter()
const logout = async () => {
const supabase = createClient()
await supabase.auth.signOut()
router.push('/auth/login')
}
return <Button onClick={logout}>Sign out</Button>
}
registry/default/clients/nextjs/lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
registry/default/clients/nextjs/lib/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,
})
// With Fluid compute, don't put this client in a global environment
// variable. Always create a new one on each request.
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Do not run code between createServerClient and
// supabase.auth.getClaims(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// IMPORTANT: If you remove getClaims() and you use server-side rendering
// with the Supabase client, your users may be randomly logged out.
const { data } = await supabase.auth.getClaims()
const user = data?.claims
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth') &&
// the OAuth consent route sends unauthenticated visitors to the login page
// itself, so that it can preserve the authorization in the `next` parameter
request.nextUrl.pathname !== '/oauth/consent'
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/auth/login'
return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is.
// If you're creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
registry/default/clients/nextjs/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
/**
* If using Fluid compute: Don't put this client in a global variable. Always create a new client within each
* function when using it.
*/
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}