Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9478b7a
feat: add passkey identifier verification support
utkrishtsahu Oct 9, 2026
7043867
chore(deps): bump the codeql group with 3 updates
dependabot[bot] Oct 13, 2026
030b6c2
chore(deps): bump the codeql group with 3 updates (#1089)
pmathew92 Oct 13, 2026
68df2f5
fix(storage): authorize MGF1 digest to prevent INCOMPATIBLE_DEVICE on…
utkrishtsahu Aug 12, 2026
17b542f
docs(changelog): retain 3.21.1-beta.0 and 3.21.1 release history
utkrishtsahu Oct 14, 2026
06b07fa
fix(storage): authorize MGF1 digest to prevent INCOMPATIBLE_DEVICE on…
utkrishtsahu Oct 14, 2026
0750a5d
fix(passkey): address review feedback on identifier verification
utkrishtsahu Oct 14, 2026
d62794c
docs(passkey): reword verification intro, declare retry limit
utkrishtsahu Oct 14, 2026
22de92b
Merge remote-tracking branch 'origin/main' into feat/passkey-identifi…
utkrishtsahu Oct 14, 2026
6847019
fix(passkey): simplify verification types and retry rule
utkrishtsahu Oct 14, 2026
73836f4
fix(passkey): revert 429 mapping to too_many_attempts
utkrishtsahu Oct 15, 2026
3a2c775
fix(passkey): drop client-side code check, clarify retry cases
utkrishtsahu Oct 15, 2026
df60ab4
chore(deps): bump actions/setup-java from 6.0.1 to 6.1.0 in the setup…
dependabot[bot] Oct 21, 2026
3165d27
chore(deps): bump the codeql group with 3 updates (#1096)
dependabot[bot] Oct 21, 2026
acf4068
fix: stop reporting an unrecognized biometric as a final failure (#1100)
pmathew92 Oct 21, 2026
3371366
feat(passkey): validate verification codes against signup challenge
utkrishtsahu Oct 21, 2026
d9a9843
fix(passkey): align verification check with Auth0.swift
utkrishtsahu Oct 21, 2026
480e74a
Merge remote-tracking branch 'origin/main' into feat/passkey-identifi…
utkrishtsahu Oct 21, 2026
30ea411
refactor(passkey): pass verificationRequired instead of the challenge
utkrishtsahu Oct 22, 2026
83316ef
refactor(passkey): reuse gson and move private helper to the bottom
utkrishtsahu Oct 22, 2026
7dd83af
fix(passkey): treat whitespace-only verification codes as empty
utkrishtsahu Oct 22, 2026
b9c04ee
feat: add passkey identifier verification support (#1087)
utkrishtsahu Oct 22, 2026
418ebe7
Release 4.2.0
utkrishtsahu Oct 22, 2026
42ac50f
Release 4.2.0 (#1104)
utkrishtsahu Oct 22, 2026
d28b1ea
fix: keep scope case when appending the openid scope (#1102)
RaphaelFakhri Oct 22, 2026
f52bbf1
Merge remote-tracking branch 'origin/main' into sync/main-into-v5-202…
pmathew92 Oct 23, 2026
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
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection.

- name: Set up Java
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: 'temurin'
java-version: '17'
Expand All @@ -45,15 +45,15 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
languages: ${{ matrix.language }}
queries: +security-and-quality

- name: Autobuild
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/autobuild@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4.39.0
with:
category: "/language:${{ matrix.language }}"
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,41 @@
**Added**
- feat(embedded): add /e/authorize email OTP flow [\#1075](https://github.com/auth0/Auth0.Android/pull/1075) ([pmathew92](https://github.com/pmathew92))

## [4.2.0](https://github.com/auth0/Auth0.Android/tree/4.2.0) (2026-10-22)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/4.1.0...4.2.0)

**Added**
- feat: add passkey identifier verification support [\#1087](https://github.com/auth0/Auth0.Android/pull/1087) ([utkrishtsahu](https://github.com/utkrishtsahu))

**Fixed**
- fix: stop reporting an unrecognized biometric as a final failure [\#1100](https://github.com/auth0/Auth0.Android/pull/1100) ([pmathew92](https://github.com/pmathew92))
- fix(storage): authorize MGF1 digest to prevent INCOMPATIBLE_DEVICE on Keystore2 [\#1092](https://github.com/auth0/Auth0.Android/pull/1092) ([utkrishtsahu](https://github.com/utkrishtsahu))

## [3.21.2](https://github.com/auth0/Auth0.Android/tree/3.21.2) (2026-10-21)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/3.21.1...3.21.2)

**Fixed**
- fix: stop reporting an unrecognized biometric as a final failure [\#1098](https://github.com/auth0/Auth0.Android/pull/1098) ([pmathew92](https://github.com/pmathew92))

## [3.21.1](https://github.com/auth0/Auth0.Android/tree/3.21.1) (2026-10-13)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/3.21.0...3.21.1)

**Fixed**
- fix(storage): authorize MGF1 digest to prevent INCOMPATIBLE_DEVICE on Keystore2 [\#1048](https://github.com/auth0/Auth0.Android/pull/1048) ([utkrishtsahu](https://github.com/utkrishtsahu))

## [4.1.0](https://github.com/auth0/Auth0.Android/tree/4.1.0) (2026-08-27)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/4.0.1...4.1.0)

**Added**
- feat: enable ephemeral browsing for Auth Tab [\#1059](https://github.com/auth0/Auth0.Android/pull/1059) ([pmathew92](https://github.com/pmathew92))
- feat(authentication): add organization parameter to resetPassword [\#1038](https://github.com/auth0/Auth0.Android/pull/1038) ([NandanPrabhu](https://github.com/NandanPrabhu))

## [3.21.1-beta.0](https://github.com/auth0/Auth0.Android/tree/3.21.1-beta.0) (2026-08-18)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/3.21.0...3.21.1-beta.0)

**Fixed**
- fix(storage): authorize MGF1 digest to prevent INCOMPATIBLE_DEVICE on Keystore2 [\#1048](https://github.com/auth0/Auth0.Android/pull/1048) ([utkrishtsahu](https://github.com/utkrishtsahu))

## [4.0.1](https://github.com/auth0/Auth0.Android/tree/4.0.1) (2026-07-24)
[Full Changelog](https://github.com/auth0/Auth0.Android/compare/4.0.0...4.0.1)

Expand Down
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 @@ -17,6 +18,8 @@ import com.auth0.android.request.JsonAdapter
import com.auth0.android.request.ProfileRequest
import com.auth0.android.request.PublicKeyCredentials
import com.auth0.android.request.Request
import com.auth0.android.request.RequestOptions
import com.auth0.android.request.RequestValidator
import com.auth0.android.request.SignUpRequest
import com.auth0.android.request.UserData
import com.auth0.android.request.internal.BaseAuthenticationRequest
Expand All @@ -31,6 +34,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 +238,25 @@ 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.
* @param verificationRequired the identifiers that need a code, from
* [PasskeyRegistrationChallenge.verificationRequired]. When not empty, [verification] must contain a non-blank
* code for each of them; otherwise the request fails before any network call with an `invalid_request`
* [AuthenticationException] whose description names the first identifier without a code. It is not sent to the
* server. Defaults to an empty list, in which case nothing is validated.
* @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(),
verificationRequired: List<PasskeyVerificationMethod> = emptyList(),
): AuthenticationRequest {
val params = ParameterBuilder.newBuilder().apply {
setGrantType(ParameterBuilder.GRANT_TYPE_PASSKEY)
Expand All @@ -249,11 +265,25 @@ 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
gson.toJsonTree(authResponse)
)
if (verification.isNotEmpty()) {
request.addParameter(
VERIFICATION_KEY,
gson.toJsonTree(verification.mapKeys { it.key.value })
)
}
if (verificationRequired.isNotEmpty()) {
request.addValidator(object : RequestValidator {
override fun validate(options: RequestOptions) {
passkeyVerificationError(verificationRequired, verification)?.let { throw it }
}
})
}
return request as AuthenticationRequest
}


Expand Down Expand Up @@ -281,19 +311,38 @@ 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.
* @param verificationRequired the identifiers that need a code, from
* [PasskeyRegistrationChallenge.verificationRequired]. When not empty, [verification] must contain a non-blank
* code for each of them; otherwise the request fails before any network call with an `invalid_request`
* [AuthenticationException] whose description names the first identifier without a code. It is not sent to the
* server. Defaults to an empty list, in which case nothing is validated.
* @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(),
verificationRequired: List<PasskeyVerificationMethod> = emptyList(),
): AuthenticationRequest {
val publicKeyCredentials = gson.fromJson(
authResponse,
PublicKeyCredentials::class.java
)
return signinWithPasskey(authSession, publicKeyCredentials, realm, organization)
return signinWithPasskey(
authSession,
publicKeyCredentials,
realm,
organization,
verification,
verificationRequired
)
}


Expand Down Expand Up @@ -325,12 +374,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 +405,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 @@ -1023,6 +1078,28 @@ public class AuthenticationAPIClient @VisibleForTesting(otherwise = VisibleForTe
return factory.get(url.toString(), userProfileAdapter, dPoP)
}

/**
* Returns the error the server would report for the first identifier in [required] that has no code, or a blank
* code, in [verification]; `null` when all of them have a code.
*/
private fun passkeyVerificationError(
required: List<PasskeyVerificationMethod>,
verification: Map<PasskeyVerificationMethod, String>
): AuthenticationException? {
for (identifier in required) {
val code = verification[identifier]
val description = when {
code == null -> "Missing required parameter: verification.${identifier.value}"
code.isBlank() -> "\"verification.${identifier.value}\" is not allowed to be empty"
else -> continue
}
return AuthenticationException(
mapOf(ERROR_KEY to INVALID_REQUEST, ERROR_DESCRIPTION_KEY to description)
)
}
return null
}

internal companion object {
private const val SMS_CONNECTION = "sms"
private const val EMAIL_CONNECTION = "email"
Expand All @@ -1042,6 +1119,11 @@ 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 ERROR_KEY = "error"
private const val ERROR_DESCRIPTION_KEY = "error_description"
private const val INVALID_REQUEST = "invalid_request"
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,45 @@ 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 is an `invalid_grant` that contains an
* `auth_session`, meaning a code was wrong and the session still has attempts left.
*
* When retryable, re-collect the codes for [passkeyVerificationRequired] and 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 && 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. `null`
* unless [isPasskeyVerificationRetryable] is `true`. Identifiers this version of the SDK does
* not know are skipped.
*
* @see isPasskeyVerificationRetryable
*/
public val passkeyVerificationRequired: List<PasskeyVerificationMethod>?
get() {
if (!isPasskeyVerificationRetryable) 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 +307,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
Loading
Loading