Skip to content

sdk/android

Kotlin SDK for Android using Chrome Custom Tabs, PKCE S256 authorization code flow, and Keystore-backed EncryptedSharedPreferences token storage.

Status

Package status is Implemented · verified locally. JVM unit tests (24 passed) cover PKCE generation, OAuth state, and in-memory storage. EncryptedSharedPreferences (Keystore AES-256-GCM), Chrome Custom Tabs, and App Links behavior require a real Android device or emulator. Real IdP round-trip is pending manual verification. This page documents implemented behavior; it is not a production-readiness claim.

Requirements

  • Android API 26+ (Android 8.0)
  • Kotlin 1.9+ and AndroidX

Installation

Add the dependency to your app module’s build.gradle.kts:

dependencies {
    implementation("dev.xid:xid-android:0.1.0-alpha")
}

// Local development: add to settings.gradle.kts
includeBuild("../sdk/android")

Manifest setup

Register a callback Activity with an intent-filter. App Links (HTTPS scheme with autoVerify) are recommended over custom schemes:

<!-- AndroidManifest.xml -->
<activity android:name=".AuthCallbackActivity" android:exported="true">
    <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="yourapp.example.com"
              android:pathPrefix="/auth/callback" />
    </intent-filter>
</activity>

Quick start

import dev.xid.sdk.Xid
import dev.xid.sdk.model.XidConfig

// 1. Initialize in Application.onCreate
Xid.configure(
    context = this,
    config = XidConfig(
        issuer = "https://xid.dev",
        clientId = "your_client_id",
        redirectUri = "https://yourapp.example.com/auth/callback",
        scopes = listOf("openid", "profile", "email", "offline_access"),
    )
)

// 2. Sign in (opens Chrome Custom Tabs)
lifecycleScope.launch { Xid.signIn(requireContext()) }

// 3. Handle redirect in AuthCallbackActivity
val session = Xid.handleRedirect(intent.data.toString())

// 4. Get current session (auto-refreshes near expiry)
val session = Xid.getSession()

// 5. Get access token
val token = Xid.getAccessToken()

// 6. Sign out
Xid.signOut(context = this, openEndSession = true)

Core API

Method Signature
configure fun configure(context: Context, config: XidConfig)
signIn suspend fun signIn(context: Context, options: SignInOptions? = null)
handleRedirect suspend fun handleRedirect(url: String): XidSession
getSession suspend fun getSession(): XidSession?
getAccessToken suspend fun getAccessToken(options: GetAccessTokenOptions? = null): String
signOut suspend fun signOut(context: Context? = null, openEndSession: Boolean = false)
setTokenStorage fun setTokenStorage(adapter: TokenStorageAdapter)

Error types

All SDK errors are subtypes of the sealed class XidException:

Subclass Trigger
NotConfigured configure() was not called
UserCancelled User closed Custom Tabs without completing
StateMismatch OAuth state mismatch — possible CSRF
TokenExchangeFailed Token endpoint returned an error
TokenRefreshFailed Refresh token expired or revoked
NoSession Session method called while signed out

Security

  • Public client — no client secret stored or transmitted.
  • PKCE S256 only. Server rejects plain challenge method.
  • EncryptedSharedPreferences backed by Android Keystore (AES-256-GCM) protects the token store at rest.
  • Random OAuth state generated per request; validated on redirect to prevent CSRF.

Known limitations

  • JWKS-backed ID token verification is implemented and locally unit-tested. Android device or emulator and real IdP validation are still required before L4 support.
  • No mechanism to detect when the user closes Custom Tabs without completing authorization.
  • Single-account only — the storage layer uses fixed keys.
Navigation

Type to search...

Use arrow keys to navigateEnter to selectEscape to close