diff --git a/samples/quickstart/README.md b/samples/quickstart/README.md index ce36ac2..303a140 100644 --- a/samples/quickstart/README.md +++ b/samples/quickstart/README.md @@ -8,14 +8,28 @@ Demonstrates a native Android flow using the ThunderID Compose SDK: ## Setup -Copy `.env.example` to `.env` and add your ThunderID credentials: +Copy `config.properties.example` to `config.properties` (gitignored) and add your ThunderID credentials: ``` THUNDERID_BASE_URL=https://localhost:8090 THUNDERID_CLIENT_ID=your-client-id THUNDERID_APPLICATION_ID=your-application-id +THUNDERID_AFTER_SIGN_IN_URL= +THUNDERID_AFTER_SIGN_OUT_URL= +THUNDERID_FLOW_SECRET=your-flow-secret ``` +- `THUNDERID_APPLICATION_ID` and `THUNDERID_FLOW_SECRET` come from your application's page in the ThunderID + console (**Applications → your app → General**). The Flow Secret is only shown in plaintext at creation + time — if you didn't copy it then, regenerate it from the same page. +- `THUNDERID_CLIENT_ID` is only needed if you switch this sample to the redirect-based OAuth flow; the + embedded Flow Execution API used here (sign-in and sign-up) doesn't require it. + +To let users self-register, enable self-registration in **both** places in the console — it's disabled by +default in either and the flow fails until both are turned on: +1. The application's settings (registration flow enabled for this app). +2. The user type assigned to the application (self-registration enabled for that user type). + ## Run Open in Android Studio, sync Gradle, and run on an API 24+ emulator or device. diff --git a/samples/quickstart/build.gradle.kts b/samples/quickstart/build.gradle.kts index 14562bb..b1d778e 100644 --- a/samples/quickstart/build.gradle.kts +++ b/samples/quickstart/build.gradle.kts @@ -27,11 +27,13 @@ android { val appId = config("THUNDERID_APPLICATION_ID") val afterSignInUrl = config("THUNDERID_AFTER_SIGN_IN_URL") val afterSignOutUrl = config("THUNDERID_AFTER_SIGN_OUT_URL") + val flowSecret = config("THUNDERID_FLOW_SECRET") buildConfigField("String", "THUNDERID_BASE_URL", "\"$baseUrl\"") buildConfigField("String", "THUNDERID_CLIENT_ID", "\"$clientId\"") buildConfigField("String", "THUNDERID_APPLICATION_ID", "\"$appId\"") buildConfigField("String", "THUNDERID_AFTER_SIGN_IN_URL", "\"$afterSignInUrl\"") buildConfigField("String", "THUNDERID_AFTER_SIGN_OUT_URL", "\"$afterSignOutUrl\"") + buildConfigField("String", "THUNDERID_FLOW_SECRET", "\"$flowSecret\"") } buildFeatures { diff --git a/samples/quickstart/config.properties.example b/samples/quickstart/config.properties.example index eb02f01..9a4cdbd 100644 --- a/samples/quickstart/config.properties.example +++ b/samples/quickstart/config.properties.example @@ -3,3 +3,4 @@ THUNDERID_CLIENT_ID=your-client-id THUNDERID_APPLICATION_ID=your-application-id THUNDERID_AFTER_SIGN_IN_URL= THUNDERID_AFTER_SIGN_OUT_URL= +THUNDERID_FLOW_SECRET= diff --git a/samples/quickstart/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt b/samples/quickstart/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt index 9e3ef50..a429b91 100644 --- a/samples/quickstart/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt +++ b/samples/quickstart/src/main/kotlin/dev/thunderid/quickstart/MainActivity.kt @@ -41,6 +41,7 @@ class MainActivity : ComponentActivity() { afterSignInUrl = BuildConfig.THUNDERID_AFTER_SIGN_IN_URL.takeIf { it.isNotBlank() }, afterSignOutUrl = BuildConfig.THUNDERID_AFTER_SIGN_OUT_URL.takeIf { it.isNotBlank() }, applicationId = BuildConfig.THUNDERID_APPLICATION_ID.takeIf { it.isNotBlank() }, + flowSecret = BuildConfig.THUNDERID_FLOW_SECRET.takeIf { it.isNotBlank() }, storage = EncryptedStorageAdapter(this), allowInsecureConnections = BuildConfig.DEBUG, ) diff --git a/src/main/kotlin/dev/thunderid/android/ThunderIDClient.kt b/src/main/kotlin/dev/thunderid/android/ThunderIDClient.kt index 8569c2c..3cf16d4 100644 --- a/src/main/kotlin/dev/thunderid/android/ThunderIDClient.kt +++ b/src/main/kotlin/dev/thunderid/android/ThunderIDClient.kt @@ -61,7 +61,7 @@ class ThunderIDClient { tokenStore = store tokenValidator = TokenValidator(jwks, config) tokenRefresher = TokenRefresher(http, store) - flowClient = FlowExecutionClient(http) + flowClient = FlowExecutionClient(http, config.flowSecret) http.setAccessTokenProvider { val clientId = this.config?.clientId diff --git a/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt b/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt index 5e47756..0d8d9df 100644 --- a/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt +++ b/src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt @@ -39,6 +39,7 @@ data class ThunderIDConfig( // Application Identity val applicationId: String? = null, val organizationHandle: String? = null, + val flowSecret: String? = null, // Token Validation val tokenValidation: TokenValidationConfig = TokenValidationConfig(), // Storage & Platform diff --git a/src/main/kotlin/dev/thunderid/android/auth/FlowExecutionClient.kt b/src/main/kotlin/dev/thunderid/android/auth/FlowExecutionClient.kt index cde559d..b838de3 100644 --- a/src/main/kotlin/dev/thunderid/android/auth/FlowExecutionClient.kt +++ b/src/main/kotlin/dev/thunderid/android/auth/FlowExecutionClient.kt @@ -27,6 +27,7 @@ import dev.thunderid.android.http.HttpClient */ internal class FlowExecutionClient( private val httpClient: HttpClient, + private val flowSecret: String? = null, ) { suspend fun initiate( applicationId: String, @@ -38,7 +39,7 @@ internal class FlowExecutionClient( "flowType" to flowType.value, "verbose" to true, ) - return httpClient.post("/flow/execute", body, requiresAuth = false) + return httpClient.post("/flow/execute", body, requiresAuth = false, headers = flowSecretHeaders()) } suspend fun submit( @@ -50,9 +51,11 @@ internal class FlowExecutionClient( val body = submitBody(flowId, actionId, challengeToken).toMutableMap() body["verbose"] = true if (inputs.isNotEmpty()) body["inputs"] = inputs - return httpClient.post("/flow/execute", body, requiresAuth = false) + return httpClient.post("/flow/execute", body, requiresAuth = false, headers = flowSecretHeaders()) } + private fun flowSecretHeaders(): Map = flowSecret?.let { mapOf("Flow-Secret" to it) } ?: emptyMap() + internal fun submitBody( flowId: String, actionId: String, diff --git a/src/main/kotlin/dev/thunderid/android/http/HttpClient.kt b/src/main/kotlin/dev/thunderid/android/http/HttpClient.kt index 1dc5a90..df38f7d 100644 --- a/src/main/kotlin/dev/thunderid/android/http/HttpClient.kt +++ b/src/main/kotlin/dev/thunderid/android/http/HttpClient.kt @@ -54,13 +54,15 @@ internal class HttpClient( path: String, body: Map, requiresAuth: Boolean = true, - ): T = request("POST", path, body, requiresAuth) + headers: Map = emptyMap(), + ): T = request("POST", path, body, requiresAuth, headers) suspend inline fun request( method: String, path: String, body: Map?, requiresAuth: Boolean, + headers: Map = emptyMap(), ): T = withContext(Dispatchers.IO) { val urlString = baseUrl + path @@ -76,6 +78,7 @@ internal class HttpClient( requestMethod = method setRequestProperty("Content-Type", "application/json") setRequestProperty("Accept", "application/json") + headers.forEach { (name, value) -> setRequestProperty(name, value) } if (requiresAuth) { val token = accessTokenProvider?.invoke()