WebAuthn (Web Authentication API) to standard W3C umożliwiający bezpieczne uwierzytelnianie bez hasła przy użyciu kryptografii klucza publicznego. Zamiast haseł użytkownicy rejestrują się i uwierzytelniają przy użyciu sprzętowych urządzeń uwierzytelniających (takich jak czytniki linii papilarnych, Face ID, YubiKeys itp.) lub uwierzytelniatorów platformowych (wbudowanych, jak Touch ID).
Poniżej przedstawiono, jak obsługiwać rejestrację i uwierzytelnianie przy użyciu klienta JavaScript.
Jak działa rejestracja WebAuthn
Użytkownik inicjuje rejestrację:
Użytkownik podaje nazwę użytkownika i klika Zarejestruj.
Przeglądarka żąda opcji od serwera:
Frontend wysyła żądanie POST w celu uzyskania opcji rejestracji.
Przeglądarka tworzy poświadczenia:
Za pomocą API navigator.credentials.create() przez SimpleWebAuthnBrowser.startRegistration() tworzone jest poświadczenie.
Serwer weryfikuje rejestrację:
Przeglądarka odsyła dane uwierzytelniające do serwera.
Serwer weryfikuje rejestrację i przechowuje klucz publiczny dla tego użytkownika.
Komponent TsgcWSAPIServer_WebAuthn zawiera plik HTML służący do testowania protokołu WebAuthn. Plik HTML zawiera minimalny interfejs użytkownika oraz kod JavaScript umożliwiający interakcję z WebAuthn.
Omówienie pliku sgcWebAuthn.html
Przegląd struktury:
Dane wejściowe nazwy użytkownika: Przechwytuje identyfikator użytkownika.
Przyciski:
Register – inicjuje rejestrację WebAuthn.
Authenticate – inicjuje logowanie (obsługiwane analogicznie).
Konsola debugowania: Wyświetla informacje debugowania w czasie rzeczywistym (JSON z WebAuthn).
1. Interfejs HTML do wprowadzania danych
<input type="text" id="username" name="username" autocomplete="username webauthn" />
<button id="btnRegBegin"><strong>Register</strong></button>
2. JavaScript: procedura obsługi kliknięcia przycisku
document.querySelector('#btnRegBegin').addEventListener('click', async () => {
const username = document.getElementById("username").value;
if (username == "") {
document.getElementById('Error').innerText = 'Please enter a username to register';
return;
}
const resp = await fetch('<#webauthn_registration_options>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, algorithms: [] })
});
const options = await resp.json();
const attResp = await startRegistration(options); // WebAuthn API
3. Odpowiedź serwera (fałszywy punkt końcowy w HTML)
fetch('/sgcWebAuthn/Registration/Options', ...)
fetch('/sgcWebAuthn/Registration/Verify', ...)
4. Finalizowanie rejestracji
const verificationResp = await fetch('/webauthn/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(attResp)
});
const verificationJSON = await verificationResp.json();
if (verificationJSON && verificationJSON.verified) {
document.getElementById('Success').innerHTML = `Authenticator registered!`;
}
Jak działa uwierzytelnianie WebAuthn
Użytkownik inicjuje uwierzytelnianie:
Użytkownik podaje nazwę użytkownika i klika Zarejestruj.
Przeglądarka żąda opcji od serwera:
Interfejs użytkownika wysyła żądanie POST, aby uzyskać opcje uwierzytelniania.
Przeglądarka tworzy poświadczenia:
Za pomocą API navigator.credentials.get() przez SimpleWebAuthnBrowser.startAuthentication().
Serwer weryfikuje uwierzytelnianie:
Przeglądarka odsyła poświadczenie do serwera.
Serwer weryfikuje podpisany wynik.
1. Konfiguracja interfejsu HTML
<div class="container">
<h1>WebAuthn Authentication Sample</h1>
<section id="userdata">
<label for="username">Username:</label>
<input type="text" id="username" name="username" autocomplete="username webauthn" autofocus />
</section>
<button id="btnAuthBegin"><strong>Authenticate</strong></button>
<p id="Success" class="success"></p>
<p id="Error" class="error"></p>
<details open>
<summary>Console</summary>
<textarea id="Debug"></textarea>
</details>
</div>
2. Pobierz opcje uwierzytelniania
Przed wywołaniem startAuthentication należy wysłać nazwę użytkownika do serwera
const resp = await fetch('<#webauthn_authentication_options>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: document.getElementById("username").value,
user_verification: 'preferred'
}),
});
Serwer odpowiada obiektem JSON zawierającym:
{
"challenge": "base64url-encoded-random-string",
"allowCredentials": [
{
"id": "base64url-credential-id",
"type": "public-key"
}
],
"userVerification": "preferred",
"rpId": "yourdomain.com"
}
Jest to PublicKeyCredentialRequestOptions.
3. Odbierz odpowiedź uwierzytelniającą
Odpowiedź asseResp wygląda następująco (uproszczona):
{
"id": "credentialId",
"rawId": "base64url-encoded-id",
"response": {
"authenticatorData": "...",
"clientDataJSON": "...",
"signature": "...",
"userHandle": "..."
},
"type": "public-key",
"clientExtensionResults": {}
}
Odpowiedź ta dowodzi, że użytkownik:
4. Wyślij podpisaną odpowiedź uwierzytelniającą do serwera
Po interakcji użytkownika z jego uwierzytelniaczem (za pomocą startAuthentication()), w języku JavaScript otrzymywany jest obiekt odpowiedzi.
const verificationResp = await fetch('/webauthn/authenticate/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(asseResp), // this is the signed response
});
5. Pobierz odpowiedź serwera
Serwer odpowie wynikiem w następującej postaci:
{ "verified": true }
Lub jeśli coś poszło nie tak:
{ "verified": false, "error": "Invalid signature" }
Obsługuje się to w kodzie frontendowym w następujący sposób:
const result = await verificationResp.json();
if (result.verified) {
document.getElementById('Success').textContent = 'User authenticated!';
} else {
document.getElementById('Error').textContent = 'Authentication failed!';
}