Library for using OpenId Connect / OAuth 2.0 in Kotlin Multiplatform (iOS+Android), Android and Xcode projects. This project aims to be a lightweight implementation without sophisticated validation on client side. Simple Desktop support is included via an embedded Webserver that listens for redirects.
- Currently, it only supports the Authorization Code Grant Flow.
- Support for discovery via .well-known/openid-configuration.
- Support for PKCE
- Uses
ASWebAuthenticationSession
(iOS), Chrome Custom Tabs (Android), Embedded Webserver + Browser (Desktop) - Simple JWT parsing
- OkHttp + Ktor support
The library is designed for kotlin multiplatform, Android-only and iOS only Apps. For iOS only, use the OpenIdConnectClient Swift Package.
You can find the full Api documentation here.
Add the dependency to your commonMain sourceSet (KMP) / Android dependencies (android only):
implementation("io.github.kalinjul.kotlin.multiplatform:oidc-appsupport:0.8.0")
implementation("io.github.kalinjul.kotlin.multiplatform:oidc-okhttp4:0.8.0") // optional, android only
implementation("io.github.kalinjul.kotlin.multiplatform:oidc-ktor:0.8.0") // optional ktor support
Or, for your libs.versions.toml:
[versions]
oidc = "0.8.0"
[libraries]
oidc-appsupport = { module = "io.github.kalinjul.kotlin.multiplatform:oidc-appsupport", version.ref = "oidc" }
oidc-okhttp4 = { module = "io.github.kalinjul.kotlin.multiplatform:oidc-okhttp4", version.ref = "oidc" }
If you want to run tests, currently (as of kotlin 1.9.22), you need to pass additional linker flags (adjust the path to your Xcode installation):
iosSimulatorArm64().compilations.all {
kotlinOptions {
freeCompilerArgs = listOf("-linker-options", "-L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator")
}
}
For OpenIDConnect/OAuth to work, you have to provide the redirect uri in your Android App's build.gradle:
build.gradle.kts:
android {
defaultConfig {
addManifestPlaceholders(
mapOf("oidcRedirectScheme" to "<uri scheme>")
)
}
}
iOS does not require declaring the redirect scheme.
Create an OpenIdConnectClient:
val client = OpenIdConnectClient(discoveryUri = "<discovery url>") {
endpoints {
tokenEndpoint = "<tokenEndpoint>"
authorizationEndpoint = "<authorizationEndpoint>"
userInfoEndpoint = null
endSessionEndpoint = "<endSessionEndpoint>"
}
clientId = "<clientId>"
clientSecret = "<clientSecret>"
scope = "openid profile"
codeChallengeMethod = CodeChallengeMethod.S256
redirectUri = "<redirectUri>"
}
If you provide a Discovery URI, you may skip the endpoint configuration and call discover() on the client to retrieve the endpoint configuration.
The Code Auth Flow method is implemented by CodeAuthFlow. You'll need platform specific variants, so we'll use a factory to get an instance.
For Android, create an instance of AndroidCodeAuthFlowFactory in your Activity's onCreate():
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val factory = AndroidCodeAuthFlowFactory(this)
}
}
Important
The Factory MUST be instanciated in onCreate() or earlier, as it will attach to the ComponentActivity's lifecycle. If you don't use ComponentActivity, you need to implement your own Factory.
If you want to use Dependency Injection, make sure to request an instance early (in your Activity).
For the iOS part, you can use IosCodeAuthFlowFactory. Both factories implement a common interface and can be provided using Dependency Injection.
For more information, have a look at the KMP sample app.
Request tokens using code auth flow (this will open the browser for login):
val flow = authFlowFactory.createAuthFlow(client)
val tokens = flow.getAccessToken()
Perform refresh or endSession:
tokens.refresh_token?.let { client.refreshToken(refreshToken = it) }
tokens.id_token?.let { client.endSession(idToken = it) }
For most calls (getAccessToken()
, refreshToken()
, endSession()
), you may provide
additional configuration for the http call, like headers or parameters using the configure closure parameter:
client.endSession(idToken = idToken) {
headers.append("X-CUSTOM-HEADER", "value")
url.parameters.append("custom_parameter", "value")
}
We provide simple JWT parsing (without any validation):
val jwt = tokens.id_token?.let { Jwt.parse(it) }
println(jwt?.payload?.aud) // print audience
println(jwt?.payload?.iss) // print issuer
println(jwt?.payload?.additionalClaims?.get("email")) // get claim
Since persisting tokens is a common task in OpenID Connect Authentication, we provide a TokenStore that uses a Multiplatform Settings Library to persist tokens in Keystore (iOS) / Encrypted Preferences (Android). If you use the TokenStore, you may also make use of TokenRefreshHandler for synchronized token refreshes.
tokenstore.saveTokens(tokens)
val accessToken = tokenstore.getAccessToken()
val refreshHandler = TokenRefreshHandler(tokenStore = tokenstore)
refreshHandler.refreshAndSaveToken(client, oldAccessToken = token) // thread-safe refresh and save new tokens to store
Android implementation is AndroidEncryptedPreferencesSettingsStore, for iOS use IosKeychainTokenStore.
val authenticator = OpenIdConnectAuthenticator {
getAccessToken { tokenStore.getAccessToken() }
refreshTokens { oldAccessToken -> refreshHandler.refreshAndSaveToken(client, oldAccessToken) }
onRefreshFailed {
// provided by app: user has to authenticate again
}
buildRequest {
header("AdditionalHeader", "value") // add custom header to all requests
}
}
val okHttpClient = OkHttpClient.Builder()
.authenticator(authenticator)
.build()
HttpClient(engine) {
install(Auth) {
oidcBearer(
tokenStore = tokenStore,
refreshHandler = refreshHandler,
client = client,
)
}
}
}
Because of the way ktor works, you need to tell the client if the token is invalidated outside of ktor's refresh logic, e.g. on logout:
ktorHttpClient.clearTokens()