Przegląd
Oto ogólne omówienie najważniejszych czynności związanych z rejestracją klucza dostępu:
- Określ opcje tworzenia klucza dostępu. Wyślij je do klienta, aby przekazać je do wywołania tworzenia klucza dostępu: do wywołania interfejsu WebAuthn API
navigator.credentials.create
w przeglądarce icredentialManager.createCredential
na urządzeniu z Androidem. Gdy użytkownik potwierdzi utworzenie klucza dostępu, wywołanie tworzenia klucza dostępu zostanie zakończone i zwrócone dane logowaniaPublicKeyCredential
. - Sprawdź dane logowania i zapisz je na serwerze.
W sekcjach poniżej znajdziesz szczegółowe informacje na temat każdego kroku.
Tworzenie opcji tworzenia danych logowania
Najpierw musisz utworzyć na serwerze obiekt PublicKeyCredentialCreationOptions
.
W tym celu należy skorzystać z biblioteki FIDO po stronie serwera. Zwykle zawiera on funkcję, która umożliwia utworzenie tych opcji. Oferta SimpleWebAuthn, to na przykład generateRegistrationOptions
.
Plik PublicKeyCredentialCreationOptions
powinien zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, grupie objętej ograniczeniami i konfiguracji właściwości tworzonych danych logowania. Gdy to zrobisz, w razie potrzeby przekaż je do funkcji w bibliotece po stronie serwera FIDO, która odpowiada za utworzenie obiektu PublicKeyCredentialCreationOptions
.
Niektóre z pól PublicKeyCredentialCreationOptions
mogą być stałymi. Inne powinny być dynamicznie zdefiniowane na serwerze:
rpId
: aby wypełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które dostarczają nazwę hosta aplikacji internetowej, np.example.com
.user.name
iuser.displayName
: aby wypełnić te pola, użyj informacji o sesji zalogowanego użytkownika (lub informacji o nowym koncie, jeśli użytkownik tworzy klucz dostępu podczas rejestracji).user.name
to zwykle adres e-mail przypisany do grupy objętej ograniczeniami.user.displayName
to nazwa przyjazna dla użytkownika. Pamiętaj, że nie wszystkie platformy będą korzystać z elementudisplayName
.user.id
: losowy, unikalny ciąg znaków wygenerowany podczas tworzenia konta. Musi być trwała, w przeciwieństwie do nazwy użytkownika, którą można edytować. Identyfikator użytkownika określa konto, ale nie powinien zawierać żadnych informacji umożliwiających identyfikację osoby. Prawdopodobnie masz już w systemie identyfikator użytkownika, ale w razie potrzeby utwórz go specjalnie na potrzeby kluczy dostępu, aby nie zawierał żadnych informacji umożliwiających identyfikację.excludeCredentials
: lista istniejących identyfikatorów danych logowania, aby zapobiec duplikowaniu klucza dostępu od dostawcy kluczy dostępu. Aby wypełnić to pole, wyszukaj istniejące dane logowania tego użytkownika w bazie danych. Szczegółowe informacje znajdziesz w sekcji Blokowanie tworzenia nowego klucza dostępu, jeśli taki klucz już istnieje.challenge
: w przypadku rejestracji danych logowania wyzwanie nie ma zastosowania, chyba że korzystasz z atestu, czyli bardziej zaawansowanej metody weryfikacji tożsamości dostawcy klucza dostępu i wysyłanych przez niego danych. Jednak nawet jeśli nie korzystasz z atestu, wyzwanie jest polem wymaganym. W takim przypadku dla uproszczenia możesz ustawić dla tego wyzwania na jedną wartość0
. Instrukcje tworzenia bezpiecznego testu zabezpieczającego do uwierzytelniania znajdziesz w artykule Uwierzytelnianie klucza po stronie serwera.
Kodowanie i dekodowanie
![Opcja PublicKeyCredentialCreationOptions została wysłana przez serwer](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=6&hl=pl)
PublicKeyCredentialCreationOptions
przez serwer. Parametry challenge
, user.id
i excludeCredentials.credentials
muszą być zakodowane po stronie serwera jako base64URL
, aby można było wyświetlać PublicKeyCredentialCreationOptions
przez HTTPS.PublicKeyCredentialCreationOptions
zawierają pola typu ArrayBuffer
, więc nie są obsługiwane przez JSON.stringify()
. Oznacza to, że obecnie, aby wyświetlić PublicKeyCredentialCreationOptions
przez HTTPS, niektóre pola muszą być ręcznie zakodowane na serwerze za pomocą base64URL
, a potem dekodowane po stronie klienta.
- Na serwerze kodowanie i dekodowanie jest zwykle obsługiwane przez bibliotekę FIDO po stronie serwera.
- W tej chwili po stronie klienta kodowanie i dekodowanie musi być wykonywane ręcznie. W przyszłości stanie się to łatwiejsze: dostępna będzie metoda konwertowania opcji z formatu JSON na
PublicKeyCredentialCreationOptions
. Sprawdź stan implementacji w Chrome.
Przykładowy kod: tworzenie opcji tworzenia danych logowania
W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy tworzenie opcji danych logowania klucza publicznego do funkcji generateRegistrationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
const { user } = res.locals;
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// `excludeCredentials` prevents users from re-registering existing
// credentials for a given passkey provider
const excludeCredentials = [];
const credentials = Credentials.findByUserId(user.id);
if (credentials.length > 0) {
for (const cred of credentials) {
excludeCredentials.push({
id: isoBase64URL.toBuffer(cred.id),
type: 'public-key',
transports: cred.transports,
});
}
}
// Generate registration options for WebAuthn create
const options = generateRegistrationOptions({
rpName: process.env.RP_NAME,
rpID: process.env.HOSTNAME,
userID: user.id,
userName: user.username,
userDisplayName: user.displayName || '',
attestationType: 'none',
excludeCredentials,
authenticatorSelection: {
authenticatorAttachment: 'platform',
requireResidentKey: true
},
});
// Keep the challenge in the session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Przechowywanie klucza publicznego
![Opcja PublicKeyCredentialCreationOptions została wysłana przez serwer](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=6&hl=pl)
navigator.credentials.create
zwraca obiekt PublicKeyCredential
.Jeśli navigator.credentials.create
uda się rozwiązać problem na kliencie, będzie to oznaczać, że klucz dostępu został utworzony. Zwracany jest obiekt PublicKeyCredential
.
Obiekt PublicKeyCredential
zawiera obiekt AuthenticatorAttestationResponse
, który reprezentuje odpowiedź dostawcy klucza dostępu na instrukcję tworzenia klucza dostępu klienta. Zawiera on informacje o nowych danych logowania, które będą Ci potrzebne w ramach grupy objętej ograniczeniami do późniejszego uwierzytelnienia użytkownika. Więcej informacji o programie AuthenticatorAttestationResponse
znajdziesz w Dodatku: AuthenticatorAttestationResponse
.
Wyślij obiekt PublicKeyCredential
do serwera. Po otrzymaniu dokumentu zweryfikuj go.
Przekaż ten etap weryfikacji do swojej biblioteki po stronie serwera FIDO. Zwykle zawiera on funkcję użytkową. Oferta SimpleWebAuthn, to na przykład verifyRegistrationResponse
. Więcej informacji znajdziesz w artykule Dodatek: weryfikacja odpowiedzi rejestracyjnej.
Po pomyślnej weryfikacji zapisz dane logowania w bazie danych, aby użytkownik mógł później uwierzytelnić się za pomocą klucza dostępu powiązanego z tymi danymi logowania.
Użyj dedykowanej tabeli na dane logowania klucza publicznego powiązane z kluczami dostępu. Użytkownik może mieć tylko 1 hasło, ale może mieć kilka kluczy dostępu – na przykład klucz dostępu zsynchronizowany z pękiem kluczy Apple iCloud i drugi w Menedżerze haseł Google.
Oto przykładowy schemat, w którym możesz przechowywać informacje o danych logowania:
- Tabela Użytkownicy:
user_id
: główny identyfikator użytkownika. Losowy, unikalny, stały identyfikator użytkownika. Używaj go jako klucza podstawowego dla tabeli Użytkownicy.username
. Zdefiniowana przez użytkownika nazwa użytkownika, którą można edytować.passkey_user_id
: identyfikator użytkownika bez informacji umożliwiających identyfikację osoby powiązany z kluczem, reprezentowany przezuser.id
w opcjach rejestracji. Gdy użytkownik później spróbuje się uwierzytelnić, mechanizm uwierzytelniający udostępni ten element (passkey_user_id
) w odpowiedzi na uwierzytelnienie w narzędziuuserHandle
. Nie zalecamy ustawiania kluczapasskey_user_id
jako klucza podstawowego. Klucze podstawowe stają się zwykle w systemach informacjami umożliwiającymi identyfikację osób, ponieważ są one powszechnie używane.
- Tabela danych logowania klucza publicznego:
id
: identyfikator danych logowania. Możesz go używać jako klucza podstawowego dla tabeli Dane logowania klucza publicznego.public_key
: klucz publiczny danych logowania.passkey_user_id
: użyj go jako klucza obcego, aby utworzyć połączenie z tabelą Użytkownicy.backed_up
: Kopia zapasowa klucza dostępu jest tworzona, jeśli zostanie zsynchronizowana przez dostawcę klucza. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli zamierzasz w przyszłości zrezygnować z haseł w przypadku użytkowników, którzy mają klucze dostępu (backed_up
). Aby sprawdzić, czy klucz dostępu ma kopię zapasową, przejrzyj flagi wauthenticatorData
lub użyj funkcji biblioteki po stronie serwera FIDO, która zwykle zapewnia łatwy dostęp do tych informacji. Przechowywanie uprawnień do korzystania z kopii zapasowej może pomóc w rozwiązaniu potencjalnych pytań użytkowników.name
: opcjonalnie wyświetlana nazwa danych logowania, która umożliwia użytkownikom nadawanie im niestandardowych nazw.transports
: tablica transportu. Przechowywanie danych ułatwia uwierzytelnianie. Gdy dostępne są mechanizmy transportowe, przeglądarka może działać w odpowiedni sposób i wyświetlać interfejs zgodny z interfejsem używanym przez dostawcę klucza dostępu do komunikacji z klientami – w szczególności w przypadku ponownego uwierzytelniania, gdy poleallowCredentials
nie jest puste.
Aby zadbać o wygodę użytkowników, warto przechowywać inne informacje, takie jak dostawca klucza dostępu, czas utworzenia danych logowania i czas ostatniego użycia. Więcej informacji znajdziesz w artykule na temat projektowania interfejsu kluczy dostępu.
Przykładowy kod: przechowywanie danych logowania
W naszych przykładach używamy biblioteki SimpleWebAuthn.
Tutaj przekazujemy weryfikację odpowiedzi na żądanie rejestracji funkcji verifyRegistrationResponse
.
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
const response = req.body;
// This sample code is for registering a passkey for an existing,
// signed-in user
// Ensure you nest verification function calls in try/catch blocks.
// If something fails, throw an error with a descriptive error message.
// Return that message with an appropriate error code to the client.
try {
// Verify the credential
const { verified, registrationInfo } = await verifyRegistrationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
requireUserVerification: false,
});
if (!verified) {
throw new Error('Verification failed.');
}
const { credentialPublicKey, credentialID } = registrationInfo;
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
publicKey: base64PublicKey,
// Optional: set the platform as a default name for the credential
// (example: "Pixel 7")
name: req.useragent.platform,
transports: response.response.transports,
passkey_user_id: user.passkey_user_id,
backed_up: registrationInfo.credentialBackedUp
});
// Kill the challenge for this session
delete req.session.challenge;
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).send({ error: e.message });
}
});
Dodatek: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
zawiera 2 ważne obiekty:
response.clientDataJSON
to wersja JSON danych klienta, które w internecie są danymi widocznymi dla przeglądarki. Zawiera ona źródło grupy objętej ograniczeniami, wyzwanie iandroidPackageName
, jeśli klient jest aplikacją na Androida. Ponieważ RP udzielana jest w trybie RP, odczytclientDataJSON
umożliwia dostęp do informacji, które przeglądarka zobaczyła w momencie wysyłania żądaniacreate
.response.attestationObject
zawiera 2 informacje:attestationStatement
, który nie jest odpowiedni, chyba że użyjesz atestu.authenticatorData
to dane widoczne dla dostawcy klucza dostępu. W ramach grupy objętej ograniczeniami odczytauthenticatorData
umożliwia dostęp do danych widocznych dla dostawcy klucza dostępu i zwracanych w momencie wysyłania żądaniacreate
.
authenticatorData
zawiera ważne informacje o danych logowania klucza publicznego powiązanych z nowo utworzonym kluczem dostępu:
- Dane uwierzytelniające klucza publicznego i jego unikalny identyfikator.
- Identyfikator RP powiązany z danymi logowania.
- Flagi opisujące stan użytkownika podczas tworzenia klucza dostępu: czy użytkownik rzeczywiście był obecny i czy jego weryfikacja przebiegła pomyślnie (patrz
userVerification
). - AAGUID – identyfikuje dostawcę klucza dostępu. Wyświetlenie dostawcy kluczy dostępu może być przydatne dla użytkowników, zwłaszcza jeśli zarejestrowali oni klucz dostępu w Twojej usłudze u wielu dostawców kluczy.
Mimo że atrybut authenticatorData
jest zagnieżdżony w elemencie attestationObject
, zawarte w nim informacje są potrzebne do implementacji klucza dostępu niezależnie od tego, czy używasz atestu. Parametr authenticatorData
jest zakodowany i zawiera pola zakodowane w formacie binarnym. Biblioteka po stronie serwera zazwyczaj obsługuje analizowanie i dekodowanie. Jeśli nie korzystasz z biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData()
po stronie klienta, aby zaoszczędzić sobie czas na analizowanie i dekodowanie po stronie serwera.
Załącznik: weryfikacja odpowiedzi rejestracyjnej
Proces weryfikowania odpowiedzi rejestracji składa się z następujących etapów:
- Upewnij się, że identyfikator RP jest zgodny z identyfikatorem w witrynie.
- Upewnij się, że źródłem żądania jest oczekiwane źródło Twojej witryny (adres URL głównej witryny, aplikacja na Androida).
- Jeśli wymagasz weryfikacji użytkownika, ustaw flagę weryfikacji użytkownika
authenticatorData.uv
na wartośćtrue
. Sprawdź, czy flaga obecności użytkownikaauthenticatorData.up
totrue
, ponieważ obecność użytkownika jest zawsze wymagana w przypadku kluczy dostępu. - Sprawdź, czy klient mógł wykonać zadanie. Jeśli nie korzystasz z atestu, ten test jest nieważny. Stosowanie tej weryfikacji jest jednak sprawdzoną metodą: dzięki niej kod będzie gotowy, jeśli w przyszłości zdecydujesz się na korzystanie z atestu.
- Upewnij się, że identyfikator danych logowania nie został jeszcze zarejestrowany dla żadnego użytkownika.
- Sprawdź, czy algorytm użyty przez dostawcę klucza dostępu do utworzenia danych logowania to algorytm podany przez Ciebie (w każdym polu
alg
wpublicKeyCredentialCreationOptions.pubKeyCredParams
, który jest zwykle zdefiniowany w bibliotece po stronie serwera i nie jest dla Ciebie widoczny). Dzięki temu użytkownicy będą mogli rejestrować się tylko za pomocą dozwolonych przez Ciebie algorytmów.
Aby dowiedzieć się więcej, zapoznaj się z kodem źródłowym witryny verifyRegistrationResponse
dostępnym w witrynie SimpleWebAuthn lub zapoznaj się z pełną listą weryfikacji w specyfikacji.