Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.auth0.android.Auth0
import com.auth0.android.Auth0Exception
import com.auth0.android.NetworkErrorException
import com.auth0.android.authentication.mfa.MfaApiClient
import com.auth0.android.authentication.passwordless.DeliveryMethod
import com.auth0.android.authentication.passwordless.PasswordlessClient
import com.auth0.android.authentication.request.ActorToken
import com.auth0.android.dpop.DPoP
Expand All @@ -31,6 +32,7 @@ import com.auth0.android.result.Credentials
import com.auth0.android.result.DatabaseUser
import com.auth0.android.result.PasskeyChallenge
import com.auth0.android.result.PasskeyRegistrationChallenge
import com.auth0.android.result.PasskeyVerificationMethod
import com.auth0.android.result.SSOCredentials
import com.auth0.android.result.UserProfile
import com.google.gson.Gson
Expand Down Expand Up @@ -234,13 +236,19 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* @param authResponse the [PublicKeyCredentials] authentication response
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @param organization id of the organization to be associated with the user while signing in
* @param verification a map of identifier to the OTP code collected from the user, e.g.
* `mapOf(PasskeyVerificationMethod.EMAIL to "123456")`. The keys must match the identifiers returned in
* [PasskeyRegistrationChallenge.verificationRequired]. Defaults to an empty map when no identifier verification is
* required; the `verification` object is then omitted from the request.
* @return a request to configure and start that will yield [Credentials]
*/
@JvmOverloads
public fun signinWithPasskey(
authSession: String,
authResponse: PublicKeyCredentials,
realm: String? = null,
organization: String? = null,
verification: Map<PasskeyVerificationMethod, String> = emptyMap(),
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
Expand All @@ -249,11 +257,18 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
organization?.let { set(ORGANIZATION_KEY, organization) }
}.asDictionary()

return loginWithToken(params)
val request = loginWithToken(params)
.addParameter(
AUTH_RESPONSE_KEY,
Gson().toJsonTree(authResponse)
) as AuthenticationRequest
)
if (verification.isNotEmpty()) {
request.addParameter(
VERIFICATION_KEY,
gson.toJsonTree(verification.mapKeys { it.key.value })
)
}
return request as AuthenticationRequest
}


Expand Down Expand Up @@ -281,19 +296,25 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* @param authResponse the public key credential authentication response in JSON string format that follows the standard webauthn json format
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @param organization id of the organization to be associated with the user while signing in
* @param verification a map of identifier to the OTP code collected from the user, e.g.
* `mapOf(PasskeyVerificationMethod.EMAIL to "123456")`. The keys must match the identifiers returned in
* [PasskeyRegistrationChallenge.verificationRequired]. Defaults to an empty map when no identifier verification is
* required; the `verification` object is then omitted from the request.
* @return a request to configure and start that will yield [Credentials]
*/
@JvmOverloads
public fun signinWithPasskey(
authSession: String,
authResponse: String,
realm: String? = null,
organization: String? = null,
verification: Map<PasskeyVerificationMethod, String> = emptyMap(),
): AuthenticationRequest {
val publicKeyCredentials = gson.fromJson(
authResponse,
PublicKeyCredentials::class.java
)
return signinWithPasskey(authSession, publicKeyCredentials, realm, organization)
return signinWithPasskey(authSession, publicKeyCredentials, realm, organization, verification)
}


Expand Down Expand Up @@ -325,12 +346,17 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
* @param userData user information for registration.
* @param realm the connection to use. If excluded, the application will use the default connection configured in the tenant
* @param organization id of the organization to be associated with the user while signing up
* @param deliveryMethod the channel used to deliver the phone OTP. Only applies when the connection requires phone
* identifier verification. If the requested channel is not enabled on the connection, the server responds with an
* `invalid_request` error. Pass `null` to use the connection default.
* @return a request to configure and start that will yield [PasskeyRegistrationChallenge]
*/
@JvmOverloads
public fun signupWithPasskey(
userData: UserData,
realm: String? = null,
organization: String? = null
organization: String? = null,
deliveryMethod: DeliveryMethod? = null
): Request<PasskeyRegistrationChallenge, AuthenticationException> {
val url = auth0.getDomainUrl().toHttpUrl().newBuilder()
.addPathSegment(PASSKEY_PATH)
Expand All @@ -351,6 +377,7 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
.addParameters(params) as BaseRequest<PasskeyRegistrationChallenge, AuthenticationException>
post.addParameter(USER_PROFILE_KEY, gson.toJsonTree(userData.toUserProfile()))
userData.userMetadata?.let { post.addParameter(USER_METADATA_KEY, it) }
deliveryMethod?.let { post.addParameter(DELIVERY_METHOD_KEY, it.value) }
return post
}

Expand Down Expand Up @@ -1042,6 +1069,8 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
private const val USER_METADATA_KEY = "user_metadata"
private const val AUTH_SESSION_KEY = "auth_session"
private const val AUTH_RESPONSE_KEY = "authn_response"
private const val VERIFICATION_KEY = "verification"
private const val DELIVERY_METHOD_KEY = "delivery_method"
private const val USER_PROFILE_KEY = "user_profile"
private const val SIGN_UP_PATH = "signup"
private const val DB_CONNECTIONS_PATH = "dbconnections"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.auth0.android.provider.TokenValidationException
import com.auth0.android.result.MfaFactor
import com.auth0.android.result.MfaRequiredErrorPayload
import com.auth0.android.result.MfaRequirements
import com.auth0.android.result.PasskeyVerificationMethod

public class AuthenticationException : Auth0Exception {
private var code: String? = null
Expand Down Expand Up @@ -200,6 +201,48 @@ public class AuthenticationException : Auth0Exception {
public val isVerificationRequired: Boolean
get() = "requires_verification" == code

/**
* Whether a failed passkey signup identifier verification can be retried without restarting
* the registration. `true` when the error contains an `auth_session` and is either:
* - an `invalid_request`, because the `verification` codes were missing or empty. Re-collect
* the codes for the challenge's `verificationRequired`; or
* - an `invalid_grant`, because a code was wrong. Re-collect the codes for
* [passkeyVerificationRequired].
*
* Then call [com.auth0.android.authentication.AuthenticationAPIClient.signinWithPasskey] again
* with the same credential and [passkeyAuthSession]. When `false`, restart the registration.
*
* @see passkeyAuthSession
* @see passkeyVerificationRequired
*/
public val isPasskeyVerificationRetryable: Boolean
get() = ("invalid_grant" == code || "invalid_request" == code) && passkeyAuthSession != null

/**
* The auth session returned with a failed passkey verification, to be reused on the retry so
* the same passkey credential and session are kept. `null` when the error carries none.
*
* @see isPasskeyVerificationRetryable
*/
public val passkeyAuthSession: String?
get() = getValue(AUTH_SESSION_KEY) as? String

/**
* The identifiers whose OTP code was wrong and must be re-collected before retrying. Only set
* for a retryable `invalid_grant`; `null` otherwise, including for a retryable
* `invalid_request`, where the challenge's `verificationRequired` applies. Identifiers this
* version of the SDK does not know are skipped.
*
* @see isPasskeyVerificationRetryable
*/
public val passkeyVerificationRequired: List<PasskeyVerificationMethod>?
get() {
if (!isPasskeyVerificationRetryable || "invalid_grant" != code) return null
return (getValue(VERIFICATION_REQUIRED_KEY) as? List<*>)
?.filterIsInstance<String>()
?.mapNotNull { value -> PasskeyVerificationMethod.values().firstOrNull { it.value == value } }
}

/// When the MFA Token used on the login request is malformed or has expired
public val isMultifactorTokenInvalid: Boolean
get() = "expired_token" == code && "mfa_token is expired" == description ||
Expand Down Expand Up @@ -267,6 +310,8 @@ public class AuthenticationException : Auth0Exception {
internal const val ERROR_VALUE_CT_OPTIONS_INVALID =
"Custom tab options are received as null from the intent"
private const val ERROR_KEY = "error"
private const val AUTH_SESSION_KEY = "auth_session"
private const val VERIFICATION_REQUIRED_KEY = "verification_required"
private const val CODE_KEY = "code"
private const val DESCRIPTION_KEY = "description"
private const val ERROR_DESCRIPTION_KEY = "error_description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package com.auth0.android.authentication.passwordless
/**
* Delivery method for a phone-number OTP challenge.
*
* Maps to the `delivery_method` request parameter of `POST /otp/challenge`. [TEXT] sends the
* one-time code via SMS (the server default); [VOICE] delivers it through a voice call.
* Maps to the `delivery_method` request parameter of `POST /otp/challenge` and
* `POST /passkey/register`. [TEXT] sends the one-time code via SMS (the server default);
* [VOICE] delivers it through a voice call.
*
* @property value the wire value sent to the server.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,20 @@ import com.google.gson.annotations.SerializedName
/**
* Represents a challenge when user tries to register via passkeys.
*/
public data class PasskeyRegistrationChallenge(
public data class PasskeyRegistrationChallenge @JvmOverloads constructor(
@SerializedName("auth_session")
val authSession: String,
@SerializedName("authn_params_public_key")
val authParamsPublicKey: AuthnParamsPublicKey
val authParamsPublicKey: AuthnParamsPublicKey,
/**
* The identifiers (e.g. `[EMAIL]` or `[EMAIL, PHONE]`) that require OTP verification
* before a credential can be created and tokens issued. `null` when the server did not
* request any identifier verification. The OTP codes collected for these identifiers must
* be passed to [com.auth0.android.authentication.AuthenticationAPIClient.signinWithPasskey]
* via the `verification` parameter.
*/
@SerializedName("verification_required")
val verificationRequired: List<PasskeyVerificationMethod>? = null
)

public data class AuthnParamsPublicKey(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.auth0.android.result

import com.google.gson.annotations.SerializedName

/**
* An identifier whose ownership must be verified with a one-time code before a passkey signup
* can complete.
*
* Returned in [PasskeyRegistrationChallenge.verificationRequired] and
* [com.auth0.android.authentication.AuthenticationException.passkeyVerificationRequired]. Use it as
* the key when passing the collected codes in the `verification` map of
* [com.auth0.android.authentication.AuthenticationAPIClient.signinWithPasskey].
*
* ```kotlin
* val verification = mapOf(PasskeyVerificationMethod.EMAIL to emailCode)
* ```
*
* @property value the value used by the Auth0 Authentication API, e.g. `"email"`.
*/
public enum class PasskeyVerificationMethod(public val value: String) {
/** The user's email address. */
@SerializedName("email")
EMAIL("email"),

/** The user's phone number. */
@SerializedName("phone")
PHONE("phone")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading
Loading