Skip to content

Commit d19f90e

Browse files
committed
Addressed review comments and annded UT cases
1 parent fcb804d commit d19f90e

12 files changed

Lines changed: 2004 additions & 358 deletions

File tree

EXAMPLES.md

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
- [Authentication API](#authentication-api)
1616
- [Login with database connection](#login-with-database-connection)
1717
- [Login using MFA with One Time Password code](#login-using-mfa-with-one-time-password-code)
18+
- [MFA Flexible Factors Grant](#mfa-flexible-factors-grant)
19+
- [Handling MFA Required Errors](#handling-mfa-required-errors)
20+
- [Getting Available Authenticators](#getting-available-authenticators)
21+
- [Enrolling New Authenticators](#enrolling-new-authenticators)
22+
- [Challenging an Authenticator](#challenging-an-authenticator)
23+
- [Verifying MFA](#verifying-mfa)
1824
- [Passwordless Login](#passwordless-login)
1925
- [Step 1: Request the code](#step-1-request-the-code)
2026
- [Step 2: Input the code](#step-2-input-the-code)
@@ -418,6 +424,326 @@ authentication
418424

419425
> The default scope used is `openid profile email`. Regardless of the scopes set to the request, the `openid` scope is always enforced.
420426
427+
### MFA Flexible Factors Grant
428+
429+
The MFA Flexible Factors Grant allows you to handle MFA challenges during the authentication flow when users sign in to MFA-enabled connections. This feature requires your Application to have the *MFA* grant type enabled. Check [this article](https://auth0.com/docs/clients/client-grant-types) to learn how to enable it.
430+
431+
#### Handling MFA Required Errors
432+
433+
When a user signs in to an MFA-enabled connection, the authentication request will fail with an `AuthenticationException` that contains the MFA requirements. You can extract the MFA token and requirements from the error to proceed with the MFA flow.
434+
435+
```kotlin
436+
authentication
437+
.login("user@example.com", "password", "Username-Password-Authentication")
438+
.validateClaims()
439+
.start(object: Callback<Credentials, AuthenticationException> {
440+
override fun onFailure(exception: AuthenticationException) {
441+
if (exception.isMultifactorRequired) {
442+
// MFA is required - extract the MFA payload
443+
val mfaPayload = exception.mfaRequiredErrorPayload
444+
val mfaToken = mfaPayload?.mfaToken
445+
val requirements = mfaPayload?.mfaRequirements
446+
447+
// Check what actions are available
448+
val canChallenge = requirements?.challenge // List of authenticators to challenge
449+
val canEnroll = requirements?.enroll // List of factor types that can be enrolled
450+
451+
// Proceed with MFA flow using mfaToken
452+
}
453+
}
454+
455+
override fun onSuccess(credentials: Credentials) {
456+
// Login successful without MFA
457+
}
458+
})
459+
```
460+
461+
<details>
462+
<summary>Using coroutines</summary>
463+
464+
```kotlin
465+
try {
466+
val credentials = authentication
467+
.login("user@example.com", "password", "Username-Password-Authentication")
468+
.validateClaims()
469+
.await()
470+
println(credentials)
471+
} catch (e: AuthenticationException) {
472+
if (e.isMultifactorRequired) {
473+
val mfaPayload = e.mfaRequiredErrorPayload
474+
val mfaToken = mfaPayload?.mfaToken
475+
// Proceed with MFA flow
476+
}
477+
}
478+
```
479+
</details>
480+
481+
#### Creating the MFA API Client
482+
483+
Once you have the MFA token, create an MFA API client to perform MFA operations:
484+
485+
```kotlin
486+
val mfaClient = authentication.mfaClient(mfaToken)
487+
```
488+
489+
#### Getting Available Authenticators
490+
491+
Retrieve the list of authenticators that the user has enrolled and are allowed for this authentication flow. The `factorsAllowed` parameter filters the authenticators based on the allowed factor types from the MFA requirements.
492+
493+
```kotlin
494+
mfaClient
495+
.getAuthenticators(factorsAllowed = requirements?.challenge ?: emptyList())
496+
.start(object: Callback<List<MfaAuthenticator>, MfaListAuthenticatorsException> {
497+
override fun onFailure(exception: MfaListAuthenticatorsException) {
498+
// Handle error
499+
}
500+
501+
override fun onSuccess(authenticators: List<MfaAuthenticator>) {
502+
// Display authenticators for user to choose
503+
authenticators.forEach { auth ->
504+
println("Type: ${auth.authenticatorType}, ID: ${auth.id}")
505+
}
506+
}
507+
})
508+
```
509+
510+
<details>
511+
<summary>Using coroutines</summary>
512+
513+
```kotlin
514+
try {
515+
val authenticators = mfaClient
516+
.getAuthenticators(factorsAllowed = requirements?.challenge ?: emptyList())
517+
.await()
518+
println(authenticators)
519+
} catch (e: MfaListAuthenticatorsException) {
520+
e.printStackTrace()
521+
}
522+
```
523+
</details>
524+
525+
#### Enrolling New Authenticators
526+
527+
If the user doesn't have an authenticator enrolled, or needs to enroll a new one, you can use the enrollment methods. The available enrollment types depend on your tenant configuration.
528+
529+
##### Enroll Phone (SMS/Voice)
530+
531+
```kotlin
532+
mfaClient
533+
.enrollPhone("+11234567890", PhoneEnrollmentType.SMS)
534+
.start(object: Callback<MfaEnrollment, MfaEnrollmentException> {
535+
override fun onFailure(exception: MfaEnrollmentException) { }
536+
537+
override fun onSuccess(enrollment: MfaEnrollment) {
538+
// Phone enrolled - need to verify with OOB code
539+
val oobCode = enrollment.oobCode
540+
val bindingMethod = enrollment.bindingMethod
541+
}
542+
})
543+
```
544+
545+
##### Enroll Email
546+
547+
```kotlin
548+
mfaClient
549+
.enrollEmail("user@example.com")
550+
.start(object: Callback<MfaEnrollment, MfaEnrollmentException> {
551+
override fun onFailure(exception: MfaEnrollmentException) { }
552+
553+
override fun onSuccess(enrollment: MfaEnrollment) {
554+
// Email enrolled - need to verify with OOB code
555+
val oobCode = enrollment.oobCode
556+
}
557+
})
558+
```
559+
560+
##### Enroll OTP (Authenticator App)
561+
562+
```kotlin
563+
mfaClient
564+
.enrollOtp()
565+
.start(object: Callback<MfaEnrollment, MfaEnrollmentException> {
566+
override fun onFailure(exception: MfaEnrollmentException) { }
567+
568+
override fun onSuccess(enrollment: MfaEnrollment) {
569+
// Display QR code or secret for user to scan/enter in authenticator app
570+
val secret = enrollment.secret
571+
val barcodeUri = enrollment.barcodeUri
572+
}
573+
})
574+
```
575+
576+
##### Enroll Push Notification
577+
578+
```kotlin
579+
mfaClient
580+
.enrollPush()
581+
.start(object: Callback<MfaEnrollment, MfaEnrollmentException> {
582+
override fun onFailure(exception: MfaEnrollmentException) { }
583+
584+
override fun onSuccess(enrollment: MfaEnrollment) {
585+
// Display QR code for user to scan with Guardian app
586+
val barcodeUri = enrollment.barcodeUri
587+
}
588+
})
589+
```
590+
591+
#### Challenging an Authenticator
592+
593+
After selecting an authenticator, initiate a challenge. This will send an OTP code (for email/SMS) or push notification to the user.
594+
595+
```kotlin
596+
mfaClient
597+
.challenge(authenticatorId = "phone|dev_xxxx")
598+
.start(object: Callback<MfaChallengeResponse, MfaChallengeException> {
599+
override fun onFailure(exception: MfaChallengeException) { }
600+
601+
override fun onSuccess(challengeResponse: MfaChallengeResponse) {
602+
// Challenge initiated
603+
val challengeType = challengeResponse.challengeType
604+
val oobCode = challengeResponse.oobCode
605+
val bindingMethod = challengeResponse.bindingMethod
606+
}
607+
})
608+
```
609+
610+
<details>
611+
<summary>Using coroutines</summary>
612+
613+
```kotlin
614+
try {
615+
val challengeResponse = mfaClient
616+
.challenge(authenticatorId = "phone|dev_xxxx")
617+
.await()
618+
println(challengeResponse)
619+
} catch (e: MfaChallengeException) {
620+
e.printStackTrace()
621+
}
622+
```
623+
</details>
624+
625+
#### Verifying MFA
626+
627+
Complete the MFA flow by verifying with the appropriate method based on the authenticator type.
628+
629+
##### Verify with OTP (Authenticator App)
630+
631+
```kotlin
632+
mfaClient
633+
.verifyOtp(otp = "123456")
634+
.validateClaims()
635+
.start(object: Callback<Credentials, MfaVerifyException> {
636+
override fun onFailure(exception: MfaVerifyException) { }
637+
638+
override fun onSuccess(credentials: Credentials) {
639+
// MFA verification successful - user is now logged in
640+
}
641+
})
642+
```
643+
644+
<details>
645+
<summary>Using coroutines</summary>
646+
647+
```kotlin
648+
try {
649+
val credentials = mfaClient
650+
.verifyOtp(otp = "123456")
651+
.validateClaims()
652+
.await()
653+
println(credentials)
654+
} catch (e: MfaVerifyException) {
655+
e.printStackTrace()
656+
}
657+
```
658+
</details>
659+
660+
##### Verify with OOB (Email/SMS/Push)
661+
662+
For email, SMS, or push notification verification, use the OOB code from the challenge response along with the binding code (OTP) received by the user:
663+
664+
```kotlin
665+
mfaClient
666+
.verifyOob(oobCode = oobCode, bindingCode = "123456") // bindingCode is optional for push
667+
.validateClaims()
668+
.start(object: Callback<Credentials, MfaVerifyException> {
669+
override fun onFailure(exception: MfaVerifyException) { }
670+
671+
override fun onSuccess(credentials: Credentials) {
672+
// MFA verification successful
673+
}
674+
})
675+
```
676+
677+
##### Verify with Recovery Code
678+
679+
If the user has lost access to their MFA device, they can use a recovery code:
680+
681+
```kotlin
682+
mfaClient
683+
.verifyRecoveryCode(recoveryCode = "ABCD1234EFGH5678")
684+
.validateClaims()
685+
.start(object: Callback<Credentials, MfaVerifyException> {
686+
override fun onFailure(exception: MfaVerifyException) { }
687+
688+
override fun onSuccess(credentials: Credentials) {
689+
// MFA verification successful
690+
// Note: A new recovery code may be returned in credentials
691+
}
692+
})
693+
```
694+
695+
#### Complete MFA Flow Example
696+
697+
Here's a complete example showing the typical MFA flow:
698+
699+
```kotlin
700+
// Step 1: Attempt login
701+
authentication
702+
.login(email, password, connection)
703+
.validateClaims()
704+
.start(object: Callback<Credentials, AuthenticationException> {
705+
override fun onFailure(exception: AuthenticationException) {
706+
if (exception.isMultifactorRequired) {
707+
val mfaPayload = exception.mfaRequiredErrorPayload ?: return
708+
val mfaToken = mfaPayload.mfaToken ?: return
709+
val requirements = mfaPayload.mfaRequirements
710+
711+
// Step 2: Create MFA client
712+
val mfaClient = authentication.mfaClient(mfaToken)
713+
714+
// Step 3: Get available authenticators
715+
mfaClient
716+
.getAuthenticators(factorsAllowed = requirements?.challenge ?: emptyList())
717+
.start(object: Callback<List<MfaAuthenticator>, MfaListAuthenticatorsException> {
718+
override fun onSuccess(authenticators: List<MfaAuthenticator>) {
719+
if (authenticators.isNotEmpty()) {
720+
// Step 4: Challenge the first authenticator
721+
val authenticator = authenticators.first()
722+
mfaClient
723+
.challenge(authenticatorId = authenticator.id)
724+
.start(object: Callback<MfaChallengeResponse, MfaChallengeException> {
725+
override fun onSuccess(challengeResponse: MfaChallengeResponse) {
726+
// Step 5: Prompt user for OTP and verify
727+
// ... show OTP input UI, then call verifyOtp/verifyOob
728+
}
729+
override fun onFailure(e: MfaChallengeException) { }
730+
})
731+
} else {
732+
// No authenticators enrolled - need to enroll one
733+
// ... show enrollment UI
734+
}
735+
}
736+
override fun onFailure(e: MfaListAuthenticatorsException) { }
737+
})
738+
}
739+
}
740+
741+
override fun onSuccess(credentials: Credentials) {
742+
// Login successful without MFA
743+
}
744+
})
745+
```
746+
421747
### Passwordless Login
422748

423749
This feature requires your Application to have the *Passwordless OTP* enabled. See [this article](https://auth0.com/docs/clients/client-grant-types) to learn how to enable it.

auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
9595
* if (error.isMultifactorRequired) {
9696
* val mfaToken = error.mfaToken
9797
* if (mfaToken != null) {
98-
* val mfaClient = authClient.mfa(mfaToken)
98+
* val mfaClient = authClient.mfaClient(mfaToken)
9999
* // Use mfaClient to handle MFA flow
100100
* }
101101
* }
@@ -105,7 +105,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
105105
* @param mfaToken The token received in the 'mfa_required' error from a login attempt.
106106
* @return A new [MfaApiClient] instance configured for the transaction.
107107
*/
108-
public fun mfa(mfaToken: String): MfaApiClient {
108+
public fun mfaClient(mfaToken: String): MfaApiClient {
109109
return MfaApiClient(this.auth0, mfaToken)
110110
}
111111

@@ -1147,7 +1147,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
11471147
private const val WELL_KNOWN_PATH = ".well-known"
11481148
private const val JWKS_FILE_PATH = "jwks.json"
11491149
private const val TAG = "AuthenticationAPIClient"
1150-
internal fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
1150+
private fun createErrorAdapter(): ErrorAdapter<AuthenticationException> {
11511151
val mapAdapter = forMap(GsonProvider.gson)
11521152
return object : ErrorAdapter<AuthenticationException> {
11531153
override fun fromRawResponse(

0 commit comments

Comments
 (0)