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:
9
passkeys/scripts/attestation.sh
Executable file
9
passkeys/scripts/attestation.sh
Executable 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
9
passkeys/scripts/bootstrap.sh
Executable 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
11
passkeys/scripts/ceremony.sh
Executable 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
9
passkeys/scripts/counter.sh
Executable 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
9
passkeys/scripts/duplicate.sh
Executable 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
9
passkeys/scripts/filters.sh
Executable 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
13
passkeys/scripts/jdbc.sh
Executable 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
63
passkeys/scripts/lib.sh
Executable 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
15
passkeys/scripts/origin.sh
Executable 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
|
||||
9
passkeys/scripts/ott-fallback.sh
Executable file
9
passkeys/scripts/ott-fallback.sh
Executable 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
13
passkeys/scripts/run-all.sh
Executable 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
13
passkeys/scripts/run.sh
Executable 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
9
passkeys/scripts/step-up.sh
Executable 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
5
passkeys/scripts/test-run.sh
Executable 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"
|
||||
14
passkeys/scripts/user-verification.sh
Executable file
14
passkeys/scripts/user-verification.sh
Executable 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"
|
||||
Reference in New Issue
Block a user