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
51 changes: 50 additions & 1 deletion EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,58 @@ WebAuthProvider.logout(account)

})
```
> [!NOTE]
> [!NOTE]
> DPoP is supported only on Android version 6.0 (API level 23) and above. Trying to use DPoP in any older versions will result in an exception.

## Handling Configuration Changes During Authentication

When the Activity is destroyed during authentication due to a configuration change (e.g. device rotation, locale change, dark mode toggle), the SDK caches the authentication result internally. Use `consumePendingLoginResult()` or `consumePendingLogoutResult()` in your `onResume()` to recover it.

```kotlin
class LoginActivity : AppCompatActivity() {

private val loginCallback = object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
// Handle successful login
}
override fun onFailure(error: AuthenticationException) {
// Handle error
}
}

private val logoutCallback = object : Callback<Void?, AuthenticationException> {
override fun onSuccess(result: Void?) {
// Handle successful logout
}
override fun onFailure(error: AuthenticationException) {
// Handle error
}
}

override fun onResume() {
super.onResume()
// Recover any result that arrived while the Activity was being recreated
WebAuthProvider.consumePendingLoginResult(loginCallback)
WebAuthProvider.consumePendingLogoutResult(logoutCallback)
}

fun onLoginClick() {
WebAuthProvider.login(account)
.withScheme("demo")
.start(this, loginCallback)
}

fun onLogoutClick() {
WebAuthProvider.logout(account)
.withScheme("demo")
.start(this, logoutCallback)
}
}
```

> [!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 need `consumePending*` calls.

## Authentication API

The client provides methods to authenticate the user against the Auth0 server.
Expand Down
40 changes: 40 additions & 0 deletions V4_MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ v4 of the Auth0 Android SDK includes significant build toolchain updates, update
- [**Dependency Changes**](#dependency-changes)
+ [Gson 2.8.9 → 2.11.0](#️-gson-289--2110-transitive-dependency)
+ [DefaultClient.Builder](#defaultclientbuilder)
- [**New APIs**](#new-apis)
+ [Handling Configuration Changes During Authentication](#handling-configuration-changes-during-authentication)

---

Expand Down Expand Up @@ -283,6 +285,44 @@ The legacy constructor is deprecated but **not removed** — existing code will
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 now uses `WeakReference` for
callbacks, so destroyed Activities are properly garbage collected.

If the authentication result arrives while the Activity is being recreated, it is cached internally.
Use `consumePendingLoginResult()` or `consumePendingLogoutResult()` in your `onResume()` to recover it:

```kotlin
class LoginActivity : AppCompatActivity() {
private val callback = object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) { /* handle credentials */ }
override fun onFailure(error: AuthenticationException) { /* handle error */ }
}

override fun onResume() {
super.onResume()
// Recover result that arrived during configuration change
WebAuthProvider.consumePendingLoginResult(callback)
}

fun onLoginClick() {
WebAuthProvider.login(account)
.withScheme("myapp")
.start(this, callback)
}
}
```

For logout flows, use `WebAuthProvider.consumePendingLogoutResult(callback)` in the same way.

> **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 need `consumePending*` calls.
> See the sample app for a ViewModel-based example.

## Getting Help

If you encounter issues during migration:
Expand Down
30 changes: 26 additions & 4 deletions auth0/src/main/java/com/auth0/android/provider/LogoutManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,39 @@ import android.util.Log
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.Callback
import java.lang.ref.WeakReference
import java.util.*

internal class LogoutManager(
private val account: Auth0,
private val callback: Callback<Void?, AuthenticationException>,
callback: Callback<Void?, AuthenticationException>,
returnToUrl: String,
ctOptions: CustomTabsOptions,
federated: Boolean = false,
private val launchAsTwa: Boolean = false,
private val customLogoutUrl: String? = null
) : ResumableManager() {
private val callbackRef = WeakReference(callback)

private fun deliverSuccess() {
val cb = callbackRef.get()
if (cb != null) {
cb.onSuccess(null)
} else {
WebAuthProvider.pendingLogoutResult =
WebAuthProvider.PendingResult.Success(null)
}
}

private fun deliverFailure(error: AuthenticationException) {
val cb = callbackRef.get()
if (cb != null) {
cb.onFailure(error)
} else {
WebAuthProvider.pendingLogoutResult =
WebAuthProvider.PendingResult.Failure(error)
}
}
private val parameters: MutableMap<String, String>
private val ctOptions: CustomTabsOptions
fun startLogout(context: Context) {
Expand All @@ -31,15 +53,15 @@ internal class LogoutManager(
AuthenticationException.ERROR_VALUE_AUTHENTICATION_CANCELED,
"The user closed the browser app so the logout was cancelled."
)
callback.onFailure(exception)
deliverFailure(exception)
} else {
callback.onSuccess(null)
deliverSuccess()
}
return true
}

override fun failure(exception: AuthenticationException) {
callback.onFailure(exception)
deliverFailure(exception)
}

private fun buildLogoutUri(): Uri {
Expand Down
39 changes: 31 additions & 8 deletions auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,42 @@ import com.auth0.android.dpop.DPoPException
import com.auth0.android.request.internal.Jwt
import com.auth0.android.request.internal.OidcUtils
import com.auth0.android.result.Credentials
import java.lang.ref.WeakReference
import java.security.SecureRandom
import java.util.*

internal class OAuthManager(
private val account: Auth0,
private val callback: Callback<Credentials, AuthenticationException>,
callback: Callback<Credentials, AuthenticationException>,
parameters: Map<String, String>,
ctOptions: CustomTabsOptions,
private val launchAsTwa: Boolean = false,
private val customAuthorizeUrl: String? = null,
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val dPoP: DPoP? = null
) : ResumableManager() {
private val callbackRef = WeakReference(callback)

private fun deliverSuccess(credentials: Credentials) {
val cb = callbackRef.get()
if (cb != null) {
cb.onSuccess(credentials)
} else {
WebAuthProvider.pendingLoginResult =
WebAuthProvider.PendingResult.Success(credentials)
}
}

private fun deliverFailure(error: AuthenticationException) {
val cb = callbackRef.get()
if (cb != null) {
cb.onFailure(error)
} else {
WebAuthProvider.pendingLoginResult =
WebAuthProvider.PendingResult.Failure(error)
}
}

private val parameters: MutableMap<String, String>
private val headers: MutableMap<String, String>
private val ctOptions: CustomTabsOptions
Expand Down Expand Up @@ -68,7 +91,7 @@ internal class OAuthManager(
try {
addDPoPJWKParameters(parameters)
} catch (ex: DPoPException) {
callback.onFailure(
deliverFailure(
AuthenticationException(
ex.message ?: "Error generating the JWK",
ex
Expand Down Expand Up @@ -97,7 +120,7 @@ internal class OAuthManager(
AuthenticationException.ERROR_VALUE_AUTHENTICATION_CANCELED,
"The user closed the browser app and the authentication was canceled."
)
callback.onFailure(exception)
deliverFailure(exception)
return true
}
val values = CallbackHelper.getValuesFromUri(result.intentData)
Expand All @@ -110,7 +133,7 @@ internal class OAuthManager(
assertNoError(values[KEY_ERROR], values[KEY_ERROR_DESCRIPTION])
assertValidState(parameters[KEY_STATE]!!, values[KEY_STATE])
} catch (e: AuthenticationException) {
callback.onFailure(e)
deliverFailure(e)
return true
}

Expand All @@ -123,14 +146,14 @@ internal class OAuthManager(
credentials.idToken,
object : Callback<Void?, Auth0Exception> {
override fun onSuccess(result: Void?) {
callback.onSuccess(credentials)
deliverSuccess(credentials)
}

override fun onFailure(error: Auth0Exception) {
val wrappedError = AuthenticationException(
ERROR_VALUE_ID_TOKEN_VALIDATION_FAILED, error
)
callback.onFailure(wrappedError)
deliverFailure(wrappedError)
}
})
}
Expand All @@ -142,14 +165,14 @@ internal class OAuthManager(
"Unable to complete authentication with PKCE. PKCE support can be enabled by setting Application Type to 'Native' and Token Endpoint Authentication Method to 'None' for this app at 'https://manage.auth0.com/#/applications/" + apiClient.clientId + "/settings'."
)
}
callback.onFailure(error)
deliverFailure(error)
}
})
return true
}

public override fun failure(exception: AuthenticationException) {
callback.onFailure(exception)
deliverFailure(exception)
}

private fun assertValidIdToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,66 @@ public object WebAuthProvider {
internal var managerInstance: ResumableManager? = null
private set

/**
* Represents a pending authentication or logout result that arrived while
* the original callback was no longer reachable (e.g. Activity destroyed
* during a configuration change).
*/
internal sealed class PendingResult<out S, out E> {
data class Success<S>(val result: S) : PendingResult<S, Nothing>()
data class Failure<E>(val error: E) : PendingResult<Nothing, E>()
}

@Volatile
@JvmStatic
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var pendingLoginResult: PendingResult<Credentials, AuthenticationException>? = null

@Volatile
@JvmStatic
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var pendingLogoutResult: PendingResult<Void?, AuthenticationException>? = null

/**
* Check for and consume a pending login result that arrived during a configuration change.
* Call this in your Activity's `onResume()` to recover results that were delivered while the
* Activity was being recreated (e.g. due to screen rotation).
*
* @param callback the callback to deliver the pending result to
* @return true if a pending result was found and delivered, false otherwise
*/
@JvmStatic
public fun consumePendingLoginResult(callback: Callback<Credentials, AuthenticationException>): Boolean {
val result = pendingLoginResult ?: return false
pendingLoginResult = null
when (result) {
is PendingResult.Success -> callback.onSuccess(result.result)
is PendingResult.Failure -> callback.onFailure(result.error)
}
resetManagerInstance()
return true
}

/**
* Check for and consume a pending logout result that arrived during a configuration change.
* Call this in your Activity's `onResume()` to recover results that were delivered while the
* Activity was being recreated (e.g. due to screen rotation).
*
* @param callback the callback to deliver the pending result to
* @return true if a pending result was found and delivered, false otherwise
*/
@JvmStatic
public fun consumePendingLogoutResult(callback: Callback<Void?, AuthenticationException>): Boolean {
val result = pendingLogoutResult ?: return false
pendingLogoutResult = null
when (result) {
is PendingResult.Success -> callback.onSuccess(result.result)
is PendingResult.Failure -> callback.onFailure(result.error)
}
resetManagerInstance()
return true
}

@JvmStatic
public fun addCallback(callback: Callback<Credentials, AuthenticationException>) {
callbacks += callback
Expand Down
Loading
Loading