webauthn-client-flow¶
Generic passkey ceremony orchestration over PasskeyClient and application-owned backend
contracts, without library-owned product state.
What it provides¶
PasskeyFlowfor registration and authentication start/prompt/finish sequencing.RegistrationBackend<Input, State, Output>andAuthenticationBackend<Input, State, Output>contracts.- Exact forwarding of opaque backend
Statefromstarttofinish. - Application-defined finish
Outputand observablePasskeyPhasechanges. - An explicit
AlreadyInProgressresult when the same flow instance is already running a ceremony.
The module depends on webauthn-client-core. It has no Ktor engine, HTTP contract, or JSON
implementation dependency.
When to use¶
Use this module when the library should coordinate a platform prompt but your application owns the
backend API and presentation state. Use PasskeyClient directly when you only need the OS ceremony,
or add webauthn-client-ktor when your backend contract is transported with Ktor.
How to use¶
Model backend continuation data as an opaque application type. The flow will return it to the same backend unchanged after the platform produces a raw credential response.
data class RegistrationInput(val userName: String)
data class ContinuationToken(val value: String)
data class SignedInAccount(val userName: String)
data class RegistrationStartEnvelope(
val continuation: ContinuationToken,
val options: PublicKeyCredentialCreationOptions,
)
interface RegistrationApi {
suspend fun start(input: RegistrationInput): RegistrationStartEnvelope
suspend fun finish(
continuation: ContinuationToken,
response: RawRegistrationResponse,
): SignedInAccount
}
class AppRegistrationBackend(
private val api: RegistrationApi,
) : RegistrationBackend<RegistrationInput, ContinuationToken, SignedInAccount> {
override suspend fun start(
input: RegistrationInput,
): CeremonyStart<ContinuationToken, PublicKeyCredentialCreationOptions> {
val started = api.start(input)
return CeremonyStart(started.continuation, started.options)
}
override suspend fun finish(
state: ContinuationToken,
response: RawRegistrationResponse,
): SignedInAccount = api.finish(state, response)
}
suspend fun register(
passkeyClient: PasskeyClient,
backend: AppRegistrationBackend,
onPhaseChanged: (PasskeyPhase) -> Unit,
): CeremonyResult<SignedInAccount> = PasskeyFlow(passkeyClient).register(
input = RegistrationInput("alice"),
backend = backend,
onPhaseChanged = onPhaseChanged,
)
Handle CeremonyResult.Failure.Platform and CeremonyFailure.AlreadyInProgress as deliberate flow
outcomes. Exceptions from backend start/finish, phase callbacks, and unexpected custom
PasskeyClient implementations propagate to the application; coroutine cancellation also remains
control flow. Use try/catch only where your application can apply a meaningful error policy.
How it fits in the system¶
flowchart LR
APP["Application UI and state"] --> FLOW["webauthn-client-flow"]
FLOW --> CORE["webauthn-client-core"]
FLOW --> BACKEND["Application backend contract"]
CORE --> PLATFORM["Android or iOS platform bridge"]
Pitfalls and limits¶
- A
PasskeyFlowinstance allows one ceremony at a time; concurrent calls are rejected, not queued. onPhaseChangedis synchronous and application-owned. Do not perform blocking work in it.- Do not decode or reinterpret opaque continuation state inside the flow layer.
- The flow does not own retries, backend exception mapping, navigation, dialogs, or persisted UI state.
Status¶
Beta. The generic contracts and concurrency behavior have common tests; platform runtime coverage
belongs to webauthn-client-platform and host applications.