Skip to content

Commit dfe218c

Browse files
committed
Addressed the review comments for Dpop instance being saved for configuration changes
1 parent 507f0c9 commit dfe218c

7 files changed

Lines changed: 140 additions & 40 deletions

File tree

V4_MIGRATION_GUIDE.md

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Overview
44

5-
v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest Android development environment. This guide documents the changes required when migrating from v3 to v4.
5+
v4 of the Auth0 Android SDK includes significant build toolchain updates to support the latest
6+
Android development environment. This guide documents the changes required when migrating from v3 to
7+
v4.
68

79
## Requirements Changes
810

@@ -50,7 +52,8 @@ buildscript {
5052

5153
### Kotlin Version
5254

53-
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.
55+
v4 uses **Kotlin 2.0.21**. If you're using Kotlin in your project, you may need to update your
56+
Kotlin version to ensure compatibility.
5457

5558
```groovy
5659
buildscript {
@@ -62,14 +65,20 @@ buildscript {
6265

6366
### Classes Removed
6467

65-
- The `com.auth0.android.provider.PasskeyAuthProvider` class has been removed. Use the APIs from the [AuthenticationAPIClient](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt) class for passkey operations:
66-
- [passkeyChallenge()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L366-L387) - Request a challenge to initiate passkey login flow
67-
- [signinWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L235-L253) - Sign in a user using passkeys
68-
- [signupWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L319-L344) - Sign up a user and returns a challenge for key generation
68+
- The `com.auth0.android.provider.PasskeyAuthProvider` class has been removed. Use the APIs from
69+
the [AuthenticationAPIClient](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt)
70+
class for passkey operations:
71+
- [passkeyChallenge()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L366-L387) -
72+
Request a challenge to initiate passkey login flow
73+
- [signinWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L235-L253) -
74+
Sign in a user using passkeys
75+
- [signupWithPasskey()](auth0/src/main/java/com/auth0/android/authentication/AuthenticationAPIClient.kt#L319-L344) -
76+
Sign up a user and returns a challenge for key generation
6977

7078
### DPoP Configuration Moved to Builder
7179

72-
The `useDPoP()` 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.
80+
The `useDPoP(context: Context)` method has been moved from the `WebAuthProvider` object to the login
81+
`Builder` class. This change allows DPoP to be configured per-request instead of globally.
7382

7483
**v3 (global configuration — no longer supported):**
7584

@@ -87,21 +96,28 @@ WebAuthProvider
8796
// ✅ Use this instead
8897
WebAuthProvider
8998
.login(account)
90-
.useDPoP(context)
99+
.useDPoP(context)
91100
.start(context, callback)
92101
```
93102

94-
This change ensures that DPoP configuration is scoped to individual login requests rather than persisting across the entire application lifecycle.
103+
This change ensures that DPoP configuration is scoped to individual login requests rather than
104+
persisting across the entire application lifecycle.
95105

96106
## Dependency Changes
97107

98108
### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency)
99109

100-
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+:
110+
v4 updates the internal Gson dependency from **2.8.9** to **2.11.0**. While the SDK does not expose
111+
Gson types in its public API, Gson is included as a transitive runtime dependency. If your app also
112+
uses Gson, be aware of the following changes introduced in Gson 2.10+:
101113

102-
- **`TypeToken` with unresolved type variables is rejected at runtime.** Code like `object : TypeToken<List<T>>() {}` (where `T` is a generic parameter) will throw `IllegalArgumentException`. Use Kotlin `reified` type parameters or pass concrete types instead.
103-
- **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 see `JsonSyntaxException`.
104-
- **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.
114+
- **`TypeToken` with unresolved type variables is rejected at runtime.** Code like
115+
`object : TypeToken<List<T>>() {}` (where `T` is a generic parameter) will throw
116+
`IllegalArgumentException`. Use Kotlin `reified` type parameters or pass concrete types instead.
117+
- **Strict type coercion is enforced.** Gson no longer silently coerces JSON objects or arrays to
118+
`String`. If your code relies on this behavior, you will see `JsonSyntaxException`.
119+
- **Built-in ProGuard/R8 rules are included.** Gson 2.11.0 ships its own keep rules, so you may be
120+
able to remove custom Gson ProGuard rules from your project.
105121

106122
If you need to pin Gson to an older version, you can use Gradle's `resolutionStrategy`:
107123

@@ -120,11 +136,14 @@ implementation('com.auth0.android:auth0:<version>') {
120136
implementation 'com.google.code.gson:gson:2.8.9' // your preferred version
121137
```
122138

123-
> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and validated against Gson 2.11.0.
139+
> **Note:** Pinning or excluding is not recommended long-term, as the SDK has been tested and
140+
> validated against Gson 2.11.0.
124141
125142
### DefaultClient.Builder
126143

127-
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.
144+
v4 introduces a `DefaultClient.Builder` for configuring the HTTP client. This replaces the
145+
constructor-based approach with a more flexible builder pattern that supports additional options
146+
such as write/call timeouts, custom interceptors, and custom loggers.
128147

129148
**v3 (constructor-based — deprecated):**
130149

@@ -149,7 +168,9 @@ val client = DefaultClient.Builder()
149168
.build()
150169
```
151170

152-
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.
171+
The legacy constructor is deprecated but **not removed** — existing code will continue to compile
172+
and run. Your IDE will show a deprecation warning with a suggested `ReplaceWith` quick-fix to
173+
migrate to the Builder.
153174

154175
## Getting Help
155176

auth0/src/main/java/com/auth0/android/provider/AuthenticationActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public open class AuthenticationActivity : Activity() {
4040
override fun onCreate(savedInstanceState: Bundle?) {
4141
super.onCreate(savedInstanceState)
4242
if (savedInstanceState != null) {
43-
WebAuthProvider.onRestoreInstanceState(savedInstanceState)
43+
WebAuthProvider.onRestoreInstanceState(savedInstanceState, this)
4444
intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false)
4545
}
4646
}

auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,8 @@ internal class OAuthManager(
211211
auth0 = account,
212212
idTokenVerificationIssuer = idTokenVerificationIssuer,
213213
idTokenVerificationLeeway = idTokenVerificationLeeway,
214-
customAuthorizeUrl = this.customAuthorizeUrl
214+
customAuthorizeUrl = this.customAuthorizeUrl,
215+
dPoPEnabled = dPoP != null
215216
)
216217
}
217218

@@ -387,14 +388,21 @@ internal class OAuthManager(
387388

388389
internal fun OAuthManager.Companion.fromState(
389390
state: OAuthManagerState,
390-
callback: Callback<Credentials, AuthenticationException>
391+
callback: Callback<Credentials, AuthenticationException>,
392+
context: Context
391393
): OAuthManager {
394+
// Enable DPoP on the restored PKCE's AuthenticationAPIClient so that
395+
// the token exchange request includes the DPoP proof after process restore.
396+
if (state.dPoPEnabled && state.pkce != null) {
397+
state.pkce.apiClient.useDPoP(context)
398+
}
392399
return OAuthManager(
393400
account = state.auth0,
394401
ctOptions = state.ctOptions,
395402
parameters = state.parameters,
396403
callback = callback,
397-
customAuthorizeUrl = state.customAuthorizeUrl
404+
customAuthorizeUrl = state.customAuthorizeUrl,
405+
dPoP = if (state.dPoPEnabled ) DPoP(context) else null
398406
).apply {
399407
setHeaders(
400408
state.headers

auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import android.util.Base64
66
import androidx.core.os.ParcelCompat
77
import com.auth0.android.Auth0
88
import com.auth0.android.authentication.AuthenticationAPIClient
9-
import com.auth0.android.dpop.DPoP
109
import com.auth0.android.request.internal.GsonProvider
1110
import com.google.gson.Gson
1211

@@ -20,7 +19,7 @@ internal data class OAuthManagerState(
2019
val idTokenVerificationLeeway: Int?,
2120
val idTokenVerificationIssuer: String?,
2221
val customAuthorizeUrl: String? = null,
23-
val dPoP: DPoP? = null
22+
val dPoPEnabled: Boolean = false
2423
) {
2524

2625
private class OAuthManagerJson(
@@ -37,7 +36,7 @@ internal data class OAuthManagerState(
3736
val idTokenVerificationLeeway: Int?,
3837
val idTokenVerificationIssuer: String?,
3938
val customAuthorizeUrl: String? = null,
40-
val dPoP: DPoP? = null
39+
val dPoPEnabled: Boolean
4140
)
4241

4342
fun serializeToJson(
@@ -62,7 +61,7 @@ internal data class OAuthManagerState(
6261
idTokenVerificationIssuer = idTokenVerificationIssuer,
6362
idTokenVerificationLeeway = idTokenVerificationLeeway,
6463
customAuthorizeUrl = this.customAuthorizeUrl,
65-
dPoP = this.dPoP
64+
dPoPEnabled = this.dPoPEnabled
6665
)
6766
return gson.toJson(json)
6867
} finally {
@@ -112,7 +111,7 @@ internal data class OAuthManagerState(
112111
idTokenVerificationIssuer = oauthManagerJson.idTokenVerificationIssuer,
113112
idTokenVerificationLeeway = oauthManagerJson.idTokenVerificationLeeway,
114113
customAuthorizeUrl = oauthManagerJson.customAuthorizeUrl,
115-
dPoP = oauthManagerJson.dPoP
114+
dPoPEnabled = oauthManagerJson.dPoPEnabled
116115
)
117116
} finally {
118117
parcel.recycle()

auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public object WebAuthProvider {
112112
}
113113
}
114114

115-
internal fun onRestoreInstanceState(bundle: Bundle) {
115+
internal fun onRestoreInstanceState(bundle: Bundle, context: Context) {
116116
if (managerInstance == null) {
117117
val stateJson = bundle.getString(KEY_BUNDLE_OAUTH_MANAGER_STATE).orEmpty()
118118
if (stateJson.isNotBlank()) {
@@ -131,7 +131,8 @@ public object WebAuthProvider {
131131
callback.onFailure(error)
132132
}
133133
}
134-
}
134+
},
135+
context
135136
)
136137
}
137138
}

auth0/src/test/java/com/auth0/android/provider/OAuthManagerStateTest.kt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,88 @@ internal class OAuthManagerStateTest {
4444
Assert.assertEquals(1, deserializedState.idTokenVerificationLeeway)
4545
Assert.assertEquals("issuer", deserializedState.idTokenVerificationIssuer)
4646
}
47+
48+
@Test
49+
fun `serialize should persist dPoPEnabled flag as true`() {
50+
val auth0 = Auth0.getInstance("clientId", "domain")
51+
val state = OAuthManagerState(
52+
auth0 = auth0,
53+
parameters = mapOf("param1" to "value1"),
54+
headers = mapOf("header1" to "value1"),
55+
requestCode = 1,
56+
ctOptions = CustomTabsOptions.newBuilder()
57+
.showTitle(true)
58+
.withBrowserPicker(
59+
BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build()
60+
)
61+
.build(),
62+
pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")),
63+
idTokenVerificationLeeway = 1,
64+
idTokenVerificationIssuer = "issuer",
65+
dPoPEnabled = true
66+
)
67+
68+
val json = state.serializeToJson()
69+
70+
Assert.assertTrue(json.isNotBlank())
71+
Assert.assertTrue(json.contains("\"dPoPEnabled\":true"))
72+
73+
val deserializedState = OAuthManagerState.deserializeState(json)
74+
75+
Assert.assertTrue(deserializedState.dPoPEnabled)
76+
}
77+
78+
@Test
79+
fun `serialize should persist dPoPEnabled flag as false by default`() {
80+
val auth0 = Auth0.getInstance("clientId", "domain")
81+
val state = OAuthManagerState(
82+
auth0 = auth0,
83+
parameters = mapOf("param1" to "value1"),
84+
headers = mapOf("header1" to "value1"),
85+
requestCode = 1,
86+
ctOptions = CustomTabsOptions.newBuilder()
87+
.showTitle(true)
88+
.withBrowserPicker(
89+
BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build()
90+
)
91+
.build(),
92+
pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")),
93+
idTokenVerificationLeeway = 1,
94+
idTokenVerificationIssuer = "issuer"
95+
)
96+
97+
val json = state.serializeToJson()
98+
99+
val deserializedState = OAuthManagerState.deserializeState(json)
100+
101+
Assert.assertFalse(deserializedState.dPoPEnabled)
102+
}
103+
104+
@Test
105+
fun `deserialize should default dPoPEnabled to false when field is missing from JSON`() {
106+
val auth0 = Auth0.getInstance("clientId", "domain")
107+
val state = OAuthManagerState(
108+
auth0 = auth0,
109+
parameters = emptyMap(),
110+
headers = emptyMap(),
111+
requestCode = 0,
112+
ctOptions = CustomTabsOptions.newBuilder()
113+
.showTitle(true)
114+
.withBrowserPicker(
115+
BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build()
116+
)
117+
.build(),
118+
pkce = PKCE(mock(), "redirectUri", emptyMap()),
119+
idTokenVerificationLeeway = null,
120+
idTokenVerificationIssuer = null
121+
)
122+
123+
val json = state.serializeToJson()
124+
// Remove the dPoPEnabled field to simulate legacy JSON
125+
val legacyJson = json.replace(",\"dPoPEnabled\":false", "")
126+
127+
val deserializedState = OAuthManagerState.deserializeState(legacyJson)
128+
129+
Assert.assertFalse(deserializedState.dPoPEnabled)
130+
}
47131
}

auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2851,19 +2851,6 @@ public class WebAuthProviderTest {
28512851
assertThat(uri, UriMatchers.hasParamWithName("dpop_jkt"))
28522852
}
28532853

2854-
@Test
2855-
public fun shouldNotAffectLogoutWhenDPoPIsEnabled() {
2856-
logout(account)
2857-
.start(activity, voidCallback)
2858-
2859-
verify(activity).startActivity(intentCaptor.capture())
2860-
val uri =
2861-
intentCaptor.firstValue.getParcelableExtra<Uri>(AuthenticationActivity.EXTRA_AUTHORIZE_URI)
2862-
assertThat(uri, `is`(notNullValue()))
2863-
// Logout should not have DPoP parameters
2864-
assertThat(uri, not(UriMatchers.hasParamWithName("dpop_jkt")))
2865-
}
2866-
28672854
@Test
28682855
public fun shouldHandleDPoPKeyGenerationFailureGracefully() {
28692856
`when`(mockKeyStore.hasKeyPair()).thenReturn(false)

0 commit comments

Comments
 (0)