Skip to content

Commit

Permalink
Native flow adjustments (#151)
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski authored Nov 10, 2024
1 parent e5f1342 commit 68b54f5
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 51 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ written for Android. You can read more on the [Descope Website](https://descope.
Add the following to your `build.gradle` dependencies:

```groovy
implementation 'com.descope:descope-kotlin:0.11.1'
implementation 'com.descope:descope-kotlin:0.12.0'
```

## Quickstart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.descope.internal.http.JwtServerResponse
import com.descope.internal.http.REFRESH_COOKIE_NAME
import com.descope.internal.http.SESSION_COOKIE_NAME
import com.descope.internal.others.activityHelper
import com.descope.internal.others.stringOrEmptyAsNull
import com.descope.internal.others.with
import com.descope.internal.routes.convert
import com.descope.internal.routes.getPackageOrigin
Expand All @@ -42,14 +41,14 @@ import java.net.HttpCookie
internal class DescopeFlowCoordinator(private val webView: WebView) {

private lateinit var flow: DescopeFlow
private var state: State = State.Initial
private val handler: Handler = Handler(Looper.getMainLooper())
private val sdk: DescopeSdk
get() = if (this::flow.isInitialized) flow.sdk ?: Descope.sdk else Descope.sdk
private val logger: DescopeLogger?
get() = sdk.client.config.logger

private var currentFlowUrl: Uri? = null
private var pendingFinishUrl: Uri? = null

init {
webView.settings.javaScriptEnabled = true
Expand All @@ -59,6 +58,11 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {
webView.addJavascriptInterface(object {
@JavascriptInterface
fun onReady() {
if (state != State.Started) {
logger?.log(Info, "Flow onReady called in state $state - ignoring")
return
}
state = State.Ready
logger?.log(Info, "Flow is ready")
handler.post {
flow.lifecycle?.onReady()
Expand All @@ -67,6 +71,11 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {

@JavascriptInterface
fun onSuccess(success: String, url: String) {
if (state != State.Ready) {
logger?.log(Info, "Flow onSuccess called in state $state - ignoring")
return
}
state = State.Finished
logger?.log(Info, "Flow finished successfully")
val jwtServerResponse = JwtServerResponse.fromJson(success, emptyList())
// take tokens from cookies if missing
Expand All @@ -81,6 +90,11 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {

@JavascriptInterface
fun onError(error: String) {
if (state != State.Ready) {
logger?.log(Info, "Flow onError called in state $state - ignoring")
return
}
state = State.Failed
logger?.log(Error, "Flow finished with an exception", error)
handler.post {
flow.lifecycle?.onError(DescopeException.flowFailed.with(desc = error))
Expand All @@ -92,9 +106,12 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {
currentFlowUrl = url.toUri()
webView.findViewTreeLifecycleOwner()?.lifecycleScope?.launch(Dispatchers.Main) {
val nativeResponse = JSONObject()
var type = ""
try {
if (response == null) return@launch
when (val nativePayload = NativePayload.fromJson(response)) {
val nativePayload = NativePayload.fromJson(response)
type = nativePayload.type
when (nativePayload) {
is NativePayload.OAuthNative -> {
logger?.log(Info, "Launching system UI for native oauth")
val resp = nativeAuthorization(webView.context, nativePayload.start)
Expand All @@ -106,14 +123,12 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {

is NativePayload.OAuthWeb -> {
logger?.log(Info, "Launching custom tab for web-based oauth")
nativePayload.finishUrl?.run { pendingFinishUrl = this.toUri() }
launchCustomTab(webView.context, nativePayload.startUrl, flow.presentation?.createCustomTabsIntent(webView.context))
return@launch
}

is NativePayload.Sso -> {
logger?.log(Info, "Launching custom tab for sso")
nativePayload.finishUrl?.run { pendingFinishUrl = this.toUri() }
launchCustomTab(webView.context, nativePayload.startUrl, flow.presentation?.createCustomTabsIntent(webView.context))
return@launch
}
Expand Down Expand Up @@ -145,7 +160,7 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {
}

// we call the callback even when we fail
webView.evaluateJavascript("document.getElementsByTagName('descope-wc')[0]?.nativeComplete(`${nativeResponse.toString().escapeForBackticks()}`)") {}
webView.evaluateJavascript("document.getElementsByTagName('descope-wc')[0]?.nativeResume('$type', `${nativeResponse.toString().escapeForBackticks()}`)") {}
}
}
}, "flow")
Expand Down Expand Up @@ -187,40 +202,16 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {

internal fun run(flow: DescopeFlow) {
this.flow = flow
state = State.Started
webView.loadUrl(flow.uri.toString())
}

internal fun resumeFromDeepLink(deepLink: Uri) {
if (!this::flow.isInitialized) throw DescopeException.flowFailed.with(desc = "`resumeFromDeepLink` cannot be called before `startFlow`")
activityHelper.closeCustomTab(webView.context)
val stepId = deepLink.getQueryParameter("descope-login-flow")?.split("_")?.get(1)
val t = deepLink.getQueryParameter("t")
val code = deepLink.getQueryParameter("code")
val uri = pendingFinishUrl

when {
// magic link
t != null && stepId != null -> {
logger?.log(Info, "resumeFromDeepLink received a token ('t') query param")
webView.evaluateJavascript("document.getElementsByTagName('descope-wc')[0]?.flowState.update({ token: '$t', stepId: '$stepId'})") {}
}
// oauth web / sso
code != null && (uri == null || uri.host == flow.uri.host) -> {
logger?.log(Info, "resumeFromDeepLink received an exchange code ('code') query param")
val nativeResponse = JSONObject()
nativeResponse.put("exchangeCode", code)
nativeResponse.put("idpInitiated", true)
webView.evaluateJavascript("document.getElementsByTagName('descope-wc')[0]?.nativeComplete(`${nativeResponse.toString().escapeForBackticks()}`)") {}
}
// anything else || finishUrl != flowUrl
else -> {
logger?.log(Info, "resumeFromDeepLink defaulting to navigation")
// create the redirect flow URL by copying all url parameters received from the incoming URI
val uriBuilder = (pendingFinishUrl ?: currentFlowUrl ?: flow.uri).buildUpon()
deepLink.queryParameterNames.forEach { uriBuilder.appendQueryParameter(it, deepLink.getQueryParameter(it)) }
webView.loadUrl(uriBuilder.build().toString())
}
}
val response = JSONObject().apply { put("url", deepLink.toString()) }
val type = if (deepLink.queryParameterNames.contains("t")) "magicLink" else "oauthWeb"
webView.evaluateJavascript("document.getElementsByTagName('descope-wc')[0]?.nativeResume('$type', `${response.toString().escapeForBackticks()}`)") {}
}

}
Expand All @@ -229,20 +220,29 @@ internal class DescopeFlowCoordinator(private val webView: WebView) {

internal sealed class NativePayload {
internal class OAuthNative(val start: JSONObject) : NativePayload()
internal class OAuthWeb(val startUrl: String, val finishUrl: String?) : NativePayload()
internal class Sso(val startUrl: String, val finishUrl: String?) : NativePayload()
internal class OAuthWeb(val startUrl: String) : NativePayload()
internal class Sso(val startUrl: String) : NativePayload()
internal class WebAuthnCreate(val transactionId: String, val options: String) : NativePayload()
internal class WebAuthnGet(val transactionId: String, val options: String) : NativePayload()

val type
get() = when (this) {
is OAuthNative -> "oauthNative"
is OAuthWeb -> "oauthWeb"
is Sso -> "sso"
is WebAuthnCreate -> "webauthnCreate"
is WebAuthnGet -> "webauthnGet"
}

companion object {
fun fromJson(jsonString: String): NativePayload {
val json = JSONObject(jsonString)
val type = json.getString("type")
return json.getJSONObject("payload").run {
when (type) {
"oauthNative" -> OAuthNative(start = getJSONObject("start"))
"oauthWeb" -> OAuthWeb(startUrl = getString("startUrl"), finishUrl = stringOrEmptyAsNull("finishUrl"))
"sso" -> Sso(startUrl = getString("startUrl"), finishUrl = stringOrEmptyAsNull("finishUrl"))
"oauthWeb" -> OAuthWeb(startUrl = getString("startUrl"))
"sso" -> Sso(startUrl = getString("startUrl"))
"webauthnCreate" -> WebAuthnCreate(transactionId = getString("transactionId"), options = getString("options"))
"webauthnGet" -> WebAuthnGet(transactionId = getString("transactionId"), options = getString("options"))
else -> throw DescopeException.flowFailed.with(desc = "Unexpected server response in flow")
Expand All @@ -252,6 +252,14 @@ internal sealed class NativePayload {
}
}

private enum class State {
Initial,
Started,
Ready,
Failed,
Finished,
}

// JS

private fun setupScript(
Expand Down
34 changes: 23 additions & 11 deletions descopesdk/src/main/java/com/descope/android/DescopeFlowView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,26 @@ data class DescopeFlow(val uri: Uri) {
var oauthProvider: OAuthProvider? = null

/**
* A a deep link URL to configure web-based OAuth redirect chain
* to return back to your app from the browser.
* An optional deep link link URL to use when performing OAuth authentication, overriding
* whatever is configured in the flow or project.
* - **IMPORTANT NOTE**: even the Application links are the recommended way to configure
* deep links, some browsers, such as Opera, do not honor them and open the URLs inline.
* It is possible to circumvent this issue by using a custom scheme, albeit less secure.
*/
var oauthRedirect: String? = null

/**
* A a deep link URL to configure SSO redirect chain to return
* back to your app from the browser.
* An optional deep link link URL to use performing SSO authentication, overriding
* whatever is configured in the flow or project
* - **IMPORTANT NOTE**: even the Application links are the recommended way to configure
* deep links, some browsers, such as Opera, do not honor them and open the URLs inline.
* It is possible to circumvent this issue by using a custom scheme, albeit less secure.
*/
var ssoRedirect: String? = null

/**
* A a deep link URL to configure Magic Link authentication to return back to your app.
* An optional deep link link URL to use when sending magic link emails, overriding
* whatever is configured in the flow or project
*/
var magicLinkRedirect: String? = null

Expand Down Expand Up @@ -125,10 +132,10 @@ data class DescopeFlow(val uri: Uri) {
}

/**
* Authenticate a user using the Descope Flows.
* Authenticate a user using Descope Flows.
*
* Embed this view into your UI to be able to run flows built with the
* Descope Flows: https://app.descope.com/flows
* [Descope Flow builder](https://app.descope.com/flows)
*
* **Setup**
*
Expand All @@ -137,16 +144,21 @@ data class DescopeFlow(val uri: Uri) {
* yourself. Read more [here.](https://docs.descope.com/auth-hosting-app)
*
* - To use the Descope authentication methods, it is required
* to configure desired the authentication methods in the [Descope console.](https://app.descope.com/settings/authentication)
* to configure the desired authentication methods in the [Descope console.](https://app.descope.com/settings/authentication)
* Some of the default configurations might be OK to start out with,
* but it is likely the modifications will be required before release.
* but it is likely that modifications will be required before release.
*
* - **IMPORTANT NOTE**: even the Application links are the recommended way to configure
* deep links, some browsers, such as Opera, do not honor them and open the URLs inline.
* It is possible to circumvent this issue by using a custom scheme, albeit less secure.
*
* - Beyond that, in order to use navigation / redirection based authentication,
* namely `Magic Link` and `OAuth (social)`, it's required to set up app links.
* namely `Magic Link`, `OAuth (social)` and SSO, it's required to set up app links.
* App Links allow the application to receive navigation to specific URLs,
* instead of opening the browser. Follow the [Android official documentation](https://developer.android.com/training/app-links)
* to set up App link in your application.
*
* - Finally, it is possibles for users to authenticate using their Google accounts used
* - Finally, it is possible for users to authenticate using the Google account or accounts they are logged into
* on their Android devices. If you haven't already configured your app to support `Sign in with Google` you'll
* probably need to set up your [Google APIs console project](https://developer.android.com/identity/sign-in/credential-manager-siwg#set-google)
* for this. You should also configure an OAuth provider for Google in the in the [Descope console](https://app.descope.com/settings/authentication/social),
Expand Down
2 changes: 1 addition & 1 deletion descopesdk/src/main/java/com/descope/sdk/Sdk.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,6 @@ class DescopeSdk(context: Context, projectId: String, configure: DescopeConfig.(
const val name = "DescopeAndroid"

/** The Descope SDK version */
const val version = "0.11.2"
const val version = "0.12.0"
}
}

0 comments on commit 68b54f5

Please sign in to comment.