Skip to main content

Direct Auth Kotlin SDK

Proposal:SDK-0003
Authors:Fei Chen
Status:Draft
Languages:Kotlin

This proposal outlines the design for a Kotlin Multiplatform library that provides the interfaces and methods necessary to implement native sign-in flows using Okta's Direct Authentication APIs. This concept is part of a larger pattern for Authentication Flows within the Okta SDK. By using this library, developers can build fully customized and integrated sign-in experiences, including support for Multi-Factor Authentication (MFA), directly within their native applications.The design is guided by the principles and API surface defined in the reference Direct Authentication API documentation, while adapting them to be idiomatic for modern Kotlin development.

Motivation​

Currently, developers building native applications that integrate with Okta typically rely on a browser-based, redirected authentication flow. While secure and effective, this approach has several drawbacks for native clients:

  • Poor User Experience: Redirecting users out of the application to a system browser for sign-in creates a jarring and disconnected experience. It breaks the flow of the native application and can feel less polished and integrated.
  • Limited UI Customization: The sign-in and MFA pages are hosted by Okta. Developers have limited control over the look and feel, making it difficult to create a seamless experience that matches their application's branding and design system.
  • Infeasibility in Browser-less Environments: Many valid use-cases for authentication exist where a web browser is not available. This includes command-line interface (CLI) tools, IoT devices, and other "headless" systems. The browser-based flow is not a viable option for these platforms.

To work around these issues, developers might attempt to call Okta's low-level authentication APIs directly. This is a complex and error-prone process, requiring the developer to manually manage the authentication state machine, handle various MFA challenges, and ensure secure token handling.

This proposal seeks to address these problems by providing a high-level, stateful SDK specifically for Direct Authentication. This will empower developers to easily build custom, native authentication flows that provide a superior user experience, match their application's branding, and support a wider range of client types, all without needing to manage the underlying complexity of the authentication protocol.

Design objectives​

Authenticate users using Okta's Direct Authentication API, enabling native sign-in experiences supporting Multi-Factor Authentication (MFA).

This enables developers to build native sign-in workflows into their applications, while leveraging MFA to securely authenticate users, without the need to present a browser. Furthermore, this enables passwordless authentication scenarios by giving developers the power to choose which primary and secondary authentication factors to use when challenging a user for their credentials.

This library provides the classes and methods necessary to implement native sign-in, directed by the developer, to follow specific workflows to meet your application's user experience.

Proposed solution

This proposal outlines the design for a Kotlin Multiplatform library that provides the interfaces and methods necessary to implement native sign-in flows using Okta's Direct Authentication APIs. By using this library, developers can build fully customized and integrated sign-in experiences, including support for Multi-Factor Authentication (MFA), directly within their native applications.

The initial implementation of this multiplatform library will focus on delivering support for Android as the first target platform. This will allow us to build a robust foundation and gather feedback from the mobile development community. Following the successful launch on Android, the library will be extended to support Java, enabling its use in a wider range of desktop and server-side applications.

A hybrid approach that combines both imperative and reactive paradigms will be used. This will be achieved by exposing a StateFlow<DirectAuthenticationState> alongside the imperative start function. This allows developers to choose the pattern that best fits their platform's architecture and development style.

Detailed design​

The core of the library is designed around a central interface, DirectAuthenticationFlow, which is configured via a DirectAuthenticationFlowBuilder. The authentication process is modeled as a state machine, where each step returns a DirectAuthenticationState that informs the application of the next required action.

DirectAuthenticationFlow Interface​

The DirectAuthenticationFlow is the primary entry point for developers. It exposes a single start method to initiate the authentication process. Once started, the flow returns a DirectAuthenticationState. Upon receiving a DirectAuthenticationState.Authenticated state, the application is responsible for storing the returned Token object, for example by using Credential.store() from the AuthFoundation library.

The authenticationState property is a StateFlow that emits updates to the current state of the authentication process. This is ideal for applications that prefer a reactive programming model, allowing them to observe state changes and update the UI accordingly.

/**
* The primary interface for interacting with the Okta Direct Authentication API.
*
* This interface defines the contract for initiating an authentication flow. An instance
* of this interface can be created using the `DirectAuthenticationFlow.create` builder.
*/
interface DirectAuthenticationFlow {

/**
* Indicates authentication flow state.
*/
val authenticationState: StateFlow<DirectAuthenticationState>

/**
* Starts the direct authentication flow with an initial factor.
*
* This is the entry point for authenticating a user. Depending on the server's policy
* and the provided factor, the flow may complete in a single step or require
* additional steps, such as providing a secondary factor.
*
* @param loginHint A hint to the authorization server about the user's identity,
* such as a username or email address.
* @param primaryFactor The initial authentication factor to use (e.g., a [PrimaryFactor.Password]).
* @return A [DirectAuthenticationState] of the flow after processing the initial factor
*/
suspend fun start(loginHint: String, primaryFactor: PrimaryFactor): DirectAuthenticationState
}

DirectAuthenticationFlowBuilder​

To create an instance of DirectAuthenticationFlow, developers use a builder that provides a fluent API for configuration. The builder validates essential parameters such as issuerUrl and clientId at creation time to prevent runtime errors. Upon creation, all configured properties are collected into a DirectAuthenticationContext object, which encapsulates the configuration for the authentication session, as described in the Authentication Flow Context documentation. This context is then passed to the internal DirectAuthenticationFlow implementation.

/**
* A builder used to configure and create an instance of [DirectAuthenticationFlow].
*
* This class provides a fluent API for setting the necessary parameters for the
* Direct Authentication flow, such as the issuer URL, client ID, and scopes.
*
* An instance of this builder should be created using the [DirectAuthenticationFlow.create] factory method.
*/
class DirectAuthenticationFlowBuilder private constructor() {
/**
* The intended user action for the authentication flow.
*
* This parameter indicates what the user is trying to achieve with the authentication request.
* For example, [DirectAuthenticationIntent.SIGN_IN] or [DirectAuthenticationIntent.RECOVERY].
*
* Defaults to [DirectAuthenticationIntent.SIGN_IN].
*/
var directAuthenticationIntent = DirectAuthenticationIntent.SIGN_IN

/**
* The list of grant types the client application supports for the Direct Authentication flow.
*
* This list informs the authorization server about the authentication methods the client can handle,
* such as passwords, OTP, or OOB (out-of-band) factors.
*/
var supportedGrantType = listOf(
GrantType.Password,
GrantType.Oob,
GrantType.Otp,
GrantType.OobMfa,
GrantType.OtpMfa,
GrantType.WebAuthn,
GrantType.WebAuthnMfa
)

/**
* A list of Authentication Context Class Reference values.
*
* This OIDC parameter requests that the user be authenticated with a particular level of assurance.
* The values are defined by the authorization server's policy.
*/
var acrValues = listOf<String>()

/**
* The HTTP client executor used to make network requests.
*
* This allows consumers to provide a custom implementation for making HTTP requests,
* such as one that integrates with their existing networking stack or adds custom
* headers for logging or analytics.
*
* Defaults to an instance of [KtorHttpExecutor] which uses Ktor's HTTP client.
*/
var apiExecutor: ApiExecutor = KtorHttpExecutor()

/**
* The logger used by the SDK to output diagnostic information.
*
* This allows consumers to integrate the SDK's logging with their application's
* existing logging framework, such as Timber or a custom solution. It can also be
* replaced with a mock implementation for testing purposes.
*
* Defaults to an instance of [AuthFoundationLoggerImpl] which logs to Android's Logcat.
*/
var logger: AuthFoundationLogger = AuthFoundationLoggerImpl()

/**
* The clock used for time-sensitive operations.
*
* This defaults to a clock that returns the current time in epoch seconds.
*/
var clock: OidcClock = OidcClock { System.currentTimeMillis() / 1000 }

companion object {
/**
* Creates an instance of [DirectAuthenticationFlow] using the builder pattern.
*
* @param issuerUrl The base URL of the Authorization Server. This is the issuer URI for the authorization server that will be used for the flow. For example: `https://dev-123456.okta.com`.
* @param clientId The client ID of the application. This ID is obtained from the Okta developer console when you register your application.
* @param scope The OAuth 2.0 scopes the application is requesting. Scopes are used to specify what access privileges are being requested for access tokens. For example: `openid`, `profile`, `email`, and `offline_access`.
* @param buildAction A lambda with a [DirectAuthenticationFlowBuilder] receiver to configure the flow's parameters.
*
* @return A [Result] containing the configured [DirectAuthenticationFlow] on success,
* or an exception on failure.
*/
fun create(
issuerUrl: String,
clientId: String,
scope: List<String>,
buildAction: (DirectAuthenticationFlowBuilder.() -> Unit)? = null,
): Result<DirectAuthenticationFlow> = runCatching {
// TODO(https://oktainc.atlassian.net/browse/OKTA-10087 return the DirectAuthenticatorFlow implementation)
}.getOrElse { Result.failure(it) }
}
}

Network abstraction​

The SDK's networking layer is designed to be flexible and extensible through a set of defined interfaces, ensuring it can be adapted to various project requirements without being tied to a specific HTTP client library. This abstraction is centered around the ApiExecutor interface.

  • ApiExecutor: This is the core of the network abstraction. It defines a single execute method that takes an ApiRequest and returns an ApiResponse. By providing a custom implementation of this interface, developers can integrate the SDK with their application's existing networking stack, whether it's based on Ktor, OkHttp or another library. This is particularly useful for sharing a single HTTP client instance, adding custom interceptors for logging or analytics, or managing advanced configurations like certificate pinning.

  • ApiRequest Hierarchy: Instead of depending on a specific library's request builder, the SDK defines its own request interfaces:

    • ApiRequest: The base interface defining common elements like the URL, HTTP method, headers, and query parameters.
    • ApiFormRequest: An extension for requests with application/x-www-form-urlencoded bodies, which is standard for many OAuth 2.0 token exchanges. It includes a formParameters() method.
    • ApiRequestBody: An extension for requests with a raw body, such as application/json, providing the body as a ByteArray.

    The SDK internally constructs objects that implement these interfaces. The ApiExecutor implementation is then responsible for translating these abstract request definitions into a concrete HTTP request for its underlying client library.

  • ApiResponse: This interface standardizes the representation of an HTTP response, providing access to the status code, headers, and raw body (ByteArray). This ensures that the SDK's internal logic can parse the response consistently, regardless of which HTTP client is used to perform the request.

By abstracting the entire request/response cycle, the DirectAuthenticationFlow remains completely decoupled from the underlying networking implementation. This design enhances flexibility, allowing developers to use the tools they are already familiar with, and dramatically improves testability by making it simple to provide a mock ApiExecutor to simulate various network responses and error conditions during unit testing.

/**
* An interface for executing API requests.
*/
interface ApiExecutor {
/**
* Executes the given API request.
*
* @param request The [ApiRequest] to execute.
* @return The [ApiResponse] containing the response from the API.
*/
suspend fun execute(request: ApiRequest): ApiResponse
}

/**
* A specification for an HTTP API request to be executed by an [ApiExecutor].
*
* This interface defines all the necessary components of a request, such as the
* target URI, HTTP method, headers, and body.
*/
interface ApiRequest {
/**
* The HTTP method to be used for the request (e.g., GET, POST).
*
* @return The HTTP method as an [ApiRequestMethod].
*/
fun method(): ApiRequestMethod

/**
* Returns a map of HTTP headers to be included with the request.
*
* @return A map of header names to their corresponding values.
*/
fun headers(): Map<String, List<String>>

/**
* Returns the complete URL for the request, including the scheme, host, and path.
*
* @return The request URL as a string.
*/
fun url(): String

/**
* Returns a map of URL query parameters to be appended to the request URI.
*
* @return A map of query parameters, or null if there are none.
*/
fun query(): Map<String, String>? = null
}

/**
* An extension of [ApiRequest] for requests that include form parameters in the body.
*/
interface ApiFormRequest : ApiRequest {
/**
* The MIME type of the request body (e.g., `application/x-www-form-urlencoded`).
*
* This is typically used for methods like POST or PUT to indicate the format of the body content.
*/
fun contentType(): String

/**
* Returns a map of form parameters to be sent as the request body with a
* `application/x-www-form-urlencoded` content type.
*
* This allows for multiple values per key, which will be encoded as repeated key-value pairs
* in the request body (e.g., `key=value1&key=value2`).
*
* @return A map of form parameters, or null if there are none.
*/
fun formParameters(): Map<String, List<String>>
}

/**
* An extension of [ApiRequest] for requests that include a body.
*/
interface ApiRequestBody : ApiRequest {
/**
* The MIME type of the request body (e.g., `application/json`).
*
* This is typically used for methods like POST or PUT to indicate the format of the body content.
*/
fun contentType(): String

/**
* Returns the body of the request as a byte array.
*
* This is typically used for methods like POST or PUT with content types like `application/json`.
*
* @return The request body, or null if the request has no body.
*/
fun body(): ByteArray
}

/**
* Represents the response from an HTTP API request executed by an [ApiExecutor].
*
* This interface provides access to all components of the response, including the
* status code, headers, and body.
*/
interface ApiResponse {
/** The HTTP status code from the server's response (e.g., 200 for OK). */
val statusCode: Int

/** The raw response body as a byte array, or null if there is no body. */
val body: ByteArray?

/** A map of all response headers. */
val headers: Map<String, List<String>>

/** The length of the response body in bytes, as reported by the `Content-Length` header. */
val contentLength: Long

/** The MIME type of the response body, as reported by the `Content-Type` header. */
val contentType: String
}

Factors​

Authentication factors are represented by a sealed interface hierarchy, which maps directly to the PrimaryFactor, SecondaryFactor types in the reference API. This allows for a type-safe representation of the different authentication methods.

/**
* Represents an authentication factor that can be used as a secondary factor in an MFA flow.
* This excludes primary-only factors like passwords.
*/
sealed interface SecondaryFactor : PrimaryFactor

/**
* Represents an authentication factor used in a Direct Authentication flow.
*
* Factors are categorized as either Primary or Secondary to support multi-stage
* authentication workflows.
*/
sealed interface PrimaryFactor {
/**
* A password factor, which can only be used as a primary factor.
*
* @param password The user's password.
*/
data class Password(val password: String) : PrimaryFactor

/**
* An OTP (One-Time Passcode) factor.
*
* @param passCode The one-time passcode provided by the user.
*/
data class Otp(val passCode: String) : SecondaryFactor

/**
* An OOB (Out-of-Band) factor, such as a push notification.
*
* @param channel The channel through which the OOB challenge should be sent.
*/
data class Oob(val channel: OobChannel) : SecondaryFactor

/**
* A WebAuthn factor, used for phishing-resistant authentication with hardware
* authenticators or biometrics.
*/
data object WebAuthn : SecondaryFactor
}

DirectAuthenticationState​

The DirectAuthenticationState sealed class represents the various states of the authentication flow. Each state corresponds to a specific step in the process, guiding the application on what action to take next.

/**
* Represents the current state of a Direct Authentication flow.
*
* This sealed interface defines the possible outcomes after an authentication step. The flow can
* either be complete, resulting in a [Authenticated] state, or require further user interaction,
* such as in an [MfaRequired] state or [Continuation] state
*/
sealed interface DirectAuthenticationState {

/**
* The initial state of the authentication flow, before any action has been taken.
*/
data object Idle : DirectAuthenticationState

/**
* The authentication flow has been canceled
*/
data object Canceled : DirectAuthenticationState

/**
* This state indicates that the authentication process is still pending and awaiting user action.
* Such as waiting for the user to complete an out-of-band (OOB) authentication step.
*
* @param timestamp The time in milliseconds when the authorization became pending.
*/
class AuthorizationPending internal constructor(val timestamp: Long) : DirectAuthenticationState

/**
* This state indicates that the user has been authenticated and tokens have been issued.
*
* @param token The [Token] containing the access, refresh, and ID tokens.
*/
class Authenticated internal constructor(val token: Token) : DirectAuthenticationState

/**
* The authentication flow requires an additional factor to complete.
*
* This state indicates that the user must perform a Multi-Factor Authentication (MFA)
* step to proceed.
*
* @param context The [DirectAuthenticationContext] associated with this state.
* @param mfaContext The context required to continue the MFA flow.
*/
class MfaRequired internal constructor(
private val context: DirectAuthenticationContext,
internal val mfaContext: MfaContext
) : DirectAuthenticationState {
/**
* Continues the authentication flow with the selected MFA factor.
*
* @param secondaryFactor The secondary [SecondaryFactor] to use for MFA (e.g., [PrimaryFactor.Otp], [PrimaryFactor.WebAuthn], etc.).
* @return The next [DirectAuthenticationState] in the flow.
*/
suspend fun resume(secondaryFactor: SecondaryFactor): DirectAuthenticationState {
TODO("Not yet implemented")
}
}

/**
* Represents a state where an out-of-band (OOB) challenge has been initiated, and the
* client must poll to determine the result.
*
* This state occurs after initiating an OOB flow, such as sending a push notification to
* Okta Verify. The client should use the [poll] method to check if the user has
* completed the authentication step.
*
* @param context The [DirectAuthenticationContext] associated with this state.
* @param pollContext The context required to poll for the OOB authentication result.
*/
class OobAuthenticate internal constructor(
private val context: DirectAuthenticationContext,
internal val pollContext: PollContext
) : DirectAuthenticationState {
/**
* Polls the token endpoint to check the status of the OOB authentication.
*
* This method should be called periodically until the state transitions to [Authenticated]
* or an error occurs.
*
* @return A [Result] containing the new [DirectAuthenticationState]. If the user has not
* yet completed the action, the result will be a success containing [AuthorizationPending].
*/
suspend fun poll(): DirectAuthenticationState {
TODO("Not yet implemented")
}
}
}

DirectAuthenticationError​

The DirectAuthenticationError sealed class represents various error states that can occur during the direct authentication process. It provides detailed information about the nature of the error, distinguishing between internal SDK errors, API-specific errors, and OAuth2-specific errors.

/**
* Represents the various error states of direct authentication.
*/
sealed class DirectAuthenticationError : DirectAuthenticationState {
/**
* Represents an internal error that occurred during the direct authentication process.
* This is typically used for non-HTTP related errors caused by unexpected conditions such as network failures or JSON parsing issues.
*
* @param errorCode A string representing the specific error code.
* @param description An optional description of the error.
* @param throwable The [Throwable] that caused this error.
*/
class InternalError internal constructor(
val errorCode: String,
val description: String? = null,
val throwable: Throwable
) : DirectAuthenticationError()

/**
* Represents an HTTP error that occurred during the direct authentication process.
*
* @param error A string representing the error.
* @param httpStatusCode The [HttpStatusCode] associated with this error.
*/
sealed class HttpError(val httpStatusCode: HttpStatusCode) : DirectAuthenticationError() {

/**
* An API-specific error that occurred during the direct authentication process.
*
* https://developer.okta.com/docs/reference/api/error-codes/
*
* @param errorCode A string representing the specific error code.
* @param errorSummary An optional summary of the error.
* @param errorLink An optional link to more information about the error.
* @param errorId An optional unique identifier for the error instance.
* @param errorCauses An optional list of causes for the error.
* @param httpStatusCode The [HttpStatusCode] associated with this error.
*/
class ApiError(
val errorCode: String,
val errorSummary: String?,
val errorLink: String?,
val errorId: String?,
val errorCauses: List<String>?,
httpStatusCode: HttpStatusCode,
) : HttpError(httpStatusCode)

/**
* An OAuth2-specific error that occurred during the direct authentication process.
*
* https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
*
* @param error A string representing the error.
* @param errorDescription An optional description of the error.
* @param httpStatusCode The [HttpStatusCode] associated with this error.
*/
class Oauth2Error(
val error: String,
httpStatusCode: HttpStatusCode,
val errorDescription: String?,
) : HttpError(httpStatusCode)
}
}

DirectAuthenticationErrorCode​

The DirectAuthenticationErrorCode enum defines specific error codes used within the Direct Authentication flow. These codes help in identifying the exact nature of an authentication error, including custom codes for MFA requirements and standard OAuth2 error codes.

/**
* Error codes specific to Direct Authentication.
*/
internal enum class DirectAuthenticationErrorCode(val code: String) {
MFA_REQUIRED("mfa_required"), // Custom error code for MFA requirement
AUTHORIZATION_PENDING("authorization_pending"), // Custom error code for authorization pending

// following are standard OAuth2 error codes https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1
ACCESS_DENIED("access_denied"),
INVALID_GRANT("invalid_grant"),
INVALID_REQUEST("invalid_request"),
INVALID_SCOPE("invalid_scope"),
INVALID_CLIENT("invalid_client"),
UNAUTHORIZED_CLIENT("unauthorized_client"),
UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"),
UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"),
TEMPORARILY_UNAVAILABLE("temporarily_unavailable"),
SERVER_ERROR("server_error")
}

Continuation​

The Continuation sealed class is a specialized DirectAuthenticationState that represents points in the authentication flow where the process is paused, awaiting further client-side action. This is common in flows that require more than a simple data entry, such as interacting with platform APIs (like WebAuthn) or handling out-of-band (OOB) verification. Each subclass of Continuation provides a proceed function, which the application calls to continue the flow after the required action is completed.

/**
* Represents a state in the Direct Authentication flow where further action is required to proceed.
*
* This is a special type of [DirectAuthenticationState] that indicates the flow is paused,
* pending input from the user or another client-side process.
*
* @param context The [DirectAuthenticationContext] associated with this state.
* @param mfaContext An optional context for MFA flows, which may be present if the continuation
* is part of a multi-factor sequence.
*/
sealed class Continuation(
private val context: DirectAuthenticationContext,
private val mfaContext: MfaContext? = null
) : DirectAuthenticationState {
/**
* A continuation that requires a WebAuthn (passkey) challenge to be completed.
*
* This state provides the necessary challenge data to interact with the platform's
* WebAuthn/passkey APIs.
*
* @param challengeData The challenge data from the server, to be passed to the platform's WebAuthn API.
* @param context The [DirectAuthenticationContext] associated with this state.
* @param mfaContext An optional context for MFA flows.
*/
class WebAuthn internal constructor(
val challengeData: String,
context: DirectAuthenticationContext,
mfaContext: MfaContext? = null
) : Continuation(context, mfaContext) {
/**
* Proceeds with the authentication flow with the response from the WebAuthn/passkey ceremony.
*
* @param authenticationResponseJson The JSON response from the platform's WebAuthn API.
* @return The next [DirectAuthenticationState] in the flow.
*/
suspend fun proceed(authenticationResponseJson: String): Result<DirectAuthenticationState> {
TODO("Not yet implemented")
}
}

/**
* A continuation that requires the user to enter a code, such as an OTP from an
* authenticator app or a code sent via email/SMS.
*
* @param bindingContext Internal context required to continue the flow.
* @param context The [DirectAuthenticationContext] associated with this state.
* @param mfaContext An optional context for MFA flows.
*/
class Prompt internal constructor(
private val bindingContext: BindingContext,
context: DirectAuthenticationContext,
mfaContext: MfaContext? = null
) : Continuation(context, mfaContext) {
/**
* Proceeds with the authentication flow with the user-provided code.
*
* @param code The one-time code entered by the user.
* @return The next [DirectAuthenticationState] in the flow.
*/
suspend fun proceed(code: String): Result<DirectAuthenticationState> {
TODO("Not yet implemented")
}
}

/**
* A continuation for an out-of-band (OOB) flow, where authentication happens on another device.
*
* This state is common for flows like "Okta Verify Push" where the user approves a prompt
* on their phone.
*
* @param bindingContext Internal context required to continue the flow.
* @param context The [DirectAuthenticationContext] associated with this state.
* @param mfaContext An optional context for MFA flows.
*/
class Transfer internal constructor(
private val bindingContext: BindingContext,
context: DirectAuthenticationContext,
mfaContext: MfaContext? = null
) : Continuation(context, mfaContext) {
/**
* A code that can be displayed to the user to link devices or verify the transaction.
*/
val bindingCode = bindingContext.bindingCode

/**
* Proceeds with the authentication flow by polling the server to check the status of the
* out-of-band authentication.
*
* @return The next [DirectAuthenticationState] in the flow, which will typically be
* [DirectAuthenticationState.Authenticated] once the user has approved the OOB prompt.
*/
suspend fun proceed(): Result<DirectAuthenticationState> {
TODO("Not yet implemented")
}
}
}

Example usage​

Here is a simplified example of how a developer might use the Direct Authentication SDK to implement a custom sign-in flow in an Android application using a imperative style. This example assumes the existence of UI components to collect user input and display messages.

// Create the DirectAuthenticationFlow instance using the builder
val flow = DirectAuthenticationFlowBuilder.create {
issuerUrl = "https://example.com"
clientId = "client-id"
scope = listOf("openid", "profile", "email")
}.getOrThrow()

// Start the authentication flow with the user's login and password
flow.start(
loginHint = "fake.user@okta.com",
primaryFactor = PrimaryFactor.Password("SuperSecretPassword")
).also { initialStatus ->
// Recursively handle all intermediate steps until a Success status or exception thrown.
runCatching { resolveAuthStatus(initialStatus) }
.onSuccess { completedStatus ->
val token = completedStatus.token
// Credential.store(token) // Store tokens securely
// Proceed with authenticated actions
}.onFailure {
// Handle errors (e.g., network issues, invalid credentials)
println("Authentication failed: ${it.message}")
}
}

// A tail-recursive function to handle the authentication flow until a terminal state is reached.
// Such as Authenticated, Canceled or DirectAuthenticationError.
// Recursively handles MFA requirements and continuations.
private tailrec suspend fun resolveAuthStatus(authStatus: DirectAuthenticationState): DirectAuthenticationState {
return when (authStatus) {
is DirectAuthenticationState.Authenticated, is DirectAuthenticationState.Canceled, is DirectAuthenticationError -> {
// Base case. Authenticated, Canceled or error
authStatus
}

is DirectAuthenticationState.MfaRequired -> {
// MFA is required, handle the MFA context
// Ask the user to choose a factor. Here we automatically choose an OTP factor for demonstration.
resolveAuthStatus(authStatus.resume(PrimaryFactor.Otp("123456")))
}

is DirectAuthenticationState.OobAuthenticate -> {
// Prompt the user to complete an out-of-band action. For Okta Verify, show the bindingCode. Resume will poll the server for status.
resolveAuthStatus(authStatus.poll())
}

is Continuation.Prompt -> {
// Prompt the user for a code. Here we automatically provide a code for demonstration.
resolveAuthStatus(authStatus.proceed("123456").getOrThrow())
}

is Continuation.Transfer -> {
// Prompt the user to complete an out-of-band action. For Okta Verify, show the bindingCode. Resume will poll the server for status.
resolveAuthStatus(authStatus.proceed().getOrThrow())
}

is Continuation.WebAuthn -> {
// Invoke the platform's WebAuthn API with the challengeData. For demonstration, we simulate a WebAuthn response.
resolveAuthStatus(authStatus.proceed("authenticationResponseJson").getOrThrow())
}
}
}

Alternatively, developers can choose to use a reactive style by collecting the authenticationState flow. This approach is well-suited for applications that leverage reactive programming paradigms, such as those using Jetpack Compose or other state-driven UI frameworks. In this example, we launch a coroutine to collect state changes and handle them accordingly.

 val flow = DirectAuthenticationFlowBuilder.create(
issuerUrl = "https://example.com",
clientId = "client-id",
scope = listOf("openid", "profile", "email")
).getOrThrow()

// Launch a coroutine to collect state changes from the flow.
val job = launch {
flow.authenticationState.collect {
handleAuthStatus(it)
}
}

// Start the flow, which will emit the initial status to the collector.
flow.start("name", PrimaryFactor.Password("secret"))

// job.cancel()

private suspend fun handleAuthStatus(authStatus: DirectAuthenticationState) {
when (authStatus) {
is DirectAuthenticationState.Authenticated -> {
// Authentication is complete. You can access the token here.
authStatus.token
}

is DirectAuthenticationState.MfaRequired -> {
// Ask the user to choose a factor. Here we automatically choose an OTP factor for demonstration.
authStatus.resume(PrimaryFactor.Otp("123456"))
}

is DirectAuthenticationState.OobAuthenticate -> {
// Prompt the user to complete an out-of-band action and continue. Resume will poll the server for status.
authStatus.poll()
}
else -> { /* Handle other states like Idle, Canceled, AuthorizationPending, or Errors */
}
}
}

Example Sequence Diagram​

The following sequence diagrams are simplified for clarity. In a real-world scenario, additional error handling, logging, and edge cases would need to be considered. Handling of polling intervals is not shown for brevity.

Sequence Push OOB MFA flow​

Sequence OTP MFA flow​

The following sequence diagram illustrates a multi-factor authentication flow where the user first authenticates with a password and is then prompted for a second factor. In this example, the application provides a One-Time Passcode (OTP).

Sequence Okta Verify push with number challenge flow​

The following sequence diagram illustrates a multi-factor authentication flow where the user first authenticates with a password and is then prompted for a second factor. In this example, the application provides an out-of-band (OOB) verification method, such as Okta Verify Push with a binding code(number challenge). This flow initiates the second factor by calling the /challenge endpoint, unlike the OTP example which calls /token directly. The /challenge endpoint is optional and is used when the client needs the server to drive the next step, such as sending a push notification. If the client can determine the next factor on its own (for example, by prompting the user for an OTP), it can bypass this step and call /token directly with the factor details.

Implications on adoption​

This library introduces a new, additive capability for building native authentication experiences. It does not introduce any breaking changes to existing functionalities, such as browser-based redirect flows. Developers can continue to use existing authentication methods without any impact. The primary implication for developers choosing to adopt the Direct Authentication flow is that they assume full responsibility for building and maintaining the user interface for the entire authentication process. This includes screens for username/password entry, multi-factor authentication (MFA) challenges, and account recovery steps. While this provides complete control over the user experience and branding, it requires a more significant development effort compared to redirect-based flows where the UI is hosted by Okta.

More information about the advantages and disadvantages of using this library can be found at the following: https://developer.okta.com/docs/concepts/direct-authentication/

Future directions​

The following are potential enhancements and directions for the Direct Authentication SDK after its initial release.

  • Multiplatform Expansion: The initial implementation will target Android. Future work could extend support to other platforms, including JVM for server-side and desktop applications. Based on developer demand, this could be further expanded to include JavaScript (for web clients) and native iOS/macOS targets.

  • AuthFoundation Evolution: Supporting a broader set of platforms will require evolving the underlying auth-foundation library to be fully multiplatform. This may involve refactoring some of its core interfaces to be more flexible and abstract away platform-specific dependencies, ensuring a consistent and reusable architecture across all supported environments.

  • New Authenticators: The SDK is designed to be extensible. As new authentication methods become available or are prioritized by Okta, support for additional factors can be seamlessly integrated into the existing Factor and Continuation models.

  • Integration with OAuth2Client: To simplify configuration for developers already using the auth-foundation library, the DirectAuthenticationFlowBuilder could be enhanced to accept an OAuth2Client instance. This would allow the builder to automatically populate common parameters like clientId, scope, and issuerUrl from the existing client, reducing boilerplate code and ensuring consistency across different authentication-related components of an application.

Alternatives considered​

The design of DirectAuthenticationFlow intentionally combines elements from both imperative and reactive programming paradigms to offer developers flexibility. Several alternative approaches were considered:

  • Purely Imperative (suspend functions only): An API could be designed using only suspend functions, where each function call blocks (asynchronously) and returns the next state.

    • Pros: Simple to understand for developers familiar with sequential code. Follows a clear request-response pattern.
    • Cons: This model is less suitable for modern, state-driven UI frameworks (like Jetpack Compose or SwiftUI) which are designed to react to streams of data. Managing UI updates would require more manual, imperative code from the developer. It also makes handling background state changes (like from a polling operation) more complex to propagate to the UI.
  • Purely Reactive (StateFlow only): The API could be designed to expose only a StateFlow and functions to submit actions (e.g., submitFactor(factor)). The result of an action would only be observable through the flow.

    • Pros: Aligns perfectly with reactive UI patterns. All state changes are handled through a single, consistent mechanism.
    • Cons: This can make handling the result of a specific, one-off action more complex. For example, the result of the initial start call (which might fail due to a simple configuration error) would be delivered asynchronously through the flow, which can be unintuitive. It mixes immediate validation feedback with asynchronous state transitions.
  • Chosen Hybrid Approach: The proposed design uses an imperative start function that returns an initial state directly, providing immediate feedback. Subsequent state changes can then be observed through the authenticationState StateFlow.

    • Pros: It offers the best of both worlds. The imperative start call is natural for initiating an action and getting a direct result. The StateFlow is ideal for observing the subsequent, asynchronous state transitions in a reactive way, fitting well with modern UI development.
    • Cons: Developers need to be comfortable with both suspend functions and Flow, which adds a slight learning curve compared to a single-paradigm approach.

By providing both an imperative entry point and a reactive state stream, the SDK empowers developers to choose the pattern that best fits their application's architecture while promoting best practices for modern native development.

Alignment with Reference Architecture​

The KMP implementation, while heavily influenced by the original Apple (Swift) reference architecture, introduces several key differences to provide a more idiomatic and type-safe experience for Kotlin developers. The core concepts remain consistent, but the execution and developer interaction have been tailored to the strengths of the Kotlin language and its ecosystem.

Key Architectural Differences:​

FeatureApple (Swift) Implementation (Reference)KMP (Kotlin) Implementation
State Machine & Type SafetyA single resume function on the main DirectAuthenticationFlow object is used for all state transitions.The resume and proceed functions are moved into the specific state classes that require them (e.g., MfaRequired, Continuation.WebAuthn). This creates a more robust and type-safe state machine, guiding the developer to call only the appropriate action for a given state.
Asynchronous OperationsUses Swift's modern concurrency features (async/await) for handling asynchronous operations.Leverages Kotlin's native suspend functions and coroutines. This provides first-class support for structured concurrency, cancellation, and more readable, sequential-style asynchronous code.
Reactive State ManagementEmploys a delegate pattern to communicate state changes back to the application.Uses StateFlow from Kotlin Coroutines to represent the authentication state as an observable, hot data stream. This is the idiomatic approach in modern Kotlin and integrates seamlessly with reactive UI frameworks like Jetpack Compose.
Modeling of Factors & StatesUses discriminated unions and type aliases to represent different factors and states.Utilizes Kotlin's sealed interfaces and classes for PrimaryFactor, SecondaryFactor, DirectAuthenticationState, and Continuation. This enables exhaustive when expressions, ensuring compile-time safety when handling different states and factors.
Concept of ContinuationContinuation is treated as a type of authentication factor.Continuation is modeled as a distinct DirectAuthenticationState. This clarifies that the flow is paused and awaiting an external action (like a WebAuthn ceremony), rather than requiring a new authentication factor.
State ImmutabilityDirectAuthenticationFlow can be mutable, with properties updated as the flow progresses.States are modeled as immutable data classes. Each transition function (e.g., resume, proceed) returns a new state object, ensuring thread safety and predictable state management, which aligns with reactive principles.

Analysis of the differences​

The KMP (Kotlin) implementation of Okta's Direct Authentication is a Kotlin variation of the original Apple (Swift) design. It embraces modern Kotlin features to offer a type-safe, thread-safe, and developer-friendly experience.

While the fundamental authentication flows and concepts are aligned, the KMP SDK is not a direct port. It has been redesigned to be idiomatic for the Kotlin ecosystem, prioritizing:

  • Type Safety: The state machine design in the KMP version is inherently safer, reducing the possibility of runtime errors by leveraging Kotlin's strong type system.
  • Modern Asynchronicity: The use of coroutines and suspend functions aligns with current best practices in Kotlin development, leading to cleaner and more manageable asynchronous code.
  • Reactive Architecture: The adoption of StateFlow makes the KMP SDK a natural fit for modern, declarative UI frameworks, simplifying state management for the developer.

In essence, the KMP implementation takes the core principles of the Apple reference architecture and refines them to follow Kotlin patterns, resulting in an SDK that feels native to the Kotlin language and provides a more streamlined and secure development experience.

In-depth analysis of the differences​

Here is a detailed analysis of the differences between the OktaDirectAuth Apple (Swift) and KMP (Kotlin) implementations, focusing on the networking layer and state mutability.

Networking Layer​

KMP (Kotlin) Implementation:

The KMP implementation uses the Ktor client for handling HTTP requests. This is a modern, multiplatform networking library from JetBrains that allows for a single networking implementation to be shared across all target platforms.

  • KtorHttpExecutor: This class is responsible for executing API requests. It creates and configures a Ktor HttpClient with plugins for timeouts, cookies, and caching. It's designed to be extensible, allowing developers to provide their own pre-configured Ktor client if needed.
  • DirectAuth...Request: These classes represent the individual API requests. They define the endpoint, HTTP method, headers, and body of the request. This creates a clean separation between the definition of a request and its execution.

Apple (Swift) Implementation:

The Swift implementation abstracts the networking layer into a separate AuthFoundation framework. The OktaDirectAuth module itself does not contain any code for making network calls.

  • APIRequest protocol: Request objects, such as OOBAuthenticateRequest, conform to the APIRequest protocol from the AuthFoundation framework. This protocol defines the properties of a request (URL, HTTP method, parameters, etc.).
  • AuthFoundation: This framework is responsible for taking the APIRequest object, executing the network call ( likely using URLSession internally), and decoding the response. This approach decouples the authentication logic from the underlying networking implementation.

Comparison:

FeatureKMP (Kotlin)Apple (Swift)
LibraryKtorAuthFoundation (custom framework)
AbstractionHigh (requests defined separately from execution)High (requests defined separately from execution)
PlatformMultiplatformApple-specific
ExtensibilityHigh (custom Ktor client and plugins)High (custom APIRequest conformances)

State Mutability​

KMP (Kotlin) Implementation:

The KMP implementation uses a reactive, unidirectional data flow model for state management, which is idiomatic for modern Kotlin applications.

  • DirectAuthenticationState: This is a sealed interface that represents all possible states of the authentication flow (e.g., Idle, Authenticated, MfaRequired). Using a sealed interface ensures that all possible states are known at compile time.
  • StateFlow: The DirectAuthenticationFlowImpl class uses a MutableStateFlow to hold the current DirectAuthenticationState. It exposes this as an immutable StateFlow to clients. This allows clients (such as a UI layer) to observe the state and react to changes automatically.
  • Immutability: The DirectAuthenticationState objects are immutable. State transitions are handled by creating a new state object and updating the StateFlow.

Apple (Swift) Implementation:

The Swift implementation uses Swift's modern concurrency features, specifically actors, to manage state in a thread-safe manner.

  • Status enum: Similar to the KMP implementation, the Swift code defines a Status enum to represent the different states of the flow (e.g., success, mfaRequired).
  • DirectAuthenticationFlow actor: The main DirectAuthenticationFlow class is an actor. This ensures that all access to its internal mutable state (such as the _context property which holds the current Status) is synchronized and free of data races.
  • State Updates: State is updated by calling async functions on the actor (e.g., start, resume). These functions return the new Status upon completion. Additionally, a delegate protocol ( DirectAuthenticationFlowDelegate) is used to notify listeners of state changes.

Comparison:

FeatureKMP (Kotlin)Apple (Swift)
State Representationsealed interfaceenum
State ManagementStateFlow (reactive)actor (concurrency-safe)
State ObservationObserving a StateFlowasync/await and delegate pattern
Thread SafetyGuaranteed by StateFlow and coroutinesGuaranteed by actor

Acknowledgments​

  • Alex Nachbaur, for her work on the okta-client-swift SDK, which served as a reference point for this design.
  • Rajdeep Nanua, for his work on the AuthFoundation library, which provides the foundational components for this SDK.