Add passkey authentication for your Supabase users
In this blog post, we’ll be walking through the process of adding passkey authentication to a Node.js application which already has an existing user base in Supabase. Firstly, we'll focus on effortlessly integrating the Corbado web component which handles passkey authentication. Secondly, we'll adapt our backend to integrate the existing Supabase users.
In this tutorial, we will create a simple Node.js app based on Supabase with Corbado as an authentication provider. We will pay special attention on how to integrate password-based users of an existing Supabase architecture.
We use a simple Node.js app in our backend and plain HTML for the frontend. The app has a login screen as well as a profile screen where information of the current user is displayed. Also, we will use the open-source Firebase alternative Supabase to store our users as well as their data.
With Supabase, developers can build scalable and secure web and mobile applications, leveraging its robust features like data storage, real-time updates, and user authentication.
The flow of information looks like this: the Corbado web component which is integrated into the login page handles all means of authentication and talks with the Node.js backend to make sure existing Supabase users can still login with their password as fallback and are slowly transitioned to passkeys, while new users are offered passkeys during sign-up.
The structure of this tutorial is as follows:
The final code can be found on Github. If you want to run it straight away, make sure you have gone through steps 1-3 as you need to set up a Supabase project (step 1) as well as a Corbado project (step 2) and provide the environment variables (step 3) to the application. Afterwards, start the project by running
You can now visit http://localhost:19915 to test the app yourself:
Now back to the implementation details - the code of our project is structured as follows:
For this step, we head over to Supabase and create an account. Afterwards, we create a new project. Select name, password and region according to your preferences.
Then, we click on “Create new project” which makes Supabase instantiate our project. Beware this can take some time (~5-10 seconds during our test).
As we use Supabase’s user infrastructure for storing users, we do NOT need to create a user table inside Supabase.
Under Authentication > Users, we’ll now add some password-based users, by clicking on “Add user”.
Inside the Supabase web interface, we use the SQL Editor to create a new function “get_user_id_by_email” by executing the following PostgreSQL query:
This function returns the ID of a user for a given email (assuming the user exists in Supabase).
Before building our frontend, we need to create a Corbado project. We start by heading over to the Corbado developer panel and create an account. After successful sign-up, we see this screen:
We click on “Integration guide” to do everything step by step:
We want to integrate via “Web component”, so that’s what we select.
We have a system with existing users, so we click “Yes”. Afterwards, we find ourselves at the overview of the developer panel (you need to confirm your account via email if it’s your first time).
We’ll head over to “Getting started” >“Integration guide”. This guides us through all the steps necessary for the integration to work.
Remember to select “Node.js” as progamming language in the top right corner.
In step 1 of the integration guide, we configure our authorized origin. The authorized origin is the browser URL of the site where the web component is integrated (with protocol and port but without path). We need it for CORS (cross-origin request sharing). In our case, we set it to http://localhost:19915. Next, we create an API secret. We need the project ID and the generated API secret later in order to communicate with Corbado’s Backend API.
In the second, optional step, we configure the webhook. Webhooks are needed, so Corbado can communicate with our backend / Supabase, e.g. for checking if a username and password of an existing user match. More details on that later.
We will later set up our webhook at http://localhost:19915/corbado-webhook with “webhookUsername” and “webhookPassword” as credentials, so we can already enter that here.
In step 3, we add our Application URL, Redirect URL and Relying Party ID. The Application URL is the URL in the frontend where the web component runs. For example, it’s used to forward users to the web component again after they clicked on an email magic link.
The Redirect URL is the URL where the user is directed to once the authentication has succeeded. In our case, this will be the /profile page, so we can enter http://localhost:19915/profile here already.
The Relying Party ID is the domain where we bind our passkeys to. The domain in the browser where a passkey is bount to must be a matching domain to the Relying Party ID. As we test locally, we set the Relying Party ID to “localhost”.
Just like that, the project in Corbado is set up!
We use a simple Node.js express app to deliver our plain HTML frontend. For this to work, the following Node.js environment variables should be configured in the respective .env file.
Inside our Node.js app, we have two screens:
- Login screen
- User profile screen showing information when a user is logged in
Our login screen contains only the Corbado web component which will handle the authentication. For more details on its usage, visit the web component docs.
Once a user has authenticated, Corbado will create a session and redirect the user to the Redirect URL we defined beforehand (our “http://localhost:19915/profile” page).
We created a profile.ejs template that just displays some basic info about the user as well as a logout button:
Here we need the Corbado SDK, which we can install by executing
Before delivering the profile page, the backend uses the Corbado SDK to retrieve the user of the currently active session.
Then, we add the user to our Supabase database if it doesn’t exist there yet. As an intermediate between our profile-page endpoint and Supabase, we created the UserService which will be explained in a minute.
Once we have got our Supabase user, we render the page displaying the Supabase user ID, email and name.
We initialize the client with the Supabase role key as parameter because we will use the auth.admin calls to manage our users and the role key authorizes us as the admin.
Remember to never expose your Supabase role key!
The UserService contains methods which handle the following processes:
- create a new user
- verify the passwords of an existing user
- retrieve a user, given the user ID
- retrieve a user ID, given an email address
In step 1, we added password-based users to our Supabase userbase. To let these existing users still login with their passwords, we set up webhooks.
Beforehand, we set up in the Corbado developer panel that the webhook we provide would be reachable via http://localhost:19915/corbado-webhook, so we create a controller for the route which handles Corbado’s webhooks.
There, we initialize the Corbado Node.js SDK using the project ID and API Secret from step 2. The webhook method below can be taken as a communication template for the Corbado webhooks.
The only methods you would have to change here are "getUserStatus" and "verifyPassword". Inside these two methods, we again use our UserService to check if a certain user exists and if a password matches for a certain user.
Corbado will attempt to call our webhooks, but currently they are hosted locally on http://localhost:19915/corbado-webhook, so we need to make them publicly available. We do so by using the Corbado CLI. It creates a tunnel between Corbado and our local instance, so Corbado can send webhooks to our local instance. Install it as described in our Corbado CLI docs. Then execute
This will start the tunnel on port 19915.
you should be able to run the application now. When visiting http://localhost:19915 you should see the Corbado web component:
If we login as the password-based user we created in step 1 using our password, we are offered to create a passkey after successful password authentication:
We can now create tables in Supabase which have a foreign key linking to the ID of the auth.users table. Remember to configure Row Level Security (RLS) before querying.
This should only be done in your backend. Remember to never publicly expose your Supabase JWT secret!
You must remember to set up Row Level Security for tables you create in Supabase, otherwise you cannot perform any actions.
If the session initialization does not work, please make sure you have Corbado session management enabled in the developer panel under Settings > Sessions.
This tutorial showed how easy it is to add passkey authentication to a Node.js application which uses Supabase as a database provider and already has an existing user base (with passwords).
Authentication as well as session management is handled by Corbado all while we manage the users ourselves and can save user-data tied to our Supabase users instead of using a user identifier from Corbado. This means we are still independent and in control of our users and their data. If you want to read more about how you can leverage the session management to retrieve backend data, please see our Corbado docs.
Due to clear documentation and a well-structured client, Supabase is very convenient and easy to use. Although one must watch out which API keys and JWT secrets to use where and which of them to keep secret.
Integrating other authentication providers is not as easy as it could be as Supabase is optimized towards their own password-based authentication and session management solution. In general, for some features like passkeys you need external solutions because Supabase does not offer an implementation itself (yet).
Supabase also has a limited predefined interface which is why a custom rpc function was needed for mapping emails to user IDs.
Although one can create a user via the admin auth API without requiring a password, the entry in the auth.users table in Supabase for that new user will contain a hash in the password column which leads to the assumption that a random password might be generated nevertheless when creating a user without specifying a password.
Enjoyed this read?
Stay up to date with the latest news, strategies and insights about passkeys sent straight to your inbox!