Authentication Methods Reference (amr) claim in JWT
Access tokens issued by Supabase Auth contain an amr (Authentication Methods Reference) claim, which is an array of objects that indicate what authentication methods the user has used. Each entry contains a method and timestamp, with entries ordered by most recent method first. Currently recognized authentication methods are: oauth (any OAuth-based sign-in), password (any password based sign in), otp (any one-time password based sign in), totp (a TOTP additional factor), sso/saml (any Single Sign On method), and anonymous (any anonymous sign in). Additional claims available when using PKCE flow are: invite (any sign in via an invitation), magiclink (any sign in via magic link, excluding logins from signUp), email/signup (any sign-in from email signup), and email_change (any sign-in from a change in email).
Example amr claim structure in JWT
An example of an amr claim structure describing a user that first signed in with a password-based method, then went through TOTP MFA:
{
"amr": [
{
"method": "totp",
"timestamp": 1666086056
},
{
"method": "password",
"timestamp": 1666085924
}
]
}
getAuthenticatorAssuranceLevel() method
Use the supabase.auth.mfa.getAuthenticatorAssuranceLevel() method to get easy access to Authentication Methods Reference information in your browser app.
listFactors() method in UnenrollMFA example
The supabase.auth.mfa.listFactors() endpoint fetches all existing factors together with their details. It returns data with totp and phone arrays. Each factor object contains properties: id, friendly_name, factor_type, status, and phone.
Authenticator Assurance Level (AAL) explained
Supabase Auth adds an Authenticator Assurance Level (AAL) to the user's access token (JWT) as a standard measure of the assurance of the user's identity. AAL1 means the user's identity was verified using a conventional sign-in method such as email+password, magic link, one-time password, phone auth or social sign-in. AAL2 means the user's identity was additionally verified using at least one second factor, such as a TOTP code or One-Time Password code. This assurance level is encoded in the aal claim in the JWT. JWTs without an aal claim are at the aal1 level.
Four steps to add MFA to your app
Adding MFA to your app involves four steps: (1) Add enrollment flow to provide a UI within your app where users can set up MFA, (2) Add unenroll flow to support a UI where users can see existing devices and unenroll devices, (3) Add challenge step to sign in so users can prove they have access to the additional factor, and (4) Enforce rules for MFA logins across your app on the frontend, backend, API servers or Row-Level Security policies.
Unenroll MFA factor with JavaScript
To unenroll a factor, call supabase.auth.mfa.unenroll() with the ID of the factor.
Unenroll MFA example code
Example of unenrolling a factor with JavaScript: supabase.auth.mfa.unenroll({ factorId: 'd30fd651-184e-4748-a928-0a4b9be1d429' })
Downgrade from AAL2 to AAL1 after unenroll
Unenrolling a factor will downgrade the assurance level from aal2 to aal1 only after the refresh interval has lapsed. For an immediate downgrade from aal2 to aal1 after unenrolling one must manually call refreshSession().
Three enforcement options for MFA policies
There are three ways to enforce MFA: (1) Enforce for all users (new and existing) where any user account must enroll MFA to continue using the app, (2) Enforce for new users only where only new users are forced to enroll MFA while old users are encouraged to do so, and (3) Enforce only for users that have opted-in where users that want MFA can enroll in it.
MFA enrollment and authentication flows required
Applications using MFA require two important flows: an Enrollment flow that lets users set up and control MFA in the app, and an Authentication flow that lets users sign in using any factors after the conventional sign-in step.
MFA APIs provided by Supabase Auth
Supabase Auth provides an Enrollment API for building rich user interfaces for adding and removing factors, Challenge and Verify APIs for securely verifying that the user has access to a factor, and a List Factors API for building rich user interfaces for signing in with additional factors. Access to the Enrollment API and Challenge and Verify APIs can be controlled via the Supabase Dashboard with a setting of Verification Disabled that disables both the challenge API and the verification API.
MFA methods supported by Supabase Auth
Supabase Auth implements MFA via two methods: App Authenticator, which uses Time-based One-Time Password (TOTP), and phone messaging, which uses a code generated by Supabase Auth.
React example: EnrollMFA component for phone MFA enrollment
```tsx
export function EnrollMFA({
onEnrolled,
onCancelled,
}: {
onEnrolled: () => void
onCancelled: () => void
}) {
const [phoneNumber, setPhoneNumber] = useState('')
const [factorId, setFactorId] = useState('')
const [verifyCode, setVerifyCode] = useState('')
const [error, setError] = useState('')
const [challengeId, setChallengeId] = useState('')
const onEnableClicked = () => {
setError('')
;(async () => {
const verify = await auth.mfa.verify({
factorId,
challengeId,
code: verifyCode,
})
if (verify.error) {
setError(verify.error.message)
throw verify.error
}
onEnrolled()
})()
}
const onEnrollClicked = async () => {
setError('')
try {
const factor = await auth.mfa.enroll({
phone: phoneNumber,
factorType: 'phone',
})
if (factor.error) {
setError(factor.error.message)
throw factor.error
}
setFactorId(factor.data.id)
} catch (error) {
setError('Failed to Enroll the Factor.')
}
}
const onSendOTPClicked = async () => {
setError('')
try {
const challenge = await auth.mfa.challenge({ factorId })
if (challenge.error) {
setError(challenge.error.message)
throw challenge.error
}
setChallengeId(challenge.data.id)
} catch (error) {
setError('Failed to resend the code.')
}
}
return (
<>
{error && <div className="error">{error}</div>}
<input
type="text"
placeholder="Phone Number"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value.trim())}
/>
<input
type="text"
placeholder="Verification Code"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value.trim())}
/>
<input type="button" value="Enroll" onClick={onEnrollClicked} />
<input type="button" value="Submit Code" onClick={onEnableClicked} />
<input type="button" value="Send OTP Code" onClick={onSendOTPClicked} />
<input type="button" value="Cancel" onClick={onCancelled} />
</>
)
}
```
This example shows the key pieces of phone MFA enrollment: calling enroll() to start enrollment, challenge() to send the OTP code, and verify() to confirm the code and activate the factor.
React example: AuthMFA component for phone MFA verification
```tsx
function AuthMFA() {
const [verifyCode, setVerifyCode] = useState('')
const [error, setError] = useState('')
const [factorId, setFactorId] = useState('')
const [challengeId, setChallengeId] = useState('')
const [phoneNumber, setPhoneNumber] = useState('')
const startChallenge = async () => {
setError('')
try {
const factors = await supabase.auth.mfa.listFactors()
if (factors.error) {
throw factors.error
}
const phoneFactor = factors.data.phone[0]
if (!phoneFactor) {
throw new Error('No phone factors found!')
}
const factorId = phoneFactor.id
setFactorId(factorId)
setPhoneNumber(phoneFactor.phone)
const challenge = await supabase.auth.mfa.challenge({ factorId })
if (challenge.error) {
setError(challenge.error.message)
throw challenge.error
}
setChallengeId(challenge.data.id)
} catch (error) {
setError(error.message)
}
}
const verifyCode = async () => {
setError('')
try {
const verify = await supabase.auth.mfa.verify({
factorId,
challengeId,
code: verifyCode,
})
if (verify.error) {
setError(verify.error.message)
throw verify.error
}
} catch (error) {
setError(error.message)
}
}
return (
<>
<div>Please enter the code sent to your phone.</div>
{phoneNumber && <div>Phone number: {phoneNumber}</div>}
{error && <div className="error">{error}</div>}
<input
type="text"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value.trim())}
/>
{!challengeId ? (
<input type="button" value="Start Challenge" onClick={startChallenge} />
) : (
<input type="button" value="Verify Code" onClick={verifyCode} />
)}
</>
)
}
```
This example shows how to list enrolled phone factors using listFactors(), create a challenge with challenge(), and verify the user-entered code with verify() during the MFA sign-in flow.
Phone MFA workflow: enrollment and sign-in flows
Phone multi-factor authentication involves a shared code generated by Supabase Auth and delivered via SMS or WhatsApp. The enrollment flow has three steps: call supabase.auth.mfa.enroll() with the phone number and factorType 'phone', then call supabase.auth.mfa.challenge() which sends the code and returns a challenge ID, then call supabase.auth.mfa.verify() with the factorId, challengeId, and user-entered code to activate the factor. In the sign-in flow, after a user signs in (reaching AAL1), call supabase.auth.mfa.listFactors() to check for enrolled factors. If factors exist, the user selects their phone factor and enters the code sent via challenge and verify. If no factors are enrolled, the user goes through the enrollment setup flow.
Phone messaging configuration shared with phone auth sign-in
The phone messaging configuration for MFA is shared with phone auth sign-in. The same provider configuration that is used for phone sign-in is used for MFA. If you need to use an MFA (Phone) messaging provider different from what is supported natively, you can use the Send SMS Hook.
Phone MFA code validity and configuration
Each phone MFA code is valid for up to 5 minutes, after which a new one can be sent. Successive codes remain valid until expiry. Code length can be configured in the Authentication Settings, with a minimum recommended length of 6 digits. Longer code lengths should be chosen when acceptable for the use case.
Phone number uniqueness constraint for MFA factors
Phone numbers are unique per user. Users can only have one verified phone factor with a given phone number. Attempting to enroll a new phone factor alongside an existing verified factor with the same number will result in an error.
SIM swap attack vulnerability in phone MFA
Phone MFA is vulnerable to SIM swap attacks where an attacker calls a mobile provider to port the target's phone number to a new SIM card and then uses the SIM card to intercept the MFA code. Applications should evaluate their tolerance for such attacks when implementing phone MFA.
React example: AppWithMFA component for checking AAL after sign-in
```tsx
function AppWithMFA() {
const [readyToShow, setReadyToShow] = useState(false)
const [showMFAScreen, setShowMFAScreen] = useState(false)
useEffect(() => {
;(async () => {
try {
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (error) {
throw error
}
console.log(data)
if (data.nextLevel === 'aal2' && data.nextLevel !== data.currentLevel) {
setShowMFAScreen(true)
}
} finally {
setReadyToShow(true)
}
})()
}, [])
if (readyToShow) {
if (showMFAScreen) {
return <AuthMFA />
}
return <App />
}
return <></>
}
```
This example wraps the main App component with logic that checks if MFA verification is required after sign-in using getAuthenticatorAssuranceLevel() and shows an MFA challenge screen if needed.
TOTP MFA API availability
TOTP MFA API is free to use and is enabled on all Supabase projects by default.
TOTP MFA login flow - verification process
In the sign-in flow, the user signs in (upgrading the session to AAL1) and the List Factors API is called. If the user has one or more factors, they open their authenticator and enter a code, which follows the same Challenge and Verify path to reach AAL2. If they have no factors enrolled, they are sent through the setup flow first.
TOTP MFA overview - how it works
App Authenticator (TOTP) multi-factor authentication uses a timed one-time password generated from an authenticator app. It transmits a shared secret via QR code to generate a One Time Password. Users scan a QR code with their phone to capture the shared secret required for subsequent authentication. The QR code has an alternate URI representation following the otpauth scheme, such as otpauth://totp/supabase:alice@supabase.com?secret=<secret>&issuer=supabase, which users can manually input if QR code rendering is difficult.
TOTP MFA setup flow - enrollment process
In the setup flow, a session already at AAL1 calls the Enroll API, which returns a QR code for the user to scan with their authenticator app. The user enters the generated code, the Challenge and Verify APIs check it, and on success the session is upgraded to AAL2. If the code is incorrect, the user is prompted to enter it again.
MFA enrollment flow - typical placement in apps
An enrollment flow provides a UI for users to set up additional authentication factors. Most applications add the enrollment flow in two places: (1) Right after sign-in or sign-up to let users set up MFA immediately after they sign in or create an account, often as an opt-in step to reduce onboarding friction; (2) From within a settings page to allow users to set up, disable or modify their MFA settings.
TOTP MFA enrollment steps
Enrolling a factor for use with MFA takes three steps: (1) Call supabase.auth.mfa.enroll() which returns a QR code and a secret. Display the QR code to the user and ask them to scan it with their authenticator application. If unable to scan, show the secret in plain text which they can type or paste into their authenticator app. (2) Call supabase.auth.mfa.challenge() API which prepares Supabase Auth to accept a verification code and returns a challenge ID. (3) Call supabase.auth.mfa.verify() API which verifies that the user has added the secret into their app and is working correctly. If verification succeeds, the factor immediately becomes active. If not, repeat steps 2 and 3.
React EnrollMFA component example
```tsx
/**
* EnrollMFA shows an enrollment dialog. When shown on screen it calls
* the `enroll` API. Each time a user clicks the Enable button it calls the
* `challenge` and `verify` APIs to check if the code provided by the user is
* valid.
* When enrollment is successful, it calls `onEnrolled`. When the user clicks
* Cancel the `onCancelled` callback is called.
*/
export function EnrollMFA({
onEnrolled,
onCancelled,
}: {
onEnrolled: () => void
onCancelled: () => void
}) {
const [factorId, setFactorId] = useState('')
const [qr, setQR] = useState('') // holds the QR code image SVG
const [verifyCode, setVerifyCode] = useState('') // contains the code entered by the user
const [error, setError] = useState('') // holds an error message
const onEnableClicked = () => {
setError('')
;(async () => {
const challenge = await supabase.auth.mfa.challenge({ factorId })
if (challenge.error) {
setError(challenge.error.message)
throw challenge.error
}
const challengeId = challenge.data.id
const verify = await supabase.auth.mfa.verify({
factorId,
challengeId,
code: verifyCode,
})
if (verify.error) {
setError(verify.error.message)
throw verify.error
}
onEnrolled()
})()
}
useEffect(() => {
;(async () => {
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
})
if (error) {
throw error
}
setFactorId(data.id)
// Supabase Auth returns an SVG QR code which you can convert into a data
// URL that you can place in an <img> tag.
setQR(data.totp.qr_code)
})()
}, [])
return (
<>
{error && <div className="error">{error}</div>}
<img src={qr} />
<input
type="text"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value.trim())}
/>
<input type="button" value="Enable" onClick={onEnableClicked} />
<input type="button" value="Cancel" onClick={onCancelled} />
</>
)
}
```
This example illustrates MFA enrollment flow with QR code display, user code input, challenge creation, and verification.
Checking MFA requirement after sign-in
After a user signs in, use supabase.auth.mfa.getAuthenticatorAssuranceLevel() API to check if additional factors need to be verified. This method returns the user's current and next authenticator assurance level (AAL). If currentLevel is aal1 but nextLevel is aal2, the user should be given the option to go through MFA. This method is very fast (microseconds) and rarely uses the network.
Authenticator Assurance Level (AAL) states and meanings
AAL states have combined meanings: (1) Current Level: aal1, Next Level: aal1 = User does not have MFA enrolled. (2) Current Level: aal1, Next Level: aal2 = User has an MFA factor enrolled but has not verified it. (3) Current Level: aal2, Next Level: aal2 = User has verified their MFA factor. (4) Current Level: aal2, Next Level: aal1 = User has disabled their MFA factor (Stale JWT).
React AppWithMFA component example
```tsx
function AppWithMFA() {
const [readyToShow, setReadyToShow] = useState(false)
const [showMFAScreen, setShowMFAScreen] = useState(false)
useEffect(() => {
;(async () => {
try {
const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (error) {
throw error
}
console.log(data)
if (data.nextLevel === 'aal2' && data.nextLevel !== data.currentLevel) {
setShowMFAScreen(true)
}
} finally {
setReadyToShow(true)
}
})()
}, [])
if (readyToShow) {
if (showMFAScreen) {
return <AuthMFA />
}
return <App />
}
return <></>
}
```
This example wraps the App component with logic that shows an MFA challenge screen if necessary before showing the full authenticated application.
React AuthMFA challenge component example
```tsx
function AuthMFA() {
const [verifyCode, setVerifyCode] = useState('')
const [error, setError] = useState('')
const onSubmitClicked = () => {
setError('')
;(async () => {
const factors = await supabase.auth.mfa.listFactors()
if (factors.error) {
throw factors.error
}
const totpFactor = factors.data.totp[0]
if (!totpFactor) {
throw new Error('No TOTP factors found!')
}
const factorId = totpFactor.id
const challenge = await supabase.auth.mfa.challenge({ factorId })
if (challenge.error) {
setError(challenge.error.message)
throw challenge.error
}
const challengeId = challenge.data.id
const verify = await supabase.auth.mfa.verify({
factorId,
challengeId,
code: verifyCode,
})
if (verify.error) {
setError(verify.error.message)
throw verify.error
}
})()
}
return (
<>
<div>Please enter the code from your authenticator app.</div>
{error && <div className="error">{error}</div>}
<input
type="text"
value={verifyCode}
onChange={(e) => setVerifyCode(e.target.value.trim())}
/>
<input type="button" value="Submit" onClick={onSubmitClicked} />
</>
)
}
```
This example implements the challenge and verify logic for MFA sign-in, extracting available factors using listFactors() and creating a new challenge for each submit.
Using listFactors API for MFA
Extract available MFA factors for the user by calling supabase.auth.mfa.listFactors(). This method is very quick and rarely uses the network. If listFactors() returns more than one factor or of a different type, the user should be presented with a choice of which factor to use.
TOTP code validity period
In Supabase's TOTP implementation, each generated code remains valid for one interval, which spans 30 seconds. To account for minor time discrepancies, Supabase allows for a one-interval clock skew. This ensures that users can successfully authenticate within this timeframe, even if there are slight variations in system clocks.
MFA and SSO amr array warning
When Multi-Factor Authentication is used with SSO, the amr array may have a different method at index 0. This affects JWT extraction methods like auth.jwt()#>>'{amr,0,method}' which may not return 'sso/saml'.
MFA challenge rate limits
The /auth/v1/factors/:id/challenge and /auth/v1/factors/:id/verify endpoints for creating or verifying an MFA challenge are rate-limited by IP address. The limit is auth.rate_limits.mfa.requests_per_minute requests per minute, with bursts up to auth.rate_limits.mfa.requests_burst requests. This limit is not customizable.