Okta Mobile Kotlin SDK
Authentication Library
Okta Mobile Kotlin SDK
Overview
The Okta Mobile Kotlin SDK is a next-generation authentication library for the Android platform. Designed to replace legacy mobile SDKs, it aims to streamline development, improve maintainability, and enable new use cases that were previously difficult or impractical to implement. Built on top of Kotlin, Kotlin Coroutines, and OkHttp, it features a modular design consisting of multiple libraries.
Details
The Okta Mobile Kotlin SDK is a modern Android authentication solution provided in the latest version 2.0.3 as of 2024. The SDK consists of three main components: AuthFoundation (foundation classes for credential management), OktaOAuth2 (OAuth2 authentication capabilities), and WebAuthenticationUI (web-based OIDC flows).
Key features of this SDK include providing non-blocking APIs through Kotlin coroutines, on-device encryption using device encryption keys, and comprehensive OAuth flow support (Authorization Code, Interaction Code, Refresh Token, Resource Owner Password, Device Authorization, Token Exchange). All methods can be called from any thread (including the main thread), and internally switch to background threads automatically when performing network I/O or expensive computations.
Pros and Cons
Pros
- Modern Architecture: Contemporary design based on Kotlin, coroutines, and OkHttp
- Non-blocking API: Asynchronous processing that doesn't block the main thread
- Comprehensive OAuth Support: Comprehensive support for major OAuth flows
- On-device Encryption: Secure data storage using device encryption keys
- Automatic Token Management: Automated refresh, storage, and validation
- Modular Design: Flexible configuration allowing selection of only necessary features
Cons
- Android Only: Android platform exclusive with no multi-platform support
- Learning Cost: Requires understanding of Kotlin coroutines
- New SDK: Migration work required from legacy SDKs
- Documentation: Limited Japanese resources
Reference Pages
Code Examples
Gradle Dependencies Configuration
// build.gradle.kts (Module: app)
dependencies {
// Unified version management using BOM
implementation(platform("com.okta.kotlin:bom:2.0.3"))
// Add required components
implementation("com.okta.kotlin:auth-foundation")
implementation("com.okta.kotlin:oauth2")
implementation("com.okta.kotlin:web-authentication-ui")
}
Basic OAuth2 Client Configuration
import com.okta.authfoundation.client.OidcClient
import com.okta.authfoundation.client.OidcClientResult
import com.okta.authfoundation.client.OidcConfiguration
import com.okta.authfoundation.credential.CredentialDataSource.Companion.createCredentialDataSource
import com.okta.oauth2.OAuth2Client
import com.okta.oauth2.OAuth2ClientResult
class AuthManager(private val context: Context) {
private val oidcConfiguration = OidcConfiguration(
clientId = "your-client-id",
defaultScope = "openid email profile offline_access"
)
private val client = OAuth2Client.createFromDiscoveryUrl(
configuration = oidcConfiguration,
discoveryUrl = "https://your-org.okta.com/oauth2/default/.well-known/openid_configuration",
credentialDataSource = context.createCredentialDataSource()
)
suspend fun isAuthenticated(): Boolean {
return when (val result = client.credentialDataSource.default()) {
is OidcClientResult.Error -> false
is OidcClientResult.Success -> {
result.result.token.isValid()
}
}
}
}
Authentication with Authorization Code Flow
import com.okta.webauthenticationui.WebAuthenticationClient.Companion.createWebAuthenticationClient
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope
class LoginActivity : AppCompatActivity() {
private lateinit var authManager: AuthManager
private lateinit var webAuthClient: WebAuthenticationClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
authManager = AuthManager(this)
webAuthClient = createWebAuthenticationClient()
findViewById<Button>(R.id.loginButton).setOnClickListener {
performLogin()
}
}
private fun performLogin() {
lifecycleScope.launch {
try {
val result = webAuthClient.login(
context = this@LoginActivity,
redirectUrl = "com.example.app:/callback",
extraRequestParameters = mapOf(
"prompt" to "login"
)
)
when (result) {
is OAuth2ClientResult.Success -> {
// Authentication successful
val token = result.result
handleSuccessfulLogin(token)
}
is OAuth2ClientResult.Error -> {
// Authentication error
handleLoginError(result.exception)
}
}
} catch (e: Exception) {
handleLoginError(e)
}
}
}
private fun handleSuccessfulLogin(token: Token) {
// Navigate to main screen
startActivity(Intent(this, MainActivity::class.java))
finish()
}
private fun handleLoginError(exception: Throwable) {
Toast.makeText(this, "Login error: ${exception.message}", Toast.LENGTH_LONG).show()
}
}
Retrieving User Information
import com.okta.authfoundation.claims.DefaultClaimsProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class UserRepository(private val authManager: AuthManager) {
suspend fun getCurrentUser(): UserInfo? = withContext(Dispatchers.IO) {
return@withContext when (val result = authManager.client.credentialDataSource.default()) {
is OidcClientResult.Error -> null
is OidcClientResult.Success -> {
val credential = result.result
// Get user information from token
val claimsResult = credential.getUserInfo()
when (claimsResult) {
is OidcClientResult.Success -> {
UserInfo(
id = claimsResult.result["sub"] as? String ?: "",
email = claimsResult.result["email"] as? String ?: "",
name = claimsResult.result["name"] as? String ?: "",
profilePicture = claimsResult.result["picture"] as? String
)
}
is OidcClientResult.Error -> null
}
}
}
}
suspend fun refreshUserToken(): Boolean = withContext(Dispatchers.IO) {
return@withContext when (val result = authManager.client.credentialDataSource.default()) {
is OidcClientResult.Error -> false
is OidcClientResult.Success -> {
val credential = result.result
when (val refreshResult = credential.refreshToken()) {
is OidcClientResult.Success -> true
is OidcClientResult.Error -> false
}
}
}
}
}
data class UserInfo(
val id: String,
val email: String,
val name: String,
val profilePicture: String?
)
Using Authentication Headers in API Calls
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class AuthInterceptor(private val authManager: AuthManager) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
// Check if request requires authentication
if (!requiresAuth(originalRequest)) {
return chain.proceed(originalRequest)
}
// Get token synchronously (Note: Execute this outside main thread)
val token = runBlocking {
when (val result = authManager.client.credentialDataSource.default()) {
is OidcClientResult.Success -> result.result.token.accessToken
is OidcClientResult.Error -> null
}
}
return if (token != null) {
val authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.build()
chain.proceed(authenticatedRequest)
} else {
chain.proceed(originalRequest)
}
}
private fun requiresAuth(request: Request): Boolean {
return request.url.host.contains("your-api.com")
}
}
// Retrofit client configuration
class ApiClientFactory(private val authManager: AuthManager) {
fun createApiClient(): ApiService {
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor(authManager))
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://your-api.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiService::class.java)
}
}
Logout Functionality Implementation
class LogoutManager(
private val authManager: AuthManager,
private val webAuthClient: WebAuthenticationClient
) {
suspend fun performLogout(context: Context): Boolean {
return try {
// Logout from Okta (via browser)
val logoutResult = webAuthClient.logoutOfBrowser(
context = context,
redirectUrl = "com.example.app:/logout"
)
when (logoutResult) {
is OAuth2ClientResult.Success -> {
// Clear local credentials
clearLocalCredentials()
true
}
is OAuth2ClientResult.Error -> {
// Clear local even if Okta logout fails
clearLocalCredentials()
false
}
}
} catch (e: Exception) {
// Clear local credentials even on error
clearLocalCredentials()
false
}
}
private suspend fun clearLocalCredentials() {
authManager.client.credentialDataSource.removeAll()
}
suspend fun performSilentLogout(): Boolean {
return try {
clearLocalCredentials()
true
} catch (e: Exception) {
false
}
}
}
Authentication State Monitoring
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.delay
class AuthStateManager(private val authManager: AuthManager) {
fun observeAuthState(): Flow<AuthState> = flow {
while (true) {
val isAuthenticated = authManager.isAuthenticated()
emit(if (isAuthenticated) AuthState.Authenticated else AuthState.Unauthenticated)
delay(5000) // Check every 5 seconds
}
}
suspend fun checkTokenExpiry(): TokenStatus {
return when (val result = authManager.client.credentialDataSource.default()) {
is OidcClientResult.Error -> TokenStatus.NoToken
is OidcClientResult.Success -> {
val credential = result.result
when {
credential.token.isValid() -> TokenStatus.Valid
credential.token.refreshToken != null -> TokenStatus.NeedsRefresh
else -> TokenStatus.Expired
}
}
}
}
}
sealed class AuthState {
object Authenticated : AuthState()
object Unauthenticated : AuthState()
}
sealed class TokenStatus {
object Valid : TokenStatus()
object NeedsRefresh : TokenStatus()
object Expired : TokenStatus()
object NoToken : TokenStatus()
}
Error Handling and Retry Functionality
class AuthErrorHandler {
suspend fun handleAuthError(
error: OAuth2ClientResult.Error<*>,
retry: suspend () -> OAuth2ClientResult<Token>
): AuthErrorResult {
return when (error.exception) {
is NetworkException -> {
// Retry on network error
delay(2000)
when (val retryResult = retry()) {
is OAuth2ClientResult.Success -> AuthErrorResult.Recovered(retryResult.result)
is OAuth2ClientResult.Error -> AuthErrorResult.Failed(retryResult.exception)
}
}
is AuthenticationException -> {
// Re-authentication required for authentication error
AuthErrorResult.RequiresReauth
}
else -> {
// Other errors
AuthErrorResult.Failed(error.exception)
}
}
}
}
sealed class AuthErrorResult {
data class Recovered(val token: Token) : AuthErrorResult()
data class Failed(val exception: Throwable) : AuthErrorResult()
object RequiresReauth : AuthErrorResult()
}
Custom Scopes and Claims
class CustomAuthManager(context: Context) {
private val customOidcConfiguration = OidcConfiguration(
clientId = "your-client-id",
defaultScope = "openid email profile custom:read custom:write"
)
private val client = OAuth2Client.createFromDiscoveryUrl(
configuration = customOidcConfiguration,
discoveryUrl = "https://your-org.okta.com/oauth2/default/.well-known/openid_configuration",
credentialDataSource = context.createCredentialDataSource()
)
suspend fun getCustomClaims(): Map<String, Any>? {
return when (val result = client.credentialDataSource.default()) {
is OidcClientResult.Error -> null
is OidcClientResult.Success -> {
val credential = result.result
when (val claimsResult = credential.getUserInfo()) {
is OidcClientResult.Success -> {
claimsResult.result.filterKeys { key ->
key.startsWith("custom:")
}
}
is OidcClientResult.Error -> null
}
}
}
}
}