Blog post

Multi-factor Authentication via Row Level Security Enforcement

2022-12-14

7 minute read

Multi-factor Authentication via Row Level Security Enforcement

Today, we’re releasing Multi Factor Authentication for everyone.

Additionally, in preparation for releasing SAML, we're "dogfooding" the feature with the introduction of Single Sign On (SSO) on our dashboard. Contact us at growth@supabase.com if you want to enable this on your Enterprise plan.

What is MFA?

Multi-factor authentication (MFA), sometimes called two-factor authentication (2FA), adds an additional layer of security to your application by letting you verify users’ identity through extra steps. This typically consists of something you know, like a password, and something you have, like an authenticator application. We built MFA in response to customer requests - developers wanted enhanced security - be it for compliance, client requirements, or simply for peace of mind. As such, we started by building MFA support for Time-Based One Time Passwords (TOTP).

An Overview of TOTP

TOTP works by generating a unique, one-time password that is valid for a limited amount of time, usually 30 seconds or less. This password is generated using a shared secret key that is known only to the device and Supabase Auth, along with the current time. To exchange the shared secret key, a user scans a QR code generated by the server in order to establish a connection. The QR code can be represented by a URI which conforms to the Google Authenticator Key URI format:


_10
otpauth://totp/supabase.io:j@supacats.io?algorithm=SHA1&digits=6&issuer=supabase.io&period=30&secret=BFSXQHFB2BGAZIOQWCDBJUF7B54A52JQ

The first portion of otpauth://totp/supabase.io describes the protocol and issuer while j@supacats.io refers to the user. The remaining parameters refer to specifics around OTP generation. In this case, the OTP code is generated using a SHA1 hash of the secret combined with the timestamp and the OTP code is valid for 30s

In the event that the user faces difficulties entering a QR code the user can also opt to manually type the secret into the authenticator device.

MFA Flows: Enrollment and Verification

An MFA flow can be broken into two key steps: Enrollment and Verification. During the Enrollment process Supabase Auth exchanges a randomly generated secret with the user’s authenticator application. During the Verification process, the device makes use of the timestamp together with the secret to produce a six digit code that the server can verify.

Flowchart of an MFA flow Enrollment

To generate a QR code, call the /enroll endpoint which returns an SVG encoded QR and the secret. Thereafter, create a challenge by calling the /challenge endpoint. Once the user has entered the six digit TOTP code generated by their authenticator app, call the/verify endpoint with the corresponding factor and challenge details.

You might wonder: why the need for the "challenge" step? This step creates an interval between MFA initiation and the action of making a verification. This is useful in cases like Yubikey authentication where a user might need to request a challenge before placing their finger on the device.

MFA Enrollment Flow Overview of Enrollment Flow

Verification

On subsequent logins attempts, redirect a user to an MFA verification after they have completed the conventional sign in process. On the verification page, wait for the user to enter the six digit OTP code from the authenticator application and then call the/challenge endpoint followed by the /verify endpoint. If a correct code is submitted, a JWT will be created with a few additional fields.

MFA Verification Flow Overview of Verification Flow

Enforcement via Row Level Security

Using MFA without enforcing it is like buying an expensive door and never locking it. We love Postgres RLS at Supabase. To support RLS integration, JWTs issued by Supabase Auth now contain two additional pieces of information:

  1. An Authenticator Assurance Level (AAL) claim. Use this to quickly identify the level of identity checks the user has performed. aal1 is reserved for conventional sign in, while aal2 is issued only after the user has verified with an additional factor.
  2. An Authenticator Method Reference (AMR) claim. Use this to identify all of the authentication methods used by the user. This is also useful if you wish to implement step-up login scenarios.

_18
{
_18
"sub": "8802c1d6-c555-46e3-aacd-b61198b058d9",
_18
"email": "j0@supacats.io",
_18
"aud": "authenticated",
_18
"exp": 1670929371,
_18
"aal": "aal2",
_18
"amr": [
_18
{
_18
"method": "password",
_18
"timestamp": 1670924394
_18
},
_18
{
_18
"method": "totp",
_18
"timestamp": 1670925771
_18
}
_18
],
_18
// ...
_18
}

The information encoded in these claims can be used for both full enforcement and partial enforcement across database queries.


_10
create policy "Enforce MFA for all end users."
_10
on table_name
_10
as restrictive
_10
to authenticated
_10
using ( auth.jwt()->>'aal' = 'aal2' );

Enforce MFA for all end users


_14
create policy "Allow access on table only if user has gone through MFA"
_14
on table_name
_14
as restrictive -- very important!
_14
to authenticated
_14
using (
_14
array[auth.jwt()->>'aal'] <@ (
_14
select
_14
case
_14
when count(id) > 0 then array['aal2']
_14
else array['aal1', 'aal2']
_14
end as aal
_14
from auth.mfa_factors
_14
where auth.uid() = user_id and status = 'verified'
_14
));

Enforce MFA for selected users

Note that both RLS policies are restrictive. By default, overlapping policies in PostgreSQL are permissive rather than restrictive. This means that RLS policies are combined with an OR clause and only one policy needs to pass in order for a row to be operated on. Therefore, we set RLS policies as restrictive to enforce the checks from multiple policies.

Be mindful of your user’s preference, though. If a user has enabled MFA, they are expecting a higher level of security for their account. Consequently, we recommend that developers enforce MFA across all operations if a user has MFA enabled. You can check out our MFA guide for more details about MFA enforcement.

What's Next

For starters, we are looking to support WebAuthn and FIDO2 compliant devices such as Yubikeys. We also hope to allow users to receive email notifications when selected MFA actions are triggered. If you have MFA requirements which are not covered here feel free to write to us at support[at]supabase.io .

We are grateful to our early MFA users for the support and feedback provided throughout this period. In particular, we would like to thank Fabian Beer, Cogram, and Happl whose detailed feedback helped to shape our implementation. We would also like to specially thank the community behind the pquerna/otp and ajstarks/svgo libraries - their work is indispensable to this implementation.

More Launch Week 6

Share this article

Last post

Supabase Wrappers, a Postgres FDW framework written in Rust

15 December 2022

Next post

Supabase Storage v2: Image resizing and Smart CDN

13 December 2022

Related articles

Offline-first React Native Apps with Expo, WatermelonDB, and Supabase

Supabase Beta September 2023

Dynamic Table Partitioning in Postgres

Supabase Beta August 2023

pgvector v0.5.0: Faster semantic search with HNSW indexes

Build in a weekend, scale to millions