Multi-Factor Authentication (MFA) with Supabase GoTrue
View SourceThis guide covers how to implement Multi-Factor Authentication (MFA) using Supabase's GoTrue authentication service in your Elixir applications.
Understanding MFA in Supabase
Multi-Factor Authentication provides an additional security layer beyond passwords. Supabase supports several forms of MFA including:
- Time-based One-Time Passwords (TOTP): Compatible with authenticator apps like Google Authenticator or Authy
- SMS-based verification: One-time codes sent via text message
- Email-based verification: One-time codes sent via email
Each user can have multiple authentication factors associated with their account, represented by the Supabase.GoTrue.User.Factor
struct.
MFA Factor Management
Viewing User Factors
To check if a user has MFA enabled and view their factors:
# Get the current user
{:ok, user} = Supabase.GoTrue.get_user(client, session)
# Check for active factors
has_mfa_enabled = Enum.any?(user.factors || [], fn factor -> factor.status == "verified" end)
# Get all factors
factors = user.factors || []
Each factor includes information such as:
%Supabase.GoTrue.User.Factor{
id: "f1d74b2c-9bb7-4c74-b9b9-44a48f261a48",
type: "totp",
status: "verified", # or "unverified"
friendly_name: "Google Authenticator",
factor_type: "totp",
created_at: ~N[2023-01-01 00:00:00],
updated_at: ~N[2023-01-01 00:00:00]
}
Working with Admin APIs
For admin operations on MFA factors, use the Supabase.GoTrue.Admin
module:
Enrolling a TOTP Factor (Admin)
{:ok, user_with_factors} = Supabase.GoTrue.Admin.enroll_factor(
client,
user_id,
%{type: "totp", friendly_name: "My Authenticator App"}
)
This returns a user object with a new unverified factor. The response also includes a QR code URL that should be displayed to the user for scanning with their authenticator app.
Verifying a TOTP Factor (Admin)
After the user scans the QR code, they need to verify their factor by entering a code:
{:ok, verified_factor} = Supabase.GoTrue.Admin.verify_factor(
client,
user_id,
factor_id,
%{challenge: "123456"} # Code from authenticator app
)
Challenging a Factor (Admin)
When a user attempts to access a protected resource, you can challenge their MFA factor:
{:ok, challenge} = Supabase.GoTrue.Admin.challenge_factor(
client,
user_id,
factor_id
)
Unenrolling a Factor (Admin)
To remove an MFA factor:
:ok = Supabase.GoTrue.Admin.delete_factor(client, user_id, factor_id)
Authenticator Assurance Levels (AAL)
Supabase uses Authenticator Assurance Levels (AAL) to determine the strength of authentication:
- AAL0: No authentication
- AAL1: Single-factor authentication (e.g., password only)
- AAL2: Multi-factor authentication (password + MFA)
You can check the current AAL:
aal = user.app_metadata["aal"] # "aal1" or "aal2"
Requiring MFA for Sensitive Operations
For sensitive operations like changing passwords or making payments, you can use the reauthentication function:
# Request a reauthentication challenge
:ok = Supabase.GoTrue.reauthenticate(client, session)
# User completes verification with OTP
{:ok, verified_session} = Supabase.GoTrue.verify_otp(
client,
%{
type: "reauthentication",
token: "123456"
}
)
# Now perform sensitive operation with the new verified session
MFA Integration with Phoenix
Enrollment Flow in Phoenix Controllers
defmodule MyAppWeb.MFAController do
use MyAppWeb, :controller
def new(conn, _params) do
user_id = conn.assigns.current_user.id
# Create a new TOTP factor
{:ok, user_with_factor} = Supabase.GoTrue.Admin.enroll_factor(
MyApp.Supabase.Client.get(),
user_id,
%{type: "totp", friendly_name: "Authenticator App"}
)
# Get the most recently created factor (the unverified one)
[new_factor | _] = Enum.sort_by(user_with_factor.factors, & &1.created_at, :desc)
# Render the enrollment page with QR code URL
render(conn, "new.html", factor: new_factor, qr_code_url: new_factor.totp.qr_code)
end
def verify(conn, %{"code" => code, "factor_id" => factor_id}) do
user_id = conn.assigns.current_user.id
case Supabase.GoTrue.Admin.verify_factor(
MyApp.Supabase.Client.get(),
user_id,
factor_id,
%{challenge: code}
) do
{:ok, _} ->
conn
|> put_flash(:info, "MFA successfully enabled!")
|> redirect(to: Routes.profile_path(conn, :show))
{:error, _} ->
conn
|> put_flash(:error, "Invalid verification code")
|> redirect(to: Routes.mfa_path(conn, :new))
end
end
end
Verification Flow in LiveView
defmodule MyAppWeb.MFALive do
use MyAppWeb, :live_view
def mount(_params, session, socket) do
user_id = socket.assigns.current_user.id
{:ok, challenge} = Supabase.GoTrue.Admin.challenge_factor(
MyApp.Supabase.Client.get(),
user_id,
socket.assigns.current_user.factors |> hd() |> Map.get(:id)
)
{:ok, assign(socket, challenge_id: challenge.id, error: nil)}
end
def handle_event("verify", %{"code" => code}, socket) do
case Supabase.GoTrue.verify_challenge(
MyApp.Supabase.Client.get(),
socket.assigns.challenge_id,
%{code: code}
) do
{:ok, new_session} ->
{:noreply,
socket
|> put_flash(:info, "Verification successful!")
|> push_redirect(to: ~p"/dashboard")}
{:error, _} ->
{:noreply, assign(socket, error: "Invalid verification code")}
end
end
end
Best Practices for MFA
Backup Codes: Provide users with backup codes in case they lose access to their MFA device.
Recovery Options: Implement account recovery processes for users who lose all authentication factors.
Gradual Adoption: Consider making MFA optional initially, then mandatory for specific user groups.
UX Considerations: Clearly explain MFA benefits to users and make the setup process straightforward.
Security Logging: Log all MFA-related activities for audit purposes.
Conclusion
MFA significantly improves the security of your application by requiring multiple forms of verification. Supabase's GoTrue service makes it relatively straightforward to implement MFA in your Elixir applications.
For more detailed information, refer to the module documentation for the Supabase.GoTrue.User.Factor
module and the Supabase.GoTrue.Admin
module.