Skip to content
Closed
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
16 changes: 15 additions & 1 deletion samples/quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we double check changes done to readme?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to use the config.properties file and using .env didn't worked for me. That's why this update.

cc: @brionmario can you confirm this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also in the repo we don't have env.example file too

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.properties is the right approach. I think README wasn't updated.


```
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a meaning of flow secret for a mobile client?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is when they not using attestation. In that case they have to go with the secret.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once they configure the attestation, secret will be skipped and attestation will be validated.

@brionmario brionmario Jul 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just mention something like THUNDERID_ATTESTATION or THUNDERID_ATTESTATION_KEY?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean instead of FLOW_SECRET value? If yes, in the attestation case we need to have some more parameters here (This will be sent via separate PR). So having a common name for both attestation and secret will confuse users IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public clients usually can't securely hold a secret. My concern is, calling this THUNDERID_FLOW_SECRET is a bit misleading because it implies the app should embed a 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).

Comment thread
coderabbitai[bot] marked this conversation as resolved.
## Run

Open in Android Studio, sync Gradle, and run on an API 24+ emulator or device.
Expand Down
2 changes: 2 additions & 0 deletions samples/quickstart/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions samples/quickstart/config.properties.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/dev/thunderid/android/ThunderIDClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/dev/thunderid/android/ThunderIDConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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<String, String> = flowSecret?.let { mapOf("Flow-Secret" to it) } ?: emptyMap()

internal fun submitBody(
flowId: String,
actionId: String,
Expand Down
5 changes: 4 additions & 1 deletion src/main/kotlin/dev/thunderid/android/http/HttpClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,15 @@ internal class HttpClient(
path: String,
body: Map<String, Any>,
requiresAuth: Boolean = true,
): T = request("POST", path, body, requiresAuth)
headers: Map<String, String> = emptyMap(),
): T = request("POST", path, body, requiresAuth, headers)

suspend inline fun <reified T : Any> request(
method: String,
path: String,
body: Map<String, Any>?,
requiresAuth: Boolean,
headers: Map<String, String> = emptyMap(),
): T =
withContext(Dispatchers.IO) {
val urlString = baseUrl + path
Expand All @@ -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()
Expand Down
Loading