Skip to content

Commit 106f960

Browse files
Support Process Death in WebAuthProvider
1 parent d5d2597 commit 106f960

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ public open class AuthenticationActivity : Activity() {
3434
override fun onSaveInstanceState(outState: Bundle) {
3535
super.onSaveInstanceState(outState)
3636
outState.putBoolean(EXTRA_INTENT_LAUNCHED, intentLaunched)
37+
WebAuthProvider.onSaveInstanceState(outState)
3738
}
3839

3940
override fun onCreate(savedInstanceState: Bundle?) {
4041
super.onCreate(savedInstanceState)
4142
if (savedInstanceState != null) {
43+
WebAuthProvider.onRestoreInstanceState(savedInstanceState)
4244
intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false)
4345
}
4446
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.auth0.android.provider
22

33
import android.content.Context
44
import android.net.Uri
5+
import android.os.Bundle
56
import android.text.TextUtils
67
import android.util.Base64
78
import android.util.Log
@@ -186,6 +187,19 @@ internal class OAuthManager(
186187
SignatureVerifier.forAsymmetricAlgorithm(tokenKeyId, apiClient, signatureVerifierCallback)
187188
}
188189

190+
internal fun toState(): OAuthManagerState {
191+
return OAuthManagerState(
192+
parameters = parameters.toMap(),
193+
headers = headers.toMap(),
194+
requestCode = requestCode,
195+
ctOptions = ctOptions,
196+
pkce = pkce,
197+
auth0 = account,
198+
idTokenVerificationIssuer = idTokenVerificationIssuer,
199+
idTokenVerificationLeeway = idTokenVerificationLeeway,
200+
)
201+
}
202+
189203
//Helper Methods
190204
@Throws(AuthenticationException::class)
191205
private fun assertNoError(errorValue: String?, errorDescription: String?) {
@@ -333,4 +347,23 @@ internal class OAuthManager(
333347
apiClient = AuthenticationAPIClient(account)
334348
this.ctOptions = ctOptions
335349
}
350+
}
351+
352+
internal fun OAuthManager.Companion.fromState(
353+
state: OAuthManagerState,
354+
callback: Callback<Credentials, AuthenticationException>
355+
): OAuthManager {
356+
return OAuthManager(
357+
account = state.auth0,
358+
ctOptions = state.ctOptions,
359+
parameters = state.parameters,
360+
callback = callback
361+
).apply {
362+
setHeaders(
363+
state.headers
364+
)
365+
setPKCE(state.pkce)
366+
setIdTokenVerificationIssuer(state.idTokenVerificationIssuer)
367+
setIdTokenVerificationLeeway(state.idTokenVerificationLeeway)
368+
}
336369
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.auth0.android.provider
2+
3+
import android.os.Parcel
4+
import android.os.Parcelable
5+
import android.util.Base64
6+
import androidx.core.os.ParcelCompat
7+
import com.auth0.android.Auth0
8+
import com.auth0.android.authentication.AuthenticationAPIClient
9+
import com.auth0.android.request.internal.GsonProvider
10+
import com.google.gson.Gson
11+
12+
internal data class OAuthManagerState(
13+
val auth0: Auth0,
14+
val parameters: Map<String, String>,
15+
val headers: Map<String, String>,
16+
val requestCode: Int = 0,
17+
val ctOptions: CustomTabsOptions,
18+
val pkce: PKCE?,
19+
val idTokenVerificationLeeway: Int?,
20+
val idTokenVerificationIssuer: String?
21+
) {
22+
23+
private class OAuthManagerJson(
24+
val auth0ClientId: String,
25+
val auth0DomainUrl: String,
26+
val auth0ConfigurationUrl: String?,
27+
val parameters: Map<String, String>,
28+
val headers: Map<String, String>,
29+
val requestCode: Int = 0,
30+
val ctOptions: String,
31+
val redirectUri: String,
32+
val codeChallenge: String,
33+
val codeVerifier: String,
34+
val idTokenVerificationLeeway: Int?,
35+
val idTokenVerificationIssuer: String?
36+
)
37+
38+
fun serializeToJson(
39+
gson: Gson = GsonProvider.gson,
40+
): String {
41+
val parcel = Parcel.obtain()
42+
try {
43+
parcel.writeParcelable(ctOptions, Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
44+
val ctOptionsEncoded = Base64.encodeToString(parcel.marshall(), Base64.DEFAULT)
45+
46+
val json = OAuthManagerJson(
47+
auth0ClientId = auth0.clientId,
48+
auth0ConfigurationUrl = auth0.configurationDomain,
49+
auth0DomainUrl = auth0.domain,
50+
parameters = parameters,
51+
headers = headers,
52+
requestCode = requestCode,
53+
ctOptions = ctOptionsEncoded,
54+
redirectUri = pkce?.redirectUri.orEmpty(),
55+
codeVerifier = pkce?.codeVerifier.orEmpty(),
56+
codeChallenge = pkce?.codeChallenge.orEmpty(),
57+
idTokenVerificationIssuer = idTokenVerificationIssuer,
58+
idTokenVerificationLeeway = idTokenVerificationLeeway,
59+
)
60+
return gson.toJson(json)
61+
} finally {
62+
parcel.recycle()
63+
}
64+
}
65+
66+
companion object {
67+
fun deserializeState(
68+
json: String,
69+
gson: Gson = GsonProvider.gson,
70+
): OAuthManagerState {
71+
val parcel = Parcel.obtain()
72+
try {
73+
val oauthManagerJson = gson.fromJson(json, OAuthManagerJson::class.java)
74+
75+
val decodedCtOptionsBytes = Base64.decode(oauthManagerJson.ctOptions, Base64.DEFAULT)
76+
parcel.unmarshall(decodedCtOptionsBytes, 0, decodedCtOptionsBytes.size)
77+
parcel.setDataPosition(0)
78+
79+
val customTabsOptions = ParcelCompat.readParcelable(
80+
parcel,
81+
CustomTabsOptions::class.java.classLoader,
82+
CustomTabsOptions::class.java
83+
) ?: error("Couldn't deserialize from Parcel")
84+
85+
val auth0 = Auth0.getInstance(
86+
clientId = oauthManagerJson.auth0ClientId,
87+
domain = oauthManagerJson.auth0DomainUrl,
88+
configurationDomain = oauthManagerJson.auth0ConfigurationUrl,
89+
)
90+
91+
return OAuthManagerState(
92+
auth0 = auth0,
93+
parameters = oauthManagerJson.parameters,
94+
headers = oauthManagerJson.headers,
95+
requestCode = oauthManagerJson.requestCode,
96+
ctOptions = customTabsOptions,
97+
pkce = PKCE(
98+
AuthenticationAPIClient(auth0),
99+
oauthManagerJson.codeVerifier,
100+
oauthManagerJson.redirectUri,
101+
oauthManagerJson.codeChallenge,
102+
oauthManagerJson.headers,
103+
),
104+
idTokenVerificationIssuer = oauthManagerJson.idTokenVerificationIssuer,
105+
idTokenVerificationLeeway = oauthManagerJson.idTokenVerificationLeeway,
106+
)
107+
} finally {
108+
parcel.recycle()
109+
}
110+
}
111+
}
112+
}

auth0/src/main/java/com/auth0/android/provider/PKCE.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ public PKCE(@NonNull AuthenticationAPIClient apiClient, String redirectUri, @Non
4747
this.headers = headers;
4848
}
4949

50+
PKCE(@NonNull AuthenticationAPIClient apiClient,
51+
@NonNull String codeVerifier,
52+
@NonNull String redirectUri,
53+
@NonNull String codeChallenge,
54+
@NonNull Map<String, String> headers) {
55+
this.apiClient = apiClient;
56+
this.codeVerifier = codeVerifier;
57+
this.redirectUri = redirectUri;
58+
this.codeChallenge = codeChallenge;
59+
this.headers = headers;
60+
}
61+
5062
/**
5163
* Returns the Code Challenge generated using a Code Verifier.
5264
*
@@ -56,6 +68,14 @@ public String getCodeChallenge() {
5668
return codeChallenge;
5769
}
5870

71+
public String getCodeVerifier() {
72+
return codeVerifier;
73+
}
74+
75+
public String getRedirectUri() {
76+
return redirectUri;
77+
}
78+
5979
/**
6080
* Performs a request to the Auth0 API to get the OAuth Token and end the PKCE flow.
6181
* The instance of this class must be disposed after this method is called.

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.auth0.android.provider
33
import android.content.Context
44
import android.content.Intent
55
import android.net.Uri
6+
import android.os.Bundle
67
import android.util.Log
78
import androidx.annotation.VisibleForTesting
89
import com.auth0.android.Auth0
@@ -14,6 +15,8 @@ import kotlinx.coroutines.Dispatchers
1415
import kotlinx.coroutines.suspendCancellableCoroutine
1516
import kotlinx.coroutines.withContext
1617
import java.util.Locale
18+
import java.util.concurrent.CopyOnWriteArrayList
19+
import java.util.concurrent.CopyOnWriteArraySet
1720
import kotlin.coroutines.CoroutineContext
1821
import kotlin.coroutines.resume
1922
import kotlin.coroutines.resumeWithException
@@ -26,12 +29,25 @@ import kotlin.coroutines.resumeWithException
2629
*/
2730
public object WebAuthProvider {
2831
private val TAG: String? = WebAuthProvider::class.simpleName
32+
private const val KEY_BUNDLE_OAUTH_MANAGER_STATE = "oauth_manager_state"
33+
34+
private val callbacks = CopyOnWriteArraySet<Callback<Credentials, AuthenticationException>>()
2935

3036
@JvmStatic
3137
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
3238
internal var managerInstance: ResumableManager? = null
3339
private set
3440

41+
@JvmStatic
42+
public fun addCallback(callback: Callback<Credentials, AuthenticationException>) {
43+
callbacks += callback
44+
}
45+
46+
@JvmStatic
47+
public fun removeCallback(callback: Callback<Credentials, AuthenticationException>) {
48+
callbacks -= callback
49+
}
50+
3551
// Public methods
3652
/**
3753
* Initialize the WebAuthProvider instance for logging out the user using an account. Additional settings can be configured
@@ -89,6 +105,35 @@ public object WebAuthProvider {
89105
managerInstance!!.failure(exception)
90106
}
91107

108+
internal fun onSaveInstanceState(bundle: Bundle) {
109+
val manager = managerInstance
110+
if (manager is OAuthManager) {
111+
val managerState = manager.toState()
112+
bundle.putString(KEY_BUNDLE_OAUTH_MANAGER_STATE, managerState.serializeToJson())
113+
}
114+
}
115+
116+
internal fun onRestoreInstanceState(bundle: Bundle) {
117+
if (managerInstance == null) {
118+
val stateJson = bundle.getString(KEY_BUNDLE_OAUTH_MANAGER_STATE).orEmpty()
119+
if (stateJson.isNotBlank()) {
120+
val state = OAuthManagerState.deserializeState(stateJson)
121+
managerInstance = OAuthManager.fromState(
122+
state,
123+
object : Callback<Credentials, AuthenticationException> {
124+
override fun onSuccess(result: Credentials) {
125+
callbacks.forEach { it.onSuccess(result) }
126+
}
127+
128+
override fun onFailure(error: AuthenticationException) {
129+
callbacks.forEach { it.onFailure(error) }
130+
}
131+
}
132+
)
133+
}
134+
}
135+
}
136+
92137
@JvmStatic
93138
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
94139
internal fun resetManagerInstance() {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.auth0.android.provider
2+
3+
import android.graphics.Color
4+
import com.auth0.android.Auth0
5+
import com.nhaarman.mockitokotlin2.mock
6+
import org.junit.Assert
7+
import org.junit.Test
8+
import org.junit.runner.RunWith
9+
import org.robolectric.RobolectricTestRunner
10+
11+
@RunWith(RobolectricTestRunner::class)
12+
internal class OAuthManagerStateTest {
13+
14+
@Test
15+
fun `serialize should work`() {
16+
val auth0 = Auth0.getInstance("clientId", "domain")
17+
val state = OAuthManagerState(
18+
auth0 = auth0,
19+
parameters = mapOf("param1" to "value1"),
20+
headers = mapOf("header1" to "value1"),
21+
requestCode = 1,
22+
ctOptions = CustomTabsOptions.newBuilder()
23+
.showTitle(true)
24+
.withToolbarColor(Color.RED)
25+
.withBrowserPicker(
26+
BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build()
27+
)
28+
.build(),
29+
pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")),
30+
idTokenVerificationLeeway = 1,
31+
idTokenVerificationIssuer = "issuer"
32+
)
33+
34+
val json = state.serializeToJson()
35+
36+
Assert.assertTrue(json.isNotBlank())
37+
38+
val deserializedState = OAuthManagerState.deserializeState(json)
39+
40+
Assert.assertEquals(mapOf("param1" to "value1"), deserializedState.parameters)
41+
Assert.assertEquals(mapOf("header1" to "value1"), deserializedState.headers)
42+
Assert.assertEquals(1, deserializedState.requestCode)
43+
Assert.assertEquals("redirectUri", deserializedState.pkce?.redirectUri)
44+
Assert.assertEquals(1, deserializedState.idTokenVerificationLeeway)
45+
Assert.assertEquals("issuer", deserializedState.idTokenVerificationIssuer)
46+
}
47+
}

sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ class DatabaseLoginFragment : Fragment() {
102102
.setDeviceCredentialFallback(true)
103103
.build()
104104

105+
private val callback = object: Callback<Credentials, AuthenticationException> {
106+
override fun onSuccess(result: Credentials) {
107+
credentialsManager.saveCredentials(result)
108+
Snackbar.make(
109+
requireView(),
110+
"Hello ${result.user.name}",
111+
Snackbar.LENGTH_LONG
112+
).show()
113+
}
114+
115+
override fun onFailure(error: AuthenticationException) {
116+
Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG)
117+
.show()
118+
}
119+
}
120+
105121
override fun onCreateView(
106122
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
107123
): View {
@@ -188,6 +204,16 @@ class DatabaseLoginFragment : Fragment() {
188204
return binding.root
189205
}
190206

207+
override fun onStart() {
208+
super.onStart()
209+
WebAuthProvider.addCallback(callback)
210+
}
211+
212+
override fun onStop() {
213+
super.onStop()
214+
WebAuthProvider.removeCallback(callback)
215+
}
216+
191217
private suspend fun dbLoginAsync(email: String, password: String) {
192218
try {
193219
val result =

0 commit comments

Comments
 (0)