Building User Authentication with Username and Password using Supabase

Building User Authentication with Username and Password using Supabase

User authentication is essential for web application security and user management. Supabase is an open-source platform that makes it easy to build secure authentication systems. In this blog post, we will show you how to enhance user login options by enabling users to log in with their username and password or their email and password. You can check this example also on the GitHub repo.

Prerequisites

Before we embark on this tutorial, it's crucial to have a foundational understanding of web development and Node.js. Familiarity with Next.js, a popular React framework, will also be advantageous. Additionally, you need Node.js and npm (Node Package Manager) installed on your system.

Setting up the Project

To kickstart our journey, let's create a new Next.js project integrated with Supabase. Execute the following command to establish the initial project structure:

npx create-next-app -e with-supabase username-login

This command will generate a Next.js project preconfigured with Supabase support with default authentication and routes.

Configuring Supabase

Before we start coding, let's set up Supabase for our project. You need a Supabase project to get the credentials you need. Make sure you have your Supabase URL and API keys ready. It's important to keep these credentials safe, such as by using environment variables. You can find more information on configuring Supabase in the Supabase documentation.

Tweaking the Sign-In Route

Now, let's closely examine the code responsible for user sign-in. Navigate to the username-login/app/auth/sign-in/route.ts file in your project. We will replace the default code for user authentication as follows:

Replace:

const { error } = await supabase.auth.signInWithPassword({
  email,
  password,
})

With:

const { data } = await supabase.functions.invoke('sign-in', {
  body: {
    email,
    password,
  }
});

const { error } = await supabase.auth.setSession({
  access_token: data.access_token,
  refresh_token: data.refresh_token
});

This code leverages Supabase Edge Functions to handle user sign-in, enabling users to choose between username and password or email for login.

Edge Function for Sign-In

Within the authentication workflow, this edge function plays a pivotal role in managing sign-in requests. This function offers users greater flexibility by enabling them to use their usernames as an alternative login option. We'll be using this cors.ts file for handling the CORS headers:

export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

To set up the Edge Function for Sign-In, create a Deno script as follows:

import { serve } from 'https://deno.land/std@0.192.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { corsHeaders } from '../_shared/cors.ts'

console.log(`Function "sign-in" is up and running!`)

const options =  {
  auth: {
    flowType: 'implicit',
    autoRefreshToken: false,
    persistSession: false,
    detectSessionInUrl: false
  }
};

serve(async (req: Request) => {
  // This is needed if you're planning to invoke your function from a browser.
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }
  try {
    const supabaseAdmin = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
      options
    )
    const { email, password } = await req.json()
    // Create a Supabase client with the Auth context of the logged-in user.
    const supabaseClient = createClient(
      // Supabase API URL - env var exported by default.
      Deno.env.get('SUPABASE_URL') ?? '',
      // Supabase API ANON KEY - env var exported by default.
      Deno.env.get('SUPABASE_ANON_KEY') ?? '',
      options
    )
    const { data: profileData, error: userError } = await supabaseAdmin
      .from('profiles')
      .select('email')
      .or(`username.eq.${email},email.eq.${email}`)
      .limit(1)
      .maybeSingle();
    console.log("profileData:" + JSON.stringify(profileData, null, 2))
    if (userError) throw userError

    const { data: { session }, error } = await supabaseClient.auth.signInWithPassword({
      email: profileData.email,
      password: password,
    })
    if (error) throw error
    console.log("data:" + JSON.stringify(session, null, 2))
    return new Response(JSON.stringify(session), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 200,
    });
  } catch (error) {
    return new Response(JSON.stringify({ error: error }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' },
      status: 400,
    })
  }
})

This Deno script handles sign-in requests and allows users to choose between usernames and email for login. It leverages Supabase Edge Functions, enhancing the flexibility of your authentication system.

To learn more about testing Supabase Edge Functions, check our dedicated blog post on Testing Supabase Edge Functions with Deno Test.

Here's an example structure for the public.profiles table that you can use in your project:

CREATE TABLE public.profiles (
  id uuid NOT NULL,
  updated_at timestamp with time zone NULL,
  username text NULL,
  full_name text NULL,
  avatar_url text NULL,
  website text NULL,
  email text NULL,
  CONSTRAINT profiles_pkey PRIMARY KEY (id),
  CONSTRAINT profiles_username_key UNIQUE (username),
  CONSTRAINT profiles_id_fkey FOREIGN KEY (id) REFERENCES auth.users (id) ON DELETE CASCADE,
  CONSTRAINT username_length CHECK (char_length(username) >= 3)
);
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

You can read more about creating such a table in Managing User Data.

Conclusion

In this extensive exploration of user authentication with Supabase, we've equipped you with the knowledge and tools to build secure and flexible login systems for your web applications. By enabling users to choose between username and password or email and password login methods, you've enhanced the user experience.

As you continue to develop and fine-tune your authentication systems, consider the following articles for further insights and optimizations:

These articles will complement your journey to master Supabase authentication and take your web development skills to the next level. Experimentation and customization are key to tailoring authentication systems to your specific project needs.

References

For additional guidance and resources, consider exploring these valuable references:

With these references and your newfound knowledge, you're well-prepared to create exceptional web applications powered by Supabase. Continue your journey in web development, and don't hesitate to explore additional features and customization options offered by Supabase and PostgreSQL.