Standard OAuth2 authentication flows for Kotlin Multiplatform (Android + JVM), including Resource Owner Password, Device Authorization, Authorization Code with PKCE, Token Exchange (Native SSO), Session Token, and Redirect End Session.
- Overview
- Requirements
- Installation
- Getting Started
- Authentication Flows
- Complete Example
- Java Usage (CompletableFuture API)
- Sample Applications
- Additional Resources
This module provides KMP flow classes for standard OAuth2 grant types. All flows are in the com.okta.oauth2.kmp package, return Kotlin Result types, and require an OAuth2Client from the auth-foundation module.
Each flow follows a consistent pattern:
- Single-step flows (
ResourceOwnerFlow,TokenExchangeFlow,SessionTokenFlow) — callstart()and get aResult<TokenInfo>. - Two-step flows (
DeviceAuthorizationFlow,AuthorizationCodeFlow,RedirectEndSessionFlow) — callstart()to get a context object, thenresume()to complete the flow.
- Android API 26+ or JVM (Java 11+)
- Okta org with the relevant OAuth2 grant types enabled
- Client application configured with the appropriate grant types on your authorization server
Current Version: 3.0.0
Add the dependency to your build.gradle.kts:
dependencies {
implementation("com.okta.kotlin:oauth2:3.0.0")
}Create an OAuth2Client using the builder from auth-foundation. You'll need your Okta issuer URL, client ID, and the scopes you want to request:
import com.okta.authfoundation.client.OAuth2ClientBuilder
import com.okta.authfoundation.client.kmp.OAuth2Client
val client: OAuth2Client =
OAuth2ClientBuilder
.create(
issuerUrl = "https://your-org.okta.com",
clientId = "your-client-id",
scope = listOf("openid", "profile", "email", "offline_access")
) {
// Optional: specify authorization server ID for custom auth servers
authorizationServerId = "default"
}.getOrThrow()By default the SDK discovers endpoints from {issuerUrl}/.well-known/openid-configuration. Use OAuth2EndpointOverrides to override individual endpoints or skip discovery entirely when all 8 fields are provided:
import com.okta.authfoundation.client.OAuth2EndpointOverrides
val client: OAuth2Client =
OAuth2ClientBuilder
.create(
issuerUrl = "https://your-org.okta.com",
clientId = "your-client-id",
scope = listOf("openid", "profile", "email", "offline_access")
) {
// Override only the token endpoint (e.g. route through a proxy)
endpointOverrides = OAuth2EndpointOverrides(
tokenEndpoint = "https://proxy.example.com/token"
)
}.getOrThrow()When all 8 endpoint fields are non-null the SDK skips the discovery HTTP request entirely, reducing startup latency. All override values must be valid HTTPS URLs.
Exchange a username and password for tokens using the Resource Owner Password grant:
import com.okta.oauth2.kmp.ResourceOwnerFlow
val flow = ResourceOwnerFlow(client)
flow.start(
username = "user@example.com",
password = "user-password",
scope = "openid profile email offline_access"
).fold(
onSuccess = { tokenInfo ->
val accessToken = tokenInfo.accessToken
val idToken = tokenInfo.idToken
},
onFailure = { error ->
// Handle error
}
)Start a device code flow, display the user code and verification URI, then poll until the user approves:
import com.okta.oauth2.kmp.DeviceAuthorizationFlow
val flow = DeviceAuthorizationFlow(client)
// Step 1: Request a device code
val context = flow.start(scope = "openid profile email offline_access").getOrThrow()
// Step 2: Display the user code and verification URI to the user
println("Go to: ${context.verificationUri}")
println("Enter code: ${context.userCode}")
println("Expires in: ${context.expiresIn} seconds")
// context.verificationUriComplete is also available (URI with code pre-filled)
// Step 3: Poll until the user approves or the code expires
flow.resume(context).fold(
onSuccess = { tokenInfo ->
val accessToken = tokenInfo.accessToken
},
onFailure = { error ->
when (error) {
is DeviceAuthorizationFlow.TimeoutException -> {
// Device code expired before user approved
}
else -> {
// Handle other errors
}
}
}
)Perform Authorization Code + PKCE authentication by opening a browser for Okta authorization and capturing the redirect callback:
import com.okta.oauth2.kmp.AuthorizationCodeFlow
val flow = AuthorizationCodeFlow(client)
// Step 1: Build the authorization URL
val context = flow.start(
redirectUrl = "your-app-scheme:/callback",
scope = "openid profile email offline_access"
).getOrThrow()
// Step 2: Open context.url in a browser (platform-specific)
// On Android, use Chrome Custom Tabs via web-authentication-ui
// On Desktop, use LocalhostBrowserRedirectHandler
// Step 3: Capture the redirect URI and exchange for tokens
flow.resume(uri = capturedRedirectUri, flowContext = context).fold(
onSuccess = { tokenInfo ->
val accessToken = tokenInfo.accessToken
},
onFailure = { error ->
// Handle error
}
)Exchange an existing ID token and device secret for new tokens (Native SSO):
import com.okta.oauth2.kmp.TokenExchangeFlow
val flow = TokenExchangeFlow(client)
flow.start(
idToken = "existing-id-token",
deviceSecret = "existing-device-secret",
scope = "openid profile email offline_access"
).fold(
onSuccess = { tokenInfo ->
val accessToken = tokenInfo.accessToken
},
onFailure = { error ->
// Handle error
}
)Exchange a session token (obtained from the Okta Authentication API) for OAuth2 tokens via a server-side redirect:
import com.okta.oauth2.kmp.SessionTokenFlow
val flow = SessionTokenFlow(client)
flow.start(
sessionToken = "session-token-from-authn-api",
redirectUrl = "your-app-scheme:/callback",
scope = "openid profile email offline_access"
).fold(
onSuccess = { tokenInfo ->
val accessToken = tokenInfo.accessToken
},
onFailure = { error ->
// Handle error
}
)Perform a browser-based logout by redirecting to the Okta end-session endpoint:
import com.okta.oauth2.kmp.RedirectEndSessionFlow
val flow = RedirectEndSessionFlow(client)
// Step 1: Build the logout URL
val context = flow.start(
idToken = "current-id-token",
redirectUrl = "your-app-scheme:/logout-callback"
).getOrThrow()
// Step 2: Open context.url in a browser (platform-specific)
// Step 3: Capture the redirect URI and validate
flow.resume(uri = capturedRedirectUri, flowContext = context).fold(
onSuccess = {
// Logout completed
},
onFailure = { error ->
// Handle error
}
)Here's a complete ViewModel example managing all OAuth2 flows:
class OAuth2ViewModel : ViewModel() {
private val client = OAuth2ClientBuilder
.create(
issuerUrl = BuildConfig.ISSUER,
clientId = BuildConfig.CLIENT_ID,
scope = listOf("openid", "profile", "email", "offline_access")
) {
authorizationServerId = BuildConfig.AUTHORIZATION_SERVER_ID
}.getOrThrow()
private val _flowState = MutableStateFlow<OAuth2FlowState>(OAuth2FlowState.Idle)
val flowState = _flowState.asStateFlow()
private var activeJob: Job? = null
fun startResourceOwner(username: String, password: String) {
cancelAndLaunch {
_flowState.value = OAuth2FlowState.Loading
val flow = ResourceOwnerFlow(client)
flow.start(username, password, "openid profile email offline_access").fold(
onSuccess = { _flowState.value = OAuth2FlowState.Authenticated(it) },
onFailure = { _flowState.value = OAuth2FlowState.Error(it.message ?: "Unknown error") }
)
}
}
fun startDeviceAuthorization() {
cancelAndLaunch {
_flowState.value = OAuth2FlowState.Loading
val flow = DeviceAuthorizationFlow(client)
val context = flow.start("openid profile email offline_access").getOrElse { error ->
_flowState.value = OAuth2FlowState.Error(error.message ?: "Unknown error")
return@cancelAndLaunch
}
_flowState.value = OAuth2FlowState.DeviceAuthPolling(
userCode = context.userCode,
verificationUri = context.verificationUri,
verificationUriComplete = context.verificationUriComplete,
expiresIn = context.expiresIn
)
flow.resume(context).fold(
onSuccess = { _flowState.value = OAuth2FlowState.Authenticated(it) },
onFailure = { _flowState.value = OAuth2FlowState.Error(it.message ?: "Authorization timed out") }
)
}
}
fun startTokenExchange(idToken: String, deviceSecret: String) {
cancelAndLaunch {
_flowState.value = OAuth2FlowState.Loading
val flow = TokenExchangeFlow(client)
flow.start(idToken, deviceSecret, scope = "openid profile email offline_access").fold(
onSuccess = { _flowState.value = OAuth2FlowState.Authenticated(it) },
onFailure = { _flowState.value = OAuth2FlowState.Error(it.message ?: "Unknown error") }
)
}
}
fun startSessionToken(sessionToken: String) {
cancelAndLaunch {
_flowState.value = OAuth2FlowState.Loading
val flow = SessionTokenFlow(client)
flow.start(sessionToken, BuildConfig.REDIRECT_URI, scope = "openid profile email offline_access").fold(
onSuccess = { _flowState.value = OAuth2FlowState.Authenticated(it) },
onFailure = { _flowState.value = OAuth2FlowState.Error(it.message ?: "Unknown error") }
)
}
}
fun reset() {
activeJob?.cancel()
activeJob = null
_flowState.value = OAuth2FlowState.Idle
}
private fun cancelAndLaunch(block: suspend () -> Unit) {
activeJob?.cancel()
activeJob = viewModelScope.launch { block() }
}
}The oauth2 module provides Java-compatible wrappers using CompletableFuture. All JVM wrapper classes are in the com.okta.oauth2.kmp.jvm package and must be close()d when no longer needed.
import com.okta.authfoundation.client.OAuth2ClientBuilder;
import com.okta.authfoundation.client.kmp.OAuth2Client;
import java.util.List;
OAuth2Client client = OAuth2ClientBuilder.Companion
.create(
"https://your-org.okta.com",
"your-client-id",
List.of("openid", "profile", "email", "offline_access"),
builder -> {
builder.setAuthorizationServerId("default");
return kotlin.Unit.INSTANCE;
}
).getOrThrow();import com.okta.oauth2.kmp.jvm.ResourceOwnerFlow;
ResourceOwnerFlow flow = new ResourceOwnerFlow(client);
flow.start("user@example.com", "user-password", "openid profile email offline_access")
.thenAccept(tokenInfo -> {
String accessToken = tokenInfo.getAccessToken();
});
flow.close();import com.okta.oauth2.kmp.jvm.DeviceAuthorizationFlow;
import com.okta.oauth2.kmp.DeviceAuthorizationFlowContext;
DeviceAuthorizationFlow flow = new DeviceAuthorizationFlow(client);
flow.start("openid profile email offline_access")
.thenCompose(context -> {
System.out.println("Go to: " + context.getVerificationUri());
System.out.println("Enter code: " + context.getUserCode());
return flow.resume(context);
})
.thenAccept(tokenInfo -> {
String accessToken = tokenInfo.getAccessToken();
});
flow.close();The Java wrapper combines the start and resume steps using a BrowserRedirectHandler:
import com.okta.oauth2.kmp.jvm.AuthorizationCodeFlow;
import com.okta.oauth2.kmp.LocalhostBrowserRedirectHandler;
AuthorizationCodeFlow flow = new AuthorizationCodeFlow(client);
BrowserRedirectHandler handler = new LocalhostBrowserRedirectHandler(8080, "/callback");
flow.start("http://localhost:8080/callback", handler)
.thenAccept(tokenInfo -> {
String accessToken = tokenInfo.getAccessToken();
});
flow.close();import com.okta.oauth2.kmp.jvm.TokenExchangeFlow;
TokenExchangeFlow flow = new TokenExchangeFlow(client);
flow.start("existing-id-token", "existing-device-secret")
.thenAccept(tokenInfo -> {
String accessToken = tokenInfo.getAccessToken();
});
flow.close();import com.okta.oauth2.kmp.jvm.SessionTokenFlow;
SessionTokenFlow flow = new SessionTokenFlow(client);
flow.start("session-token-from-authn-api", "http://localhost:8080/callback")
.thenAccept(tokenInfo -> {
String accessToken = tokenInfo.getAccessToken();
});
flow.close();The Java wrapper combines the start and resume steps using a BrowserRedirectHandler:
import com.okta.oauth2.kmp.jvm.RedirectEndSessionFlow;
import com.okta.oauth2.kmp.LocalhostBrowserRedirectHandler;
RedirectEndSessionFlow flow = new RedirectEndSessionFlow(client);
BrowserRedirectHandler handler = new LocalhostBrowserRedirectHandler(8080, "/logout-callback");
flow.start("current-id-token", "http://localhost:8080/logout-callback", handler)
.thenAccept(unit -> {
System.out.println("Logout completed");
});
flow.close();The okta-direct-auth-shared module contains a shared Compose Multiplatform sample with platform runner apps:
- Android:
okta-direct-auth-android-app - Desktop (JVM):
okta-direct-auth-desktop-app
The app launches a Home Menu where you choose between Direct Authentication and OAuth2 flows:
- Resource Owner Flow -- Username + password via OAuth2 Resource Owner Password grant
- Device Authorization Flow -- Device code + verification URI with automatic polling
- Browser Sign-In -- Authorization Code + PKCE via system browser (Chrome Custom Tabs on Android, localhost redirect on Desktop)
- Token Exchange Flow -- Native SSO token exchange using an existing ID token and device secret
- Session Token Flow -- Exchange a pre-obtained session token for OAuth2 tokens via server-side redirect
See the okta-direct-auth-shared README for full setup and configuration instructions.