Go back

OAuth with Supabase in a Vite React app

A while back, I tried extending Supabase's OAuth API into this project I'm building, but I struggled with it for a while because I was new to the whole idea of using it in a project — any project at all!

This guide is a very short one. So, I'll jump right to it.

Basic setup

For starters, if you want to enable OAuth in your app with Supabase, you would need to use a snippet similar to the one from their docs.

/pages/signin.tsx
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: `${process.env.APP_URL}/oauth`,
  },
})

You need to include the redirectTo property in theoptions object to specify where to take your users when the OAuth flow with your selected provider completes.

It is good to always specify this, so you have control of what to do with the information (mostly a token) that you get from the OAuth flow. The problem I had with Supabase's doc is that there isn't any information on how to handle redirect urls or what exactly one would do when the response comes back.

If you don't include a redirect URL, you'll be taken back to your app's index route. In this case, localhost:5173 since I'm using vite for my project. process.env.APP_URL has the value, hence the reason I used it in the snippet above.

Exchange access token for user data

Since I've specified my redirect url to be localhost:5173/oauth from the snippet above. I needed to create a new route/page so I can extract the access_token parameter from the URL.

Here's what my /oauth route looks like

/pages/oauth.tsx
import React from 'react'
import { setCookie } from 'cookies-next'
import { Center, Text } from '@chakra-ui/react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useToastContext } from '@hooks/toast'
import { authCookieOptions } from '@utils/misc'
import { useAuthContext } from '@hooks/auth'
import { exchange } from '@utils/oauth-helpers'
 
export const Oauth = () => {
  const location = useLocation()
  const navigate = useNavigate()
  const { openToast } = useToastContext()
  const { authenticator } = useAuthContext()
 
  React.useEffect(() => {
    const exchangeTokenForUser = async () => {
      const params = new URLSearchParams(location.hash.slice(1))
      const accessToken = params.get('access_token')
 
      try {
        if (accessToken) {
          setCookie('_gat', accessToken, {
            ...authCookieOptions,
          })
 
          const user = await exchange(accessToken)
 
          authenticator(true, user)
          navigate('/dashboard')
          openToast('Logged in successfully!', 'success')
        } else {
          navigate('/signin')
          openToast('something went wrong! Try again.', 'error')
        }
      } catch (error) {
        openToast(`${error}`, 'error')
      }
    }
 
    exchangeTokenForUser()
  }, [authenticator, location.hash, navigate, openToast])
 
  return (
    <Center height="100vh">
      <Text>Please wait, while we take you to your dashboard...</Text>
    </Center>
  )
}

In the snippet above, I'm doing a couple of things, but the most important one is the exchangeTokenForUser function. In that function, I read the access_token parameter from the URL segment.

But, before I could do that. I had to omit the "#" with this

const params = new URLSearchParams(location.hash.slice(1))

If the request is successful, I store the access token in a cookie — This isn't necessary since Supabase already handles the user session for us — I just chose to do so. Think of it as a personal preference.

Then I proceed to exchange the token for user data. Supabase's auth.getUser() method accepts an optional token argument that you can pass to it. So, here's what the exchange function looks like

/utils/oauth-helpers
// rest of the file
 
export const exchange = async (token: string) => {
  const { data } = await supabase.auth.getUser(token)
 
  return data?.user
}

When that's successful, I pass the user data to the authenticator function from my AuthProvider to update the user's logged-in state and re-route them to the dashboard with the useNavigate() hook from React Router, and a toast component from my ToastProvider is rendered showing the successful message.

When the process isn't successful, they're taken back to the "/signin" route. To make sure that this works accordingly. I made sure to include the necessary dependencies in the dependency array of the Effect hook.

Wrapping up.

This took me quite a while to figure out. I remember asking questions in the Supabase Discord community. Heck! I even went on to start a discussion around this in their repo.

I'm glad I was able to figure this out though, so you don't have to go through the stress I went through, trying to figure it out.

Although there may be other approaches to solving this problem, I haven't found any. This came easily to me. But, feel free to use whatever works for you and ping me, perhaps, on Twitter, when you find any.