Passkeys / WebAuthn

Passkeys / WebAuthn for directory users.

Passkeys / WebAuthn

lindir is a WebAuthn relying party. Users register platform or roaming passkeys and can sign in on the OIDC login page without a password.

What is supported

Piece Detail
Algorithms ES256 (P-256) — covers platform passkeys
Attestation none (packed authData still accepted if present)
Registration Admin Bearer API (/v1/webauthn/register/*)
Authentication Public API (/v1/webauthn/authenticate/*)
OIDC Login page Sign in with passkey → short-lived loginToken → auth code
Resident keys Preferred; discoverable login (empty username) supported

Config

{
  "issuerUrl": "https://id.example.com",
  "webauthnRpId": "id.example.com",
  "webauthnRpName": "lindir",
  "webauthnOrigins": ["https://id.example.com"]
}

Defaults: webauthnRpId = hostname of issuerUrl (or listen host); webauthnOrigins = [issuerUrl].
WebAuthn requires a secure context (HTTPS or localhost). Use requireForwardedHttps behind a TLS terminator.

Register a passkey (admin)

Admin UI: Directory → Passkeys (/admin/access/passkeys) — pick a user, register on this device, list/revoke.

Or via API:

# 1) Options for navigator.credentials.create
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"userId\":\"$USER_ID\"}" \
  https://id.example.com/v1/webauthn/register/begin

# 2) After create(), finish
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"userId\":\"$USER_ID\",\"name\":\"laptop\",\"credential\":{...}}" \
  https://id.example.com/v1/webauthn/register/finish

Binary fields in JSON are base64url (no padding). Convert to ArrayBuffer in the browser before calling the WebAuthn API.

List / revoke:

# CLI
lindir-admin passkey list --user-id "$USER_ID"
lindir-admin passkey revoke --id CREDENTIAL_ROW_UUID

# API
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://id.example.com/v1/webauthn/credentials?userId=$USER_ID"
curl -s -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"id":"CREDENTIAL_ROW_UUID"}' \
  https://id.example.com/v1/webauthn/credentials/revoke

Authenticate

curl -s -H "Content-Type: application/json" \
  -d '{"username":"alice"}' \
  https://id.example.com/v1/webauthn/authenticate/begin
# navigator.credentials.get(...)
curl -s -H "Content-Type: application/json" \
  -d '{"credential":{...}}' \
  https://id.example.com/v1/webauthn/authenticate/finish
# → { "loginToken": "wat_…", "username": "alice", ... }

loginToken is single-use (~2 minutes). OIDC authorize accepts it as form field login_token instead of a password.

OIDC UI

GET /oidc/authorize shows password and Sign in with passkey. The passkey button runs begin → credentials.get → finish → posts login_token to authorize.