Omówienie
Oto ogólny przegląd najważniejszych czynności związanych z rejestracją kluczy 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 zostanie zakończone i zwróci dane logowaniaPublicKeyCredential
. - Sprawdź dane logowania i zapisz je na serwerze.
W sekcjach poniżej znajdziesz szczegółowe informacje o poszczególnych krokach.
Tworzenie opcji tworzenia danych logowania
Pierwszym krokiem na serwerze jest utworzenie obiektu PublicKeyCredentialCreationOptions
.
W tym celu korzystaj z biblioteki FIDO po stronie serwera. Zwykle dostępna jest funkcja użytkowa umożliwiająca utworzenie tych opcji. SimpleWebAuthn oferuje np. generateRegistrationOptions
.
PublicKeyCredentialCreationOptions
powinien zawierać wszystko, co jest potrzebne do utworzenia klucza dostępu: informacje o użytkowniku, informacje o RP i konfiguracja właściwości tworzonych danych logowania. Gdy zdefiniujesz wszystkie te zasady, przekaż je odpowiednio do funkcji w bibliotece po stronie serwera FIDO odpowiedzialnej za utworzenie obiektu PublicKeyCredentialCreationOptions
.
Niektóre z PublicKeyCredentialCreationOptions
' mogą być stałe. Inne powinny być dynamicznie zdefiniowane na serwerze:
rpId
: aby uzupełnić identyfikator RP na serwerze, użyj funkcji lub zmiennych po stronie serwera, które zapewnią Ci 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 użytkownik tworzy klucz dostępu podczas rejestracji).user.name
to zwykle adres e-mail, który jest unikalny dla grupy objętej ograniczeniami.user.displayName
to przyjazna dla użytkownika nazwa. Pamiętaj, że nie wszystkie platformy korzystają zdisplayName
.user.id
: losowy, unikalny ciąg znaków wygenerowany podczas tworzenia konta. Musi być ona trwała, 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ę. 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 istniejących danych logowania identyfikatory zapobiegające duplikowaniu klucza 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 istnieje.challenge
: w przypadku rejestracji danych logowania test zabezpieczający nie ma zastosowania, chyba że korzystasz z atestu – bardziej zaawansowanej metody weryfikacji tożsamości dostawcy kluczy dostępu i przesyłanych przez niego danych. Jednak nawet wtedy, gdy nie używasz atestu, test zabezpieczający nadal jest wymagany. Dla uproszczenia w takim przypadku możesz ustawić dla tego wyzwania tylko 10
. Instrukcje tworzenia bezpiecznego testu zabezpieczającego uwierzytelnianie znajdziesz w sekcji Uwierzytelnianie za pomocą klucza dostępu po stronie serwera.
Kodowanie i dekodowanie
![PublicKeyCredentialCreationOptions wysłana przez serwer](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=8&hl=pl)
PublicKeyCredentialCreationOptions
wysłana przez serwer. Aby wiadomości PublicKeyCredentialCreationOptions
mogły być dostarczane przez HTTPS, challenge
, user.id
i excludeCredentials.credentials
muszą być zakodowane po stronie serwera w kodzie base64URL
.PublicKeyCredentialCreationOptions
zawiera pola o wartości ArrayBuffer
, więc JSON.stringify()
nie obsługuje ich. Oznacza to, że w tej chwili, aby dostarczyć PublicKeyCredentialCreationOptions
przez HTTPS, niektóre pola trzeba zakodować ręcznie na serwerze za pomocą base64URL
, a następnie zdekodować po stronie klienta.
- Na serwerze kodowaniem i dekodowaniem zajmuje się zazwyczaj biblioteka FIDO po stronie serwera.
- Po stronie klienta kodowanie i dekodowanie należy wykonać ręcznie. W przyszłości będzie to łatwiejsze: udostępnimy metodę konwertowania opcji w formacie JSON na format
PublicKeyCredentialCreationOptions
. Sprawdź stan implementacji w Chrome.
Przykładowy kod: tworzenie opcji tworzenia danych logowania
W przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy funkcję generateRegistrationOptions
tworzenia opcji danych logowania klucza publicznego.
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
![PublicKeyCredentialCreationOptions wysłana przez serwer](https://developers.google.cn/identity/passkeys/developer-guides/server-registration/images/server-publicKeyCredentialCreationOptions.png?authuser=8&hl=pl)
navigator.credentials.create
zwraca obiekt PublicKeyCredential
.Gdy uda się rozwiązać problem navigator.credentials.create
na kliencie, 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 on informacje o nowych danych logowania, których potrzebujesz jako grupy objętej ograniczeniami do późniejszego uwierzytelnienia użytkownika. Więcej informacji o AuthenticatorAttestationResponse
znajdziesz w Załączniku: AuthenticatorAttestationResponse
.
Wyślij obiekt PublicKeyCredential
na serwer. Gdy go otrzymasz, zweryfikuj go.
Przekaż ten etap weryfikacji do biblioteki FIDO po stronie serwera. Zwykle oferuje do tego funkcję użytkową. SimpleWebAuthn oferuje np. verifyRegistrationResponse
. Więcej informacji o tym, co dzieje się poza ukrytymi systemami, znajdziesz w Załączniku: weryfikacja odpowiedzi na proces rejestracji.
Po pomyślnym przeprowadzeniu 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 logowania.
Użyj dedykowanej tabeli dla danych logowania klucza publicznego powiązanych z kluczami dostępu. Użytkownik może mieć tylko 1 hasło, ale może mieć wiele kluczy dostępu – na przykład klucz zsynchronizowany przez pęk kluczy Apple iCloud i 1 za pomocą Menedżera haseł Google.
Oto przykładowy schemat, którego można użyć do przechowywania danych logowania:
- Tabela Użytkownicy:
user_id
: podstawowy identyfikator użytkownika. Losowy, unikalny, stały identyfikator użytkownika. Użyj go jako klucza podstawowego w 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ę, powiązany z kluczem dostępu, reprezentowany przezuser.id
w opcjach rejestracji. Gdy użytkownik później spróbuje się uwierzytelnić, uwierzytelnianie udostępni ten identyfikatorpasskey_user_id
w odpowiedzi uwierzytelniania wuserHandle
. Nie zalecamy ustawiania kluczapasskey_user_id
jako klucza podstawowego. Klucze podstawowe zwykle stają się informacjami umożliwiającymi identyfikację osób w systemach, ponieważ są powszechnie używane.
- Tabela danych logowania klucza publicznego:
id
: identyfikator certyfikatu. Użyj go jako klucza podstawowego w tabeli Dane logowania klucza publicznego.public_key
: klucz publiczny danych logowania.passkey_user_id
: użyj tego klucza jako obcego klucza, aby utworzyć połączenie z tabelą Users (Użytkownicy).backed_up
: klucz dostępu jest kopiowany, jeśli jest zsynchronizowany przez dostawcę klucza. Przechowywanie stanu kopii zapasowej jest przydatne, jeśli w przyszłości zechcesz usunąć hasła użytkowników, którzy mająbacked_up
kluczy dostępu. Aby sprawdzić, czy kopia zapasowa klucza została utworzona, przejrzyj flagi wauthenticatorData
lub skorzystaj z funkcji biblioteki FIDO po stronie serwera, która jest zwykle dostępna w celu zapewnienia łatwego dostępu do tych informacji. Przechowywanie kwalifikacji do tworzenia kopii zapasowych może pomóc w odpowiedzi na potencjalne pytania użytkowników.name
: opcjonalnie wyświetlana nazwa danych logowania, która umożliwia użytkownikom nadawanie danych logowania niestandardowych nazw.transports
: tablica transportów. Przechowywanie transportów jest przydatne podczas uwierzytelniania użytkowników. Gdy dostępne są przenoszenie, przeglądarka może działać odpowiednio i wyświetlić interfejs zgodny z transportem używanym przez dostawcę klucza do komunikacji z klientami – zwłaszcza w przypadku ponownego uwierzytelniania, gdy poleallowCredentials
nie jest puste.
Aby zwiększyć 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 o projektowaniu interfejsu kluczy dostępu.
Przykładowy kod: przechowywanie danych logowania
W przykładach używamy biblioteki SimpleWebAuthn.
W tym przypadku przekazujemy weryfikację odpowiedzi rejestracyjnej jej 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 });
}
});
Załącznik: AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
zawiera dwa ważne obiekty:
response.clientDataJSON
to wersja JSON danych klienta, która w internecie jest danymi widocznymi dla przeglądarki. Zawiera źródło RP, wyzwanie iandroidPackageName
, jeśli klient to aplikacja na Androida. CzytanieclientDataJSON
daje dostęp do informacji, które przeglądarka widziała w momencie wysłania żądaniacreate
.response.attestationObject
zawiera 2 informacje:attestationStatement
, który nie ma zastosowania, chyba że użyjesz atestu.authenticatorData
to dane widoczne dla dostawcy klucza dostępu.authenticatorData
daje Ci dostęp do danych wyświetlonych przez dostawcę klucza dostępu i zwróconych w momencie żądaniacreate
.
authenticatorData
zawiera kluczowe informacje o danych logowania klucza publicznego powiązanych z nowo utworzonym kluczem:
- Dane logowania klucza publicznego wraz z unikalnym identyfikatorem danych logowania.
- Identyfikator grupy objętej ograniczeniami powiązany z danym uwierzytelniającym.
- Flagi opisujące stan użytkownika w momencie utworzenia klucza dostępu: czy użytkownik był tam rzeczywiście i czy przeszedł weryfikację (patrz
userVerification
). - AAGUID, który identyfikuje dostawcę klucza dostępu. Wyświetlanie dostawcy kluczy dostępu może być przydatne dla użytkowników, zwłaszcza jeśli mają oni zarejestrowany klucz dostępu dla usługi u różnych dostawców kluczy dostępu.
Mimo że tag authenticatorData
jest umieszczony w obrębie attestationObject
, zawarte w nim informacje są potrzebne do implementacji 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 zazwyczaj zajmuje się analizą i dekodowaniem. Jeśli nie korzystasz z biblioteki po stronie serwera, rozważ wykorzystanie getAuthenticatorData()
po stronie klienta, aby zaoszczędzić na analizie i dekodowaniu plików roboczych po stronie serwera.
Załącznik: weryfikacja odpowiedzi na rejestrację
Weryfikacja odpowiedzi na rejestrację składa się z tych etapów weryfikacji:
- Upewnij się, że identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem Twojej witryny.
- Upewnij się, że źródło żądania jest oczekiwanym źródłem Twojej witryny (główny adres URL witryny, aplikacja na Androida).
- Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga weryfikacji użytkownika
authenticatorData.uv
ma wartośćtrue
. Sprawdź, czy flaga obecności użytkownikaauthenticatorData.up
totrue
, ponieważ w przypadku kluczy dostępu obecność użytkownika jest zawsze wymagana. - Sprawdź, czy klient był w stanie zrealizować zadane przez Ciebie wyzwanie. Jeśli nie używasz atestu, ta weryfikacja jest nieistotna. Wdrożenie tej metody sprawdzania jest jednak sprawdzoną metodą. Dzięki niej masz pewność, że kod będzie gotowy, jeśli w przyszłości zdecydujesz się na użycie atestu.
- Upewnij się, że identyfikator danych logowania nie jest jeszcze zarejestrowany dla żadnego użytkownika.
- Sprawdź, czy algorytm używany przez dostawcę kluczy dostępu do tworzenia danych logowania jest wymienionym przez Ciebie algorytmem wymienionym na liście (w każdym polu
alg
obiektupublicKeyCredentialCreationOptions.pubKeyCredParams
, które jest zwykle zdefiniowane w bibliotece po stronie serwera i nie jest dla Ciebie widoczne). Dzięki temu użytkownicy będą mogli rejestrować się tylko przy użyciu dozwolonych przez Ciebie algorytmów.
Aby dowiedzieć się więcej, zapoznaj się z kodem źródłowym witryny verifyRegistrationResponse
SimpleWebAuthn lub przejrzyj pełną listę weryfikacji w specyfikacji.
Następny krok
Uwierzytelnianie za pomocą klucza dostępu po stronie serwera