Solving Redirects and Token Flows in Android SDKs: My Learning Journey
A deep dive into implementing OAuth2 redirect handling and eID authentication flow in Android using Jetpack Compose, AppAuth, and Chrome Custom Tabs.
Table of contents
- Context: What I’m Building
- What I Learned / Architecture Decisions
- 1. The Problem
- 2. The Decision
- Key Concepts Explained
- 🔗 Deep Linking vs Redirect URIs
- 🔒 AppAuth and OAuth2 Flow
- Code Snippets & Scenarios
- 🧱 Building the Authorization Request
- 🚀 Launching the Browser Flow
- 🧭 Handling the Redirect
- ✅ Simplified handleRedirectUri()
- What Worked, What Didn’t
- ✅ What Worked
- ❌ What Didn’t
- What I’d Do Differently
- Real-World Analogy
- References / Further Reading
- Final Thoughts
Context: What I’m Building
In this learning sprint, I set out to integrate an eID (electronic identity) authentication flow within an Android SDK built using Jetpack Compose. The goal was to let users authenticate using external identity providers (like BankID or MitID) through an OpenID Connect (OIDC) flow, all while maintaining control over the UI and app lifecycle.
At first, this seemed like a simple redirect problem. But it quickly evolved into a deep exploration of:
- How Chrome Custom Tabs interact with Android Activities
- The difference between app deep links and https redirect URIs
- How AppAuth manages authorization codes, ID tokens, and validation
- When to perform token exchange and when not to
This post documents my full journey — from confusion and 404 errors to a clean, testable, and reusable authentication design.
What I Learned / Architecture Decisions
1. The Problem
My SDK needed to start an external browser flow for authentication. The browser would redirect back to the app with an authorization code. From there, the SDK should continue with the verification process.
The challenge: redirect URIs in Android SDKs don’t behave like those in apps. SDKs often don’t control the app’s manifest or package configuration, so relying on the host app’s setup was not an option.
2. The Decision
To isolate the flow, I decided to:
- Move the eID authentication logic into its own dedicated activity (
EidAuthActivity) - Handle deep links and redirect URIs inside the SDK, not the host app
- Use a custom scheme URI (e.g.,
myapp://eid/callback) instead of an HTTPS one - Capture only the authorization code, letting the backend handle token exchange
This decision simplified the flow, improved testability, and removed dependency on external domain verification.
Key Concepts Explained
🔗 Deep Linking vs Redirect URIs
In Android, an app can handle incoming URLs through an intent filter in the manifest. But there are two major approaches:
| Type | Example | Requires Domain Ownership? | Common Use |
|---|---|---|---|
| HTTPS-based | https://example.com/callback | ✅ Yes | Public apps using asset links |
| Custom Scheme | myapp://callback | ❌ No | SDKs, test apps, private integrations |
I initially used an HTTPS redirect like https://cc-dev-flow-test.criipto.id/android/appswitch, but it returned 404 errors because my app didn’t own that domain and couldn’t verify it via Digital Asset Links.
Switching to a custom scheme instantly solved the problem.
🔒 AppAuth and OAuth2 Flow
AppAuth for Android is a library implementing OAuth2 and OpenID Connect best practices. It simplifies building authorization requests and handling redirects.
A typical flow:
- Build an
AuthorizationRequestwith parameters likeclient_id,redirect_uri, andscope. - Launch a Chrome Custom Tab to the authorization endpoint.
- Receive the authorization code via redirect URI.
- Optionally exchange that code for an access or ID token.
Since this SDK only needed to capture the authorization code, I stopped after step 3.
Code Snippets & Scenarios
🧱 Building the Authorization Request
val request = AuthorizationRequest.Builder(
serviceConfig,
clientId = "urn:test:client:123",
responseType = ResponseTypeValues.CODE,
redirectUri = Uri.parse("myapp://eid/callback")
)
.setScope("openid profile")
.setAdditionalParameters(mapOf("acr_values" to scheme.acrValue))
.build()
🚀 Launching the Browser Flow
val customTabsIntent = CustomTabsIntent.Builder().build()
val authIntent = authService.getAuthorizationRequestIntent(request, customTabsIntent)
startActivity(authIntent)
🧭 Handling the Redirect
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val uri = intent?.data ?: return
lifecycleScope.launch {
try {
val authCode = EidManager().handleRedirectUri(uri)
finishWithSuccess(authCode)
} catch (ex: Exception) {
finishWithError(ex.message ?: "Authentication failed")
}
}
}
✅ Simplified handleRedirectUri()
suspend fun handleRedirectUri(uri: Uri): String = withContext(Dispatchers.IO) {
val request = lastRequest ?: throw IllegalStateException("No previous request")
val response = AuthorizationResponse.Builder(request).fromUri(uri).build()
val error = AuthorizationException.fromIntent(Intent().setData(uri))
if (error != null) throw error
// Only return the code — no token exchange
return@withContext response.authorizationCode
?: throw IllegalStateException("No authorization code found")
}
This minimal approach avoids ID token parsing, JWT verification, or AppAuth’s strict OIDC validation errors.
What Worked, What Didn’t
✅ What Worked
- Using a custom scheme (
myapp://eid/callback) instantly fixed the 404 redirect issue. - Moving all eID logic into a dedicated activity made lifecycle management simpler.
- Skipping token exchange avoided unnecessary security risk (since the client secret isn’t in the SDK).
- Integrating
rememberLauncherForActivityResult()into a Composable made testing and UI handling predictable.
❌ What Didn’t
- Using an HTTPS redirect URI tied to a domain I didn’t control.
- Relying on AppAuth’s built-in ID token validation (Criipto’s ID token structure didn’t match AppAuth’s strict rules).
- Attempting to maintain
AuthorizationServiceas a long-lived object — it’s better to reinitialize it per flow.
What I’d Do Differently
- Add a robust configuration layer: Right now, the redirect URI and client ID are hardcoded. In a real SDK, they should be passed from the host app or fetched from a remote config.
- Support multiple eID schemes dynamically: Instead of an enum, load schemes (like MitID, BankID, etc.) from a JSON configuration.
- Add proper error mapping: Turn AppAuth exceptions into SDK-specific errors that are easier to interpret by developers.
- Unit-test lifecycle resilience: Simulate activity recreation during authentication and ensure no state loss.
- Optional PKCE support: Implement Proof Key for Code Exchange (PKCE) for flows where the SDK performs token exchange itself.
Real-World Analogy
Think of this flow like checking into an airport:
- The user (traveler) starts in your app.
- You hand them a boarding pass (authorization request) and send them to security (Chrome Custom Tab).
- After they’re cleared (authenticated), they’re sent back with a stamp of approval (authorization code).
- You don’t open the plane doors (token exchange) — the airline backend does that securely.
This separation ensures your SDK doesn’t carry sensitive credentials but still enables smooth authentication.
References / Further Reading
- AppAuth for Android GitHub
- OpenID Connect Specification
- Chrome Custom Tabs Documentation
- Android Deep Links and App Links
- RFC 7636 - Proof Key for Code Exchange (PKCE)
Final Thoughts
This journey reminded me how authentication is as much about orchestration as it is about code. OAuth2 flows aren’t just about tokens — they’re about managing trust, ownership, and redirection across boundaries.
By understanding how redirects, intents, and browser flows truly work, I moved from “it works in the example app” to a design that’s reusable, testable, and safe to include in an SDK used by others.
That, I think, is the essence of deep learning in Android — turning complex systems into something predictable and elegant.
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
Removing Swift Package Manager (SPM) from a Flutter iOS Project: Lessons Learned
Next