Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
- [Enroll a TOTP (Authenticator App) Method](#enroll-a-totp-authenticator-app-method)
- [Enroll a Push Notification Method](#enroll-a-push-notification-method)
- [Enroll a Recovery Code](#enroll-a-recovery-code)
- [Enroll a Password Method](#enroll-a-password-method)
- [Verify an Enrollment](#verify-an-enrollment)
- [Delete an Authentication Method](#delete-an-authentication-method)
- [Update an Authentication Method](#update-an-authentication-method)
Expand Down Expand Up @@ -2595,6 +2596,69 @@ myAccountClient.enrollRecoveryCode()
```
</details>

### Enroll a Password Method
**Scopes required:** `create:me:authentication_methods`

Enrolling a password authentication method is a two-step process. First, you request an enrollment challenge, which returns the connection's password [policy](https://auth0.com/docs/authenticate/database-connections/password-options) so you can guide the user to choose a compliant password. Then, you confirm the enrollment with the new password.

#### 1. Request an enrollment challenge

```kotlin
myAccountClient.enrollPassword()
.start(object : Callback<PasswordEnrollmentChallenge, MyAccountException> {
override fun onSuccess(result: PasswordEnrollmentChallenge) {
// Use result.policy to validate the user's new password before confirming.
// Then use result.id and result.authSession to verify.
}
override fun onFailure(error: MyAccountException) { }
})
```

<details>
<summary>Using Java</summary>

```java
myAccountClient.enrollPassword()
.start(new Callback<PasswordEnrollmentChallenge, MyAccountException>() {
@Override
public void onSuccess(PasswordEnrollmentChallenge result) {
// Use result.getPolicy() to validate the user's new password before confirming.
// Then use result.getId() and result.getAuthSession() to verify.
}
@Override
public void onFailure(@NonNull MyAccountException error) { }
});
```
</details>

#### 2. Confirm the enrollment

```kotlin
myAccountClient.verifyPassword("challenge_id_from_enroll", "auth_session_from_enroll", "new_password")
.start(object : Callback<PasswordAuthenticationMethod, MyAccountException> {
override fun onSuccess(result: PasswordAuthenticationMethod) {
// Enrollment successful
}
override fun onFailure(error: MyAccountException) { }
})
```

<details>
<summary>Using Java</summary>

```java
myAccountClient.verifyPassword("challenge_id_from_enroll", "auth_session_from_enroll", "new_password")
.start(new Callback<PasswordAuthenticationMethod, MyAccountException>() {
@Override
public void onSuccess(PasswordAuthenticationMethod result) {
// Enrollment successful
}
@Override
public void onFailure(@NonNull MyAccountException error) { }
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```
</details>

### Verify an Enrollment
**Scopes required:** `create:me:authentication_methods`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import com.auth0.android.result.Factor
import com.auth0.android.result.Factors
import com.auth0.android.result.PasskeyAuthenticationMethod
import com.auth0.android.result.PasskeyEnrollmentChallenge
import com.auth0.android.result.PasswordAuthenticationMethod
import com.auth0.android.result.PasskeyRegistrationChallenge
import com.auth0.android.result.PasswordEnrollmentChallenge
import com.auth0.android.result.RecoveryCodeEnrollmentChallenge
import com.auth0.android.result.TotpEnrollmentChallenge
import com.google.gson.Gson
Expand Down Expand Up @@ -743,6 +745,100 @@ public class MyAccountAPIClient @VisibleForTesting(otherwise = VisibleForTesting
.addHeader(AUTHORIZATION_KEY, authorizationHeader)
}

/**
* Starts the enrollment of a password authentication method. This is the first part of a
* two-step flow: the returned challenge carries the [com.auth0.android.result.PasswordPolicy]
* the new password must satisfy, along with the `id` and `authSession` needed to confirm the
* enrollment via [verifyPassword].
*
* ## Scopes Required
* `create:me:authentication_methods`
*
* ## Usage
*
* ```kotlin
* val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
* val apiClient = MyAccountAPIClient(auth0, accessToken)
*
* apiClient.enrollPassword()
* .start(object : Callback<PasswordEnrollmentChallenge, MyAccountException> {
* override fun onSuccess(result: PasswordEnrollmentChallenge) {
* // Use result.policy to guide the user, then call verifyPassword(...)
* }
* override fun onFailure(error: MyAccountException) { //... }
* })
* ```
* @param userIdentity Unique identifier of the current user's identity. Needed if the user logged in with a [linked account](https://auth0.com/docs/manage-users/user-accounts/user-account-linking)
* @param connection Name of the database connection where the user is stored
* @return a request that will yield a password enrollment challenge.
*/
@JvmOverloads
public fun enrollPassword(
userIdentity: String? = null,
connection: String? = null
): Request<PasswordEnrollmentChallenge, MyAccountException> {
val url = getDomainUrlBuilder().addPathSegment(AUTHENTICATION_METHODS).build()
val params = ParameterBuilder.newBuilder().apply {
set(TYPE_KEY, AuthenticationMethodType.PASSWORD.type)
userIdentity?.let { set(USER_IDENTITY_ID_KEY, it) }
connection?.let { set(CONNECTION_KEY, it) }
}.asDictionary()
return factory.post(
url.toString(),
GsonAdapter(PasswordEnrollmentChallenge::class.java, gson),
dPoP
)
.addParameters(params)
.addHeader(AUTHORIZATION_KEY, authorizationHeader)
}

/**
* Confirms the enrollment of a password method by providing the new password.
*
* ## Scopes Required
* `create:me:authentication_methods`
*
* ## Usage
*
* ```kotlin
* val auth0 = Auth0.getInstance("YOUR_CLIENT_ID", "YOUR_DOMAIN")
* val apiClient = MyAccountAPIClient(auth0, accessToken)
*
* val authMethodId = "from_enrollment_challenge"
* val authSession = "from_enrollment_challenge"
* val newPassword = "the_users_new_password"
*
* apiClient.verifyPassword(authMethodId, authSession, newPassword)
* .start(object : Callback<PasswordAuthenticationMethod, MyAccountException> {
* override fun onSuccess(result: PasswordAuthenticationMethod) { //... }
* override fun onFailure(error: MyAccountException) { //... }
* })
* ```
* @param authenticationMethodId The ID of the method being verified (from the enrollment challenge).
* @param authSession The auth session from the enrollment challenge.
* @param newPassword The new password to set, satisfying the policy from the enrollment challenge.
* @return a request that will yield the newly verified password authentication method.
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
public fun verifyPassword(
authenticationMethodId: String,
authSession: String,
newPassword: String
): Request<PasswordAuthenticationMethod, MyAccountException> {
val url = getDomainUrlBuilder()
.addPathSegment(AUTHENTICATION_METHODS)
.addPathSegment(authenticationMethodId)
.addPathSegment(VERIFY)
.build()
val params = mapOf(NEW_PASSWORD_KEY to newPassword, AUTH_SESSION_KEY to authSession)
return factory.post(
url.toString(),
GsonAdapter(PasswordAuthenticationMethod::class.java, gson),
dPoP
)
.addParameters(params)
.addHeader(AUTHORIZATION_KEY, authorizationHeader)
}

// WebAuthn methods are private.
/**
* Starts the enrollment of a WebAuthn Platform (e.g., biometrics) authenticator.
Expand Down Expand Up @@ -824,6 +920,7 @@ public class MyAccountAPIClient @VisibleForTesting(otherwise = VisibleForTesting
private const val AUTHORIZATION_KEY = "Authorization"
private const val LOCATION_KEY = "location"
private const val AUTH_SESSION_KEY = "auth_session"
private const val NEW_PASSWORD_KEY = "new_password"
private const val AUTHN_RESPONSE_KEY = "authn_response"
private const val PREFERRED_AUTHENTICATION_METHOD = "preferred_authentication_method"
private const val AUTHENTICATION_METHOD_NAME = "name"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public sealed class EnrollmentChallenge {
jsonObject.has("barcode_uri") -> TotpEnrollmentChallenge::class.java
jsonObject.has("recovery_code") -> RecoveryCodeEnrollmentChallenge::class.java
jsonObject.has("authn_params_public_key") -> PasskeyEnrollmentChallenge::class.java
jsonObject.has("oob_code") -> OobEnrollmentChallenge::class.java
jsonObject.has("oob_code") -> OobEnrollmentChallenge::class.java
jsonObject.has("policy") -> PasswordEnrollmentChallenge::class.java
else -> MfaEnrollmentChallenge::class.java
}
return context.deserialize(jsonObject, targetClass)
Expand Down Expand Up @@ -72,4 +73,17 @@ public data class RecoveryCodeEnrollmentChallenge(
override val authSession: String,
@SerializedName("recovery_code")
public val recoveryCode: String
) : EnrollmentChallenge()

/**
* Enrollment challenge for a password authentication method. Includes the [policy] the new password
* must satisfy, so the app can guide the user before confirming the enrollment.
*/
public data class PasswordEnrollmentChallenge(
@SerializedName("id")
override val id: String,
@SerializedName("auth_session")
override val authSession: String,
@SerializedName("policy")
public val policy: PasswordPolicy
) : EnrollmentChallenge()
122 changes: 122 additions & 0 deletions auth0/src/main/java/com/auth0/android/result/PasswordPolicy.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.auth0.android.result

import com.google.gson.annotations.SerializedName

/**
* Describes the password policy that a new password must satisfy, as returned by the My Account API
* when starting a password enrollment. Use it to build a UI that guides the user toward a compliant
* password.
*/
public data class PasswordPolicy(
/**
* Rules governing the structural complexity of the password (length, character types, etc.).
*/
@SerializedName("complexity")
public val complexity: PasswordComplexity,
/**
* Rules that prevent the password from containing personal information taken from the user profile.
*/
@SerializedName("profile_data")
public val profileData: PasswordProfileData,
/**
* Rules that prevent reuse of previously used passwords.
*/
@SerializedName("history")
public val history: PasswordHistory,
/**
* Rules that prevent the use of common dictionary words as passwords.
*/
@SerializedName("dictionary")
public val dictionary: PasswordDictionary
)

/**
* Structural complexity requirements for a password.
*/
public data class PasswordComplexity(
/**
* The minimum number of characters the password must contain.
*/
@SerializedName("min_length")
public val minLength: Int?,
/**
* The character classes the password may be required to include.
* Possible values: `uppercase`, `lowercase`, `number`, `special`.
*/
@SerializedName("character_types")
public val characterTypes: List<String>?,
/**
* How the [characterTypes] requirement is enforced.
* Possible values: `all` (every listed type is required), `three_of_four`.
*/
@SerializedName("character_type_rule")
public val characterTypeRule: String?,
/**
* Whether identical consecutive characters are permitted.
* Possible values: `allow`, `block`.
*/
@SerializedName("identical_characters")
public val identicalCharacters: String?,
/**
* Whether sequential characters (e.g. `abc`, `123`) are permitted.
* Possible values: `allow`, `block`.
*/
@SerializedName("sequential_characters")
public val sequentialCharacters: String?,
/**
* How a password that exceeds the maximum allowed length is handled.
* Possible values: `truncate`, `error`.
*/
@SerializedName("max_length_exceeded")
public val maxLengthExceeded: String?
)

/**
* Rules that block the use of personal information from the user profile within a password.
*/
public data class PasswordProfileData(
/**
* Whether blocking of personal information is enabled.
*/
@SerializedName("active")
public val active: Boolean?,
/**
* The user profile fields whose values must not appear in the password
* (e.g. `name`, `email`, `user_metadata.first`).
*/
@SerializedName("blocked_fields")
public val blockedFields: List<String>?
)

/**
* Rules that prevent reuse of previously used passwords.
*/
public data class PasswordHistory(
/**
* Whether password history enforcement is enabled.
*/
@SerializedName("active")
public val active: Boolean?,
/**
* The number of previous passwords that cannot be reused.
*/
@SerializedName("size")
public val size: Int?
)

/**
* Rules that prevent the use of common dictionary words as passwords.
*/
public data class PasswordDictionary(
/**
* Whether dictionary checking is enabled.
*/
@SerializedName("active")
public val active: Boolean?,
/**
* The default dictionary used for the check.
* Possible values: `en_10k`, `en_100k`.
*/
@SerializedName("default")
public val default: String?
)
Loading
Loading