Getting Started with OAuth in NUXT3 using Supabase

This will cover how you can setup "Continue with Google" login option on your website/ web app.
Link to this project's repo can be found here

Setting Up NUXT3 Project

npx nuxi@latest init <project-name>

Installing And Set-up of Required Dependency

  • Installing Supabase Nuxt Module

    @nuxtjs/supabase is a Nuxt module for integrating Nuxt web app with Supabase.
    npx nuxi@latest module add supabase
    

Adding @nuxtjs/supabase to modules section of nuxt.config.ts :

export default defineNuxtConfig({
  modules: ['@nuxtjs/supabase'],
})
  • Setting Up .env

    SUPABASE_URL="https://example.supabase.co"
    SUPABASE_KEY="<your_key>"
    

Supabase Project Setup

  1. Create a new project on Supabase.

  2. Copy the anon public key to SUPABASE_URL and service_role secret key to SUPABASE_KEY in your .env folder.

  3. Go to Authentication from you project dashboard and providers > Google > Enable Sign in with Google. Copy the Callback URL (for OAuth) for use in further reference. The other fileds like Client ID (for OAuth) and Client Secret (for OAuth) will be filled once the project is set up on Google Developer Console.

  4. Go to console.developers.google.com and set-up a new project.

  5. Go to Dashboard of your project > Go to API's overview > OAuth consent screen and set user type to External, after that fill up the required app information like Name, email etc. Save and contiue the other steps if no special information has to be provided.

  6. After the above steps, from the project dashboard itself go to Credentials > Create New > OAuth client ID > Application type -> Web Application. In Authorized redirected URLs paste the URL we copied while enabling Google OAuth in Supabase and hit Create. Copy the Client ID and Client secret.

  7. Paste the Client ID and Client secret to Client ID (for OAuth) and Client Secret (for OAuth) in Supabase Auth set-up respectively and hit Save.

Project Structure

Create pages folder and add index.vue, login.vue, confirm.vue and secure.vue.

  • index.vue : Page that is accessible even with or without authentication.
  • login.vue : Page on which authentication will be performed. -confirm.vue : Page that confirms authentication of user and redirects to appropriate page (here /secure).
  • secure.vue : Page that cannot be accessed without authentication.

Create middleware folder and add auth.js to it.

  • auth.js : Middleware that'll keep check that the user isn't able to access the login page after loggin in / signing in.

Final folder structure should look something similar to this :

├── .nuxt
├── middleware
│   ├──auth.js
├── pages
│   ├── index.vue
│   ├── login.vue
│   ├── confirm.vue
│   ├── secure.vue
├── public
├── server
├── .env
├── .gitignore
├── app.vue
├── nuxt.config.ts
└── README.md

Coding

nuxt.config.ts

Tinkering Supabase in nuxt.config.ts as by default @nuxtjs/supabase module secures all routes, while in this case we want to ignore the home ('/') route so we will modify the settings.

export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  modules: ['@nuxtjs/supabase'],
  supabase: {
    redirectOptions: {
      login: '/login',
      callback: '/confirm',
      include: undefined,
      exclude: ['/'],
      cookieRedirect: false,
    },
  },
})

index.vue

Index page won't require any authentication to access and in this case for extras, we have added a simple login button and some text.

<script setup>
const login_to = ()=> {
    return navigateTo('/login')
}
</script>

<template>
    <div>
        <div class="for-button">
            <button type="button" @click="login_to">Login</button>
        </div>
        <div>
            <h2 class="rotate">Index</h2>
            <h2>Page</h2>
        </div>
    </div>
</template>

<style lang="css" scoped>
div {
  font-family: "didot";
}

h2 {
  font-size: 100px;
  margin: 0;
   writing-mode: vertical-lr;
  text-align: center;
  line-height: .9;
}

.rotate {
   transform: rotate(180deg);
}

div {
  display: grid;
  height: 100vh;
  justify-content: center;
  align-content: center;
  grid-template-columns: max-content max-content;
}

body {
  margin: 0;
}

.for-button {
    position: relative;
    flex: 1;
}
button {
    position: fixed;
    cursor: pointer;
    top: 15px;
    right: 15px;
    border: none;
    border-radius: 12px;
    background-color: rgba(104, 85, 224, 1);
    color: white;
    padding: 13px 30px;
    font-size: 18px;
    transition-duration: 0.4s;
}
button:hover {
    border: 1px solid  rgba(104, 85, 224, 1);
    background-color: white;
    color:  rgba(104, 85, 224, 1);
}
</style>

login.vue

Login page contains the Continue with Google button along with the logic to login in and redirect the user to /confirm route which will confim if the user is signed-in or not.

<script setup lang="ts">

const supabase = useSupabaseClient()
const signInWithOAuth = async () => {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: 'http://localhost:3000/confirm',
    },
  })
  if (error) console.log(error)
}

const signOut = async () => {
  const { error } = await supabase.auth.signOut()
  if (error) console.log(error)
}

definePageMeta({
  middleware: ["auth"],
})
</script>


<template>
  <div>
    <button type="button" class="login-with-google-btn" @click="signInWithOAuth" >
      Sign in with Google
    </button>
  </div>
</template>

<style scoped>
.login-with-google-btn {
  transition: background-color .3s, box-shadow .3s;

  padding: 12px 16px 12px 42px;
  border: none;
  border-radius: 3px;
  box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25);

  color: #757575;
  font-size: 14px;
  font-weight: 500;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",sans-serif;

  background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTcuNiA5LjJsLS4xLTEuOEg5djMuNGg0LjhDMTMuNiAxMiAxMyAxMyAxMiAxMy42djIuMmgzYTguOCA4LjggMCAwIDAgMi42LTYuNnoiIGZpbGw9IiM0Mjg1RjQiIGZpbGwtcnVsZT0ibm9uemVybyIvPjxwYXRoIGQ9Ik05IDE4YzIuNCAwIDQuNS0uOCA2LTIuMmwtMy0yLjJhNS40IDUuNCAwIDAgMS04LTIuOUgxVjEzYTkgOSAwIDAgMCA4IDV6IiBmaWxsPSIjMzRBODUzIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNNCAxMC43YTUuNCA1LjQgMCAwIDEgMC0zLjRWNUgxYTkgOSAwIDAgMCAwIDhsMy0yLjN6IiBmaWxsPSIjRkJCQzA1IiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48cGF0aCBkPSJNOSAzLjZjMS4zIDAgMi41LjQgMy40IDEuM0wxNSAyLjNBOSA5IDAgMCAwIDEgNWwzIDIuNGE1LjQgNS40IDAgMCAxIDUtMy43eiIgZmlsbD0iI0VBNDMzNSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PHBhdGggZD0iTTAgMGgxOHYxOEgweiIvPjwvZz48L3N2Zz4=);
  background-color: white;
  background-repeat: no-repeat;
  background-position: 12px 11px;

  &:hover {
    box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 2px 4px rgba(0, 0, 0, .25);
  }

  &:active {
    background-color: #eeeeee;
  }

  &:focus {
    outline: none;
    box-shadow: 
      0 -1px 0 rgba(0, 0, 0, .04),
      0 2px 4px rgba(0, 0, 0, .25),
      0 0 0 3px #c8dafc;
  }

  &:disabled {
    filter: grayscale(100%);
    background-color: #ebebeb;
    box-shadow: 0 -1px 0 rgba(0, 0, 0, .04), 0 1px 1px rgba(0, 0, 0, .25);
    cursor: not-allowed;
  }
}


div {
  text-align: center;
  padding-top: 2rem;
}
</style>

confirm.vue

Confirm page is just confirms if user is signed-in or not and then redirects them to appropriate page (here /secure).

<script setup lang="ts">
const user = useSupabaseUser()

// Get redirect path from cookies
const cookieName = useRuntimeConfig().public.supabase.cookieName
const redirectPath = useCookie(`${cookieName}-redirect-path`).value

watch(user, () => {
  if (user.value) {
      // Clear cookie
      useCookie(`${cookieName}-redirect-path`).value = null
      // Redirect to path
      return navigateTo(redirectPath || '/secure'); 
  }
}, { immediate: true })
</script>
<template>
  <div>Waiting for login...</div>
</template>

secure.vue

Secure page cannot be accessed without authentication and has logic to log-out a user.

<script setup>
const supabase = useSupabaseClient()
const signOut = async () => {
  const { error } = await supabase.auth.signOut()
  if (error) console.log(error)
  navigateTo('/login')
}
</script>


<template>
    <div class="centre">
        <div class="space">secure page</div>
        <button type="button" @click="signOut">Log Out</button>
    </div>
</template>

<style scoped>
.centre {
    text-align: center;
    padding-top: 2rem;
    font-size: 30px;
}

.space {
    padding-top: 50px;
    padding-bottom: 50px;
}

button {
    border: none;
    font-size: 25px;
    background-color: rgb(39, 39, 39);
    color: white;
    padding: 15px 32px;
    border-radius: 12px;
    transition-duration: 0.4s;
}
button:hover {
    background-color: white;
    color: black;
    border: 1px solid black;
}
</style>

Running Project

npm run dev

Or as per your package manager.

The following tutorial covered how to add OAuth functionality (Google OAuth to be specific) to your website/ webapp.
You can find more info from the Official documentations :