Registrations- und Authentifizierungsablauf
WebAuthn hat zwei ceremony (Zeremonien): Registrierung und Authentifizierung. Beide haben symmetrische Struktur – die RP stellt zuerst Optionen mit challenge (Herausforderung) aus, der Browser treibt die Authentifizierer-Erstellung an, und die RP führt dann strikte Validierung durch. Für Terminologie siehe Kernkonzepte, für Felddetails siehe Referenz.
Eins. Registrierungszeremonie
Schrittweiser Ablauf
- RP-Server erzeugt
PublicKeyCredentialCreationOptions: Enthält eine einmaligechallenge,rp(RP-Information),user(Benutzerhandle),pubKeyCredParams(akzeptable Algorithmen) sowie optionaleauthenticatorSelection,excludeCredentials,attestation.challengeunduser.idsind binär und müssen in Base64URL für das Frontend kodiert werden. - Frontend dekodiert und ruft
navigator.credentials.create()auf: Nach dem Dekodieren von Base64URL-Feldern zuArrayBufferaufrufen. Browser validiert die Übereinstimmung zwischenrp.idund aktueller Origin. - Browser + Authentifizierer: Browser stellt
clientDataJSONzusammen und fordert über CTAP den Authentifizierer auf. Der Authentifizierer validiert Benutzer (UP/UV), generiert ein neues Schlüsselpaar, speichert den privaten Schlüssel und produziertattestationObject. - Authentifizierer gibt
PublicKeyCredentialzurück: SeinresponseistAuthenticatorAttestationResponse, enthältattestationObjectundclientDataJSON. - Frontend sendet Ergebnis (Base64URL-kodiert) POST an RP.
- RP validiert und persistiert: Challenge/Origin/Type/Flags validieren,
attestationObjectparsen umcredentialPublicKey,credentialId,signCountzu extrahieren und zusammen mit dem Benutzerkonto zu speichern.
Frontend-Beispiel: Registrierung einleiten
// options von RP, challenge / user.id / excludeCredentials[].id sind Base64URL-Strings
const options = await fetch('/webauthn/register/options', {
method: 'POST', credentials: 'include',
}).then(r => r.json());
// Base64URL zu ArrayBuffer konvertieren
const b64urlToBuf = (s) =>
Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)).buffer;
options.challenge = b64urlToBuf(options.challenge);
options.user.id = b64urlToBuf(options.user.id);
if (options.excludeCredentials) {
options.excludeCredentials = options.excludeCredentials.map(c => ({ ...c, id: b64urlToBuf(c.id) }));
}
const cred = await navigator.credentials.create({ publicKey: options });
// Ergebnis zurück zu Base64URL kodieren und an RP senden
const bufToB64url = (buf) =>
btoa(String.fromCharCode(...new Uint8Array(buf)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
await fetch('/webauthn/register/verify', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: cred.id,
rawId: bufToB64url(cred.rawId),
type: cred.type, // "public-key"
response: {
clientDataJSON: bufToB64url(cred.response.clientDataJSON),
attestationObject: bufToB64url(cred.response.attestationObject),
transports: cred.response.getTransports?.() ?? [],
},
}),
});
JSON-Beispiel: PublicKeyCredentialCreationOptions (von RP ausgegeben, Felder sind Base64URL)
{
"rp": { "id": "example.com", "name": "Example Corp" },
"user": {
"id": "S3v9Y2k...Base64URL...",
"name": "[email protected]",
"displayName": "Alice"
},
"challenge": "b3Blbi1jaGFsbGVuZ2UtcmFuZG9t",
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"timeout": 60000,
"excludeCredentials": [
{ "type": "public-key", "id": "ZXhpc3RpbmctY3JlZC1pZA", "transports": ["internal"] }
],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "preferred"
},
"attestation": "none"
}
JSON-Beispiel: Registrierungsrückgabe (Frontend POST an RP)
{
"id": "AbCd...credentialId-base64url",
"rawId": "AbCd...credentialId-base64url",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwi...",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YV...",
"transports": ["internal", "hybrid"]
}
}
Tips
excludeCredentials listet bereits registrierte Anmeldedaten dieses Benutzers auf und verhindert eine Neu-Registrierung auf demselben Authentifizierer. Wenn der Authentifizierer eine entsprechende vorhandene Anmeldedaten findet, weigert er sich und meldet InvalidStateError.
Zwei. Authentifizierungszeremonie
Schrittweiser Ablauf
- RP erzeugt
PublicKeyCredentialRequestOptions: Enthält eine neue einmaligechallenge,rpId, optionaleallowCredentials(Credential ID-Liste des registrierten Benutzers),userVerification. Für namenslose Passkey-Anmeldung kannallowCredentialsleer gelassen werden. - Frontend ruft
navigator.credentials.get()auf: Browser validiert Origin erneut und fordert Benutzer auf, einen Authentifizierer auszuwählen/zu validieren. - Authentifizierer: Lokalisiert den privaten Schlüssel (über
allowCredentialsoder erkennbare Anmeldedaten), validiert UP/UV, signiertauthenticatorData ‖ SHA-256(clientDataJSON)mit dem privaten Schlüssel. - Gibt
PublicKeyCredentialzurück:responseistAuthenticatorAssertionResponse, enthältauthenticatorData,clientDataJSON,signatureund im Szenario erkennbarer AnmeldedatenuserHandle. - Frontend sendet POST an RP.
- RP validiert: Ruft den gespeicherten öffentlichen Schlüssel über Credential ID (oder
userHandle) ab, validiert Challenge/Origin/Type/Flags/Counter, validiert die Signatur mit dem öffentlichen Schlüssel. Bei Erfolg ist die Anmeldung erfolgreich undsignCountwird aktualisiert.
Frontend-Beispiel: Authentifizierung einleiten
const options = await fetch('/webauthn/login/options', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: '[email protected]' }), // kann bei namenloser Anmeldung weggelassen werden
}).then(r => r.json());
options.challenge = b64urlToBuf(options.challenge);
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(c => ({ ...c, id: b64urlToBuf(c.id) }));
}
const assertion = await navigator.credentials.get({ publicKey: options });
await fetch('/webauthn/login/verify', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
rawId: bufToB64url(assertion.rawId),
type: assertion.type,
response: {
clientDataJSON: bufToB64url(assertion.response.clientDataJSON),
authenticatorData: bufToB64url(assertion.response.authenticatorData),
signature: bufToB64url(assertion.response.signature),
userHandle: assertion.response.userHandle
? bufToB64url(assertion.response.userHandle) : null,
},
}),
});
JSON-Beispiel: PublicKeyCredentialRequestOptions (von RP ausgegeben)
{
"challenge": "YXV0aC1jaGFsbGVuZ2UtcmFuZG9t",
"timeout": 60000,
"rpId": "example.com",
"allowCredentials": [
{ "type": "public-key", "id": "AbCd...credentialId-base64url", "transports": ["internal"] }
],
"userVerification": "preferred"
}
JSON-Beispiel: Authentifizierungsrückgabe (Frontend POST an RP)
{
"id": "AbCd...credentialId-base64url",
"rawId": "AbCd...credentialId-base64url",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoi...",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MdAAAAAA",
"signature": "MEUCIQD...der-encoded-ecdsa-signature",
"userHandle": "S3v9Y2k...Base64URL"
}
}
Drei. RP-Server-Validierungscheckliste
Unabhängig von Registrierung oder Authentifizierung muss der Server jeden Punkt validieren (vertrauen Sie nicht dem Frontend):
- Parsen Sie
clientDataJSONzu JSON. typekorrekt: Registrierung muss"webauthn.create"sein, Authentifizierung muss"webauthn.get"sein.- Challenge stimmt überein:
clientDataJSON.challenge(Base64URL) dekodiert und byte-für-byte identisch mit dieser Sitzung ausgegebener Challenge; diese Challenge wurde noch nicht verwendet. - Origin stimmt überein:
clientDataJSON.origingehört zu einem berechtigten Origin-Set der RP (genaue Zeichenfolge, einschließlich Protokoll und Port). - (Optional)
crossOriginWenntrue, muss es den Geschäftserwartungen entsprechen, normalerweise sollte esfalsesein. rpIdHashvalidieren: Erste 32 Bytes vonauthenticatorDataentsprechenSHA-256(rpId).- Flags validieren:
UPBit muss 1 sein. Wenn Multi-Faktor erforderlich ist, mussUVBit 1 sein. - Signatur-Zähler: Vergleichen Sie
signCountmit gespeichertem Wert. Neuer Wert sollte größer sein (außer für Authentifizierer mit konstant 0). - Signatur-Validierung (nur Authentifizierungszeremonie): Verwenden Sie den gespeicherten öffentlichen Schlüssel, um
signaturegegenauthenticatorData ‖ SHA-256(clientDataJSON)zu validieren. - Algorithmus konsistent: Der Validierungsalgorithmus stimmt mit bei Registrierung aufgezeichnetem
algüberein.
Tips
Es wird dringend empfohlen, etablierte Bibliotheken zu verwenden (wie Server @simplewebauthn/server, Frontend @simplewebauthn/browser, Gos go-webauthn, Javas webauthn4j) um CBOR-Parsing, COSE-Public-Key-Konvertierung und Signatur-Validierung zu handhaben, statt diese kryptographischen Details manuell zu schreiben.
Vier. Häufige Fallstricke
Warnung
- Challenge ist nicht einmalig: Nicht nach Validierung verworfen, führt zu Wiederholung. Muss vom Server gespeichert, an Sitzung gebunden und nach Verwendung gelöscht werden.
- Origin nicht exakt verglichen: Mit
endsWithoder ignorierten Ports/Protokollen verglichen, kann umgangen werden. Muss exakt die ganze Zeichenfolge vergleichen. - Counter-Verarbeitung fehlerhaft: Synchronisierte Passkeys mit konstant 0 direkt wegen "nicht steigernd" ablehnen; oder echte Rückfallserkennung verpassen.
- rpId stimmt nicht mit Origin überein:
rp.idist nicht die registrierbare Domäne der aktuellen Domäne oder deren übergeordnet,create()/get()wirft direktSecurityError. - Base64 und Base64URL gemischt: WebAuthn verwendet Base64URL ohne Padding; Verwechslung mit standardmäßigem Base64 führt zu Dekodierungsverwerfung.
excludeCredentialsvergessen: Dasselbe Authentifizierer wird doppelt registriert, erzeugt redundante Anmeldedaten.- Nur auf
userVerificationin Anfrageparametern vertrauen: Muss UV-Flag in der Antwort auf dem Server überprüfen, nicht annehmen, dass der Authentifizierer die Anforderung befolgt hat. - Keine Benutzer-Präsenz Timeout/Wiederholung-Verarbeitung:
create()/get()wirft möglicherweiseNotAllowedErrorbei Timeout oder Benutzer-Abbruch. Frontend sollte freundlich vorgehen.
Weiterlesen: Parameter- und Datenstruktur-Referenz.