SERIES: Let's build SupaAuth #5
Protect the Inner Pages
9 min read • –––
You're on part 5 of the 6-part series Let's build SupaAuth where You'll build an authentication system using Supabase and Next.js
Protect the Inner Pages
In the previous article, we got to leverage Supabase's event system, to take actions that are based on the user's authentication state like
- redirection to a default protected page
- hiding/showing content
but the security has still room for some good improvements. In this article, we'll make the system even more robust.
Securing the user's profile
On the /profile
page if you'll visit after sign out, you'll have the content unavailable, and a message indicating that you're not logged in. That's okay for very simplistic scenarios, and lightweight apps, where you just want to protect the content but not the routes. Realistically, that's not even desired in highly secure apps and we don't give anonymous users even the opportunity to visit and stay on such pages. Let's see how we could implement that
Secure by Client-Side Redirection
Amazingly since the heavy-lifting of sharing logged-in information(user object) is already done expressing this logic will be as simple as checking the logged-in state in a useEffect
hook and redirecting the user based on it.
Let's express is in code
// ...all the previous imports
import { useEffect } from 'react'
import Router from 'next/router'
import { ROUTE_AUTH } from '~/config'
const ProfilePage = ({}) => {
const { user, userLoading, signOut, loggedIn } = useAuth()
useEffect(() => {
if (!userLoading && !loggedIn) {
Router.push(ROUTE_AUTH)
}
}, [userLoading, loggedIn]);
if(userLoading) {
return <SpinnerFullPage/>
}
// ...all the previous code
With this, we now have an approach we can use on any page that we'd like to protect. The code for this could be found here -
Add client-side route protection
To re-use and avoid repetition this can also be turned into a React HoC that we can wrap around all the pages that we'd like to protect.
Since the Layout page <Layout>...
gets called on every page, we can even create a Private Layout component, where we can place the auth check logic and control whether we're going to let a user view or redirect. I'll leave that as an exercise.
Secure by Server-Side Redirection
Next.js is a very essential component of our setup, but we're not leveraging some of Next.js great features so far besides the tooling. Classic React has proven just sufficient for all of the use cases expressed so far. Let's learn a bit more about Next.js and apply some of the constructs offered by it to achieve server-side redirection.
A bit about Next.js
Next.js is not just a React Front-end Framework for SPAs. It's a Full-stack UI framework that doubles up as a pre-rendering(SSR, SSG) engine. Every page we create(if not opted for SSG/static builds) is akin to an independent serverless function, capable of running and returning the full markup from the server-side. This characteristic of pre-rendering leads to blazing-fast TTFB, and it's one of its main selling points. The ability to pre-render needs options and presents opportunities that the framework creatively utilizes to offer us ways to prepare data, and take actions on the server-side before your render functions are called. We'll see two of the most important features offered for the same
getServerSideProps
Next.js exposes getServerSideProps
to prepare data/props for the mark-up on the server-side. It's a pure state-less function that gets executed before Next.js gets on with the duty of rendering. Amazingly, this method can do almost everything you imagine doing on the backend side. It can poll services, call APIs, set response headers, establish DB connections, and also choose if your application can go about rendering the page or not. As an added benefit, getServerSideProps
helps us deal with FOUC too and will come in handy to fix the drawback with our implementation. As the redirection solution implemented previously is client-side and is executed when the page is ready, your users get to see a flash of unstyled content before the logic to validate them kicks in and resolves. That's not a very desirable thing to have in robust systems. getServerSideProps
can let us elegantly deal with the issue through server-side redirection.
export const getServerSideProps: GetServerSideProps = async ({ req }): Promise<NextAppPageServerSideProps> => {
// we can check for an active user session here...
// and can do a re-direction from the server
if(!user) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}
// or, alternatively, can send properties that could be available to the page compononents
// The following lines won't be used as we're redirecting above
return {
props: {
// ...props here
}
}
}
API routes
API routes are special Next.js conventions that facilitate easy API route creation. Just like the way we create pages, we can create APIs by putting any .ts
file under /pages/api/*
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
res.status(200).json({ name: 'John Doe' })
}
will lead to /api/hello
that returns { name: 'John Doe' }
as a JSON response.
We'll use the getServerSideProps
and API routes to solve for Server Side Security, but let's understand why we're doing this again.
The majority of interaction in Supabase is between the client(your browser) and the Supabase service. Your server-side of the app has no clue if such a talk is taking place. To inform, and use the functionality we need to tell Supabse to retain the user information for APIs. This makes the user information capable of being known on to the other side of the wire too. As we're verifying the user's authentication on the server we'll have to set a cookie on the server-side. This cookie will be a key to knowing your users on the server.
Let's apply what we've learned
We'll first set a cookie on the server as soon as the user logs in through Supabase user setter method. To support that we'll need an API in our application. Let's go ahead and register an API route at /api/auth
by putting a file at auth.ts
at /pages/api/
import { NextApiRequest, NextApiResponse} from 'next'
import { supabase } from '~/lib/supabase'
export default function handler(req: NextApiRequest , res: NextApiResponse) {
supabase.auth.api.setAuthCookie(req, res)
}
Supabase exposes the cookie setters and getter methods for server-side usage on the auth.api
namespace. In Next.js all the .ts
files placed under /pages/api
register an API route where you can process client requests.
The above exercise will leave us with an api/auth
that we can hit to set the user cookie, which can be further be retrieved from any of the pages on the server-side. Once we have it set up, the client will be responsible to hit the APIs and make a user session known to the server.
Let's introduce the following method for making the API calls
//
import { User, Session, AuthChangeEvent } from '@supabase/supabase-js' // Import Session, and AuthChangeEvent
//
// put this where the signIn, signOut, etc. methods are placed
const setServerSession = async (event: AuthChangeEvent, session: Session) => {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
Now the questions comes is where shuld you call this method? on AuthChangeEvent
we used earlier is the perfect place to hit this API. Let's tweak our AuthContext
hook to get the API hit as soon as a iuser logs in. Update the onAuthStateChange
method too
const { data: authListener } = supabase.auth.onAuthStateChange(
async (event, session) => {
const user = session?.user! ?? null
setUserLoading(false)
await setServerSession(event, session) // <- placing here will take care of setting, as well as re-setting the API maintained user session
if (user) {
setUser(user)
setLoggedin(true)
Router.push(ROUTE_HOME)
} else {
setUser(null)
Router.push(ROUTE_AUTH)
}
}
)
We now have taken care of making the user session known to the APIs. setServerSession
paired with the api/auth
endpoint, will take care of setting as well as removing the cookie. Let's use it to enforce redirection on the server-side.
Cookie once set can later be checked for availability or authentication using a supabase.auth.api.getAuthCookie(req)
method, and we'll use it to get the logged-in user.
export const getServerSideProps: GetServerSideProps = async ({ req }): Promise<NextAppPageServerSideProps> => {
const { user } = await supabase.auth.api.getUserByCookie(req)
// We can do a re-direction from the server
if(!user) {
return {
redirect: {
destination: '/',
permanent: false,
},
}
}
// or, alternatively, can send the same values that client-side context populates to check on the client and redirect
// The following lines won't be used as we're redirecting above
return {
props: {
user,
loggedIn: !!user
}
}
}
Go ahead and add the types this page uses in src/types/app.ts
import { User } from '@supabase/supabase-js'
// pre-existing code
export type NextAppPageUserProps = {
props: {
user: User,
loggedIn: boolean
}
}
export type NextAppPageRedirProps = {
redirect: {
destination: string,
permanent: boolean
}
}
export type NextAppPageServerSideProps = NextAppPageUserProps | NextAppPageRedirProps
Sign-in, sign-out, sign-in, and sign-out again and it should work like a charm now. The code for all the SSR redirection work could be found here -
Add server-side route protection
and in a fix here.
Fix a Sign-out Gotcha!
You'd notice a bit of latency while signing out, and you must be wondering why's that so?. It's because with signout you first send an event to Supbase and then in turn it tells your app that sign-out has taken place. This round trip could lead to a janky, odd experience and leave your app in an inconsistent state in few cases. Although Supabase considers, a sign-out as a client-side event, to give a better experience we can immediately redirect a user to a dedicated sign-out page, post flushing the cookies to not let the sign-out process be perceived as delayed. Refer this to understand the issue in detail.
The case of two approaches
Having the user information from the server make few things we're doing in the context like setting user redundant...but I'm leaving the code for flexibility in choosing what you wanna go with
Takeaways/What's Next?
This kinda completes the full circle of authorization related work, necessary to have a good security of the inner pages in place. In the next part, we'll see few add-on things we can do with this solid email/password login system we have been able to create.
Thank you for being till the end 🙌 . If you enjoyed this article, or learned something new, please take a second to tweet this article or share on LinkedIn so others can discover it. Or add to the discussion on Twitter.