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.
