Przegląd
Oto ogólny przegląd kluczowych kroków rejestracji 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: wywołania interfejsu WebAuthn API
navigator.credentials.create
w internecie icredentialManager.createCredential
na Androidzie. Gdy użytkownik potwierdzi utworzenie klucza dostępu, wywołanie tworzenia klucza dostępu zostanie rozwiązane i zwróci dane logowaniaPublicKeyCredential
. - Sprawdź dane logowania i zapisz je na serwerze.
W kolejnych sekcjach znajdziesz szczegółowe informacje o każdym kroku.
Tworzenie opcji tworzenia danych logowania
Pierwszym krokiem, jaki musisz wykonać na serwerze, jest utworzenie obiektu PublicKeyCredentialCreationOptions
.
W tym celu użyj biblioteki FIDO po stronie serwera. Zwykle oferuje funkcję narzędziową, która może utworzyć te opcje. SimpleWebAuthn oferuje na przykład generateRegistrationOptions
.
PublicKeyCredentialCreationOptions
powinna zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, o podmiocie zależnym oraz konfigurację właściwości tworzonych danych logowania. Po zdefiniowaniu wszystkich tych wartości przekaż je w razie potrzeby do funkcji w bibliotece po stronie serwera FIDO, która jest odpowiedzialna za tworzenie obiektu PublicKeyCredentialCreationOptions
.
Niektóre pola PublicKeyCredentialCreationOptions
mogą być stałymi. Pozostałe powinny być definiowane dynamicznie na serwerze:
rpId
: aby wypełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które podają 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 użytkownika, jeśli tworzy on klucz dostępu podczas rejestracji).user.name
to zwykle adres e-mail, który jest unikalny dla RP.user.displayName
to przyjazna dla użytkownika nazwa. Pamiętaj, że nie wszystkie platformy będą używaćdisplayName
.user.id
: losowy, unikalny ciąg znaków generowany podczas tworzenia konta. Powinien być trwały, w przeciwieństwie do nazwy użytkownika, którą można edytować. Identyfikator użytkownika identyfikuje konto, ale nie powinien zawierać żadnych informacji umożliwiających identyfikację osoby. Prawdopodobnie masz już w swoim systemie identyfikator użytkownika, ale w razie potrzeby utwórz go specjalnie dla kluczy dostępu, aby nie zawierał żadnych informacji umożliwiających identyfikację.excludeCredentials
: lista identyfikatorów istniejących danych logowania, która zapobiega duplikowaniu klucza dostępu przez dostawcę kluczy dostępu. Aby wypełnić to pole, wyszukaj w bazie danych istniejące dane logowania tego użytkownika. Szczegółowe informacje znajdziesz w artykule Zapobieganie tworzeniu nowego klucza dostępu, jeśli już istnieje.challenge
: w przypadku rejestracji danych logowania test nie ma znaczenia, chyba że używasz atestu, czyli bardziej zaawansowanej techniki weryfikacji tożsamości dostawcy klucza dostępu i emitowanych przez niego danych. Nawet jeśli nie używasz atestu, wyzwanie jest polem wymaganym. Instrukcje tworzenia bezpiecznego wyzwania na potrzeby uwierzytelniania znajdziesz w artykule Uwierzytelnianie za pomocą klucza dostępu po stronie serwera.
Kodowanie i dekodowanie

PublicKeyCredentialCreationOptions
wysłane przez serwer. challenge
, user.id
i excludeCredentials.credentials
muszą być zakodowane po stronie serwera w base64URL
, aby PublicKeyCredentialCreationOptions
mogło być dostarczane przez HTTPS.PublicKeyCredentialCreationOptions
zawierają pola, które są ArrayBuffer
, więc nie są obsługiwane przez JSON.stringify()
. Oznacza to, że obecnie, aby dostarczyć PublicKeyCredentialCreationOptions
przez HTTPS, niektóre pola muszą być ręcznie zakodowane na serwerze za pomocą base64URL
, a następnie zdekodowane na kliencie.
- Na serwerze kodowanie i dekodowanie jest zwykle obsługiwane przez bibliotekę FIDO po stronie serwera.
- Na kliencie kodowanie i dekodowanie musi być obecnie wykonywane ręcznie. W przyszłości będzie to łatwiejsze: dostępna będzie metoda konwertowania opcji w formacie JSON na
PublicKeyCredentialCreationOptions
. Sprawdź stan wdrożenia w Chrome.
Przykładowy kod: tworzenie opcji tworzenia danych logowania
W naszych przykładach używamy biblioteki SimpleWebAuthn. W tym przypadku przekazujemy tworzenie opcji danych logowania za pomocą klucza publicznego do funkcji generateRegistrationOptions
.
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 = await 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

navigator.credentials.create
zwraca obiekt PublicKeyCredential
.Gdy navigator.credentials.create
zostanie pomyślnie rozwiązany na urządzeniu klienta, oznacza to, ż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ę klienta dotyczącą utworzenia klucza dostępu. Zawiera informacje o nowych danych logowania, które są potrzebne stronie ufającej do późniejszego uwierzytelniania użytkownika. Więcej informacji o AuthenticatorAttestationResponse
znajdziesz w Dodatku: AuthenticatorAttestationResponse
.
Wyślij obiekt PublicKeyCredential
na serwer. Po otrzymaniu adresu e-mail zweryfikuj go.
Przekaż ten krok weryfikacji do biblioteki FIDO po stronie serwera. Zwykle oferuje w tym celu funkcję narzędziową. SimpleWebAuthn oferuje na przykład verifyRegistrationResponse
. Więcej informacji o tym, co dzieje się w tle, znajdziesz w Dodatku: weryfikacja odpowiedzi na rejestrację.
Po pomyślnej weryfikacji zapisz informacje o danych logowania w bazie danych, aby użytkownik mógł później uwierzytelnić się za pomocą klucza dostępu powiązanego z tymi danymi.
Używaj osobnej tabeli na potrzeby danych logowania klucza publicznego powiązanych z kluczami dostępu. Użytkownik może mieć tylko jedno hasło, ale może mieć wiele kluczy dostępu – na przykład klucz dostępu zsynchronizowany za pomocą pęku kluczy iCloud od Apple i klucz dostępu zsynchronizowany za pomocą Menedżera haseł Google.
Oto przykładowy schemat, którego możesz użyć do przechowywania informacji o danych logowania:
- Tabela Użytkownicy:
user_id
: identyfikator głównego użytkownika. Losowy, unikalny i trwały identyfikator użytkownika. Użyj go jako klucza podstawowego w tabeli Użytkownicy.username
Nazwa użytkownika zdefiniowana przez użytkownika, którą można edytować.passkey_user_id
: identyfikator użytkownika bez informacji umożliwiających identyfikację osoby, który jest powiązany z kluczem dostępu i reprezentowany przez symboluser.id
w opcjach rejestracji. Gdy użytkownik spróbuje później się uwierzytelnić, uwierzytelniający udostępni tenpasskey_user_id
w odpowiedzi na uwierzytelnianie wuserHandle
. Nie zalecamy ustawianiapasskey_user_id
jako klucza podstawowego. Klucze podstawowe zwykle stają się w systemach danymi osobowymi, ponieważ są powszechnie używane.
- Tabela Dane logowania za pomocą klucza publicznego:
id
: identyfikator dokumentu potwierdzającego tożsamość. Użyj go jako klucza podstawowego w tabeli Dane logowania za pomocą klucza publicznego.public_key
: klucz publiczny danych logowania.passkey_user_id
: użyj tego pola jako klucza obcego, aby utworzyć połączenie z tabelą Users.backed_up
: Klucz dostępu jest zapisywany w kopii zapasowej, jeśli jest synchronizowany przez dostawcę kluczy dostępu. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli w przyszłości chcesz zrezygnować z haseł w przypadku użytkowników, którzy mająbacked_up
klucze dostępu. Możesz sprawdzić, czy klucz dostępu jest objęty kopią zapasową, sprawdzając flagę BE wauthenticatorData
lub korzystając z funkcji biblioteki po stronie serwera FIDO, która zwykle jest dostępna, aby ułatwić Ci dostęp do tych informacji. Przechowywanie informacji o możliwości utworzenia kopii zapasowej może być przydatne w odpowiadaniu na potencjalne pytania użytkowników.name
: opcjonalnie nazwa wyświetlana uprawnień, która umożliwia użytkownikom nadawanie im niestandardowych nazw.transports
: tablica środków transportu. Przechowywanie transportów jest przydatne w przypadku uwierzytelniania użytkowników. Gdy dostępne są transporty, przeglądarka może odpowiednio reagować i wyświetlać interfejs użytkownika dopasowany do transportu, którego dostawca klucza dostępu używa do komunikacji z klientami – w szczególności w przypadku ponownego uwierzytelniania, gdy poleallowCredentials
nie jest puste.
Inne informacje mogą być przydatne do przechowywania ze względu na wygodę użytkownika, w tym takie jak dostawca klucza dostępu, czas utworzenia danych logowania i czas ostatniego użycia. Więcej informacji znajdziesz w artykule Projekt interfejsu użytkownika kluczy dostępu.
Przykładowy kod: zapisywanie danych logowania
W naszych przykładach używamy biblioteki SimpleWebAuthn.
W tym miejscu przekazujemy weryfikację odpowiedzi na rejestrację do 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 {
aaguid,
credentialPublicKey,
credentialID,
credentialBackedUp
} = registrationInfo;
// Name the credential based on AAGUID
const name =
aaguid === undefined ||
aaguid === '000000-0000-0000-0000-00000000' ?
req.useragent?.platform : aaguids[aaguid].name;
const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
// Existing, signed-in user
const { user } = res.locals;
// Save the credential
await Credentials.update({
id: base64CredentialID,
passkey_user_id: user.passkey_user_id,
publicKey: base64PublicKey,
name,
aaguid,
transports: response.response.transports,
backed_up: credentialBackedUp,
registered_at: new Date().getTime()
});
// 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, czyli danych widocznych w przeglądarce. Zawiera źródło RP, wyzwanie i informację,androidPackageName
czy klient jest aplikacją na Androida. Jako RP możesz odczytaćclientDataJSON
, aby uzyskać dostęp do informacji, które przeglądarka widziała w momencie wysłania żądaniacreate
.response.attestationObject
zawiera 2 rodzaje informacji:attestationStatement
, która nie jest istotna, chyba że używasz atestu.authenticatorData
to dane widoczne dla dostawcy klucza dostępu. Jako RP czytanieauthenticatorData
umożliwia dostęp do danych widocznych dla dostawcy klucza dostępu i zwracanych w momenciecreate
żądania.
authenticatorData
zawiera niezbędne informacje o danych logowania za pomocą klucza publicznego, które są powiązane 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 w momencie tworzenia klucza dostępu: czy użytkownik był obecny i czy został zweryfikowany (więcej informacji znajdziesz w artykule Szczegółowe informacje o weryfikacji użytkownika).
- AAGUID to identyfikator dostawcy klucza dostępu, np. Menedżera haseł Google. Na podstawie identyfikatora AAGUID możesz określić dostawcę klucza dostępu i wyświetlić jego nazwę na stronie zarządzania kluczami dostępu. (patrz Określanie dostawcy klucza dostępu za pomocą identyfikatora AAGUID)
Chociaż authenticatorData
jest zagnieżdżony w attestationObject
, informacje, które zawiera, są potrzebne do wdrożenia klucza dostępu niezależnie od tego, czy używasz atestu. authenticatorData
jest zakodowany i zawiera pola zakodowane w formacie binarnym. Biblioteka po stronie serwera zwykle zajmuje się analizowaniem i dekodowaniem. Jeśli nie używasz biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData()
biblioteki po stronie klienta, aby zaoszczędzić sobie pracy związanej z parsowaniem i dekodowaniem po stronie serwera.
Dodatek: weryfikacja odpowiedzi na rejestrację
Weryfikacja odpowiedzi na rejestrację obejmuje te sprawdzenia:
- Upewnij się, że identyfikator dostawcy tożsamości jest zgodny z Twoją witryną.
- Upewnij się, że źródło żądania jest oczekiwanym źródłem w przypadku Twojej witryny (główny adres URL witryny, aplikacja na Androida).
- Jeśli wymagana jest weryfikacja użytkownika, upewnij się, że flaga weryfikacji użytkownika
authenticatorData.uv
ma wartośćtrue
. - Oczekuje się, że flaga obecności użytkownika
authenticatorData.up
będzie miała wartośćtrue
, ale jeśli dane logowania zostały utworzone warunkowo, oczekuje się, że będzie miała wartośćfalse
. - Sprawdź, czy klient był w stanie rozwiązać zadanie, które mu zostało przydzielone. Jeśli nie używasz atestu, to sprawdzenie nie ma znaczenia. Warto jednak wdrożyć to sprawdzenie, aby mieć pewność, że kod jest gotowy, jeśli w przyszłości zdecydujesz się używać atestu.
- Sprawdź, czy identyfikator danych logowania nie jest jeszcze zarejestrowany dla żadnego użytkownika.
- Sprawdź, czy algorytm używany przez dostawcę klucza dostępu do utworzenia danych logowania jest algorytmem, który został przez Ciebie wymieniony (w każdym polu
alg
wpublicKeyCredentialCreationOptions.pubKeyCredParams
, które jest zwykle zdefiniowane w bibliotece po stronie serwera i niewidoczne dla Ciebie). Dzięki temu użytkownicy mogą rejestrować się tylko za pomocą algorytmów, które zezwolisz.
Aby dowiedzieć się więcej, zapoznaj się z kodem źródłowym SimpleWebAuthn verifyRegistrationResponse
lub przejrzyj pełną listę weryfikacji w specyfikacji.
Następny krok
Uwierzytelnianie za pomocą klucza dostępu po stronie serwera