Protected routes

Hello! I'd like to achieve a route for example /admin which should be only accessible for "admin" role users. If the user have "user" role, they should be redirected to the home page. Also I'd followed the docs, but I don't know how to make the /sign-in route inaccessible for those who are already signed in. Later I'd like more complex role based access control for routes. How to achieve this?
30 Replies
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
palicz
paliczOP2mo ago
I wanted to know how to do it.
daveycodez
daveycodez2mo ago
There are 3 ways to do this, you can do it Client side with the useSession hook, through middleware, or through the route itself What is your setup
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
daveycodez
daveycodez2mo ago
I'm wondering why the docs are recommending sending an entire fetch request in middleware when you already have the cookie Yea so it looks like you need to use better-fetch in middleware to get the latest verified session data, however I made a helper function that checks the Cookie directly and verifies it without needing an extra request, so if you need extremely low latency middleware for some reason
if (request.cookies.has("better-auth.session_data")) {
const session = await verifySession(request.cookies.get("better-auth.session_data")!.value)
}
if (request.cookies.has("better-auth.session_data")) {
const session = await verifySession(request.cookies.get("better-auth.session_data")!.value)
}
export type Session = typeof auth.$Infer.Session

export const verifySession = async (sessionDataCookie?: string) => {
const sessionDataPayload = sessionDataCookie
? safeJSONParse<{
session: Session
signature: string
expiresAt: number
}>(binary.decode(base64.decode(sessionDataCookie)))
: null

if (sessionDataPayload) {
const isValid = await createHMAC("SHA-256", "base64urlnopad").verify(
process.env.BETTER_AUTH_SECRET!,
JSON.stringify(sessionDataPayload.session),
sessionDataPayload.signature,
)

if (isValid) return sessionDataPayload

}

return null
}

function safeJSONParse<T>(data: string): T | null {
try {
return JSON.parse(data)
} catch {
return null
}
}
export type Session = typeof auth.$Infer.Session

export const verifySession = async (sessionDataCookie?: string) => {
const sessionDataPayload = sessionDataCookie
? safeJSONParse<{
session: Session
signature: string
expiresAt: number
}>(binary.decode(base64.decode(sessionDataCookie)))
: null

if (sessionDataPayload) {
const isValid = await createHMAC("SHA-256", "base64urlnopad").verify(
process.env.BETTER_AUTH_SECRET!,
JSON.stringify(sessionDataPayload.session),
sessionDataPayload.signature,
)

if (isValid) return sessionDataPayload

}

return null
}

function safeJSONParse<T>(data: string): T | null {
try {
return JSON.parse(data)
} catch {
return null
}
}
middleware.ts
const protectedRoutes = ["/books"]

export async function middleware(request: NextRequest) {
if (protectedRoutes.includes(request.nextUrl.pathname)) {
let session = (await verifySession(request.cookies.get("better-auth.session_data")?.value))?.session

if (!session && request.cookies.has("better-auth.session_token")) {
const { data } = await betterFetch<Session>(
"/api/auth/get-session",
{
baseURL: request.nextUrl.origin,
headers: {
//get the cookie from the request
cookie: request.headers.get("cookie") || "",
},
},
)

if (data) {
session = data
}
}

if (!session) {
return NextResponse.redirect(new URL("/", request.url))
}
}

return NextResponse.next();
}
const protectedRoutes = ["/books"]

export async function middleware(request: NextRequest) {
if (protectedRoutes.includes(request.nextUrl.pathname)) {
let session = (await verifySession(request.cookies.get("better-auth.session_data")?.value))?.session

if (!session && request.cookies.has("better-auth.session_token")) {
const { data } = await betterFetch<Session>(
"/api/auth/get-session",
{
baseURL: request.nextUrl.origin,
headers: {
//get the cookie from the request
cookie: request.headers.get("cookie") || "",
},
},
)

if (data) {
session = data
}
}

if (!session) {
return NextResponse.redirect(new URL("/", request.url))
}
}

return NextResponse.next();
}
This won't respect if you get "logged out" from another computer like during a reset password flow or something though, just a low latency function if you need rapid verification or less HTTP requests / queries in general and don't care too much about log out others feature It will first try to verify the session if session_data cookie is present, and if that cookie is not present or it fails (expired session) then it will fallback to send a request to get-session if the session_token cookie is present If you are using a static export or need to do it client side then ..
export default function ProtectedPage() {
const router = useRouter()
const { data: session, isPending } = useSession()

useEffect(() => {
if (!session && !isPending) router.push("/")
}, [session, isPending, router])

if (!session) return <>Loading...</>
}
export default function ProtectedPage() {
const router = useRouter()
const { data: session, isPending } = useSession()

useEffect(() => {
if (!session && !isPending) router.push("/")
}, [session, isPending, router])

if (!session) return <>Loading...</>
}
palicz
paliczOP2mo ago
Thanks! I will try out your approach for sure @daveycodez I actually just realized, that I don't understand your explaination This is your middleware.ts Or this one can also be the middleware.ts What's this? Also I don't really know how to create an adminroute from this. I mean I tried, but I got errors
daveycodez
daveycodez2mo ago
The difference with my solution is that it uses cookies first before sending the fetch request It's not good practice to use HTTP requests in Next.js middleware, it should be optimistic routing
palicz
paliczOP2mo ago
Okay, I just realized what you were talking about here I had problem with this:
palicz
paliczOP2mo ago
No description
palicz
paliczOP2mo ago
The commented part It complitely ruined the terminate session-like stuff
daveycodez
daveycodez2mo ago
That enforces the session to be alive for 5 minutes even if it gets logged out elsewhere
palicz
paliczOP2mo ago
import { betterFetch } from '@better-fetch/fetch';
import { type NextRequest, NextResponse } from 'next/server';

import type { auth } from '@/lib/auth';

// Define Session type based on auth module's session type
type Session = typeof auth.$Infer.Session;

/**
* Middleware to protect routes by checking if user is authenticated
* @param request - The incoming Next.js request object
* @returns NextResponse with either redirect or next()
*/
export default async function authMiddleware(request: NextRequest) {
// Fetch the current session by making API call to auth endpoint
const { data: session } = await betterFetch<Session>(
'/api/auth/get-session',
{
baseURL: request.nextUrl.origin,
headers: {
// Get the cookie from the request headers
cookie: request.headers.get('cookie') || '',
},
},
);

// If no session exists, redirect to sign-in page
const isSignInRoute = request.nextUrl.pathname === '/sign-in';
if (!session && !isSignInRoute) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}

if (session && isSignInRoute) {
// Redirect signed-in users attempting to access sign-in page
return NextResponse.redirect(new URL('/', request.url));
}
// Check if route requires admin access
const isAdminRoute = request.nextUrl.pathname.startsWith('/admin');
if (isAdminRoute && session?.user.role !== 'admin') {
// Redirect non-admin users attempting to access admin routes
return NextResponse.redirect(new URL('/', request.url));
}

// Otherwise allow request to continue
return NextResponse.next();
}

// Configure which routes this middleware should run on
export const config = {
matcher: ['/dashboard', '/admin/:path*', '/sign-in'],
};
import { betterFetch } from '@better-fetch/fetch';
import { type NextRequest, NextResponse } from 'next/server';

import type { auth } from '@/lib/auth';

// Define Session type based on auth module's session type
type Session = typeof auth.$Infer.Session;

/**
* Middleware to protect routes by checking if user is authenticated
* @param request - The incoming Next.js request object
* @returns NextResponse with either redirect or next()
*/
export default async function authMiddleware(request: NextRequest) {
// Fetch the current session by making API call to auth endpoint
const { data: session } = await betterFetch<Session>(
'/api/auth/get-session',
{
baseURL: request.nextUrl.origin,
headers: {
// Get the cookie from the request headers
cookie: request.headers.get('cookie') || '',
},
},
);

// If no session exists, redirect to sign-in page
const isSignInRoute = request.nextUrl.pathname === '/sign-in';
if (!session && !isSignInRoute) {
return NextResponse.redirect(new URL('/sign-in', request.url));
}

if (session && isSignInRoute) {
// Redirect signed-in users attempting to access sign-in page
return NextResponse.redirect(new URL('/', request.url));
}
// Check if route requires admin access
const isAdminRoute = request.nextUrl.pathname.startsWith('/admin');
if (isAdminRoute && session?.user.role !== 'admin') {
// Redirect non-admin users attempting to access admin routes
return NextResponse.redirect(new URL('/', request.url));
}

// Otherwise allow request to continue
return NextResponse.next();
}

// Configure which routes this middleware should run on
export const config = {
matcher: ['/dashboard', '/admin/:path*', '/sign-in'],
};
This is my middleware.ts for now. It gets the job done, but I don't know if I did anything that's not supposed to be
daveycodez
daveycodez2mo ago
If the matcher is just those 3 paths its probably fine Just need to be careful if you end up using middleware for everything (internationalization or something)
palicz
paliczOP2mo ago
Well I'll have the dashboard which is the user's settings page. I'll have the admin routes for admin users, and the sign in page is no longer accessible if the user is logged in. I think this is all for now
daveycodez
daveycodez2mo ago
Yea as long as its not sending out a fetch request on all of your routes it can be ok
palicz
paliczOP2mo ago
Later I will have buttons that will have an action, but won't work without sessions. I think I don't need the middleware for that. I'm doing my Uni thesis, and that's what I need it for, but I need my app to be insanely fast (at least the teacher should think that it is) So that's why I wanted to know if there's a better way of implementing this Because performance is what I need mostly
daveycodez
daveycodez2mo ago
If you want faster performance for those 3 pages specifically you can use the cookie "optimistic" middleware which would just check if the cookie exists at all, then the routes themselves will handle getting the current user request.cookies.has("better-auth.session_data")
palicz
paliczOP2mo ago
I don't find performance issues just yet but I want to implement metrics somehow to actually see if the performance of the page. If I find issues with performance for these routes, I will implement what you adviced
daveycodez
daveycodez2mo ago
Well right now if you refresh an admin page or navigate an admin page you will have this... fetch -> /admin -> fetch ->/api/get-session-> Query database If you do the cookie method it will just be fetch -> /admin -> check cookie
palicz
paliczOP2mo ago
But does the cookie method have any disadvantages?
daveycodez
daveycodez2mo ago
Well middleware is mainly meant to be used for "optimistic" routing and not protection itself the protection should live on the route itself, where you'll get the session on the route and handle it over there the middleware just optimistically decides whether the user is logged in or not the cookie only gets set if they are logged in, but it could be "expired" https://www.youtube.com/watch?v=N_sUsq_y10U This video covers how they handle authentication best practices if you were to "roll your own" auth, but shows examples of how routes protect themselves and middleware is used for optimistic routing This is from the Next.js head of developer relations So pretty much the middleware says -> Hey, the user isn't even logged in, so just skip loading the route.
palicz
paliczOP2mo ago
Understandable, great to know! Could I contact you in private for not so better-auth connected questions about NextJS?
daveycodez
daveycodez2mo ago
Yea that's no prob
if (protectedRoutes.includes(request.nextUrl.pathname)) {
const isLoggedIn = request.cookies.has("better-auth.session_token")

if (!isLoggedIn) {
return NextResponse.redirect(new URL("/", request.url))
}
}
if (protectedRoutes.includes(request.nextUrl.pathname)) {
const isLoggedIn = request.cookies.has("better-auth.session_token")

if (!isLoggedIn) {
return NextResponse.redirect(new URL("/", request.url))
}
}
This is how I think I'm going to be handling middleware routing But yea I'm going to make a GitHub issue to update the better-auth docs or something because fetch requests in middleware or Edge is high latency
palicz
paliczOP2mo ago
I barely found anything in the docs about how to structure an optimal middleware.
daveycodez
daveycodez2mo ago
Yea better-auth is platform agnostic so we're still figuring out some of the platform specific optimizations
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
daveycodez
daveycodez2mo ago
I think you would still have the cookie here in your middleware, so you can just check if the cookie exists
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View
daveycodez
daveycodez2mo ago
Did you guys try these auth settings to get cross-domain cookies? I was using Bearer for Capacitor localhost since it isnt hosted on the domain, but with these settings, I don't need Bearer
trustedOrigins: ["http://localhost:3000"],
advanced: {
defaultCookieAttributes: process.env.NODE_ENV === "production" ? {
sameSite: "none",
secure: true,
} : undefined
},
trustedOrigins: ["http://localhost:3000"],
advanced: {
defaultCookieAttributes: process.env.NODE_ENV === "production" ? {
sameSite: "none",
secure: true,
} : undefined
},
Unknown User
Unknown User2mo ago
Message Not Public
Sign In & Join Server To View

Did you find this page helpful?