Signatur
Beschreibung
Die schreibgeschützte Eigenschaft clientDataJSON der Schnittstelle AuthenticatorResponse enthält die sogenannten Client-Daten. Der Browser stellt sie bei einem Aufruf von navigator.credentials.create() (Registrierung) oder navigator.credentials.get() (Anmeldung) zusammen. Es handelt sich um einen JSON-String, der als UTF-8-Bytefolge in einem ArrayBuffer liegt. Verfügbar ist die Eigenschaft sowohl auf AuthenticatorAttestationResponse als auch auf AuthenticatorAssertionResponse.
Das dekodierte Objekt enthält unter anderem:
type:"webauthn.create"bei der Registrierung,"webauthn.get"bei der Anmeldung.challenge: die vom Server erzeugte Challenge, base64url-kodiert.origin: der vollständige Origin der aufrufenden Seite, z. B."https://example.com".crossOrigin(optional):true, wenn der Aufruf aus einem Cross-Origin-iframe kam.topOrigin(optional): der Origin des obersten Dokuments bei Cross-Origin-Aufrufen.
Die Signatur des Authenticators wird über authenticatorData zusammen mit dem SHA-256-Hash der exakten Bytes von clientDataJSON gebildet. Deshalb schickt man den Puffer unverändert an den Server, meist base64url-kodiert. Dort werden type, challenge und origin geprüft, danach wird die Signatur verifiziert.
Rückgabewert
ArrayBuffer mit dem UTF-8-kodierten JSON der Client-Daten. Mit new TextDecoder().decode() und JSON.parse() wird daraus ein Objekt.Beispiele
Client-Daten nach der Registrierung dekodieren
// challenge und userId kommen in der Praxis vom Server
const challenge = crypto.getRandomValues(new Uint8Array(32));
const credential = await navigator.credentials.create({
publicKey: {
challenge,
rp: { name: 'Beispiel GmbH' },
user: {
id: crypto.getRandomValues(new Uint8Array(16)),
name: 'max@example.com',
displayName: 'Max Mustermann'
},
pubKeyCredParams: [{ type: 'public-key', alg: -7 }]
}
});
const json = new TextDecoder().decode(credential.response.clientDataJSON);
const clientData = JSON.parse(json);
console.log(clientData);
Assertion base64url-kodiert an den Server senden
const toBase64Url = (buffer) =>
btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const login = async (challenge) => {
const assertion = await navigator.credentials.get({
publicKey: { challenge, userVerification: 'preferred' }
});
const { response } = assertion;
await fetch('/webauthn/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
clientDataJSON: toBase64Url(response.clientDataJSON), // Bytes unverändert!
authenticatorData: toBase64Url(response.authenticatorData),
signature: toBase64Url(response.signature)
})
});
};
Serverseitige Prüfung der Client-Daten (Node.js)
import { createHash } from 'node:crypto';
const verifyClientData = (clientDataB64, expectedChallenge, expectedOrigin) => {
const raw = Buffer.from(clientDataB64, 'base64url');
const data = JSON.parse(raw.toString('utf8'));
if (data.type !== 'webauthn.get') throw new Error('Falscher Typ');
if (data.challenge !== expectedChallenge) throw new Error('Challenge ungültig');
if (data.origin !== expectedOrigin) throw new Error('Origin ungültig');
// Dieser Hash fließt in die Signaturprüfung ein
return createHash('sha256').update(raw).digest();
};
console.log(verifyClientData(
Buffer.from(JSON.stringify({ type: 'webauthn.get', challenge: 'abc', origin: 'https://example.com' })).toString('base64url'),
'abc',
'https://example.com'
).length);
// Wichtig · Fallstricke
- Nicht neu serialisieren: Der Hash für die Signatur wird über die Original-Bytes gebildet. Wenn du das JSON parst und mit
JSON.stringify()neu erzeugst, ändern sich Reihenfolge oder Leerzeichen, und die Prüfung schlägt fehl. - Die Challenge ist base64url-kodiert: Sie steht also nicht als Standard-Base64 im JSON, sondern mit
-und_und ohne=-Auffüllung. Vergleiche sie entsprechend. - Sicherheit: Prüfe
origin,typeund die Challenge immer serverseitig, und nutze die Challenge nur einmal (Schutz vor Wiederholungsangriffen). Eine Prüfung im Client allein bietet keinen Schutz vor Phishing oder manipulierten Clients. - Zusätzliche Felder: Das JSON kann weitere, auch künftige Felder enthalten, z. B.
topOrigin. Deshalb nicht strikt auf eine feste Feldliste prüfen. - Kompatibilität: Die Eigenschaft funktioniert nur in sicheren Kontexten (HTTPS oder
localhost). In Chrome, Edge, Firefox und Safari wird sie seit Jahren unterstützt. Für die Serialisierung gibt es neuere Hilfsmittel wiePublicKeyCredential.prototype.toJSON(), die noch nicht überall verfügbar sind.