1
0

Add passkeys project: WebAuthn ceremonies, a software authenticator and the one-time-token fallback

Fourth Maven project in the repository. Registration and authentication run end to
end with no browser and no hardware key: VirtualAuthenticator emits real CBOR
attestation objects and real ES256 assertion signatures, and tools/PasskeyCeremony.java
drives the live HTTP endpoints with them.

Profiles cover userVerification REQUIRED, DIRECT attestation, a disallowed origin and
JDBC persistence. Eleven doc chapters and twelve captured transcripts under docs/passkeys
and docs/output/pk-*.txt, all regenerated by passkeys/scripts/run-all.sh.
This commit is contained in:
2026-08-25 22:54:40 +05:30
parent e9381dc5be
commit f6dd692177
59 changed files with 3567 additions and 4 deletions

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Ask for DIRECT attestation. Register with fmt "none" and an all-zero AAGUID anyway.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app attestationdirect > /dev/null
{
header "attestation: DIRECT requested, attestation: none accepted"
ceremony register-and-login
} 2>&1 | tee "$OUTPUT_DIR/pk-attestation.txt"
stop_app

9
passkeys/scripts/bootstrap.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# The chicken-and-egg problem: a passkey cannot be a user's first credential.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "Bootstrapping - registering a passkey requires an existing authenticated session"
ceremony bootstrap
} 2>&1 | tee "$OUTPUT_DIR/pk-bootstrap.txt"
stop_app

11
passkeys/scripts/ceremony.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# The whole thing, end to end: password login, passkey registration, logout, passkey login.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "Registration and authentication ceremonies, driven without a browser"
echo "Spring Security 7.1.1, Spring Boot 4.1.1, rpId localhost, default settings."
echo "The authenticator is src/main/java/com/ankurm/passkeys/virtual/VirtualAuthenticator.java."
ceremony register-and-login
} 2>&1 | tee "$OUTPUT_DIR/pk-ceremony.txt"
stop_app

9
passkeys/scripts/counter.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Three assertions with an increasing signature counter, then a replay of a stale one.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "Signature counter: stored on every assertion, compared against on none"
ceremony clone-counter
} 2>&1 | tee "$OUTPUT_DIR/pk-counter.txt"
stop_app

9
passkeys/scripts/duplicate.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# excludeCredentials, and what happens when the client ignores it.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "Registering the same credential id twice"
ceremony duplicate
} 2>&1 | tee "$OUTPUT_DIR/pk-duplicate.txt"
stop_app

9
passkeys/scripts/filters.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Where the four WebAuthn filters and the two one-time-token filters sit in the chain.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "The security filter chain with webAuthn() and oneTimeTokenLogin() configured"
ceremony filters
} 2>&1 | tee "$OUTPUT_DIR/pk-filters.txt"
stop_app

13
passkeys/scripts/jdbc.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Credentials in a database, using the DDL that ships inside spring-security-web.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app jdbc > /dev/null
{
header "JDBC persistence - H2, with Spring Security's own schema"
echo "schema-locations point at classpath:org/springframework/security/user-entities-schema.sql"
echo "and user-credentials-schema.sql, which live in spring-security-web, not in"
echo "spring-security-webauthn. Nothing creates these tables for you."
echo
ceremony register-and-login
} 2>&1 | tee "$OUTPUT_DIR/pk-jdbc.txt"
stop_app

63
passkeys/scripts/lib.sh Executable file
View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Shared helpers. Sourced by every script in this directory.
#
# Two traps are baked in here because both have cost real time:
#
# * never `pkill -f spring-boot` - the pattern matches the shell that is running this
# script and kills it. Kill by main class instead, which is what stop_app does.
# * `mvn -o` cannot run the Boot plugin until one online build has cached it, so the first
# run of run.sh is deliberately not offline.
set -euo pipefail
MODULE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT_DIR="$(cd "$MODULE_DIR/.." && pwd)/docs/output"
MAIN_CLASS="PasskeysDemoApplication"
BASE_URL="${BASE_URL:-http://localhost:8080}"
APP_LOG="${APP_LOG:-/tmp/passkeys-demo.log}"
mkdir -p "$OUTPUT_DIR"
stop_app() {
for pid in $(ps -eo pid,cmd | grep "[${MAIN_CLASS:0:1}]${MAIN_CLASS:1}" | awk '{print $1}'); do
kill -9 "$pid" 2>/dev/null || true
done
# ss -lptn sometimes reports the port with no PID, so a port-based kill silently does
# nothing and the stale process keeps serving. Wait for the port to actually close.
for _ in $(seq 1 20); do
curl -sf -o /dev/null "$BASE_URL/health" || return 0
sleep 0.5
done
}
start_app() {
local profiles="${1:-}"
stop_app
cd "$MODULE_DIR"
local args=(-B org.springframework.boot:spring-boot-maven-plugin:run)
[ -n "$profiles" ] && args+=("-Dspring-boot.run.profiles=$profiles")
setsid nohup mvn "${args[@]}" > "$APP_LOG" 2>&1 < /dev/null &
for _ in $(seq 1 90); do
curl -sf -o /dev/null "$BASE_URL/health" && { echo "started${profiles:+ with profiles: $profiles}"; return 0; }
sleep 2
done
echo "the application did not come up; see $APP_LOG" >&2
tail -40 "$APP_LOG" >&2
return 1
}
classpath() {
cd "$MODULE_DIR"
[ -f target/deps.txt ] || mvn -B -q dependency:build-classpath -Dmdep.outputFile=target/deps.txt -DincludeScope=runtime
echo "target/classes:$(cat target/deps.txt)"
}
ceremony() {
cd "$MODULE_DIR"
java --class-path "$(classpath)" tools/PasskeyCeremony.java "$@"
}
header() {
echo "=============================================================================="
echo "$1"
echo "=============================================================================="
}

15
passkeys/scripts/origin.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# What a phishing attempt looks like from the relying party's side, in both ceremonies.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "Registration from a disallowed origin"
ceremony wrong-origin
echo
echo "--- what the server logged ---"
grep -A4 -m1 -E 'BadOriginException|InconsistentClientDataTypeException' "$APP_LOG" || tail -5 "$APP_LOG"
header "Assertion from a disallowed origin"
ceremony wrong-origin-login
} 2>&1 | tee "$OUTPUT_DIR/pk-origin.txt"
stop_app

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# The one-time token path: generate, redeem, then try to redeem again.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "One-time token login - the way in, and the way back after a lost device"
ceremony ott
} 2>&1 | tee "$OUTPUT_DIR/pk-ott.txt"
stop_app

13
passkeys/scripts/run-all.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Regenerates every docs/output/pk-*.txt file in this repository.
#
# Timings and instants differ between runs; nothing else should.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
cd "$MODULE_DIR"
mvn -B -q compile
for script in ceremony counter user-verification origin attestation duplicate bootstrap step-up ott-fallback jdbc filters test-run; do
echo ">>> scripts/$script.sh"
"./scripts/$script.sh" > /dev/null
done
stop_app
ls -la "$OUTPUT_DIR"/pk-*.txt

13
passkeys/scripts/run.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Start the demo with the given profiles.
#
# ./scripts/run.sh defaults: rpId localhost, UV preferred, in memory
# ./scripts/run.sh uvrequired user verification REQUIRED on both ceremonies
# ./scripts/run.sh attestationdirect ask for DIRECT attestation and watch nothing change
# ./scripts/run.sh badorigin relying party expects an origin the client won't send
# ./scripts/run.sh jdbc credentials in H2 using Spring Security's own DDL
# ./scripts/run.sh trace every WebAuthn log line the framework emits
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "${1:-}"
echo "log: $APP_LOG"
echo "browser: $BASE_URL/login (user/password, then $BASE_URL/webauthn/register)"

9
passkeys/scripts/step-up.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# The same user, three sessions, one endpoint that only one of them can reach.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
start_app "" > /dev/null
{
header "FactorGrantedAuthority - password, magic link and passkey are not interchangeable"
ceremony stepup
} 2>&1 | tee "$OUTPUT_DIR/pk-step-up.txt"
stop_app

5
passkeys/scripts/test-run.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# The contract tests. These need no running server.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
cd "$MODULE_DIR"
mvn -B test 2>&1 | sed -n '/T E S T S/,$p' | tee "$OUTPUT_DIR/pk-test-run.txt"

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# The same authenticator, with the UV flag clear, against both settings.
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
{
header "userVerification PREFERRED - the default"
start_app "" > /dev/null
ceremony no-uv
stop_app
header "userVerification REQUIRED - the uvrequired profile"
start_app uvrequired > /dev/null
ceremony no-uv
stop_app
} 2>&1 | tee "$OUTPUT_DIR/pk-user-verification.txt"