Migration Guide from SDK v3 to v4
June 19, 2026 · View on GitHub
Skill for Coding Agents
If you use coding agents such as Claude Code or Cursor, we highly recommend adding the Auth0.Android migration skill to your repository: npx skills add auth0/agent-skills --skill auth0-android-major-migration
Note: This guide is actively maintained during the v4 development phase. As new changes are merged, this document will be updated to reflect the latest breaking changes and migration steps.
v4 of the Auth0 Android SDK includes significant build toolchain updates, updated default values for better out-of-the-box behavior, and behavior changes to simplify credential management. This guide documents the changes required when migrating from v3 to v4.
Table of Contents
- Requirements Changes
- Breaking Changes
- Default Values Changed
- Behavior Changes
- New APIs
- Dependency Changes
- New APIs
Requirements Changes
Minimum SDK Version
v4 requires API level 26 (Android 8.0 Oreo) or later (previously API 21 / Android 5.0 Lollipop).
Update your build.gradle if your minSdk is below 26:
android {
defaultConfig {
minSdk 26
}
}
Impact: Apps targeting devices running Android 7.1 (API 25) or lower will need to increase their minimum SDK version, or continue using v3.
Java Version
v4 requires Java 17 or later (previously Java 8+).
Update your build.gradle to target Java 17:
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
Gradle and Android Gradle Plugin
v4 requires:
- Gradle: 8.11.1 or later
- Android Gradle Plugin (AGP): 8.10.1 or later
Update your gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
Update your root build.gradle:
buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.10.1'
}
}
Kotlin Version
v4 uses Kotlin 2.0.21. If you're using Kotlin in your project, you may need to update your Kotlin version to ensure compatibility.
buildscript {
ext.kotlin_version = "2.0.21"
}
Breaking Changes
Classes Removed
-
The
com.auth0.android.provider.PasskeyAuthProviderclass has been removed. Use the APIs from the AuthenticationAPIClient class for passkey operations:- passkeyChallenge() - Request a challenge to initiate passkey login flow
- signinWithPasskey() - Sign in a user using passkeys
- signupWithPasskey() - Sign up a user and returns a challenge for key generation
-
The Management API support has been removed. This includes the
UsersAPIClientclass,ManagementException, andManagementCallback.Note: This only impacts you if your app used the Management API client (
UsersAPIClient).Impact: Any code that references
UsersAPIClient,ManagementException, orManagementCallbackwill no longer compile.Migration: Instead of calling the Management API directly from your mobile app, expose dedicated endpoints in your own backend that perform the required operations, and call those from the app using the access token you already have.
For example, if you were reading or updating user metadata:
- Create a backend endpoint (e.g.
PATCH /me/metadata) that accepts the operation your app needs. - Call that endpoint from your app, passing the user's access token as a
Bearertoken in theAuthorizationheader. - On your backend, obtain a machine-to-machine token via the Client Credentials flow and use it to call the Management API with the precise scopes required.
- Create a backend endpoint (e.g.
Deprecated MFA Methods Removed from AuthenticationAPIClient
The following MFA methods have been removed from AuthenticationAPIClient. They were deprecated in v3 in favor of the MfaApiClient class APIs.
loginWithOTP(mfaToken, otp)loginWithOOB(mfaToken, oobCode, bindingCode)loginWithRecoveryCode(mfaToken, recoveryCode)multifactorChallenge(mfaToken, challengeType, authenticatorId)
Use AuthenticationAPIClient.mfaClient(mfaToken) to obtain a MfaApiClient instance and handle MFA flows using the new APIs. See the MFA Flexible Factors Grant section in EXAMPLES.md for usage guidance.
DPoP Configuration Moved to Builder
The useDPoP(context: Context) method has been moved from the WebAuthProvider object to the login
Builder class. This change allows DPoP to be configured per-request instead of globally.
v3 (global configuration — no longer supported):
// ❌ This no longer works
WebAuthProvider
.useDPoP(context)
.login(account)
.start(context, callback)
v4 (builder-based configuration — required):
// ✅ Use this instead
WebAuthProvider
.login(account)
.useDPoP(context)
.start(context, callback)
This change ensures that DPoP configuration is scoped to individual login requests rather than persisting across the entire application lifecycle.
DPoPException.UNSUPPORTED_ERROR Removed
The DPoPException.UNSUPPORTED_ERROR constant has been removed. With the minimum SDK raised to API 26, the SDK no longer needs to guard against unsupported Android versions for DPoP, so this error code is no longer applicable.
Impact: If your code references DPoPException.UNSUPPORTED_ERROR (e.g., in a catch block or error-handling logic), remove that reference. DPoP is supported on all API levels that v4 targets, so this check is no longer needed.
SSOCredentials.expiresIn Renamed to expiresAt
Change: The expiresIn property in SSOCredentials has been renamed to expiresAt and its type changed from Int to Date.
In v3, expiresIn held the raw number of seconds until the session transfer token expired. In v4, the SDK now automatically converts this value into an absolute expiration Date (computed as current time + seconds) during deserialization, consistent with how Credentials.expiresAt works. The property has been renamed to expiresAt to reflect that it now represents an absolute point in time rather than a duration.
v3:
val ssoCredentials: SSOCredentials = // ...
val secondsUntilExpiry: Int = ssoCredentials.expiresIn
v4:
val ssoCredentials: SSOCredentials = // ...
val expirationDate: Date = ssoCredentials.expiresAt
Impact: If your code references ssoCredentials.expiresIn, rename it to ssoCredentials.expiresAt. The value is now an absolute Date instead of a duration in seconds.
SecureCredentialsManager Auth0 Constructors Removed
Change: The two SecureCredentialsManager constructors that accepted an Auth0 instance as their first parameter have been removed. Only the AuthenticationAPIClient-based constructors remain.
In v3, SecureCredentialsManager offered four public constructors — two that accepted an Auth0 object (which the manager used internally to create an AuthenticationAPIClient) and two that accepted a pre-built AuthenticationAPIClient directly. In v4 the Auth0-based constructors are gone, leaving two constructors that both require an AuthenticationAPIClient.
v3 (removed):
// ❌ Auth0-based constructor — no longer exists
val manager = SecureCredentialsManager(
auth0, // Auth0 instance
context,
storage
)
// ❌ Auth0-based biometric constructor — no longer exists
val manager = SecureCredentialsManager(
auth0, // Auth0 instance
context,
storage,
fragmentActivity,
localAuthenticationOptions
)
v4 (required):
The manager uses the supplied AuthenticationAPIClient for all token renewals and DPoP-bound refreshes, so configure that client first and then pass the same instance into SecureCredentialsManager.
// ✅ Create the AuthenticationAPIClient first, then pass it in
val apiClient = AuthenticationAPIClient(auth0)
val manager = SecureCredentialsManager(apiClient, context, storage)
// ✅ Biometric variant
val apiClient = AuthenticationAPIClient(auth0)
val manager = SecureCredentialsManager(
apiClient,
context,
storage,
fragmentActivity,
localAuthenticationOptions
)
Using Java
// ✅ Standard
AuthenticationAPIClient apiClient = new AuthenticationAPIClient(auth0);
SecureCredentialsManager manager = new SecureCredentialsManager(apiClient, context, storage);
// ✅ Biometric variant
AuthenticationAPIClient apiClient = new AuthenticationAPIClient(auth0);
SecureCredentialsManager manager = new SecureCredentialsManager(
apiClient, context, storage, fragmentActivity, localAuthenticationOptions);
Impact: Any code that constructs SecureCredentialsManager with an Auth0 instance as the first argument will no longer compile. Create an AuthenticationAPIClient(auth0) first and pass that instead. The same change applies to Java — there is no Java-specific overload.
Reason: The Auth0 parameter was redundant — AuthenticationAPIClient already holds a reference to the Auth0 configuration object. Removing it eliminates the duplication, makes DPoP opt-in configuration (via AuthenticationAPIClient.useDPoP(context)) a natural part of construction, and reduces the public API surface.
Default Values Changed
Credentials Manager minTTL
Change: The default minTtl value changed from 0 to 60 seconds.
This change affects the following Credentials Manager methods:
getCredentials(callback)/awaitCredentials()getCredentials(scope, minTtl, callback)/awaitCredentials(scope, minTtl)getCredentials(scope, minTtl, parameters, callback)/awaitCredentials(scope, minTtl, parameters)getCredentials(scope, minTtl, parameters, forceRefresh, callback)/awaitCredentials(scope, minTtl, parameters, forceRefresh)getCredentials(scope, minTtl, parameters, headers, forceRefresh, callback)/awaitCredentials(scope, minTtl, parameters, headers, forceRefresh)hasValidCredentials()
Impact: Credentials will be renewed if they expire within 60 seconds, instead of only when already expired.
Migration example
// v3 - minTtl defaulted to 0, had to be set explicitly
credentialsManager.getCredentials(scope = null, minTtl = 60, callback = callback)
// v4 - minTtl defaults to 60 seconds
credentialsManager.getCredentials(callback)
// v4 - use 0 to restore v3 behavior
credentialsManager.getCredentials(scope = null, minTtl = 0, callback = callback)
Reason: A minTtl of 0 meant credentials were not renewed until expired, which could result in delivering access tokens that expire immediately after retrieval, causing subsequent API requests to fail. Setting a default value of 60 seconds ensures the access token remains valid for a reasonable period.
Behavior Changes
CredentialsManager Now Uses the Global Executor
Change: CredentialsManager no longer creates a per-instance Executor. It now uses the same process-wide single-thread executor already used by SecureCredentialsManager .
In v3, each CredentialsManager instance created its own Executors.newSingleThreadExecutor(). Two CredentialsManager instances could therefore run getCredentials concurrently, racing to exchange the same refresh token and potentially triggering duplicate invalid_grant errors on token rotation. SecureCredentialsManager was already on the global executor — this was an inconsistency between the two manager types.
In v4, getCredentials and getApiCredentials calls from any manager instance backed by the same Auth0 object are queued on one global single-thread executor. The first caller renews the token, saves the updated credentials, and returns. Subsequent callers find the already-refreshed credentials in storage and return without making a network request.
Impact: If your app creates multiple CredentialsManager instances backed by the same Auth0 object, their renewal operations are now serialized rather than concurrent. In practice this eliminates duplicate refresh-token exchanges. The only observable downside is that a slow renewal blocks other callers until it completes.
No code changes are required. This is a runtime-only behavior change.
clearCredentials() Now Clears All Storage
Change: clearCredentials() now calls Storage.removeAll() instead of removing individual credential keys.
In v3, clearCredentials() removed only specific credential keys (access token, refresh token, ID token, etc.) from the underlying Storage.
In v4, clearCredentials() calls Storage.removeAll(), which clears all values in the storage — including any API credentials stored for specific audiences.
Impact: If you need to remove only the primary credentials while preserving other stored data, consider using a separate Storage instance for API credentials.
Reason: This simplifies credential cleanup and ensures no stale data remains in storage after logout. It aligns the behavior with the Swift SDK's clear() method, which also clears all stored values.
Storage Interface: New removeAll() Method
Change: The Storage interface now includes a removeAll() method with a default empty implementation.
Impact: Existing custom Storage implementations will continue to compile and work without changes. Override removeAll() to provide the actual clearing behavior if your custom storage is used with clearCredentials().
New APIs
clearAll() — Full Credential and Key Cleanup
v4 introduces a new clearAll() method on CredentialsManager and SecureCredentialsManager that performs a complete cleanup of all stored credentials and cryptographic key pairs.
Usage:
// Clear everything on logout — credentials, DPoP keys, and encryption keys
credentialsManager.clearAll()
When to use clearAll() vs clearCredentials():
- Use
clearCredentials()when you only need to remove stored tokens (e.g., forcing a re-login) but want to preserve cryptographic keys for future sessions. - Use
clearAll()on full logout or account removal, when you want to ensure no credentials or key material remain on the device.
Note:
clearAll()catches any errors from DPoP key pair deletion internally, so it will not throw even if the DPoP key pair was never created or has already been removed.
Dependency Changes
⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency)
v4 updates the internal Gson dependency from 2.8.9 to 2.11.0. While the SDK does not expose Gson types in its public API, Gson is included as a transitive runtime dependency. If your app also uses Gson, be aware of the following changes introduced in Gson 2.10+:
TypeTokenwith unresolved type variables is rejected at runtime. Code likeobject : TypeToken<List<T>>() {}(whereTis a generic parameter) will throwIllegalArgumentException. Use Kotlinreifiedtype parameters or pass concrete types instead.- Strict type coercion is enforced. Gson no longer silently coerces JSON objects or arrays to
String. If your code relies on this behavior, you will seeJsonSyntaxException. - Built-in ProGuard/R8 rules are included. Gson 2.11.0 ships its own keep rules, so you may be able to remove custom Gson ProGuard rules from your project.
If you need to pin Gson to an older version, you can use Gradle's resolutionStrategy:
configurations.all {
resolutionStrategy.force 'com.google.code.gson:gson:2.8.9'
}
Alternatively, you can exclude Gson from the SDK entirely and provide your own version:
implementation('com.auth0.android:auth0:<version>') {
exclude group: 'com.google.code.gson', module: 'gson'
}
implementation 'com.google.code.gson:gson:2.8.9' // your preferred version
Note: Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0.
DefaultClient.Builder
v4 introduces a DefaultClient.Builder for configuring the HTTP client. This replaces the
constructor-based approach with a more flexible builder pattern that supports additional options
such as write/call timeouts, custom interceptors, and custom loggers.
v3 (constructor-based — deprecated):
// ⚠️ Deprecated: still compiles but shows a warning
val client = DefaultClient(
connectTimeout = 30,
readTimeout = 30,
enableLogging = true
)
v4 (builder pattern — recommended):
val client = DefaultClient.Builder()
.connectTimeout(30)
.readTimeout(30)
.writeTimeout(30)
.callTimeout(120)
.enableLogging(true)
.build()
The legacy constructor is deprecated but not removed — existing code will continue to compile
and run. Your IDE will show a deprecation warning with a suggested ReplaceWith quick-fix to
migrate to the Builder.
New APIs
Handling Configuration Changes During Authentication
v4 fixes a memory leak and lost callback issue when the Activity is destroyed during authentication
(e.g. device rotation, locale change, dark mode toggle). The SDK wraps the callback in a
LifecycleAwareCallback that observes the host Activity/Fragment lifecycle. When onDestroy fires,
the reference to the callback is immediately nulled out so the destroyed Activity is no longer held
in memory.
If the authentication result arrives while the Activity is being recreated, it is cached internally.
Call WebAuthProvider.registerCallbacks() once in your onCreate() — this single call handles both
recovery scenarios and manages the callback lifecycle automatically:
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WebAuthProvider.registerCallbacks(
lifecycleOwner = this,
loginCallback = object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) { /* handle credentials */ }
override fun onFailure(error: AuthenticationException) { /* handle error */ }
},
logoutCallback = object : Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) { /* handle logout */ }
override fun onFailure(error: AuthenticationException) { /* handle error */ }
}
)
}
fun onLoginClick() {
WebAuthProvider.login(account)
.withScheme("myapp")
.start(this, callback)
}
}
registerCallbacks() covers both scenarios in one call:
| Scenario | How it's handled |
|---|---|
| Configuration change (rotation, locale, dark mode) | The result is delivered directly to the registered callback once the async token exchange completes. If no callback is registered yet, the result is cached and delivered on the next onResume |
| Process death (system killed the app while browser was open) | AuthenticationActivity restores the OAuth state and processes the redirect. Since all static state (including callbacks) was wiped, the result is cached in pendingLoginResult. When your Activity is recreated and calls registerCallbacks(), the cached result is delivered on the next onResume |
Note: Both
loginCallbackandlogoutCallbackare required — this ensures results from either flow are never lost during configuration changes or process death.
Note: If you use the
suspend fun await()API from a ViewModel coroutine scope, the Activity is never captured in the callback chain, so you do not needregisterCallbacks()calls. See the sample app for a ViewModel-based example.
Getting Help
If you encounter issues during migration:
- GitHub Issues - Report bugs or ask questions
- Auth0 Community - Community support