German eID (Personalausweis) on Android with OIDC: A Practical Learning Journey
Step-by-step notes on integrating German Personalausweis eID with an Android app using OIDC, Custom Tabs, deep links, and transaction info (txinfo). What I tried, learned, and would change next time.
Table of contents
- Context: What I’m Building
- What I Learned / Architecture Decisions
- Key Concepts Explained
- 1) Transaction Information (txinfo)
- 2) The Redirect Chain and Why Chrome Reopens
- 3) Multiple Implementation Paths
- Code Snippets & Scenarios
- A) Building the OIDC Authorization Request (Kotlin + AppAuth)
- B) Launching with Chrome Custom Tabs
- C) Deep Link Setup (Android App Links)
- D) Handling the Redirect in-App
- E) Verifying txinfo Is Actually Used
- What Worked, What Didn’t
- What I’d Do Differently
- References / Further Reading
Context: What I’m Building
I’m connecting an Android app to the German eID (Personalausweis) ecosystem using OpenID Connect (OIDC). The user flow I’m aiming for:
App (Custom Tab) → eID Service (browser) → AusweisApp2 (native) → eID Service → back to my app
The trickiest parts were:
- Providing the transaction information (
txinfo) that declares which attributes should be read from the ID card. - Ensuring the final redirect doesn’t strand users in the system browser but returns to the app automatically via deep linking.
This post is an AI-assisted learning log of how I approached it, the blind alleys I explored, and what ultimately worked. For clarity, the code and identifiers are dummy examples, not tied to any vendor or production tenant.
What I Learned / Architecture Decisions
- Brokered OIDC matters: My app talks OIDC to a broker (Identity Provider). The broker talks to the German eID service. That means some parameters (like
txinfo) must be recognized and forwarded by the broker, not just set in the mobile app. txinfois not optional when a template expects it: If the broker’s configuration contains a placeholder fortxinfo, the downstream eID service expects it. Missing/strippedtxinfocauses the well-known error: Creating transaction information failed.- WebView vs Custom Tabs: WebViews often struggle with native app handoffs. Chrome Custom Tabs plus proper deep links is the right way for this flow.
- App Links (autoVerify) are essential: To avoid a chooser or getting stuck in Chrome, the final
redirect_urishould be an Android App Link that your app owns and verifies.
Key Concepts Explained
1) Transaction Information (txinfo)
German eID flows require you to declare what attributes you want from the ID card. Conceptually, txinfo is a JSON payload (often Base64-encoded) that lists attributes like GivenNames, FamilyNames, DateOfBirth, etc. If your broker’s configuration references this payload (e.g., via a template like txinfo=${txinfo}), you must supply it in the OIDC request so it can be forwarded downstream.
2) The Redirect Chain and Why Chrome Reopens
After the user authenticates in AusweisApp2, control returns to the eID service in the browser, then to the broker, and finally to your redirect_uri. If that last URI is a regular web URL you don’t own, the journey stays in Chrome. If it’s an App Link that your app has verified ownership of, the OS opens your app directly, allowing you to finish the OIDC code exchange.
3) Multiple Implementation Paths
- Path A: Everything in WebView — Simplest to embed, but brittle for native handoffs and deep linking. I dropped this path.
- Path B: Custom Tabs + OIDC — More reliable for app-to-app flows. Requires deep links and correct redirect URIs.
- Path C: Device Switch (QR) — For cross-device verification. Useful fallback when the eID app isn’t on the same device.
Code Snippets & Scenarios
Note: All code is intentionally generic. Replace domains, client IDs, and paths with your own.
A) Building the OIDC Authorization Request (Kotlin + AppAuth)
// Pseudocode — replace with real values
data class Scheme(val id: String, val acrValue: String)
val scheme = Scheme(
id = "urn:grn:authn:de:personalausweis",
acrValue = "urn:grn:authn:de:personalausweis"
)
val additional = mutableMapOf(
"acr_values" to scheme.acrValue
)
// Only include txinfo for German Personalausweis
if (scheme.id.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
)
additional["txinfo"] = txInfoB64
}
val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://idp.example.com/oauth2/authorize"),
Uri.parse("https://idp.example.com/oauth2/token")
)
val authRequest = AuthorizationRequest.Builder(
serviceConfig,
/* clientId = */ "my-client-id",
/* responseType = */ ResponseTypeValues.CODE,
/* redirectUri = */ Uri.parse("https://app.example.com/android/appswitch")
)
.setScope("openid")
.setPrompt("login")
.setAdditionalParameters(additional)
.setLoginHint("appswitch:android appswitch:resumeUrl:https://app.example.com/android/appswitch")
.build()
Gotcha: Even if you add txinfo here, the broker must be configured to accept and forward it. If the broker drops unknown params, the eID service will complain that txinfo is missing.
B) Launching with Chrome Custom Tabs
val authService = AuthorizationService(context)
val customTabsIntent = CustomTabsIntent.Builder()
.setShowTitle(false)
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.build()
val intent = authService.getAuthorizationRequestIntent(authRequest, customTabsIntent)
context.startActivity(intent)
C) Deep Link Setup (Android App Links)
AndroidManifest.xml
<activity android:name=".AuthRedirectActivity"
android:exported="true"
android:launchMode="singleTask">
<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="app.example.com"
android:path="/android/appswitch" />
</intent-filter>
</activity>
Digital Asset Links (hosted at https://app.example.com/.well-known/assetlinks.json)
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"AA:BB:CC:DD:…:ZZ"
]
}
}
]
With a verified App Link, the final redirect to https://app.example.com/android/appswitch opens your app directly.
D) Handling the Redirect in-App
class AuthRedirectActivity : Activity() {
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val uri = intent?.data ?: return
if (uri.toString().startsWith("https://app.example.com/android/appswitch")) {
val authService = AuthorizationService(this)
authService.performActionWithIntent(intent) { resp, ex ->
when {
ex != null -> {
// TODO handle error (log, show UI)
}
resp != null -> {
// Exchange code for tokens
// tokenService.exchange(resp.authorizationCode)
}
}
}
}
finish()
}
}
E) Verifying txinfo Is Actually Used
Two pragmatic checks:
- Log your own request before launching the intent (ensure
txinfois present). - Observe the broker’s downstream URL (if visible) or response metadata. If
&txinfo=never shows up at the eID service, the broker likely isn’t forwarding it — ask for it to be enabled/whitelisted for your tenant and scheme.
What Worked, What Didn’t
Worked
- Moving from WebView to Custom Tabs to support app-to-app handoff.
- Defining App Links with
android:autoVerify="true"and assetlinks.json, to make the final hop land in my app. - Building a minimal
txinfopayload and including it only when the scheme requires it.
Didn’t Work
- Expecting WebView to handle deep links into native apps reliably.
- Assuming the broker would automatically forward arbitrary OIDC parameters.
- Hoping the system would automatically return to my Custom Tab; Android often routes the journey via the default browser unless the final hop is an App Link.
What I’d Do Differently
- Model the redirect chain first: Write down every hop and who owns it. It’s the fastest way to locate where parameters are lost.
- Confirm broker param forwarding early: A tiny end-to-end smoke test that checks for
&txinfo=downstream pays off. - Automate App Link verification: As part of CI, ensure the hosted
assetlinks.jsonmatches the release keystore. - Instrument the flow: Add logs/timestamps around request creation, browser launch, and redirect handling. Observability speeds up debugging across app/browser/app boundaries.
References / Further Reading
- OpenID Connect Core (Authorization Code Flow)
- Android App Links: https://developer.android.com/training/app-links
- Chrome Custom Tabs: https://developer.chrome.com/docs/android/custom-tabs/
- AppAuth for Android: https://github.com/openid/AppAuth-Android
- AusweisApp2 (German eID) — developer docs and integration guides
Takeaway: Integrating German eID on Android is less about one big API call and more about aligning three moving parts: (1) your app’s deep linking and browser strategy, (2) the broker’s OIDC parameter handling (especially txinfo), and (3) the eID service’s expectations. Once those layers agree, the flow feels seamless to users.
Personal reflection
Why this matters to me
This project reminded me why I love platform work: every confusing redirect or `txinfo` error maps directly to a real user trying to prove who they are. Capturing the sharp edges here forces future-me to ship confident SDK releases instead of re-learning the same lessons.
Share
More to explore
Keep exploring
1/11/2026
Kotlin to C/C++ Transition Guide: A Systems Programmer's Cheat Sheet
Preparing for a systems programming interview but haven't touched C/C++ since university? This guide bridges your Kotlin knowledge to C/C++ with side-by-side syntax comparisons, memory management deep dives, and critical undefined behaviors you need to know.
1/10/2026
The SDK Mindset: Why Your Code Isn't Your Own Anymore
A deep dive into the paradigm shift from application development to SDK design, and why building libraries requires a fundamentally different mental model
12/16/2025
Building a Frictionless Android Sample App: README Funnel, Doctor Script, and a CI Compatibility Matrix
My AI-assisted learning log on turning an Android SDK demo into a low-friction client experience: a decision-tree README, environment doctor scripts, and a GitHub Actions build matrix that generates a compatibility matrix.
Previous
German eID (Personalausweis) + Criipto on Android: What I Broke, Fixed, and Finally Understood
Next