Skip to content

Commit 038c934

Browse files
feat(mfa): Update MFA API to use 'factor_type' instead of 'authenticator_types' for enrollment and challenge
1 parent f2746e1 commit 038c934

3 files changed

Lines changed: 54 additions & 38 deletions

File tree

examples/MFA.md

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ Enroll an OTP authenticator (Google Authenticator, Microsoft Authenticator, etc.
173173
try:
174174
enrollment = await server_client.mfa.enroll_authenticator({
175175
"mfa_token": mfa_token,
176-
"authenticator_types": "otp"
176+
"factor_type": "otp"
177177
})
178178

179179
# Display QR code to user
@@ -192,8 +192,7 @@ Enroll an SMS authenticator:
192192
try:
193193
enrollment = await server_client.mfa.enroll_authenticator({
194194
"mfa_token": mfa_token,
195-
"authenticator_types": "sms",
196-
"oob_channels": "sms",
195+
"factor_type": "sms",
197196
"phone_number": "+12025551234" # E.164 format
198197
})
199198

@@ -213,8 +212,7 @@ Enroll a voice call authenticator:
213212
try:
214213
enrollment = await server_client.mfa.enroll_authenticator({
215214
"mfa_token": mfa_token,
216-
"authenticator_types": "voice",
217-
"oob_channels": "voice",
215+
"factor_type": "voice",
218216
"phone_number": "+12025551234" # E.164 format
219217
})
220218

@@ -233,8 +231,7 @@ Enroll an email authenticator:
233231
try:
234232
enrollment = await server_client.mfa.enroll_authenticator({
235233
"mfa_token": mfa_token,
236-
"authenticator_types": "email",
237-
"oob_channels": "email",
234+
"factor_type": "email",
238235
"email": "user@example.com"
239236
})
240237

@@ -255,7 +252,7 @@ After enrolling an authenticator, or when the user has existing authenticators,
255252
try:
256253
challenge = await server_client.mfa.challenge_authenticator({
257254
"mfa_token": mfa_token,
258-
"challenge_type": "oob",
255+
"factor_type": "sms",
259256
"authenticator_id": "sms|dev_xxx"
260257
})
261258

@@ -273,7 +270,7 @@ except Exception as error:
273270
try:
274271
challenge = await server_client.mfa.challenge_authenticator({
275272
"mfa_token": mfa_token,
276-
"challenge_type": "oob",
273+
"factor_type": "email",
277274
"authenticator_id": "email|dev_xxx"
278275
})
279276

@@ -293,7 +290,7 @@ except Exception as error:
293290
try:
294291
challenge = await server_client.mfa.challenge_authenticator({
295292
"mfa_token": mfa_token,
296-
"challenge_type": "otp",
293+
"factor_type": "otp",
297294
"authenticator_id": "otp|dev_xxx"
298295
})
299296

@@ -443,7 +440,7 @@ async def handle_mfa_enrollment_flow(server_client, mfa_token):
443440
# User selects OTP
444441
enrollment = await server_client.mfa.enroll_authenticator({
445442
"mfa_token": mfa_token,
446-
"authenticator_types": "otp"
443+
"factor_type": "otp"
447444
})
448445

449446
# Display QR code to user
@@ -491,11 +488,9 @@ async def handle_mfa_challenge_flow(server_client, mfa_token):
491488
selected_auth = authenticators[selected_index]
492489

493490
# Initiate challenge
494-
challenge_type = "otp" if selected_auth.authenticator_type == "otp" else "oob"
495-
496491
challenge = await server_client.mfa.challenge_authenticator({
497492
"mfa_token": mfa_token,
498-
"challenge_type": challenge_type,
493+
"factor_type": selected_auth.authenticator_type,
499494
"authenticator_id": selected_auth.id
500495
})
501496

@@ -597,7 +592,7 @@ async def handle_mfa_with_error_handling(server_client):
597592
# Initiate challenge
598593
challenge = await server_client.mfa.challenge_authenticator({
599594
"mfa_token": mfa_token,
600-
"challenge_type": "oob",
595+
"factor_type": "sms",
601596
"authenticator_id": authenticators[0].id
602597
})
603598
except ApiError as challenge_error:

src/auth0_server_python/auth_server/mfa_client.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ async def enroll_authenticator(
155155
156156
Args:
157157
options: Dict containing enrollment parameters.
158-
Required: 'mfa_token', 'authenticator_types'.
159-
Optional: 'oob_channels', 'phone_number', 'email'.
158+
Required: 'mfa_token', 'factor_type' (otp, sms, voice, email).
159+
Optional: 'phone_number', 'email'.
160160
161161
Returns:
162162
OtpEnrollmentResponse or OobEnrollmentResponse.
@@ -165,15 +165,28 @@ async def enroll_authenticator(
165165
MfaEnrollmentError: When enrollment fails.
166166
"""
167167
mfa_token = options["mfa_token"]
168+
factor_type = options["factor_type"]
168169
url = f"{self._base_url}/mfa/associate"
169170

171+
# Map factor_type to Auth0 API parameters
172+
if factor_type == "otp":
173+
authenticator_type = "otp"
174+
oob_channels = None
175+
elif factor_type in ["sms", "voice", "email"]:
176+
authenticator_type = "oob"
177+
oob_channels = factor_type
178+
else:
179+
raise MfaEnrollmentError(
180+
f"Unsupported factor_type: {factor_type}. Supported types: otp, sms, voice, email"
181+
)
182+
170183
# Build API request body
171184
body: dict[str, Any] = {
172-
"authenticator_types": options["authenticator_types"]
185+
"authenticator_types": authenticator_type
173186
}
174187

175-
if "oob_channels" in options:
176-
body["oob_channels"] = options["oob_channels"]
188+
if oob_channels:
189+
body["oob_channels"] = oob_channels
177190

178191
if "phone_number" in options and options["phone_number"]:
179192
body["phone_number"] = options["phone_number"]
@@ -224,7 +237,7 @@ async def challenge_authenticator(
224237
Initiates an MFA challenge for user verification.
225238
226239
Args:
227-
options: Dict containing 'mfa_token', 'challenge_type',
240+
options: Dict containing 'mfa_token', 'factor_type' (otp, sms, voice, email),
228241
and optionally 'authenticator_id'.
229242
230243
Returns:
@@ -234,12 +247,23 @@ async def challenge_authenticator(
234247
MfaChallengeError: When the challenge fails.
235248
"""
236249
mfa_token = options["mfa_token"]
250+
factor_type = options["factor_type"]
237251
url = f"{self._base_url}/mfa/challenge"
238252

253+
# Map factor_type to Auth0 API challenge_type
254+
if factor_type == "otp":
255+
challenge_type = "otp"
256+
elif factor_type in ["sms", "voice", "email"]:
257+
challenge_type = "oob"
258+
else:
259+
raise MfaChallengeError(
260+
f"Unsupported factor_type: {factor_type}. Supported types: otp, sms, voice, email"
261+
)
262+
239263
body: dict[str, Any] = {
240264
"mfa_token": mfa_token,
241265
"client_id": self._client_id,
242-
"challenge_type": options["challenge_type"]
266+
"challenge_type": challenge_type
243267
}
244268

245269
if "authenticator_id" in options and options["authenticator_id"]:

src/auth0_server_python/tests/test_mfa_client.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ async def test_enroll_otp_success(self, mocker):
189189

190190
result = await client.enroll_authenticator({
191191
"mfa_token": "tok",
192-
"authenticator_types": ["otp"]
192+
"factor_type": "otp"
193193
})
194194
assert isinstance(result, OtpEnrollmentResponse)
195195
assert result.secret == "JBSWY3DPEHPK3PXP"
@@ -210,8 +210,7 @@ async def test_enroll_sms_oob_success(self, mocker):
210210

211211
result = await client.enroll_authenticator({
212212
"mfa_token": "tok",
213-
"authenticator_types": ["oob"],
214-
"oob_channels": ["sms"],
213+
"factor_type": "sms",
215214
"phone_number": "+1234567890"
216215
})
217216
assert isinstance(result, OobEnrollmentResponse)
@@ -231,8 +230,7 @@ async def test_enroll_email_oob_success(self, mocker):
231230

232231
result = await client.enroll_authenticator({
233232
"mfa_token": "tok",
234-
"authenticator_types": ["oob"],
235-
"oob_channels": ["email"],
233+
"factor_type": "email",
236234
"email": "user@example.com"
237235
})
238236
assert isinstance(result, OobEnrollmentResponse)
@@ -254,8 +252,7 @@ async def test_enroll_push_auth0_channel_success(self, mocker):
254252

255253
result = await client.enroll_authenticator({
256254
"mfa_token": "tok",
257-
"authenticator_types": ["oob"],
258-
"oob_channels": ["auth0"]
255+
"factor_type": "sms"
259256
})
260257
assert isinstance(result, OobEnrollmentResponse)
261258
assert result.oob_channel == "auth0"
@@ -274,7 +271,7 @@ async def test_enroll_api_error(self, mocker):
274271
with pytest.raises(MfaEnrollmentError) as exc:
275272
await client.enroll_authenticator({
276273
"mfa_token": "tok",
277-
"authenticator_types": ["otp"]
274+
"factor_type": "otp"
278275
})
279276
assert "Bad enrollment request" in str(exc.value)
280277

@@ -291,9 +288,9 @@ async def test_enroll_unexpected_authenticator_type(self, mocker):
291288
with pytest.raises(MfaEnrollmentError) as exc:
292289
await client.enroll_authenticator({
293290
"mfa_token": "tok",
294-
"authenticator_types": ["unknown_type"]
291+
"factor_type": "unknown"
295292
})
296-
assert "Unexpected authenticator type" in str(exc.value)
293+
assert "Unsupported factor_type" in str(exc.value)
297294

298295

299296
# ── delete_authenticator ─────────────────────────────────────────────────────
@@ -313,7 +310,7 @@ async def test_challenge_otp_success(self, mocker):
313310

314311
result = await client.challenge_authenticator({
315312
"mfa_token": "tok",
316-
"challenge_type": "otp"
313+
"factor_type": "otp"
317314
})
318315
assert isinstance(result, ChallengeResponse)
319316
assert result.challenge_type == "otp"
@@ -332,7 +329,7 @@ async def test_challenge_oob_success(self, mocker):
332329

333330
result = await client.challenge_authenticator({
334331
"mfa_token": "tok",
335-
"challenge_type": "oob",
332+
"factor_type": "sms",
336333
"authenticator_id": "auth|456"
337334
})
338335
assert result.challenge_type == "oob"
@@ -352,7 +349,7 @@ async def test_challenge_api_error(self, mocker):
352349
with pytest.raises(MfaChallengeError) as exc:
353350
await client.challenge_authenticator({
354351
"mfa_token": "tok",
355-
"challenge_type": "otp"
352+
"factor_type": "otp"
356353
})
357354
assert "Token expired" in str(exc.value)
358355

@@ -371,7 +368,7 @@ async def test_challenge_expired_mfa_token(self, mocker):
371368
with pytest.raises(MfaChallengeError) as exc:
372369
await client.challenge_authenticator({
373370
"mfa_token": "expired_tok",
374-
"challenge_type": "otp"
371+
"factor_type": "otp"
375372
})
376373
assert "mfa_token is expired" in str(exc.value)
377374

@@ -390,7 +387,7 @@ async def test_challenge_email_with_authenticator_id(self, mocker):
390387

391388
result = await client.challenge_authenticator({
392389
"mfa_token": "tok",
393-
"challenge_type": "oob",
390+
"factor_type": "email",
394391
"authenticator_id": "email|dev_Fvx38nHufsGL5lWI"
395392
})
396393
assert result.challenge_type == "oob"
@@ -412,7 +409,7 @@ async def test_challenge_sms_with_authenticator_id(self, mocker):
412409

413410
result = await client.challenge_authenticator({
414411
"mfa_token": "tok",
415-
"challenge_type": "oob",
412+
"factor_type": "sms",
416413
"authenticator_id": "sms|dev_h1uXXoVjQ5BpU9iQ"
417414
})
418415
assert result.challenge_type == "oob"

0 commit comments

Comments
 (0)