Reference: Security: WebAuthn

pro
This is an http4k Pro feature and is licensed under the http4k Commercial License Find out more

Installation (Gradle)#

dependencies {
    
    implementation(platform("org.http4k:http4k-bom:6.54.0.0"))

    implementation("org.http4k:http4k-security-webauthn")
}

About#

Passwordless authentication using WebAuthn/FIDO2 passkeys. The module handles the full registration and authentication ceremonies, session management, and cryptographic verification of credentials, so users can sign in with a fingerprint, face, security key, or device PIN instead of a password.

Usage#

Everything is orchestrated through the Passkeys type, which exposes two ways to wire passkeys into your app:

  • Passkeys.passwordless(...) - passkey-only signup and login, with no prior session required.
  • Passkeys.onTopOfExistingLogin(...) - let an already-logged-in user add a passkey to their account.

Either way you get a pre-wired set of routes for the browser-side ceremonies:

RoutePurpose
POST /register/optionsBegin registration - issue a challenge and creation options
POST /registerComplete registration - verify and store the new credential
POST /authenticate/optionsBegin authentication - issue a challenge
POST /authenticateComplete authentication - verify the signature and establish a session

Plus an authFilter to protect routes and a logout handler to clear sessions.

Extension points#

The behaviour is composed from three pluggable interfaces, each with a production and a testing implementation:

InterfaceResponsibilityProductionTesting
PasskeyVerifierCryptographic verification of attestation & signaturesWebAuthn4jPasskeyVerifierInsecurePasskeyVerifier
PasskeyPersistenceStoring & retrieving credentials and ceremony stateyour storeInMemoryPasskeyPersistence
PrincipalsReading/writing/clearing the user sessionyour sessionInsecureCookieBasedPrincipals

Supporting model types include RelyingParty (your server identity), PasskeyUser (the user identity), WebAuthnPolicy (configurable security policy - user verification, attestation, resident key, etc.) and the sealed PasskeyError describing ceremony failures (bad signature, challenge/origin mismatch, sign-counter regression for clone detection, and so on).

Code#

Kotlin example.kt
package content.ecosystem.http4k.reference.webauthn

import org.http4k.connect.model.Base64UriBlob
import org.http4k.core.Method.GET
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Uri
import org.http4k.lens.RequestKey
import org.http4k.routing.bind
import org.http4k.routing.routes
import org.http4k.security.passkeys.Passkeys
import org.http4k.security.passkeys.model.PasskeyUser
import org.http4k.security.passkeys.model.RelyingParty
import org.http4k.security.passkeys.randomHandle
import org.http4k.security.passkeys.testing.InMemoryPasskeyPersistence
import org.http4k.security.passkeys.testing.InsecureCookieBasedPrincipals
import org.http4k.security.passkeys.webauthn4j.WebAuthn4jPasskeyVerifier
import org.http4k.server.SunHttp
import org.http4k.server.asServer

/**
 * Simplest possible passkey demo: register, then log in. Open: http://localhost:9000,
 */
fun main() {
    val rp = RelyingParty(id = "localhost", name = "http4k passkeys demo", origin = Uri.of("http://localhost:9000"))
    val handle = RequestKey.required<Base64UriBlob>("handle")

    val passkeys = Passkeys.passwordless(
        rp = rp,
        verifier = WebAuthn4jPasskeyVerifier(),
        persistence = InMemoryPasskeyPersistence(),
        principals = InsecureCookieBasedPrincipals("http4k", handle),
        // the registering user comes straight off the signup request - here just a ?username=
        user = { req ->
            req.query("username")?.takeIf(String::isNotBlank)?.let { PasskeyUser(Base64UriBlob.randomHandle(), it, it) }
        }
    )

    val app = routes(
        "/" bind GET to { Response(OK).header("content-type", "text/html; charset=utf-8").body(page) },
        "/passkeys" bind passkeys.routes
    )

    app.asServer(SunHttp(9000)).start().also { println("passkeys demo: http://localhost:9000") }.block()
}
scarf