Supabase. Auth. MFA
(supabase_auth v1.0.1)
View Source
Multi-Factor Authentication (MFA) operations for Supabase Auth.
This module provides functions to manage MFA factors including TOTP (Time-based One-Time Password), Phone (SMS/WhatsApp), and WebAuthn authentication methods.
Factor Types
- TOTP: Time-based one-time passwords (authenticator apps like Google Authenticator, Authy)
- Phone: SMS or WhatsApp-based verification codes
- WebAuthn: Hardware security keys and biometric authentication
Typical MFA Flow
- Enroll: User enrolls a new MFA factor using
enroll/3 - Challenge: System issues a challenge for verification using
challenge/4 - Verify: User provides response to complete verification using
verify/5
Alternatively, for TOTP factors, you can use challenge_and_verify/4 to combine steps 2 and 3.
Authenticator Assurance Levels (AAL)
- AAL1: Single-factor authentication (password, magic link, OAuth)
- AAL2: Multi-factor authentication (at least one MFA factor verified)
After successful MFA verification, the session will be upgraded to AAL2.
Examples
# Enroll TOTP factor
{:ok, factor} = Supabase.Auth.MFA.enroll(client, session, %{
factor_type: :totp,
friendly_name: "My Authenticator"
})
# Display QR code to user (factor.totp.qr_code contains SVG data)
qr_code_svg = factor.totp.qr_code
# Enroll Phone factor
{:ok, phone_factor} = Supabase.Auth.MFA.enroll(client, session, %{
factor_type: :phone,
phone: "+1234567890",
friendly_name: "My Phone"
})
# Challenge and verify in one step (TOTP only)
{:ok, new_session} = Supabase.Auth.MFA.challenge_and_verify(
client,
session,
factor.id,
"123456"
)
# Or use separate challenge and verify steps
{:ok, challenge} = Supabase.Auth.MFA.challenge(client, session, factor.id, %{})
{:ok, new_session} = Supabase.Auth.MFA.verify(
client,
session,
factor.id,
challenge.id,
%{code: "123456"}
)
# List all factors
{:ok, %{all: factors, totp: totp_factors}} = Supabase.Auth.MFA.list_factors(client, session)
# Get current authentication level
{:ok, %{current_level: :aal2}} =
Supabase.Auth.MFA.get_authenticator_assurance_level(client, session)
# Unenroll a factor
{:ok, %{id: factor_id}} = Supabase.Auth.MFA.unenroll(client, session, factor.id)Related Modules
Supabase.Auth.MFA.Behaviour- Type definitions for MFA operationsSupabase.Auth.Session- Session containing access tokensSupabase.Auth.User- User profile with factors
Summary
Functions
Creates a challenge for an MFA factor.
Combines challenge and verify operations in a single call (TOTP only).
Enrolls a new MFA factor for the authenticated user.
Gets the current and next possible authenticator assurance levels.
Lists all MFA factors for the authenticated user.
Removes an MFA factor from the user's account.
Verifies an MFA challenge with the user's response.
Functions
@spec challenge(Supabase.Client.t(), Supabase.Auth.Session.t(), String.t(), map()) :: {:ok, Supabase.Auth.MFA.Behaviour.challenge_response() | Supabase.Auth.MFA.Behaviour.webauthn_challenge_response()} | {:error, term()}
Creates a challenge for an MFA factor.
The challenge must be verified using verify/5 with the appropriate response.
Different factor types require different challenge parameters:
- TOTP: No additional parameters needed (empty map)
- Phone: Requires
channel(:smsor:whatsapp) - WebAuthn: Requires
webauthnmap withrp_idand optionalrp_origins
Parameters
client- The Supabase clientsession- Active user sessionfactor_id- ID of the factor to challengeparams- Challenge parameters (type-specific):- For TOTP:
%{}(empty map) - For Phone:
%{channel: :sms}or%{channel: :whatsapp} - For WebAuthn:
%{webauthn: %{rp_id: "example.com", rp_origins: ["https://example.com"]}}
- For TOTP:
Returns
{:ok, challenge}- Challenge created with:id- Challenge ID for verificationtype- Factor typeexpires_at- Unix timestamp when challenge expireswebauthn- WebAuthn credential options (WebAuthn only)
{:error, error}- Challenge creation failed
Examples
# TOTP challenge
iex> Supabase.Auth.MFA.challenge(client, session, totp_factor_id, %{})
{:ok, %{id: "challenge-id", type: :totp, expires_at: 1234567890}}
# Phone challenge via SMS
iex> Supabase.Auth.MFA.challenge(client, session, phone_factor_id, %{channel: :sms})
{:ok, %{id: "challenge-id", type: :phone, expires_at: 1234567890}}
# WebAuthn challenge
iex> Supabase.Auth.MFA.challenge(client, session, webauthn_factor_id, %{
...> webauthn: %{rp_id: "example.com"}
...> })
{:ok, %{
id: "challenge-id",
type: :webauthn,
expires_at: 1234567890,
webauthn: %{type: "create", credential_options: %{...}}
}}
@spec challenge_and_verify( Supabase.Client.t(), Supabase.Auth.Session.t(), String.t(), String.t() ) :: {:ok, Supabase.Auth.Session.t()} | {:error, term()}
Combines challenge and verify operations in a single call (TOTP only).
This is a convenience function for TOTP factors where the code is immediately available from the user's authenticator app. It internally creates a challenge and immediately verifies it with the provided code.
Parameters
client- The Supabase clientsession- Active user sessionfactor_id- ID of the TOTP factorcode- The 6-digit TOTP code from the authenticator app
Returns
{:ok, session}- New session with AAL2 authentication{:error, error}- Verification failed
Examples
iex> Supabase.Auth.MFA.challenge_and_verify(client, session, factor_id, "123456")
{:ok, %Supabase.Auth.Session{...}}Note
This function only works with TOTP factors. For Phone or WebAuthn factors,
use separate challenge/4 and verify/5 calls to handle the asynchronous
nature of those verification methods.
@spec enroll(Supabase.Client.t(), Supabase.Auth.Session.t(), map()) :: {:ok, Supabase.Auth.MFA.Behaviour.enroll_response()} | {:error, term()}
Enrolls a new MFA factor for the authenticated user.
The enrollment process differs based on the factor type:
- TOTP: Returns QR code, secret, and URI for scanning into authenticator apps
- Phone: Returns the enrolled phone number
- WebAuthn: Returns the factor ID for WebAuthn credential registration
Parameters
client- The Supabase clientsession- Active user session containing access tokenparams- Factor enrollment parameters (map with factor_type and type-specific fields):- For TOTP:
%{factor_type: :totp, friendly_name: "...", issuer: "..."} - For Phone:
%{factor_type: :phone, phone: "+1234567890", friendly_name: "..."} - For WebAuthn:
%{factor_type: :webauthn, friendly_name: "..."}
- For TOTP:
Returns
{:ok, factor}- Successfully enrolled factor with type-specific data:- TOTP: includes
totpfield withqr_code,secret, anduri - Phone: includes
phonefield with E.164 formatted number - WebAuthn: basic factor information only
- TOTP: includes
{:error, error}- Enrollment failed
Examples
# Enroll TOTP factor
iex> Supabase.Auth.MFA.enroll(client, session, %{
...> factor_type: :totp,
...> friendly_name: "My Authenticator"
...> })
{:ok, %{
id: "factor-uuid",
factor_type: :totp,
friendly_name: "My Authenticator",
status: :unverified,
totp: %{
qr_code: "data:image/svg+xml;utf-8,...",
secret: "SECRET123",
uri: "otpauth://totp/..."
}
}}
# Enroll Phone factor
iex> Supabase.Auth.MFA.enroll(client, session, %{
...> factor_type: :phone,
...> phone: "+1234567890"
...> })
{:ok, %{
id: "factor-uuid",
factor_type: :phone,
status: :unverified,
phone: "+1234567890"
}}
@spec get_authenticator_assurance_level( Supabase.Client.t(), Supabase.Auth.Session.t() ) :: {:ok, Supabase.Auth.MFA.Behaviour.aal_response()} | {:error, term()}
Gets the current and next possible authenticator assurance levels.
AAL (Authenticator Assurance Level) indicates the strength of authentication:
- AAL1: Single-factor authentication (password, magic link, OAuth)
- AAL2: Multi-factor authentication (at least one verified MFA factor)
This function extracts AAL information from the session's JWT claims without making an API call. It also determines the next achievable AAL based on the user's enrolled factors.
Parameters
client- The Supabase clientsession- Active user session
Returns
{:ok, aal_info}- Map with::current_level- Current AAL (:aal1,:aal2, ornil):next_level- Next achievable AAL (:aal1,:aal2, ornil):current_authentication_methods- List of authentication method references from JWTamrclaim
{:error, error}- Failed to parse AAL information
Examples
# User with password authentication only (no MFA)
iex> Supabase.Auth.MFA.get_authenticator_assurance_level(client, session)
{:ok, %{
current_level: :aal1,
next_level: :aal2,
current_authentication_methods: ["password"]
}}
# User with verified MFA factor
iex> Supabase.Auth.MFA.get_authenticator_assurance_level(client, session_after_mfa)
{:ok, %{
current_level: :aal2,
next_level: :aal2,
current_authentication_methods: ["password", "totp"]
}}Note
This function does not make an HTTP request. It decodes the JWT token from the session to extract AAL claims.
@spec list_factors(Supabase.Client.t(), Supabase.Auth.Session.t()) :: {:ok, Supabase.Auth.MFA.Behaviour.factors_list()} | {:error, term()}
Lists all MFA factors for the authenticated user.
Returns factors organized by type for easy filtering. Only verified factors
are included in the type-specific lists (:totp, :phone, :webauthn),
while the :all list includes both verified and unverified factors.
Parameters
client- The Supabase clientsession- Active user session
Returns
{:ok, factors_map}- Map with keys::all- All factors (verified and unverified):totp- Verified TOTP factors only:phone- Verified Phone factors only:webauthn- Verified WebAuthn factors only
{:error, error}- Failed to retrieve factors
Examples
iex> {:ok, factors} = Supabase.Auth.MFA.list_factors(client, session)
iex> length(factors.all)
3
iex> length(factors.totp)
2
iex> Enum.map(factors.totp, & &1.friendly_name)
["My Phone", "Work Authenticator"]
@spec unenroll(Supabase.Client.t(), Supabase.Auth.Session.t(), String.t()) :: {:ok, %{id: String.t()}} | {:error, term()}
Removes an MFA factor from the user's account.
The factor must be verified before it can be unenrolled. This operation removes the factor permanently and cannot be undone.
Parameters
client- The Supabase clientsession- Active user sessionfactor_id- ID of the factor to remove
Returns
{:ok, %{id: factor_id}}- Factor successfully removed{:error, error}- Unenrollment failed
Examples
iex> Supabase.Auth.MFA.unenroll(client, session, "factor-uuid")
{:ok, %{id: "factor-uuid"}}
@spec verify( Supabase.Client.t(), Supabase.Auth.Session.t(), String.t(), String.t(), map() ) :: {:ok, Supabase.Auth.Session.t()} | {:error, term()}
Verifies an MFA challenge with the user's response.
Returns a new session with elevated authentication assurance level (AAL2). The verification parameters differ based on factor type:
- TOTP/Phone: Provide the 6-digit code
- WebAuthn: Provide the WebAuthn credential response
Parameters
client- The Supabase clientsession- Active user sessionfactor_id- ID of the challenged factorchallenge_id- ID of the challenge to verifyparams- Verification parameters (type-specific):- For TOTP/Phone:
%{code: "123456"} - For WebAuthn:
%{webauthn: %{type: "...", rp_id: "...", credential_response: {...}}}
- For TOTP/Phone:
Returns
{:ok, session}- New session with AAL2 authentication including:access_token- New JWT token with elevated AALrefresh_token- New refresh tokenuser- Updated user objectexpires_in- Token expiration time
{:error, error}- Verification failed (invalid code, expired challenge, etc.)
Examples
# Verify TOTP code
iex> Supabase.Auth.MFA.verify(client, session, factor_id, challenge_id, %{code: "123456"})
{:ok, %Supabase.Auth.Session{
access_token: "eyJhbGci...",
user: %Supabase.Auth.User{...}
}}
# Verify Phone code
iex> Supabase.Auth.MFA.verify(client, session, factor_id, challenge_id, %{code: "654321"})
{:ok, %Supabase.Auth.Session{...}}
# Verify WebAuthn credential
iex> Supabase.Auth.MFA.verify(client, session, factor_id, challenge_id, %{
...> webauthn: %{
...> type: "create",
...> rp_id: "example.com",
...> credential_response: credential
...> }
...> })
{:ok, %Supabase.Auth.Session{...}}