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

  1. Enroll: User enrolls a new MFA factor using enroll/3
  2. Challenge: System issues a challenge for verification using challenge/4
  3. 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)

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

challenge(client, session, factor_id, params)

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 (:sms or :whatsapp)
  • WebAuthn: Requires webauthn map with rp_id and optional rp_origins

Parameters

  • client - The Supabase client
  • session - Active user session
  • factor_id - ID of the factor to challenge
  • params - 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"]}}

Returns

  • {:ok, challenge} - Challenge created with:
    • id - Challenge ID for verification
    • type - Factor type
    • expires_at - Unix timestamp when challenge expires
    • webauthn - 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: %{...}}
}}

challenge_and_verify(client, session, factor_id, code)

@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 client
  • session - Active user session
  • factor_id - ID of the TOTP factor
  • code - 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.

enroll(client, session, params)

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 client
  • session - Active user session containing access token
  • params - 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: "..."}

Returns

  • {:ok, factor} - Successfully enrolled factor with type-specific data:
    • TOTP: includes totp field with qr_code, secret, and uri
    • Phone: includes phone field with E.164 formatted number
    • WebAuthn: basic factor information only
  • {: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"
}}

get_authenticator_assurance_level(client, session)

@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 client
  • session - Active user session

Returns

  • {:ok, aal_info} - Map with:
    • :current_level - Current AAL (:aal1, :aal2, or nil)
    • :next_level - Next achievable AAL (:aal1, :aal2, or nil)
    • :current_authentication_methods - List of authentication method references from JWT amr claim
  • {: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.

list_factors(client, session)

@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 client
  • session - 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"]

unenroll(client, session, factor_id)

@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 client
  • session - Active user session
  • factor_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"}}

verify(client, session, factor_id, challenge_id, params)

@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 client
  • session - Active user session
  • factor_id - ID of the challenged factor
  • challenge_id - ID of the challenge to verify
  • params - Verification parameters (type-specific):
    • For TOTP/Phone: %{code: "123456"}
    • For WebAuthn: %{webauthn: %{type: "...", rp_id: "...", credential_response: {...}}}

Returns

  • {:ok, session} - New session with AAL2 authentication including:
    • access_token - New JWT token with elevated AAL
    • refresh_token - New refresh token
    • user - Updated user object
    • expires_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{...}}