Skip to content

Auties00/Warden

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Warden

A tiny, dependency-free, cross-platform library that runs the operating system's native passkey flow from Java.

What is Warden

A passkey assertion proves possession of a credential a relying party registered by signing a server-issued challenge. Every desktop platform ships a credential service that can reach a passkey stored on the machine, synced from the user's phone, or reached over the cross-device hybrid transport, and surface the platform's own confirmation UI. Warden drives those services, Windows Hello, macOS AuthenticationServices, and Linux libfido2, behind one small contract, so a desktop application can satisfy a WebAuthn get ceremony with a real platform prompt and no embedded browser.

It does the WebAuthn plumbing for you: it builds the clientDataJSON, hands the challenge to the platform, and returns the authenticatorData, signature, credential id, user handle, and optional PRF output your server needs to verify the assertion.

Usage

import com.github.auties00.warden.PasskeyRequest;
import com.github.auties00.warden.WardenAuthenticator;

// 1. Get an authenticator for the host platform.
var authenticator = WardenAuthenticator.create();

// 2. Build a request. The short constructor fills sensible defaults
//    (no allow list, "preferred" user verification, a 2-minute timeout).
var request = new PasskeyRequest("example.com", challengeBytes, "https://example.com");

// 3. Run the ceremony, anchoring the prompt to your window. This opens the platform
//    prompt and blocks until the user completes or declines it, so call it off any UI
//    thread. Pass your window's native handle (a Win32 HWND, an NSWindow* on macOS), or
//    WardenAuthenticator.NO_WINDOW to let the backend pick a platform default.
//
//    PasskeyException is a sealed hierarchy, so the outcomes can be handled exhaustively.
try {
    var assertion = authenticator.getAssertion(request, myWindowHandle);
    send(assertion.authenticatorData(), assertion.clientDataJson(),
         assertion.signature(), assertion.credentialId(), assertion.userHandle());
} catch (PasskeyException failure) {
    // See the outcomes section
}

For full control, use the canonical constructor (allow list, user verification, timeout, PRF input):

var request = new PasskeyRequest(
        "example.com", challengeBytes,
        new byte[][] { credentialId },          // allow list (empty for a discoverable request)
        UserVerification.REQUIRED,
        Duration.ofMinutes(1),
        prfEvalFirst,                            // or null for no PRF
        "https://example.com");

PasskeyRequest.toWebAuthnGetJson() serialises the request into the { "publicKey": ... } JSON a browser's navigator.credentials.get accepts, for when you'd rather relay the ceremony to a device you control.

Outcomes

getAssertion returns the PasskeyAssertion on success and otherwise throws one of a sealed PasskeyException hierarchy, so every case can be handled exhaustively:

Exception Meaning
PasskeyNotAllowedException The user cancelled or dismissed the prompt, the ceremony timed out, or no usable credential was available. WebAuthn deliberately makes these indistinguishable, so Windows throws this base type; Linux and macOS throw a leaf where they can: PasskeyCancelled/TimedOut/NoCredentialException.
PasskeyConstraintException The requested user verification could not be satisfied, for example UserVerification.REQUIRED on an authenticator with no PIN or biometric. PREFERRED and DISCOURAGED never raise it.
PasskeyFailedException A genuine platform failure: a malformed request, an authenticator or transport error, anything not modelled above.
PasskeyUnavailableException The native backend could not be initialised (also thrown by create() when the platform service is present but cannot come up).

For handling the result, prefer a switch over the caught exception, as in the snippet above. Because the hierarchy is sealed the switch is exhaustive, so the compiler accepts it with no default clause (Java 21's pattern matching for switch). The draft exception handling in switch proposal extends this further, letting a switch catch the selector's exceptions directly with case throws, which the sealed hierarchy again makes exhaustive.

Install

Maven:

<dependency>
    <groupId>com.github.auties00</groupId>
    <artifactId>warden</artifactId>
    <version>1.0.0</version>
</dependency>

Gradle:

implementation("com.github.auties00:warden:1.0.0")

Native access

The backends issue restricted FFM calls into the platform credential service. On Java 24+ this prints a warning unless native access is granted. Grant it to the module (or the unnamed module on the classpath):

# modular
java --enable-native-access=com.github.auties00.warden -m your.app/...

# classpath
java --enable-native-access=ALL-UNNAMED -cp ... YourApp

Platform support

Platform Backend Mechanism
Windows (10 1903+) webauthn.dll WebAuthNAuthenticatorGetAssertion (Windows Hello)
macOS (experimental) AuthenticationServices ASAuthorizationController driven through the Objective-C runtime
Linux libfido2 fido_dev_get_assert against a roaming/hybrid FIDO2 authenticator
other none unsupported (create() throws UnsupportedOperationException)

WardenAuthenticator.create() picks the backend for the host and resolves its native library eagerly, so a missing service surfaces immediately as a PasskeyUnavailableException. WardenAuthenticator.isSupported() reports whether the host platform family has a backend at all, and authenticator.platform() reports which one you got.

macOS is experimental. AuthenticationServices delivers its result asynchronously and demands a presentation anchor (a key window) and an app bound to the relying party by an associated-domains entitlement. A library cannot synthesise those, so the macOS backend works only when Warden runs inside a Cocoa application that supplies them; in a headless process the ceremony fails cleanly rather than returning a wrong result. The macOS backend also returns the credential's own rawClientDataJSON, because AuthenticationServices builds the clientDataJSON itself rather than from the request's origin.

PRF / hmac-secret

A non-null PasskeyRequest.prfEvalFirst() requests the WebAuthn PRF (hmac-secret) extension. Warden maps the evaluation input to the salt the same way a browser does (SHA-256("WebAuthn PRF" || 0x00 || input)) and returns the result in PasskeyAssertion.prfOutput(). PRF is wired on Windows (assertion-options version 5+) and Linux (libfido2); it is not wired on the macOS backend.

License

MIT © Alessandro Autiero

About

A tiny, dependency-free, cross-platform library that runs the operating system's native passkey flow from Java

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors