Authentication with Supabase Auth + Vue + NestJS [Implicit flow]

The Brown BoxThe Brown Box
3 min read

Step 1: Google Cloud console settings

Link: https://console.cloud.google.com/apis/credentials?

You need to have google cloud project. Then go to Clients → OAuth 2.0 Client IDs

  1. Add the supabase project url to the origins and redirect urls

<PROJECT_ID>.supabase.co

  1. Now you have to take note these 2 keys: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
    These keys gonna be used in the next step.

Step 2: Supabase Auth settings

  1. Go to Authentication → Sign in / Provider. In the Auth Providers list, enable Google

  1. Then click to the Google to setup. Paste above two key: GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET into below. Also save the redirect url to SUPABASE_CALLBACK_URL

  1. After authenticated, you have to redirect the user to an url, the url need to be listed in the below settings:

Step 3: Frontend setup

Here I’m using Vue (quasar for the components), but it basically the same for all frontend frameworks.
Install: npm install @supabase/supabase-js

  1. Create a supabase client: supabase.ts
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.SUPABASE_URL as string;
const supabaseKey = process.env.SUPABASE_KEY as string;

export const supabase = createClient(supabaseUrl, supabaseKey);
  1. just need to call signInWithOAuth from above instance.

After authentication it gonna be navigated to SUPABASE_CALLBACK_URL that we declare above

const signIn = async () => {
  await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `SUPABASE_CALLBACK_URL`,
    },
  });
};

And this is data it send back to the callback:

access_token=eyJhbGciOiJIUzI1NiIsImtpZCI6IlA5MHlVN3NwL2RzUFhzeUEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2ZweWh6dHlneGtoaHVibW5vYXV4LnN1cGFiYXNlLmNvL2F1dGgvdjEiLCJzdWIiOiJmOGFjOWEyOC05MGVhLTQ0M2UtYTM0Yi00YThhMmYyYjcwZjEiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzUzMDE4MjQwLCJpYXQiOjE3NTMwMTQ2NDAsImVtYWlsIjoiY29uZy5ob2FuZ0BzbGVlay5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6Imdvb2dsZSIsInByb3ZpZGVycyI6WyJnb29nbGUiXX0sInVzZXJfbWV0YWRhdGEiOnsiYXZhdGFyX3VybCI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9hL0FDZzhvY0lMRk0zZFRjdW40ZTQ2djE4aG4xZFFNV1V1UHFHQWVXNktSWExLYXhEUFFKZ21Zdz1zOTYtYyIsImN1c3RvbV9jbGFpbXMiOnsiaGQiOiJzbGVlay5jb20ifSwiZW1haWwiOiJjb25nLmhvYW5nQHNsZWVrLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmdWxsX25hbWUiOiJDb25nIEhvYW5nIiwiaXNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwibmFtZSI6IkNvbmcgSG9hbmciLCJwaG9uZV92ZXJpZmllZCI6ZmFsc2UsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJTEZNM2RUY3VuNGU0NnYxOGhuMWRRTVdVdVBxR0FlVzZLUlhMS2F4RFBRSmdtWXc9czk2LWMiLCJwcm92aWRlcl9pZCI6IjExMDY0NjM0MjgyMzY2NDMzMTg0MSIsInN1YiI6IjExMDY0NjM0MjgyMzY2NDMzMTg0MSJ9LCJyb2xlIjoiYXV0aGVudGljYXRlZCIsImFhbCI6ImFhbDEiLCJhbXIiOlt7Im1ldGhvZCI6Im9hdXRoIiwidGltZXN0YW1wIjoxNzUzMDE0NjQwfV0sInNlc3Npb25faWQiOiIyMWM3ZTg3Yy03MTUxLTRlZmMtOWM4Yi0yNzVkYzU3MWNiZjUiLCJpc19hbm9ueW1vdXMiOmZhbHNlfQ.rZWA7NJKytBsqK27W3Vv9uuDaeDnzv6_RdHNf0nYW5M&expires_at=1753018240&expires_in=3600&provider_token=ya29.a0AS3H6NwoQaW7XYww3TUbZgAdcTD7W6_86hnnpvKVwPQhTMPFdlEIHTfy09ZCm8GAaq5o_U8kk7Cf3aG3GqkySypzPchRcB8J1Vvj65nC5WKE9FcB8-FalwvC2DgWW4CUyP6uwhq09WwjRXdU_atWJ424M76FzyrwiOkD0vB7FdoaCgYKAR8SARcSFQHGX2MiYJlTLLqt8Etq-xARlCbS1g0178&refresh_token=w4jw5h76h3c3&token_type=bearer

So for the simplicity now we can save the accessToken above to localStorage and use it as authentication for the request to the backend.

  1. Update user in the store whenever page is loaded

Handle this event onAuthStateChange to match with your need on handle auth in the app.
Below is my current implementation.


import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { supabase } from "@/supabase";
import { useUserStore } from "@/stores/userStore";

export const AuthHandler = () => {
    const navigate = useNavigate();
    const { setUser, clearUser } = useUserStore();

    useEffect(() => {
        console.log("Setting up Supabase auth listener...");

        const { data: { subscription } } = supabase.auth.onAuthStateChange(
            (event, session) => {
                console.log("🔐 Supabase Auth Event:", event);
                console.log("👤 Session:", session);

                switch (event) {
                    case 'SIGNED_IN':
                        console.log("✅ User signed in successfully");
                        console.log("User ID:", session?.user?.id);
                        console.log("User Email:", session?.user?.email);

                        // Store user data in Zustand store
                        if (session?.user) {
                            setUser(session.user);
                        }
                        break;

                    case 'SIGNED_OUT':
                        console.log("❌ User signed out");

                        // Clear user data from Zustand store
                        clearUser();

                        // Navigate to home page
                        console.log("🏠 Navigating to home page after logout");
                        navigate("/");
                        break;

                    default:
                        console.log("❓ Unknown auth event:", event);
                }
            }
        );

        // Cleanup subscription on unmount
        return () => {
            console.log("Cleaning up Supabase auth listener...");
            subscription.unsubscribe();
        };
    }, [setUser, clearUser, navigate]);

    return null; // This component doesn't render anything
};

Step 4: Validate token in backend

We already have the token from above step, so whenever user call API let add it to the header→authentication, now we create a guard to validate it.

auth.guard.ts

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createClient, SupabaseClient } from '@supabase/supabase-js';

@Injectable()
export class AuthGuard implements CanActivate {
  private supabase: SupabaseClient;

  constructor(private readonly configService: ConfigService) {
    this.supabase = createClient(
      this.configService.get('SUPABASE_URL'),
      this.configService.get('SUPABASE_KEY'),
    );
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const accessToken = request.headers['authorization'];

    if (!accessToken) {
      return false;
    }

    const user = await this.supabase.auth.getUser(accessToken);
    if (!user.data.user) {
      return false;
    }
    request.user = user.data.user;
    return true;
  }
}

I just create simple authentication here.
Now we add it to our endpoint/controller:

@Controller()
export class AppController {
  @Get('user')
  @UseGuards(AuthGuard)
  getUser(@Req() req: Request) {
    return req['user'];
  }
}

So now whenever we got 403, we should navigate user to login page.

Refs:
https://supabase.com/docs/guides/auth/social-login/auth-google

0
Subscribe to my newsletter

Read articles from The Brown Box directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

The Brown Box
The Brown Box