clouddatabasereal-time

SERIES: Let's build SupaAuth #5

Protect the Inner Pages

Aftab Alam

Aftab AlamApril 27, 2021

9 min read–––

Aftab Alam

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

5

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

profile.tsx
// ...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 -

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.

profile.tsx
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/*

hello.ts
// 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/

auth.ts
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

AuthContext.tsx
//
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

AuthContext.tsx
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.

profile.tsx
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

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 -

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.

Tweet about this post(by clicking the button above) or like/re-tweet one of the tweets that shares this article, and it will show up here eventually