5 min read

German eID (Personalausweis) + Criipto on Android: What I Broke, Fixed, and Finally Understood

A developer’s learning log integrating German Personalausweis eID with Criipto using OIDC, Custom Tabs, deep links, and txinfo. What worked, what didn’t, and how I made the app switch back automatically.

#Android#Identity/OIDC#Learning-log#Dev-tools

Table of contents

  1. Context: What I’m Building
  2. What I Learned / Architecture Decisions
  3. Key Concepts Explained
  4. Code Snippets & Scenarios
  5. A) Building the OIDC Request (Kotlin + AppAuth)
  6. B) Launching in Custom Tabs
  7. C) Android App Link for Automatic Return
  8. D) Handling the Redirect
  9. E) Fallback: Proving txinfo Is Missing Downstream
  10. What Worked, What Didn’t
  11. What I’d Do Differently
  12. References / Further Reading

Context: What I’m Building

I’m integrating German eID (Personalausweis) into an Android SDK using Criipto Verify as the OIDC broker. The goal: start the flow in-app (via Chrome Custom Tabs), launch AusweisApp2 on-device for verification, and return users back into my SDK automatically—no manual browser juggling.

Along the way, I ran into a deceptively simple error from the German test service (eid-test.de):

IDENT_CREATING_TRANSACTION_INFORMATION_FAILED
No txinfo parameter was provided by the user although a template string with a placeholder is present.

This post documents the questions I asked, how I validated assumptions, and the changes that made the flow stable across app → AusweisApp2 → app.

What I Learned / Architecture Decisions

  • OpenID flow shape: My app talks to Criipto; Criipto brokers to the German eID test service. The return path after AusweisApp2 is eID service → Criipto → my redirect URI. If that last hop isn’t an app link I own and verify, Chrome reopens instead of my app.
  • txinfo is mandatory when your Criipto configuration expects it. Sending it from Android is necessary—but not sufficient—unless Criipto whitelists/forwards it.
  • WebView is a dead end for mobile app handoff. Custom Tabs + OIDC is the right track.
  • App Links (autoVerify) and a Criipto “appswitch” redirect are key to jump back into the app after the AusweisApp2 journey.

Key Concepts Explained

1) txinfo (Transaction Information)
German eID flows require the RP (via the broker) to declare what attributes to read from the ID card. That’s the txinfo JSON (often Base64-encoded) with keys like RequestedAttributes (e.g., GivenNames, FamilyNames, DateOfBirth). If Criipto’s scheme template references ${txinfo}, but the parameter isn’t forwarded, the German service complains.

2) Why my txinfo didn’t reach eid-test.de
I added txinfo to AuthorizationRequest.setAdditionalParameters(...) in Android. Criipto still didn’t pass it along. Root cause: Criipto must allow/whitelist txinfo for that scheme in my tenant. Without that, Criipto builds the downstream URL without txinfo, triggering the error above.

3) Custom Tabs vs. the system browser on return
When AusweisApp2 finishes, it returns to eid-test.de (browser context), then to Criipto, then to my redirect_uri. Unless that redirect URI is an Android App Link I control and is verified, Android may keep me in Chrome. With a verified App Link + Criipto “appswitch” redirect, the OS routes the final hop straight into my app.

Code Snippets & Scenarios

A) Building the OIDC Request (Kotlin + AppAuth)

val extras = mutableMapOf(
    "acr_values" to scheme.acrValue
)

// Inject txinfo only for German Personalausweis
if (scheme.schemeId.equals("urn:grn:authn:de:personalausweis", ignoreCase = true)) {
    val txInfoJson = """
        {
          "RequestedAttributes": [
            "GivenNames",
            "FamilyNames",
            "DateOfBirth",
            "PlaceOfResidence"
          ]
        }
    """.trimIndent()
    val txInfoB64 = android.util.Base64.encodeToString(
        txInfoJson.toByteArray(), android.util.Base64.NO_WRAP
    )
    extras["txinfo"] = txInfoB64
}

val request = AuthorizationRequest.Builder(
    serviceConfig,
    EidConfiguration.CLIENT_ID,
    ResponseTypeValues.CODE,
    EidConfiguration.redirectUri // e.g. https://cc-dev-flow-test.criipto.id/android/appswitch
)
    .setScope("openid")
    .setPrompt("login")
    .setAdditionalParameters(extras)
    .setLoginHint("appswitch:android appswitch:resumeUrl:${'$'}{EidConfiguration.appSwitchUri}")
    .build()

Important: This only works once Criipto enables forwarding of txinfo for your German scheme. Otherwise the parameter is dropped server-side.

B) Launching in Custom Tabs

val customTabsIntent = CustomTabsIntent.Builder()
    .setShowTitle(false)
    .setShareState(CustomTabsIntent.SHARE_STATE_OFF)
    .build()

val authIntent = authService.getAuthorizationRequestIntent(request, customTabsIntent)
startActivity(authIntent)

Manifest

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data
    android:scheme="https"
    android:host="cc-dev-flow-test.criipto.id"
    android:path="/android/appswitch" />
</intent-filter>

Make sure the same URI is registered under Redirect URIs in Criipto.

D) Handling the Redirect

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    val uri = intent?.data ?: return
    if (uri.toString().startsWith(EidConfiguration.redirectUri)) {
        // AppAuth will parse the code, you continue with token exchange
        AuthorizationService(this).performActionWithIntent(intent!!) { response, ex ->
            if (ex != null) {
                // handle error
            } else if (response != null) {
                // exchange code → tokens
            }
        }
    }
}

E) Fallback: Proving txinfo Is Missing Downstream

When you land on Criipto’s hosted screen, you can inspect the data bootstrap (if exposed) or simply watch the URL Criipto builds for eid-test.de. If it lacks &txinfo=..., ask Criipto to enable/whitelist that parameter for your tenant & scheme.

What Worked, What Didn’t

Worked

  • Switching from WebView to Custom Tabs for all mobile-app handoffs.
  • Adding txinfo in Android and getting Criipto to forward it.
  • Using Android App Links with android:autoVerify="true" so the final redirect jumps into my app.

Didn’t Work

  • Expecting WebView to launch native apps reliably via deep links.
  • Assuming Criipto forwards arbitrary OIDC params by default—it doesn’t. Tenant configuration matters.
  • Hoping AusweisApp2 → browser return would reuse the Custom Tab context automatically; Android often routes that via Chrome unless you finish with a verified App Link.

What I’d Do Differently

  • Start with an integration map: Draw the exact hops (App → Criipto → eID → AusweisApp2 → eID → Criipto → App). It clarifies where each redirect actually lives.
  • Confirm broker param forwarding early: Before UI, validate that txinfo survives from app to broker to eID service.
  • Automate deep link verification in CI (assetlinks.json checks), so my android:autoVerify doesn’t silently fail.

References / Further Reading


Final takeaway: German eID on Android isn’t hard—it’s distributed. The trick is aligning three layers: (1) your Android deep links + Custom Tabs, (2) Criipto’s broker settings (forward txinfo, use your App Link as redirect), and (3) the eID service’s expectations. Once the parameters and redirects line up, the on-device flow feels native and reliable.

Share

More to explore

Keep exploring

Previous

Android Learning Log: Compose Permissions, SavedStateHandle, and Testing Pitfalls

Next

German eID (Personalausweis) on Android with OIDC: A Practical Learning Journey