Supabase + Passkeys = Supapasskeys: this tutorial explains how to integrate passkeys with Supabase for secure and simple auth solutions.
Nicolai
Created: July 13, 2023
Updated: July 24, 2024
We're currently reworking the Corbado Connect approach. Reach out if you want to get early access.
Get early accessIn this tutorial, we will create a simple Node.js app based on Supabase with Corbado as an authentication provider to provide passkeys authentication ("Supapasskeys"). 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:
3. Configure Environment Variables
4. Integrate the Corbado Web Component
4.1. Create HTML Frontend Delivered by Node.js
4.3 Add Corbado Session Management
5. Connect Passkeys App to Supabase
6.2. Connect Local Instance to the Internet
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:
├── app.js ├── .env ├── src | ├── controllers | | ├── authController.js # renders views and uses Corbado SDK for sessions | | └── corbadoWebhookController.js # Takes all requests belonging to the Corbado webhook logic | ├── routes | | ├── authRoutes.js # All routes belonging to certain views | | └── corbadoWebhookRoutes.js # All routes belonging to the Corbado webhook | ├── services | | └── userService.js # Communicates with Supabase | ├── views/pages | | ├── login.ejs # Login page with the webcomponent | | └── profile.ejs # Profile page showing user info
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 Supabases user infrastructure for storing users, we do NOT need to create a user table inside Supabase.
Under Authentication > Users, well now add some password-based users, by clicking on Add user.
Come up with an email and password and click on create user. Here, we used "max@company.com" as email and maxPW as password. Make sure to auto confirm the user so it is a full-fledged account. Only then are we able to login as this user using the Supabase JavaScript client. We will also use the JavaScript client to access information stored in Supabases tables.
To later integrate these existing Supabase users into our app, we need to be able to tell if a user already exists in Supabase based on the provided email address. The Supabase JavaScript client does not provide a function to get a user by his email, so we must resort to something else. Well use the Supabase rpc functions, which are custom functions that can be defined inside the Supabase web interface and subsequently called via the Supabase JavaScript client.
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:
CREATE OR REPLACE FUNCTION get_user_id_by_email(email TEXT) RETURNS TABLE (id uuid) SECURITY definer AS $$ BEGIN RETURN QUERY SELECT au.id FROM auth.users au WHERE au.email = $1; END; $$ LANGUAGE plpgsql;
This function returns the ID of a user for a given email (assuming the user exists in Supabase).
Now, we are done with the Supabase part of our app. The necessary credentials for communication with the Supabase JavaScript client can be obtained under "Settings > API". We will put the Project URL, the service_role API key and the JWT Secret into our .env file in step 3. The service_role key authorizes us as an administrator of the corresponding project, thus enabling additional API calls.
Add passkey authentication to your Supabase users.
Start For FreeBefore 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 thats 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 its your first time).
Well 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 Corbados 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, its 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.
Supabase variables can be taken from step 1. PROJECT_ID, API_SECRET as well as CLI_SECRET should be taken from step 2.
Inside our Node.js app, we have two screens:
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:
// src/views/pages/profile.ejs <!DOCTYPE html> <html> <head> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> <style> body { padding: 2em; } button { border-radius: 5px; padding: 3px; } </style> </head> <body> <script src="https://<%= process.env.PROJECT_ID %>.frontendapi.corbado.io/auth.js"></script> <corbado-auth-provider project-id="<%= process.env.PROJECT_ID %>"> <div slot="authed"> <div class="main-container"> <h1>Welcome!</h1> <p>Email: <%= username %><br> Name: <%= userFullName %><br> SupabaseID: <%= supabaseID %><br> </p> </div> <corbado-logout-handler project-id="<%= process.env.PROJECT_ID %>" redirect-url="/logout"> <button>Logout</button> </corbado-logout-handler> </div> </corbado-auth-provider> </body> </html>
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 doesnt 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.
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
SubscribeAs mentioned before, we created the UserService to handle all actions concerning the Supabase JavaScript client. But first, we need to install the Supabase JavaScript client:
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:
Apart from get_user_id_by_email, we only use predefined methods of the Supabase JavaScript client which uses internally the Supabase user infrastructure. All users are hereby stored in the auth.users table which can be viewed in the Supabase dashboard.
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 Corbados 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 prompt you for your project ID from step 2 as well as your CLI secret which you can find in our app.
Afterwards execute
This will start the tunnel on port 19915.
With
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:
Become part of our Passkeys Community for updates and support.
JoinWe 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.
To enforce RLS, we can then act on behalf of a specific user when using the Supabase JavaScript client. Therefore, the RLS policies will only let us perform actions that the respective user should be allowed to do. Use the following snippet to create a Supabase JavaScript client which identifies itself as a specific Supabase user whose ID is stored in userID.
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.
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour
Start for free