1. Zanim zaczniesz
Używanie kluczy dostępu zamiast haseł to świetny sposób na zwiększenie bezpieczeństwa kont użytkowników w witrynach, a także na uproszczenie i ułatwienie korzystania z nich. Umożliwiają logowanie się w witrynie lub aplikacji za pomocą funkcji blokady ekranu urządzenia, np. odcisku palca, skanu twarzy lub kodu PIN. Zanim użytkownik będzie mógł zalogować się za pomocą klucza dostępu, musi go utworzyć, powiązać z kontem użytkownika i zapisać jego klucz publiczny na serwerze.
W tym laboratorium nauczysz się przekształcać podstawowe logowanie za pomocą formularza z nazwą użytkownika i hasłem w logowanie obsługujące klucze dostępu. Obejmuje ono te elementy:
- Przycisk, który tworzy klucz dostępu po zalogowaniu się użytkownika.
- Interfejs z listą zarejestrowanych kluczy dostępu.
- Istniejący formularz logowania, który umożliwia użytkownikom logowanie się za pomocą zarejestrowanego klucza dostępu dzięki autouzupełnianiu formularza.
Wymagania wstępne
- Podstawowa znajomość JavaScriptu
- Podstawowa wiedza o kluczach dostępu
- Podstawowa wiedza na temat interfejsu Web Authentication API (WebAuthn)
Czego się nauczysz
- Jak utworzyć klucz dostępu
- Jak uwierzytelniać użytkowników za pomocą klucza dostępu.
- Jak umożliwić formularzowi sugerowanie klucza dostępu jako opcji logowania.
Czego potrzebujesz
Jedna z tych kombinacji urządzeń:
- Google Chrome na urządzeniu z Androidem w wersji 9 lub nowszej, najlepiej z czujnikiem biometrycznym.
- Chrome na urządzeniu z systemem Windows 10 lub nowszym.
- Safari 16 lub nowsza na iPhonie z systemem iOS 16 lub nowszym albo na iPadzie z systemem iPadOS 16 lub nowszym.
- Safari 16 lub nowsza albo Chrome na urządzeniu stacjonarnym Apple z systemem macOS Ventura lub nowszym.
2. Konfiguracja
W tym module użyjesz usługi Glitch, która umożliwia edytowanie kodu po stronie klienta i serwera w JavaScript oraz wdrażanie go bezpośrednio z przeglądarki.
Otwórz projekt
- Otwórz projekt w Glitchu.
- Kliknij Remix, aby rozwidlić projekt Glitch.
- W menu nawigacyjnym u dołu Glitcha kliknij Podgląd > Podgląd w nowym oknie. W przeglądarce otworzy się kolejna karta.
Sprawdź stan początkowy witryny
- Na karcie podglądu wpisz losową nazwę użytkownika, a następnie kliknij Dalej.
- Wpisz losowe hasło, a potem kliknij Zaloguj się. Hasło jest ignorowane, ale użytkownik jest uwierzytelniany i przekierowywany na stronę główną.
- Jeśli chcesz zmienić nazwę wyświetlaną, zrób to. To wszystko, co możesz zrobić w stanie początkowym.
- Kliknij Wyloguj się.
W tym stanie użytkownicy muszą wpisywać hasło przy każdym logowaniu. Dodaj do tego formularza obsługę kluczy dostępu, aby użytkownicy mogli logować się za pomocą funkcji blokady ekranu urządzenia. Stan końcowy możesz sprawdzić na stronie https://passkeys-codelab.glitch.me/.
Więcej informacji o działaniu kluczy dostępu znajdziesz w artykule Jak działają klucze dostępu?.
3. Dodawanie możliwości utworzenia klucza dostępu
Aby umożliwić użytkownikom uwierzytelnianie za pomocą klucza dostępu, musisz dać im możliwość utworzenia i zarejestrowania klucza dostępu oraz przechowywania jego klucza publicznego na serwerze.
Chcesz zezwolić na tworzenie klucza dostępu po zalogowaniu się użytkownika za pomocą hasła i dodać interfejs, który umożliwi użytkownikom tworzenie klucza dostępu i wyświetlanie listy wszystkich zarejestrowanych kluczy dostępu na stronie /home
. W następnej sekcji utworzysz funkcję, która tworzy i rejestruje klucz dostępu.
Tworzenie funkcji registerCredential()
- W Glitch otwórz plik
public/client.js
i przewiń go do końca. - Po odpowiednim komentarzu dodaj tę funkcję
registerCredential()
:
public/client. js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to create a passkey: Create a credential.
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
};
Ta funkcja tworzy i rejestruje klucz dostępu na serwerze.
Pobieranie wyzwania i innych opcji z punktu końcowego serwera
Zanim utworzysz klucz dostępu, musisz poprosić serwer o przekazanie parametrów do WebAuthn, w tym wyzwania. WebAuthn to interfejs API przeglądarki, który umożliwia użytkownikowi utworzenie klucza dostępu i uwierzytelnienie go za jego pomocą. Na szczęście w tym laboratorium masz już punkt końcowy serwera, który odpowiada takimi parametrami.
- Aby uzyskać wyzwanie i inne opcje z punktu końcowego serwera, dodaj ten kod do treści funkcji
registerCredential()
po odpowiednim komentarzu:
public/client.js
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');
Poniższy fragment kodu zawiera przykładowe opcje, które otrzymujesz z serwera:
{
challenge: *****,
rp: {
id: "example.com",
},
user: {
id: *****,
name: "john78",
displayName: "John",
},
pubKeyCredParams: [{
alg: -7, type: "public-key"
},{
alg: -257, type: "public-key"
}],
excludeCredentials: [{
id: *****,
type: 'public-key',
transports: ['internal', 'hybrid'],
}],
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
}
}
Protokół między serwerem a klientem nie jest częścią specyfikacji WebAuthn. Serwer w tym laboratorium kodu został jednak zaprojektowany tak, aby zwracać plik JSON jak najbardziej podobny do słownika PublicKeyCredentialCreationOptions
przekazywanego do interfejsu WebAuthn navigator.credentials.create()
API.
Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w słowniku PublicKeyCredentialCreationOptions
:
Parametry | Teksty reklam |
Wygenerowane przez serwer wyzwanie w obiekcie | |
Unikalny identyfikator użytkownika. Ta wartość musi być obiektem | |
To pole powinno zawierać unikalny identyfikator konta rozpoznawalny przez użytkownika, np. jego adres e-mail lub nazwę użytkownika. Jest on widoczny w selektorze kont. (Jeśli używasz nazwy użytkownika, użyj tej samej wartości co w przypadku uwierzytelniania hasłem). | |
To pole zawiera opcjonalną, przyjazną dla użytkownika nazwę konta. Nie musi być niepowtarzalna i może być wybraną przez użytkownika nazwą. Jeśli w Twojej witrynie nie ma odpowiedniej wartości, przekaż pusty ciąg znaków. W zależności od przeglądarki może się ona wyświetlać w selektorze kont. | |
Identyfikator podmiotu polegającego na tożsamości (RP) to domena. Witryna może określić swoją domenę lub sufiks, który można zarejestrować. Jeśli na przykład źródło RP to https://login.example.com:1337, identyfikator RP może mieć postać | |
To pole określa obsługiwane przez RP algorytmy klucza publicznego. Zalecamy ustawienie go na | |
Zawiera listę zarejestrowanych identyfikatorów danych logowania, aby zapobiec dwukrotnej rejestracji tego samego urządzenia. Jeśli jest podany, element | |
Ustaw wartość | |
Ustaw wartość logiczną | |
Ustaw wartość |
Tworzenie danych logowania
- W treści funkcji
registerCredential()
po odpowiednim komentarzu przekonwertuj niektóre parametry zakodowane w standardzie Base64URL z powrotem na postać binarną, a mianowicie ciągi tekstoweuser.id
ichallenge
oraz wystąpienia ciągu tekstowegoid
zawarte w tablicyexcludeCredentials
:
public/client.js
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
- W następnym wierszu ustaw
authenticatorSelection.authenticatorAttachment
na"platform"
iauthenticatorSelection.requireResidentKey
natrue
. Umożliwia to używanie tylko uwierzytelniania platformy (samego urządzenia) z funkcją wykrywalnych danych logowania.
public/client.js
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
- W następnym wierszu wywołaj metodę
navigator.credentials.create()
, aby utworzyć dane logowania.
public/client.js
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
W ramach tego wywołania przeglądarka próbuje zweryfikować tożsamość użytkownika za pomocą blokady ekranu urządzenia.
Zarejestruj dane logowania w punkcie końcowym serwera.
Po zweryfikowaniu tożsamości użytkownika tworzony i zapisywany jest klucz dostępu. Witryna otrzymuje obiekt danych logowania, który zawiera klucz publiczny, który możesz wysłać na serwer, aby zarejestrować klucz dostępu.
Ten fragment kodu zawiera przykładowy obiekt danych logowania:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"attestationObject": *****,
"transports": ["internal", "hybrid"]
},
"authenticatorAttachment": "platform"
}
Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w obiekcie PublicKeyCredential
:
Parametry | Teksty reklam |
Identyfikator utworzonego klucza dostępu zakodowany w formacie Base64URL. Ten identyfikator pomaga przeglądarce określić, czy podczas uwierzytelniania na urządzeniu znajduje się pasujący klucz dostępu. Ta wartość musi być przechowywana w bazie danych na backendzie. | |
| |
Obiekt | |
Zakodowany obiekt atestu | |
Lista transportów obsługiwanych przez urządzenie: | |
Zwraca wartość |
Aby wysłać obiekt danych logowania na serwer:
- Zakoduj parametry binarne danych logowania w formacie Base64URL, aby można było przekazać je do serwera jako ciąg tekstowy:
public/client.js
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
- W następnym wierszu wyślij obiekt na serwer:
public/client.js
return await _fetch('/auth/registerResponse', credential);
Gdy uruchomisz program, serwer zwróci wartość HTTP code 200
, co oznacza, że dane logowania są zarejestrowane.
Masz już pełną funkcję registerCredential()
.
Sprawdź kod rozwiązania w tej sekcji
public/client.js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
const options = await _fetch('/auth/registerRequest');
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject =
base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ?
cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
return await _fetch('/auth/registerResponse', credential);
};
4. Tworzenie interfejsu do rejestrowania danych logowania za pomocą klucza dostępu i zarządzania nimi
Teraz, gdy funkcja registerCredential()
jest dostępna, potrzebujesz przycisku, aby ją wywołać. Musisz też wyświetlać listę zarejestrowanych kluczy dostępu.
Dodawanie kodu HTML obiektu zastępczego
- W Glitch otwórz plik
views/home.html
. - Po odpowiednim komentarzu dodaj element zastępczy interfejsu, który wyświetla przycisk rejestracji klucza dostępu i listę kluczy dostępu:
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
Element div#list
jest elementem zastępczym listy.
Sprawdzanie obsługi kluczy dostępu
Aby wyświetlać opcję tworzenia klucza dostępu tylko użytkownikom, których urządzenia obsługują klucze dostępu, musisz najpierw sprawdzić, czy WebAuthn jest dostępny. Jeśli tak, musisz usunąć klasę hidden
, aby wyświetlić przycisk Utwórz klucz dostępu.
Aby sprawdzić, czy środowisko obsługuje klucze dostępu, wykonaj te czynności:
- Na końcu pliku
views/home.html
, po odpowiednim komentarzu, napisz warunek, który zostanie wykonany, jeśli zmiennewindow.PublicKeyCredential
,PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
iPublicKeyCredential.isConditionalMediationAvailable
mają wartośćtrue
.
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
- W treści warunku sprawdź, czy urządzenie może utworzyć klucz dostępu, a następnie sprawdź, czy klucz dostępu można zasugerować w ramach automatycznego wypełniania formularza.
views/home.html
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
- Jeśli wszystkie warunki są spełnione, wyświetl przycisk tworzenia klucza dostępu. W przeciwnym razie wyświetl komunikat ostrzegawczy.
views/home.html
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
Wyświetlanie zarejestrowanych kluczy dostępu na liście
- Zdefiniuj funkcję
renderCredentials()
, która pobiera zarejestrowane klucze dostępu z serwera i wyświetla je na liście. Na szczęście masz już/auth/getKeys
punkt końcowy serwera, który umożliwia pobieranie zarejestrowanych kluczy dostępu dla zalogowanego użytkownika.
views/home.html
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}"
data-name="${cred.name || 'Unnamed' }" @click="${rename}"
icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
- W następnym wierszu wywołaj funkcję
renderCredentials()
, aby wyświetlić zarejestrowane klucze dostępu, gdy tylko użytkownik wejdzie na stronę/home
w ramach inicjowania.
views/home.html
renderCredentials();
Tworzenie i rejestrowanie klucza dostępu
Aby utworzyć i zarejestrować klucz dostępu, musisz wywołać funkcję registerCredential()
, którą zaimplementowano wcześniej.
Aby wywołać funkcję registerCredential()
po kliknięciu przycisku Utwórz klucz dostępu, wykonaj te czynności:
- W pliku po zastępczym kodzie HTML znajdź to wyrażenie
import
:
views/home.html
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
} from '/client.js';
- Na końcu treści instrukcji
import
dodaj funkcjęregisterCredential()
.
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
- Na końcu pliku po odpowiednim komentarzu zdefiniuj funkcję
register()
, która wywołuje funkcjęregisterCredential()
i interfejs ładowania oraz wywołuje funkcjęrenderCredentials()
po rejestracji. Wyjaśnia to, że przeglądarka tworzy klucz dostępu i wyświetla komunikat o błędzie, gdy coś pójdzie nie tak.
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
- W treści funkcji
register()
przechwyć wyjątki. Metodanavigator.credentials.create()
zgłasza błądInvalidStateError
, gdy na urządzeniu istnieje już klucz dostępu. Jest to sprawdzane za pomocą tablicyexcludeCredentials
. W takim przypadku wyświetlasz użytkownikowi odpowiedni komunikat. W przypadku anulowania okna uwierzytelniania przez użytkownika zwraca też błądNotAllowedError
. W takim przypadku zignoruj go.
views/home.html
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
- W wierszu po funkcji
register()
dołącz funkcjęregister()
do zdarzeniaclick
dla przycisku Utwórz klucz dostępu.
views/home.html
createPasskey.addEventListener('click', register);
Sprawdź kod rozwiązania w tej sekcji
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
renderCredentials();
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
createPasskey.addEventListener('click', register);
Wypróbuj
Jeśli wykonasz wszystkie opisane dotychczas czynności, zaimplementujesz w witrynie możliwość tworzenia, rejestrowania i wyświetlania kluczy dostępu.
Aby wypróbować tę funkcję, wykonaj te czynności:
- Na karcie podglądu zaloguj się, podając losową nazwę użytkownika i hasło.
- Kliknij Utwórz klucz dostępu.
- Potwierdź swoją tożsamość za pomocą blokady ekranu urządzenia.
- Sprawdź, czy klucz dostępu jest zarejestrowany i wyświetla się w sekcji Twoje zarejestrowane klucze dostępu na stronie internetowej.
Zmienianie nazwy i usuwanie zarejestrowanych kluczy dostępu
Powinna być możliwość zmiany nazwy zarejestrowanych kluczy dostępu lub ich usunięcia z listy. Możesz sprawdzić, jak to działa w kodzie, ponieważ jest on dostępny w ramach laboratorium programowania.
W Chrome możesz usunąć zarejestrowane klucze dostępu ze strony chrome://settings/passkeys na komputerze lub z menedżera haseł w ustawieniach na urządzeniu z Androidem.
Informacje o tym, jak zmieniać nazwy zarejestrowanych kluczy dostępu i usuwać je na innych platformach, znajdziesz na odpowiednich stronach pomocy tych platform.
5. Dodanie możliwości uwierzytelniania za pomocą klucza dostępu
Użytkownicy mogą teraz tworzyć i rejestrować klucze dostępu, a potem bezpiecznie używać ich do uwierzytelniania w Twojej witrynie. Teraz musisz dodać do swojej witryny możliwość uwierzytelniania za pomocą klucza dostępu.
Tworzenie funkcji authenticate()
- W pliku
public/client.js
po odpowiednim komentarzu utwórz funkcję o nazwieauthenticate()
, która lokalnie weryfikuje użytkownika, a następnie na serwerze:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
};
Pobieranie wyzwania i innych opcji z punktu końcowego serwera
Zanim poprosisz użytkownika o uwierzytelnienie, musisz poprosić serwer o przekazanie parametrów do WebAuthn, w tym wyzwania.
- W treści funkcji
authenticate()
po odpowiednim komentarzu wywołaj funkcję_fetch()
, aby wysłać żądaniePOST
do serwera:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
Serwer w tym laboratorium kodowania został zaprojektowany tak, aby zwracać kod JSON jak najbardziej podobny do słownika PublicKeyCredentialRequestOptions
przekazywanego do interfejsu WebAuthn API navigator.credentials.get()
. Poniższy fragment kodu zawiera przykładowe opcje, które powinny zostać zwrócone:
{
"challenge": *****,
"rpId": "passkeys-codelab.glitch.me",
"allowCredentials": []
}
Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w słowniku PublicKeyCredentialRequestOptions
:
Parametry | Teksty reklam |
Wygenerowane przez serwer wyzwanie w obiekcie | |
Identyfikator RP to domena. Witryna może określić swoją domenę lub sufiks, który można zarejestrować. Ta wartość musi być zgodna z parametrem | |
Ta właściwość służy do znajdowania uwierzytelniaczy kwalifikujących się do tego uwierzytelniania. Przekaż pustą tablicę lub pozostaw ją nieokreśloną, aby przeglądarka wyświetliła selektor kont. | |
Ustaw wartość |
Lokalna weryfikacja użytkownika i uzyskanie danych logowania
- W treści funkcji
authenticate()
po odpowiednim komentarzu przekonwertuj parametrchallenge
z powrotem na postać binarną:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
- Aby otworzyć selektor konta, gdy użytkownik się uwierzytelni, przekaż pustą tablicę do parametru
allowCredentials
:
public/client.js
// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];
Selektor konta korzysta z informacji użytkownika przechowywanych w ramach klucza dostępu.
- Wywołaj metodę
navigator.credentials.get()
wraz z opcjąmediation: 'conditional'
:
public/client.js
// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
Ta opcja nakazuje przeglądarce warunkowe sugerowanie kluczy dostępu w ramach automatycznego wypełniania formularzy.
Weryfikacja danych logowania
Gdy użytkownik potwierdzi swoją tożsamość lokalnie, otrzymasz obiekt danych logowania zawierający podpis, który możesz zweryfikować na serwerze.
Ten fragment kodu zawiera przykładowy obiekt PublicKeyCredential
:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"authenticatorData": *****,
"signature": *****,
"userHandle": *****
},
authenticatorAttachment: "platform"
}
Poniższa tabela nie jest wyczerpująca, ale zawiera ważne parametry w obiekcie PublicKeyCredential
:
Parametry | Teksty reklam |
Identyfikator uwierzytelnionych danych logowania klucza dostępu zakodowany w formacie Base64URL. | |
| |
Obiekt | |
Obiekt | |
Obiekt | |
Obiekt | |
Zwraca ciąg znaków |
Aby wysłać obiekt danych logowania na serwer:
- W treści funkcji
authenticate()
po odpowiednim komentarzu zakoduj parametry binarne danych logowania, aby można je było przesłać na serwer jako ciąg znaków:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
- Wyślij obiekt na serwer:
public/client.js
return await _fetch(`/auth/signinResponse`, credential);
Po uruchomieniu programu serwer zwraca wartość HTTP code 200
, co oznacza, że dane logowania zostały zweryfikowane.
Masz teraz pełną funkcję authentication()
.
Sprawdź kod rozwiązania w tej sekcji
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the
challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
// TODO: Add an ability to authenticate with a passkey: Locally verify
the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
// The empty allowCredentials array invokes an account selector
by discoverable credentials.
options.allowCredentials = [];
// Invoke the WebAuthn get() function.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData =
base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
return await _fetch(`/auth/signinResponse`, credential);
};
6. Dodawanie kodów dostępu do automatycznego wypełniania w przeglądarce
Gdy użytkownik wróci, chcesz, aby mógł się zalogować w jak najprostszy i najbezpieczniejszy sposób. Jeśli dodasz do strony logowania przycisk Zaloguj się za pomocą klucza dostępu, użytkownik może go nacisnąć, wybrać klucz dostępu w selektorze kont w przeglądarce i użyć blokady ekranu, aby potwierdzić tożsamość.
Przejście z hasła na klucz dostępu nie następuje jednak u wszystkich użytkowników jednocześnie. Oznacza to, że nie możesz pozbyć się haseł, dopóki wszyscy użytkownicy nie przejdą na klucze dostępu, więc do tego czasu musisz pozostawić formularz logowania oparty na hasłach. Jeśli jednak pozostawisz formularz hasła i przycisk klucza dostępu, użytkownicy będą musieli dokonać niepotrzebnego wyboru, którego z nich użyć do zalogowania się. Najlepiej, aby proces logowania był prosty.
W takiej sytuacji przydaje się interfejs warunkowy. Warunkowy interfejs to funkcja WebAuthn, która umożliwia utworzenie pola wprowadzania formularza, aby sugerować klucz dostępu jako część elementów autouzupełniania oprócz haseł. Jeśli użytkownik kliknie klucz dostępu w sugestiach autouzupełniania, pojawi się prośba o użycie blokady ekranu urządzenia do lokalnego potwierdzenia tożsamości. Zapewnia to płynne logowanie, ponieważ działanie użytkownika jest niemal identyczne jak w przypadku logowania za pomocą hasła.
Włączanie interfejsu warunkowego
Aby włączyć interfejs warunkowy, wystarczy dodać token webauthn
do atrybutu autocomplete
pola wejściowego. Po ustawieniu tokena możesz wywołać metodę navigator.credentials.get()
za pomocą ciągu znaków mediation: 'conditional'
, aby warunkowo wywołać interfejs blokady ekranu.
- Aby włączyć interfejs warunkowy, zastąp istniejące pola wprowadzania nazwy użytkownika tym kodem HTML po odpowiednim komentarzu w pliku
view/index.html
:
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus />
Wykrywanie funkcji, wywoływanie WebAuthn i włączanie interfejsu warunkowego
- W pliku
view/index.html
po odpowiednim komentarzu zastąp istniejącą instrukcjęimport
tym kodem:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from "/client.js";
Ten kod importuje funkcję authenticate()
, którą zaimplementowano wcześniej.
- Sprawdź, czy obiekt
window.PulicKeyCredential
jest dostępny i czy metodaPublicKeyCredential.isConditionalMediationAvailable()
zwraca wartośćtrue
, a następnie wywołaj funkcjęauthenticate()
:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable
) {
try {
// Is conditional UI available in this browser?
const cma =
await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$("#username").value = user.username;
loading.start();
location.href = "/home";
} else {
throw new Error("User not found.");
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== "NotAllowedError") {
console.error(e);
alert(e.message);
}
}
}
Sprawdź kod rozwiązania w tej sekcji
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus
/>
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from '/client.js';
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
// Is a conditional UI available in this browser?
const cma= await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If a conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$('#username').value = user.username;
loading.start();
location.href = '/home';
} else {
throw new Error('User not found.');
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== 'NotAllowedError') {
console.error(e);
alert(e.message);
}
}
}
Wypróbuj
Wdrożono tworzenie, rejestrowanie, wyświetlanie i uwierzytelnianie kluczy dostępu w witrynie.
Aby wypróbować tę funkcję, wykonaj te czynności:
- Otwórz kartę podglądu.
- W razie potrzeby wyloguj się.
- Kliknij pole tekstowe nazwy użytkownika. Pojawi się okno.
- Wybierz konto, na które chcesz się zalogować.
- Potwierdź swoją tożsamość za pomocą blokady ekranu urządzenia. Nastąpi przekierowanie na stronę
/home
i zalogujesz się.
7. Gratulacje!
To ćwiczenie zostało ukończone. Jeśli masz pytania, zadaj je na liście adresowej FIDO-DEV lub na StackOverflow, dodając tag passkey
.