Secure Password Verification and Update with Supabase and PostgreSQL

Rodrigo Mansueli - Jul 4 '23 - - Dev Community

In the world of web and mobile app development, having a robust backend infrastructure is crucial for ensuring smooth and secure operations. Built on top of PostgreSQL, Supabase offers a complete backend platform with real-time capabilities, authentication, and database management. In this blog post, we'll explore how Supabase leverages the power and scalability of PostgreSQL to implement secure password verification and update functionality in your applications.

Background on User Password Verification

Password verification is a critical aspect of application security. It ensures that only authorized users can access and modify their accounts, protecting sensitive data and maintaining the trust of your users. When implementing password updates, it's essential to verify the old password before allowing any changes. This step prevents unauthorized modifications and enhances the overall security of your application. You can enforce secure password change by going into the dashboard -> Authentication -> Providers:

Overview of the Implementation

To implement password verification and update functionality using Supabase and PostgreSQL, we'll walk through the code that accomplishes this task. The code consists of three parts: a PostgreSQL function, an Edge function written in TypeScript, and React code that calls the Edge function.

Creating the verify_user_password Function in PostgreSQL

The first part involves creating a PostgreSQL function called verify_user_password. This function checks if the provided password matches the encrypted password stored in the auth.users table. Here's the SQL code for creating the function:

CREATE OR REPLACE FUNCTION verify_user_password(password text)
RETURNS BOOLEAN SECURITY DEFINER AS
$$
BEGIN
  RETURN EXISTS (
    SELECT id 
    FROM auth.users 
    WHERE id = auth.uid() AND encrypted_password = crypt(password::text, auth.users.encrypted_password)
  );
END;
$$ LANGUAGE plpgsql;
-- You can also protect this function with:
REVOKE EXECUTE ON FUNCTION verify_user_password from anon, authenticated;
Enter fullscreen mode Exit fullscreen mode

This function takes a password as input and returns a boolean value indicating whether the password is valid for the current user.

Implementing the Edge Function

In the previous section, we discussed the importance of password verification and update functionality for application security. Now, let's dive into the implementation details using Supabase and PostgreSQL.

To implement the password verification and update functionality, we need to create an Edge function written in TypeScript. This Edge function acts as an intermediary between the frontend and the backend, handling the logic and communication with the Supabase backend.

Here's the code for the Edge function:

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

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

serve(async (req) => {
  // Create a Supabase client with the necessary credentials
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }
  // Create a Supabase client with the necessary credentials
  const supabaseClient = createClient(
    Deno.env.get("SUPABASE_URL") ?? "",
    Deno.env.get("SUPABASE_ANON_KEY") ?? "",
    {
      global: { headers: { Authorization: req.headers.get("Authorization")! } },
      auth: {
        autoRefreshToken: false,
        persistSession: false,
        detectSessionInUrl: false
      }
    }
  );
  console.log("Supabase client created");
  // Fetch the logged-in user from Supabase
  const { data: { user }, error: userError } = await supabaseClient.auth
    .getUser();
  console.log("User fetched", user);
  if (userError) {
    console.error("User error", userError);
    return new Response(JSON.stringify({ error: userError.message }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
      status: 400,
    });
  }
  // Extract the old and new passwords from the request
  const { oldPassword, newPassword } = await req.json();
  console.log("Received old and new passwords", oldPassword, newPassword);
  // Verify the old password using the `verify_user_password` function
  const { data: isValidOldPassword, error: passwordError } =
    await supabaseClient.rpc("verify_user_password", { password: oldPassword });
  console.log("Old password verified", isValidOldPassword);
  if (passwordError || !isValidOldPassword) {
    console.error("Invalid old password", passwordError);
    return new Response(JSON.stringify({ error: "Invalid old password" }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
      status: 400,
    });
  }
  try {
    // Fetch the user's profile data
    const { data: profiles, error: profileError } = await supabaseClient.from(
      "profiles",
    ).select("id, avatar_url");
    console.log("Profile data fetched", profiles);
    if (profileError) throw profileError;
    const user_id = profiles[0].id;
    console.log("User id", user_id);
    // Update the user's password using the Supabase Admin API
    const supabaseAdmin = createClient(
      Deno.env.get("SUPABASE_URL") ?? "",
      Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
      {
        auth: {
          autoRefreshToken: false,
          persistSession: false,
          detectSessionInUrl: false
        }
      }
    );
    console.log("Admin client created");
    // Return a success response to the client
    const { error: updateError } = await supabaseAdmin
      .auth.admin.updateUserById(
        user_id,
        { password: newPassword },
      );
    console.log("Password updated");
    if (updateError) {
      console.error("Update error", updateError);
      return new Response(JSON.stringify({ error: updateError.message }), {
        status: 400,
      });
    }
  } catch (error) {
    console.error("Caught error", error);
    return new Response(JSON.stringify({ error: error }), {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
      status: 400,
    });
  }
  console.log("Password update successful");
  // Return a success response to the client
  return new Response(
    JSON.stringify({ message: "Password updated successfully" }),
    {
      headers: { ...corsHeaders, "Content-Type": "application/json" },
      status: 200,
    },
  );
});
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we start by importing the necessary modules and libraries. Then, we define the CORS headers to allow cross-origin requests. Next, we set up the server to handle incoming requests and extract the authorization header from the request.

We create a Supabase client using the provided credentials and then fetch the logged-in user from Supabase. If there are any errors during the user fetching process, we handle them accordingly.

After extracting the old and new passwords from the request, we verify the old password using the verify_user_password function by invoking the rpc method on the Supabase client. If the old password is invalid or there are errors during the verification process, we handle them and return an appropriate error response.

If the old password is valid, we proceed to fetch the user's profile data. Once we have the user ID, we create a new Supabase client with admin credentials. Using the Supabase Admin API, we update the user's password to the new password provided.

Finally, we handle any errors that may occur during the password update process and return a successful response if the password update is successful.

This Edge function serves as a crucial component in implementing secure password verification and update functionality using Supabase and PostgreSQL.

Calling the Edge Function in React

To update the password securely, we need to call the Edge function from our React application. Let's dive into the process step by step.

First, we define an asynchronous function called updatePassword(). This function handles the password update logic and communicates with the Edge function. Here's an example of how the function looks:

javascriptCopy codeasync function updatePassword() {
  try {
    setLoading(true);

    // Ensure there is a user logged in
    if (!user) throw new Error('No user');

    // Validate that the new passwords match
    if (newPassword !== confirmNewPassword) {
      alert('New passwords do not match!');
      return;
    }

    // Call the secure_update_password Edge function
    const { data, error } = await supabase.functions.invoke('secure_update_password', {
      body: {
        "oldPassword": oldPassword,
        "newPassword": newPassword
      }
    });

    if (error) throw error;

    alert('Password updated!');
  } catch (error) {
    alert('Error updating the password!');
    console.log(error);
  } finally {
    setLoading(false);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this code, we start by setting the loading state to true to indicate that the update process is in progress. We then perform a series of checks:

  1. We ensure that a user is logged in. If not, an error is thrown with the message "No user."

  2. We validate that the new passwords match. If they don't, an alert is displayed, and the function returns without further execution.

If both checks pass, we proceed to call the secure_update_password Edge function using supabase.functions.invoke(). We pass the oldPassword and newPassword as part of the request body. The function returns a response object that contains data and error properties.

If an error occurs during the function invocation, we throw the error and display an alert with the message "Error updating the password!" Additionally, the error is logged to the console for further investigation.

Finally, regardless of the outcome, we set the loading state to false to indicate that the update process has completed.

Now that we have the updatePassword() function defined, we can integrate it into our React component, specifically in the form that allows users to enter their old and new passwords.

<div>
  <label htmlFor="old-password">Old Password</label>
  <input
    id="old-password"
    type="password"
    value={oldPassword}
    onChange={(e) => setOldPassword(e.target.value)}
  />
</div>
<div>
  <label htmlFor="new-password">New Password</label>
  <input
    id="new-password"
    type="password"
    value={newPassword}
    onChange={(e) => setNewPassword(e.target.value)}
  />
</div>
<div>
  <label htmlFor="confirm-new-password">Confirm New Password</label>
  <input
    id="confirm-new-password"
    type="password"
    value={confirmNewPassword}
    onChange={(e) => setConfirmNewPassword(e.target.value)}
  />
</div>
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we render three input fields: one for the old password, one for the new password, and one to confirm the new password. Each input field is associated with its respective state variable (oldPassword, newPassword, confirmNewPassword). The onChange event handlers update the corresponding state variables as the user types in the input fields. You can find the full code including the User Self-Deletion part in a single repo.

Conclusion

Implementing secure password verification and update functionality is crucial for the overall security of your application. With Supabase and PostgreSQL, you can leverage powerful tools and features to ensure that user passwords are protected and updated securely. By combining the flexibility of Supabase's backend platform with the scalability of PostgreSQL, you can build robust applications that prioritize user security.

In this blog post, we've explored the process of implementing password verification and update functionality using Supabase and PostgreSQL. We've walked through the code snippets that create a PostgreSQL function for password verification, demonstrate its usage in a TypeScript Edge function, and show how it can be called from a React application. Remember to prioritize secure password management in your applications and consider using Supabase and PostgreSQL for your backend needs.

Start building robust and secure applications with Supabase and PostgreSQL today!

🚀 Learn more about Supabase

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .