6 min read

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.

#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. 1) Transaction Information (txinfo)
  5. 2) The Redirect Chain and Why Chrome Reopens
  6. 3) Multiple Implementation Paths
  7. Code Snippets & Scenarios
  8. A) Building the OIDC Authorization Request (Kotlin + AppAuth)
  9. B) Launching with Chrome Custom Tabs
  10. C) Deep Link Setup (Android App Links)
  11. D) Handling the Redirect in-App
  12. E) Verifying txinfo Is Actually Used
  13. What Worked, What Didn’t
  14. What I’d Do Differently
  15. 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.
  • txinfo is not optional when a template expects it: If the broker’s configuration contains a placeholder for txinfo, the downstream eID service expects it. Missing/stripped txinfo causes 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_uri should 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)

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:

  1. Log your own request before launching the intent (ensure txinfo is present).
  2. 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 txinfo payload 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

  1. Model the redirect chain first: Write down every hop and who owns it. It’s the fastest way to locate where parameters are lost.
  2. Confirm broker param forwarding early: A tiny end-to-end smoke test that checks for &txinfo= downstream pays off.
  3. Automate App Link verification: As part of CI, ensure the hosted assetlinks.json matches the release keystore.
  4. 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


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

Previous

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

Next

Removing Swift Package Manager (SPM) from a Flutter iOS Project: Lessons Learned