Okta Mobile Kotlin SDK

Authentication LibraryOktaKotlinAndroidOAuth2OIDCMobileCoroutines

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
                }
            }
        }
    }
}