kmp-oidc
0.2.0indexedLightweight OpenID Connect authentication supporting Authorization Code Flow with PKCE, discovery, token exchange/refresh, local and provider logout, secure token storage, and provider-specific customization.
Lightweight OpenID Connect authentication supporting Authorization Code Flow with PKCE, discovery, token exchange/refresh, local and provider logout, secure token storage, and provider-specific customization.
Lightweight OpenID Connect (OIDC) authentication library for Kotlin Multiplatform.
kmp-oidc is currently a pre-stable 0.2.0 release. It already supports a working browser-based OIDC flow on Android and iOS, but the API may still change before 1.0.0.
implementation("io.github.worker432:kmp-oidc:0.2.0")
Android-only artifact:
implementation("io.github.worker432:kmp-oidc-android:0.2.0")
Current project properties:
This repository already contains maven-publish configuration. For local verification:
./gradlew :auth-core:publishToMavenLocal
If you use publishToMavenLocal, add mavenLocal() to your repositories.
Before wiring the library into your app, make sure your OIDC client is configured at the provider side.
You need:
Example:
These values are client-specific. They do not come from discovery metadata.
Add the multiplatform artifact to commonMain:
commonMain.dependencies {
implementation("io.github.worker432:kmp-oidc:0.2.0")
}
If you want to use the library in a regular Android application, use the Android artifact:
dependencies {
implementation("io.github.worker432:kmp-oidc-android:0.2.0")
}
If you are consuming the library from mavenLocal() during development:
repositories {
mavenLocal()
mavenCentral()
google()
}
val config = AuthConfig(
clientId = "kmp-oidc-sdk",
issuer = "https://issuer.example.com/realms/demo",
redirectUri = "myapp://callback",
logoutRedirectUri = "myapp://logout",
scopes = listOf(
"openid",
"profile",
"email",
"offline_access"
),
storageName = "auth_tokens"
)
What these fields mean:
At a high level, client integration looks like this:
val authClient = AuthClientFactory.create(
config = config,
dependencies = platformDependencies
)
PlatformDependencies is platform-specific.
Android:
val platformDependencies = PlatformDependencies(
context = applicationContext,
activity = this
)
iOS:
val platformDependencies = PlatformDependencies()
<uses-permission android:name="android.permission.INTERNET" />
Your activity must be able to receive the login and logout callbacks.
These values must match:
class MainActivity : ComponentActivity() {
private redirectUrl mutableStateOf<String?>()
{
.onCreate(savedInstanceState)
redirectUrl = intent?.dataString
setContent {
App(
dependencies = PlatformDependencies(
context = applicationContext,
activity =
),
redirectUrl = redirectUrl
)
}
}
{
.onNewIntent(intent)
redirectUrl = intent.dataString
}
}
The important parts here:
In your Compose layer or screen logic:
LaunchedEffect(redirectUrl) {
val url = redirectUrl ?: return@LaunchedEffect
authClient.handleRedirect(url)
}
scope.launch {
val result = authClient.login()
}
On Android, login() usually returns AuthResult.Started because the browser flow continues outside the app. After the provider redirects back, call handleRedirect(url).
scope.launch {
when (val result = authClient.getValidAccessToken()) {
is TokenResult.Success -> {
val token = result.accessToken
}
TokenResult.NeedLogin -> {
authClient.login()
}
is TokenResult.Failure -> Unit
}
}
If your local IdP runs on plain http, Android 9+ blocks cleartext traffic by default.
For emulator-based local development, either:
Example:
<application
android:usesCleartextTraffic="true" />
This is only for local development. Production should use https.
Add your redirect scheme to Info.plist. If your redirectUri is myapp://callback and your logoutRedirectUri is myapp://logout, the scheme is just myapp.
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes
myapp
If your local IdP uses plain http, iOS may require App Transport Security exceptions during development.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
This is only for local development. Production should use https.
This is what a minimal local-development setup can look like:
val platformDependencies = PlatformDependencies()
val authClient = AuthClientFactory.create(
config = config,
dependencies = PlatformDependencies()
)
scope.launch {
val result = authClient.login()
}
The default iOS integration uses ASWebAuthenticationSession, and this is the main difference from Android.
That means:
Because of that, iOS usually does not need the same manual redirect wiring as Android.
scope.launch {
when (val result = authClient.getValidAccessToken()) {
is TokenResult.Success -> {
val token = result.accessToken
}
TokenResult.NeedLogin -> {
authClient.login()
}
is TokenResult.Failure -> Unit
}
}
when (val result = authClient.login()) {
AuthResult.Started -> {
// Typical Android path:
// browser opened and redirect will be delivered later.
}
AuthResult.Success -> {
// Typical iOS path:
// ASWebAuthenticationSession completed and tokens are already stored.
}
AuthResult.AccessDenied -> {
// Provider returned access_denied
}
AuthResult.Cancelled -> Unit
is AuthResult.Failure -> {
// Redirect, discovery, browser, storage, or token error
}
}
Android:
authClient.handleRedirect(redirectUrl)
iOS:
This is the minimum shape of a client integration:
val authClient = AuthClientFactory.create(
config = AuthConfig(
clientId = "kmp-oidc-sdk",
issuer = "http://10.0.2.2:8080/realms/kmp",
redirectUri = "myapp://callback",
logoutRedirectUri = "myapp://logout",
scopes = listOf("openid", "profile", "email", "offline_access"),
storageName = "auth_tokens"
),
dependencies = platformDependencies
)
For Android emulator-based local development, 10.0.2.2 is the usual host alias for services running on the development machine.
when (val result = authClient.getValidAccessToken()) {
is TokenResult.Success -> {
val accessToken = result.accessToken
}
TokenResult.NeedLogin -> {
authClient.login()
}
is TokenResult.Failure -> {
// Refresh or storage failure
}
}
If the access token is expired and a refresh token is available, the library tries to refresh it automatically.
Local logout:
authClient.logout(LogoutMode.LOCAL_ONLY)
Provider logout:
authClient.logout(LogoutMode.LOCAL_AND_PROVIDER)
Provider logout uses end_session_endpoint when it is available in discovery metadata.
If your provider needs extra query or form parameters, use IdpCustomization.
val config = AuthConfig(
clientId = "client-id",
issuer = "https://issuer.example.com/realms/demo",
redirectUri = "myapp://callback",
logoutRedirectUri = "myapp://logout",
customization = IdpCustomization(
authorizationParameters = mapOf(
"prompt" to "login"
),
tokenParameters = mapOf(),
logoutParameters = mapOf()
)
)
This is useful for providers that expect extra parameters in authorize, token, or logout requests.
Current compatibility status:
| Provider | Status | Notes |
|---|---|---|
| Keycloak | Tested | Verified in the sample app on Android and iOS |
| Other OIDC providers | Not verified yet |
interface AuthClient {
suspend fun login(): AuthResult
suspend fun : AuthResult
: TokenResult
: AuthResult
}
Licensed under the Apache License 2.0.
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="callback" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="myapp"
android:host="logout" />
</intent-filter>
</activity>
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
</dict>
</plist>
| The library follows standard OIDC flows, but they have not been verified in this repository yet |
Surfaced from shared tags and platforms — no rankings paid for.