Skip to content

ChristianKatzmann/kotlin-multiplatform-oidc

 
 

Repository files navigation

Kotlin Multiplatform OIDC

Build Maven Central Snapshot Kotlin Version

Kotlin Multiplatform Library for OpenId Connect / OAuth 2.0.

The library is designed for kotlin multiplatform, Android-only and iOS only Apps. For iOS only, use the OpenIdConnectClient Swift Package.

This is a lightweight implementation that does not provide any client-side validation of signatures.

Supported platforms:

State Implementation
Android Stable Chrome Custom Tabs
iOS Stable ASWebAuthenticationSession
Desktop Experimental Embedded Webserver + Browser
WasmJS Experimental Popup Window communicating via postMessage()

Features:

  • Only supports Authorization Code Grant Flow.
  • Support for discovery via .well-known/openid-configuration.
  • Support for PKCE
  • Simple JWT parsing (Jwt.parse())
  • OkHttp + Ktor integration
  • Uses Custom Uri Scheme (my-app://), no support for https redirect uris.

You can find the full Api documentation here.

Library dependency versions:

kmp-oidc version kotlin version ktor version
<=0.11.1 1.9.23 2.3.7
0.11.2 2.0.20 2.3.7
0.12.0 - 0.13.+ 2.0.20 3.0.+

Note that while the library may work with other kotlin/ktor versions, proceed at your own risk.

Dependency

Add the dependency to your commonMain sourceSet (KMP) / Android dependencies (android only):

implementation("io.github.kalinjul.kotlin.multiplatform:oidc-appsupport:<version>")
implementation("io.github.kalinjul.kotlin.multiplatform:oidc-ktor:<version>") // optional ktor support
implementation("io.github.kalinjul.kotlin.multiplatform:oidc-okhttp4:<version>") // optional okhttp support (android only)

Or, for your libs.versions.toml:

[versions]
oidc = "<version>"
[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" }
oidc-ktor = { module = "io.github.kalinjul.kotlin.multiplatform:oidc-ktor", version.ref = "oidc" }

Using a snapshot version

If you want try a snapshot version, just add maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") to your repositories. See available snapshots.

Compiler options

If you want to run tests, currently you need to pass additional linker flags (adjust the path to your Xcode installation):

iosSimulatorArm64().compilerOptions {
    freeCompilerArgs.set(listOf("-linker-options", "-L/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator"))
}

Usage

Setup

You will need some basic project setup to handle redirect urls and create an instance of the AuthFlowFactory:

Setup Android

Setup iOS

Setup Wasm

OpenID Configuration

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>"
    postLogoutRedirectUri = "<postLogoutRedirectUri>"
}

If you provide a Discovery URI, you may skip the endpoint configuration and call discover() on the client to retrieve the endpoint configuration.

Authenticate

The Code Auth Flow method is implemented by CodeAuthFlow. You'll need platform specific variants (see Setup). Preferably, those instances should 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) }

Token Store (experimental)

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.

Ktor support (experimental)

You can use "oidc-ktor" dependency, which provides easy integration for ktor projects:

    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()

Custom headers/url parameters

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")
}
val tokens = flow.getAccessToken(configureAuthUrl = {
    // customize url that is passed to browser for authorization requests
    parameters.append("prompt", "login")
}, configureTokenExchange = {
    // customize token exchange http request
    header("additionalHeaderField", "value")
})

End session using GET request and post_logout_redirect_uri

If you have configured a postLogoutRedirectUri and want to perform a Logout using a Web Flow, you can use the endSession flow:

val flow = authFlowFactory.createEndSessionFlow(client)
tokens.id_token?.let { flow.endSession(it) }

That way, browser cookies should be cleared so the next time a client wants to login, it get's prompted for username and password again.

JWT Parsing

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

OkHttp support (Android only) (experimental)

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()

About

Kotlin Multiplatform OpenIDConnect implementation for Android/iOS

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Kotlin 99.6%
  • Other 0.4%