Skip to content

Commit 7da0c8e

Browse files
authored
breaking : Moved the useDPoP method in the WebAuthProvider class to the login builder class (#914)
1 parent 2264e8e commit 7da0c8e

File tree

8 files changed

+234
-77
lines changed

8 files changed

+234
-77
lines changed

EXAMPLES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,12 @@ WebAuthProvider.login(account)
233233
> [!NOTE]
234234
> This feature is currently available in [Early Access](https://auth0.com/docs/troubleshoot/product-lifecycle/product-release-stages#early-access). Please reach out to Auth0 support to get it enabled for your tenant.
235235
236-
[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP()` method.
236+
[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens by proving that the app is in possession of a certain private key. You can enable it by calling the `useDPoP(context)` method on the login Builder.
237237

238238
```kotlin
239239
WebAuthProvider
240-
.useDPoP()
241240
.login(account)
241+
.useDPoP(requireContext())
242242
.start(requireContext(), object : Callback<Credentials, AuthenticationException> {
243243
override fun onSuccess(result: Credentials) {
244244
println("Credentials $result")

V4_MIGRATION_GUIDE.md

Lines changed: 60 additions & 13 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,20 +65,59 @@ 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
77+
78+
### DPoP Configuration Moved to Builder
79+
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.
82+
83+
**v3 (global configuration — no longer supported):**
84+
85+
```kotlin
86+
// ❌ This no longer works
87+
WebAuthProvider
88+
.useDPoP(context)
89+
.login(account)
90+
.start(context, callback)
91+
```
92+
93+
**v4 (builder-based configuration — required):**
94+
95+
```kotlin
96+
// ✅ Use this instead
97+
WebAuthProvider
98+
.login(account)
99+
.useDPoP(context)
100+
.start(context, callback)
101+
```
102+
103+
This change ensures that DPoP configuration is scoped to individual login requests rather than
104+
persisting across the entire application lifecycle.
69105

70106
## Dependency Changes
71107

72108
### ⚠️ Gson 2.8.9 → 2.11.0 (Transitive Dependency)
73109

74-
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+:
75113

76-
- **`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.
77-
- **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`.
78-
- **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.
79121

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

@@ -94,11 +136,14 @@ implementation('com.auth0.android:auth0:<version>') {
94136
implementation 'com.google.code.gson:gson:2.8.9' // your preferred version
95137
```
96138

97-
> **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.
98141
99142
### DefaultClient.Builder
100143

101-
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.
102147

103148
**v3 (constructor-based — deprecated):**
104149

@@ -123,7 +168,9 @@ val client = DefaultClient.Builder()
123168
.build()
124169
```
125170

126-
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.
127174

128175
## Getting Help
129176

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: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ import kotlin.coroutines.resumeWithException
2727
*
2828
* It uses an external browser by sending the [android.content.Intent.ACTION_VIEW] intent.
2929
*/
30-
public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
30+
public object WebAuthProvider {
3131
private val TAG: String? = WebAuthProvider::class.simpleName
3232
private const val KEY_BUNDLE_OAUTH_MANAGER_STATE = "oauth_manager_state"
33-
private var dPoP : DPoP? = null
3433

3534
private val callbacks = CopyOnWriteArraySet<Callback<Credentials, AuthenticationException>>()
3635

@@ -49,12 +48,6 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
4948
callbacks -= callback
5049
}
5150

52-
// Public methods
53-
public override fun useDPoP(context: Context): WebAuthProvider {
54-
dPoP = DPoP(context)
55-
return this
56-
}
57-
5851
/**
5952
* Initialize the WebAuthProvider instance for logging out the user using an account. Additional settings can be configured
6053
* in the LogoutBuilder, like changing the scheme of the return to URL.
@@ -119,7 +112,7 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
119112
}
120113
}
121114

122-
internal fun onRestoreInstanceState(bundle: Bundle) {
115+
internal fun onRestoreInstanceState(bundle: Bundle, context: Context) {
123116
if (managerInstance == null) {
124117
val stateJson = bundle.getString(KEY_BUNDLE_OAUTH_MANAGER_STATE).orEmpty()
125118
if (stateJson.isNotBlank()) {
@@ -138,7 +131,8 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
138131
callback.onFailure(error)
139132
}
140133
}
141-
}
134+
},
135+
context
142136
)
143137
}
144138
}
@@ -305,14 +299,15 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
305299
}
306300
}
307301

308-
public class Builder internal constructor(private val account: Auth0) {
302+
public class Builder internal constructor(private val account: Auth0) : SenderConstraining<Builder> {
309303
private val values: MutableMap<String, String> = mutableMapOf()
310304
private val headers: MutableMap<String, String> = mutableMapOf()
311305
private var pkce: PKCE? = null
312306
private var issuer: String? = null
313307
private var scheme: String = "https"
314308
private var redirectUri: String? = null
315309
private var invitationUrl: String? = null
310+
private var dPoP: DPoP? = null
316311
private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build()
317312
private var leeway: Int? = null
318313
private var launchAsTwa: Boolean = false
@@ -548,6 +543,18 @@ public object WebAuthProvider : SenderConstraining<WebAuthProvider> {
548543
return this
549544
}
550545

546+
/**
547+
* Enable DPoP (Demonstrating Proof-of-Possession) for this authentication request.
548+
* DPoP binds access tokens to the client's cryptographic key, providing enhanced security.
549+
*
550+
* @param context the Android context used to access the keystore for DPoP key management
551+
* @return the current builder instance
552+
*/
553+
public override fun useDPoP(context: Context): Builder {
554+
dPoP = DPoP(context)
555+
return this
556+
}
557+
551558
/**
552559
* Request user Authentication. The result will be received in the callback.
553560
* An error is raised if there are no browser applications installed in the device, or if

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
}

0 commit comments

Comments
 (0)