Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Process Death in WebAuthProvider #784

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ public open class AuthenticationActivity : Activity() {
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(EXTRA_INTENT_LAUNCHED, intentLaunched)
WebAuthProvider.onSaveInstanceState(outState)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
WebAuthProvider.onRestoreInstanceState(savedInstanceState)
intentLaunched = savedInstanceState.getBoolean(EXTRA_INTENT_LAUNCHED, false)
}
}
Expand Down
33 changes: 33 additions & 0 deletions auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.auth0.android.provider

import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.text.TextUtils
import android.util.Base64
import android.util.Log
Expand Down Expand Up @@ -186,6 +187,19 @@ internal class OAuthManager(
SignatureVerifier.forAsymmetricAlgorithm(tokenKeyId, apiClient, signatureVerifierCallback)
}

internal fun toState(): OAuthManagerState {
return OAuthManagerState(
parameters = parameters.toMap(),
headers = headers.toMap(),
requestCode = requestCode,
ctOptions = ctOptions,
pkce = pkce,
auth0 = account,
idTokenVerificationIssuer = idTokenVerificationIssuer,
idTokenVerificationLeeway = idTokenVerificationLeeway,
)
}

//Helper Methods
@Throws(AuthenticationException::class)
private fun assertNoError(errorValue: String?, errorDescription: String?) {
Expand Down Expand Up @@ -333,4 +347,23 @@ internal class OAuthManager(
apiClient = AuthenticationAPIClient(account)
this.ctOptions = ctOptions
}
}

internal fun OAuthManager.Companion.fromState(
state: OAuthManagerState,
callback: Callback<Credentials, AuthenticationException>
): OAuthManager {
return OAuthManager(
account = state.auth0,
ctOptions = state.ctOptions,
parameters = state.parameters,
callback = callback
).apply {
setHeaders(
state.headers
)
setPKCE(state.pkce)
setIdTokenVerificationIssuer(state.idTokenVerificationIssuer)
setIdTokenVerificationLeeway(state.idTokenVerificationLeeway)
}
}
112 changes: 112 additions & 0 deletions auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.auth0.android.provider

import android.os.Parcel
import android.os.Parcelable
import android.util.Base64
import androidx.core.os.ParcelCompat
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.request.internal.GsonProvider
import com.google.gson.Gson

internal data class OAuthManagerState(
val auth0: Auth0,
val parameters: Map<String, String>,
val headers: Map<String, String>,
val requestCode: Int = 0,
val ctOptions: CustomTabsOptions,
val pkce: PKCE?,
val idTokenVerificationLeeway: Int?,
val idTokenVerificationIssuer: String?
) {

private class OAuthManagerJson(
val auth0ClientId: String,
val auth0DomainUrl: String,
val auth0ConfigurationUrl: String?,
val parameters: Map<String, String>,
val headers: Map<String, String>,
val requestCode: Int = 0,
val ctOptions: String,
val redirectUri: String,
val codeChallenge: String,
val codeVerifier: String,
val idTokenVerificationLeeway: Int?,
val idTokenVerificationIssuer: String?
)

fun serializeToJson(
gson: Gson = GsonProvider.gson,
): String {
val parcel = Parcel.obtain()
try {
parcel.writeParcelable(ctOptions, Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
val ctOptionsEncoded = Base64.encodeToString(parcel.marshall(), Base64.DEFAULT)

val json = OAuthManagerJson(
auth0ClientId = auth0.clientId,
auth0ConfigurationUrl = auth0.configurationDomain,
auth0DomainUrl = auth0.domain,
parameters = parameters,
headers = headers,
requestCode = requestCode,
ctOptions = ctOptionsEncoded,
redirectUri = pkce?.redirectUri.orEmpty(),
codeVerifier = pkce?.codeVerifier.orEmpty(),
codeChallenge = pkce?.codeChallenge.orEmpty(),
idTokenVerificationIssuer = idTokenVerificationIssuer,
idTokenVerificationLeeway = idTokenVerificationLeeway,
)
return gson.toJson(json)
} finally {
parcel.recycle()
}
}

companion object {
fun deserializeState(
json: String,
gson: Gson = GsonProvider.gson,
): OAuthManagerState {
val parcel = Parcel.obtain()
try {
val oauthManagerJson = gson.fromJson(json, OAuthManagerJson::class.java)

val decodedCtOptionsBytes = Base64.decode(oauthManagerJson.ctOptions, Base64.DEFAULT)
parcel.unmarshall(decodedCtOptionsBytes, 0, decodedCtOptionsBytes.size)
parcel.setDataPosition(0)

val customTabsOptions = ParcelCompat.readParcelable(
parcel,
CustomTabsOptions::class.java.classLoader,
CustomTabsOptions::class.java
) ?: error("Couldn't deserialize from Parcel")

val auth0 = Auth0.getInstance(
clientId = oauthManagerJson.auth0ClientId,
domain = oauthManagerJson.auth0DomainUrl,
configurationDomain = oauthManagerJson.auth0ConfigurationUrl,
)

return OAuthManagerState(
auth0 = auth0,
parameters = oauthManagerJson.parameters,
headers = oauthManagerJson.headers,
requestCode = oauthManagerJson.requestCode,
ctOptions = customTabsOptions,
pkce = PKCE(
AuthenticationAPIClient(auth0),
oauthManagerJson.codeVerifier,
oauthManagerJson.redirectUri,
oauthManagerJson.codeChallenge,
oauthManagerJson.headers,
),
idTokenVerificationIssuer = oauthManagerJson.idTokenVerificationIssuer,
idTokenVerificationLeeway = oauthManagerJson.idTokenVerificationLeeway,
)
} finally {
parcel.recycle()
}
}
}
}
20 changes: 20 additions & 0 deletions auth0/src/main/java/com/auth0/android/provider/PKCE.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ public PKCE(@NonNull AuthenticationAPIClient apiClient, String redirectUri, @Non
this.headers = headers;
}

PKCE(@NonNull AuthenticationAPIClient apiClient,
@NonNull String codeVerifier,
@NonNull String redirectUri,
@NonNull String codeChallenge,
@NonNull Map<String, String> headers) {
this.apiClient = apiClient;
this.codeVerifier = codeVerifier;
this.redirectUri = redirectUri;
this.codeChallenge = codeChallenge;
this.headers = headers;
}

/**
* Returns the Code Challenge generated using a Code Verifier.
*
Expand All @@ -56,6 +68,14 @@ public String getCodeChallenge() {
return codeChallenge;
}

public String getCodeVerifier() {
return codeVerifier;
}

public String getRedirectUri() {
return redirectUri;
}

/**
* Performs a request to the Auth0 API to get the OAuth Token and end the PKCE flow.
* The instance of this class must be disposed after this method is called.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.auth0.android.provider
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.auth0.android.Auth0
Expand All @@ -14,6 +15,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.util.Locale
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.CopyOnWriteArraySet
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
Expand All @@ -26,12 +29,25 @@ import kotlin.coroutines.resumeWithException
*/
public object WebAuthProvider {
private val TAG: String? = WebAuthProvider::class.simpleName
private const val KEY_BUNDLE_OAUTH_MANAGER_STATE = "oauth_manager_state"

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

@JvmStatic
@get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal var managerInstance: ResumableManager? = null
private set

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

@JvmStatic
public fun removeCallback(callback: Callback<Credentials, AuthenticationException>) {
callbacks -= callback
}

// Public methods
/**
* Initialize the WebAuthProvider instance for logging out the user using an account. Additional settings can be configured
Expand Down Expand Up @@ -89,6 +105,35 @@ public object WebAuthProvider {
managerInstance!!.failure(exception)
}

internal fun onSaveInstanceState(bundle: Bundle) {
val manager = managerInstance
if (manager is OAuthManager) {
val managerState = manager.toState()
bundle.putString(KEY_BUNDLE_OAUTH_MANAGER_STATE, managerState.serializeToJson())
}
}

internal fun onRestoreInstanceState(bundle: Bundle) {
if (managerInstance == null) {
val stateJson = bundle.getString(KEY_BUNDLE_OAUTH_MANAGER_STATE).orEmpty()
if (stateJson.isNotBlank()) {
val state = OAuthManagerState.deserializeState(stateJson)
managerInstance = OAuthManager.fromState(
state,
object : Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
callbacks.forEach { it.onSuccess(result) }
}

override fun onFailure(error: AuthenticationException) {
callbacks.forEach { it.onFailure(error) }
}
}
)
}
}
}

@JvmStatic
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun resetManagerInstance() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.auth0.android.provider

import android.graphics.Color
import com.auth0.android.Auth0
import com.nhaarman.mockitokotlin2.mock
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
internal class OAuthManagerStateTest {

@Test
fun `serialize should work`() {
val auth0 = Auth0.getInstance("clientId", "domain")
val state = OAuthManagerState(
auth0 = auth0,
parameters = mapOf("param1" to "value1"),
headers = mapOf("header1" to "value1"),
requestCode = 1,
ctOptions = CustomTabsOptions.newBuilder()
.showTitle(true)
.withToolbarColor(Color.RED)
.withBrowserPicker(
BrowserPicker.newBuilder().withAllowedPackages(emptyList()).build()
)
.build(),
pkce = PKCE(mock(), "redirectUri", mapOf("header1" to "value1")),
idTokenVerificationLeeway = 1,
idTokenVerificationIssuer = "issuer"
)

val json = state.serializeToJson()

Assert.assertTrue(json.isNotBlank())

val deserializedState = OAuthManagerState.deserializeState(json)

Assert.assertEquals(mapOf("param1" to "value1"), deserializedState.parameters)
Assert.assertEquals(mapOf("header1" to "value1"), deserializedState.headers)
Assert.assertEquals(1, deserializedState.requestCode)
Assert.assertEquals("redirectUri", deserializedState.pkce?.redirectUri)
Assert.assertEquals(1, deserializedState.idTokenVerificationLeeway)
Assert.assertEquals("issuer", deserializedState.idTokenVerificationIssuer)
}
}
26 changes: 26 additions & 0 deletions sample/src/main/java/com/auth0/sample/DatabaseLoginFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ class DatabaseLoginFragment : Fragment() {
.setDeviceCredentialFallback(true)
.build()

private val callback = object: Callback<Credentials, AuthenticationException> {
override fun onSuccess(result: Credentials) {
credentialsManager.saveCredentials(result)
Snackbar.make(
requireView(),
"Hello ${result.user.name}",
Snackbar.LENGTH_LONG
).show()
}

override fun onFailure(error: AuthenticationException) {
Snackbar.make(requireView(), error.getDescription(), Snackbar.LENGTH_LONG)
.show()
}
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
Expand Down Expand Up @@ -188,6 +204,16 @@ class DatabaseLoginFragment : Fragment() {
return binding.root
}

override fun onStart() {
super.onStart()
WebAuthProvider.addCallback(callback)
}

override fun onStop() {
super.onStop()
WebAuthProvider.removeCallback(callback)
}

private suspend fun dbLoginAsync(email: String, password: String) {
try {
val result =
Expand Down