Skip to content

Commit 60c97a6

Browse files
committed
Introduce authenticated flows
1 parent 982c269 commit 60c97a6

File tree

5 files changed

+85
-20
lines changed

5 files changed

+85
-20
lines changed

README.md

+12-4
Original file line numberDiff line numberDiff line change
@@ -283,19 +283,27 @@ Make sure the Activity running the flow is a `SINGLE_TOP` activity, to avoid any
283283
unexpected UX. Run the flow by creating a `DescopeFlow.Runner`:
284284

285285
```kotlin
286-
Descope.flow.create(
286+
val runner = Descope.flow.create(
287287
flowUrl = "<URL_FOR_FLOW_IN_SETUP_#1>",
288288
deepLinkUrl = "<URL_FOR_APP_LINK_IN_SETUP_#2>",
289289
backupCustomScheme = "<OPTIONAL_CUSTOM_SCHEME_FROM_SETUP_#2>"
290-
).start(this@MainActivity)
290+
)
291+
292+
// Starting an authentication flow
293+
runner.start(this@MainActivity)
294+
295+
// Starting a flow for an authenticated user
296+
Descope.sessionManager.session?.run {
297+
runner.start(this@MainActivity, "flow-id", refreshJwt)
298+
}
291299
```
292300

293301
When supporting Magic Links the `resume` function must be called. In your authentication Activity
294302
inside the `onCreate` method:
295303

296304
```kotlin
297-
intent?.getStringExtra("descopeFlowUri")?.run {
298-
Descope.flow.currentRunner?.resume(this@AuthActivity, this)
305+
intent?.data?.let { incomingUri ->
306+
Descope.flow.currentRunner?.resume(this@AuthActivity, incomingUri)
299307
}
300308
```
301309

descopesdk/src/main/java/com/descope/internal/http/DescopeClient.kt

+9
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,15 @@ internal class DescopeClient(internal val config: DescopeConfig) : HttpClient(co
427427
"codeVerifier" to codeVerifier,
428428
),
429429
)
430+
431+
suspend fun flowPrime(flowId: String, refreshJwt: String): FlowPrimeResponse = post(
432+
route = "flow/prime",
433+
decoder = FlowPrimeResponse::fromJson,
434+
headers = authorization(refreshJwt),
435+
body = mapOf(
436+
"flowId" to flowId,
437+
),
438+
)
430439

431440
// Others
432441

descopesdk/src/main/java/com/descope/internal/http/Responses.kt

+15
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,18 @@ internal data class SsoServerResponse(
210210
}
211211
}
212212
}
213+
214+
internal data class FlowPrimeResponse(
215+
val codeVerifier: String,
216+
val codeChallenge: String,
217+
) {
218+
companion object {
219+
@Suppress("UNUSED_PARAMETER")
220+
fun fromJson(json: String, cookies: List<HttpCookie>) = JSONObject(json).run {
221+
FlowPrimeResponse(
222+
codeVerifier = getString("codeVerifier"),
223+
codeChallenge = getString("codeChallenge"),
224+
)
225+
}
226+
}
227+
}

descopesdk/src/main/java/com/descope/internal/routes/Flow.kt

+30-16
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@ import android.content.Context
44
import android.net.Uri
55
import androidx.browser.customtabs.CustomTabsIntent
66
import com.descope.internal.http.DescopeClient
7+
import com.descope.internal.others.toBase64
78
import com.descope.internal.others.with
89
import com.descope.sdk.DescopeFlow
910
import com.descope.sdk.DescopeLogger.Level.Info
1011
import com.descope.types.AuthenticationResponse
1112
import com.descope.types.DescopeException
1213
import com.descope.types.Result
1314
import java.security.MessageDigest
14-
import kotlin.io.encoding.Base64
15-
import kotlin.io.encoding.ExperimentalEncodingApi
1615
import kotlin.random.Random
1716

18-
@OptIn(ExperimentalEncodingApi::class)
1917
internal class Flow(
2018
override val client: DescopeClient
2119
) : Route, DescopeFlow {
@@ -44,27 +42,27 @@ internal class Flow(
4442
Random.nextBytes(randomBytes)
4543

4644
// codeVerifier == base64(randomBytes)
47-
codeVerifier = Base64.UrlSafe.encode(randomBytes)
45+
codeVerifier = randomBytes.toBase64()
4846

4947
// hash bytes using sha256
5048
val md = MessageDigest.getInstance("SHA-256")
5149
val hashed = md.digest(randomBytes)
5250

5351
// codeChallenge == base64(sha256(randomBytes))
54-
val codeChallenge = Base64.UrlSafe.encode(hashed)
52+
val codeChallenge = hashed.toBase64()
5553

56-
// embed into url parameters
57-
val uriBuilder = Uri.parse(flowUrl).buildUpon()
58-
.appendQueryParameter("ra-callback", deepLinkUrl)
59-
.appendQueryParameter("ra-challenge", codeChallenge)
60-
.appendQueryParameter("ra-initiator", "android")
61-
backupCustomScheme?.let {
62-
uriBuilder.appendQueryParameter("ra-backup-callback", it)
63-
}
64-
val uri = uriBuilder.build()
54+
startFlowViaBrowser(codeChallenge, context)
55+
}
6556

66-
// launch via chrome custom tabs
67-
launchUri(context, uri)
57+
override suspend fun start(context: Context, flowId: String, refreshJwt: String) {
58+
val primeResponse = client.flowPrime(flowId, refreshJwt)
59+
// use server generated verifier and challenge
60+
codeVerifier = primeResponse.codeVerifier
61+
startFlowViaBrowser(primeResponse.codeChallenge, context)
62+
}
63+
64+
override fun start(context: Context, flowId: String, refreshJwt: String, callback: (Result<Unit>) -> Unit) = wrapCoroutine(callback) {
65+
start(context, flowId, refreshJwt)
6866
}
6967

7068
override fun resume(context: Context, incomingUriString: String) {
@@ -92,6 +90,22 @@ internal class Flow(
9290
override fun exchange(incomingUri: Uri, callback: (Result<AuthenticationResponse>) -> Unit) = wrapCoroutine(callback) {
9391
exchange(incomingUri)
9492
}
93+
94+
// Internal
95+
96+
private fun startFlowViaBrowser(codeChallenge: String, context: Context) {
97+
val uriBuilder = Uri.parse(flowUrl).buildUpon()
98+
.appendQueryParameter("ra-callback", deepLinkUrl)
99+
.appendQueryParameter("ra-challenge", codeChallenge)
100+
.appendQueryParameter("ra-initiator", "android")
101+
backupCustomScheme?.let {
102+
uriBuilder.appendQueryParameter("ra-backup-callback", it)
103+
}
104+
val uri = uriBuilder.build()
105+
106+
// launch via chrome custom tabs
107+
launchUri(context, uri)
108+
}
95109

96110
}
97111

descopesdk/src/main/java/com/descope/sdk/Routes.kt

+19
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,25 @@ interface DescopeFlow {
854854
*/
855855
fun start(context: Context)
856856

857+
/**
858+
* Start a flow for a currently authenticated user from the current [context].
859+
*
860+
* Use this version of start if the user has an **active session**.
861+
* Note: This is an asynchronous operation that performs network requests before and
862+
* opening the browser. It is thus recommended to switch the
863+
* user interface to a loading state before calling this function, otherwise the user
864+
* might accidentally interact with the app when the authentication view is not
865+
* being displayed.
866+
*
867+
* @param context the context launching the authentication flow.
868+
* @param flowId the ID of the flow to run
869+
* @param refreshJwt the refreshJwt from an active [DescopeSession].
870+
*/
871+
suspend fun start(context: Context, flowId: String, refreshJwt: String)
872+
873+
/** @see start (async version) */
874+
fun start(context: Context, flowId: String, refreshJwt: String, callback: (Result<Unit>) -> Unit)
875+
857876
/**
858877
* Resumes an ongoing flow after a redirect back to the app.
859878
* This is required for *Magic Link only* at this stage.

0 commit comments

Comments
 (0)