Przegląd
Oto ogólne omówienie najważniejszych czynności związanych z uwierzytelnianiem przy użyciu klucza dostępu:
- Określ test zabezpieczający logowanie i inne opcje wymagane do uwierzytelnienia za pomocą klucza dostępu. Wyślij je do klienta, aby przekazać je do wywołania uwierzytelniania klucza dostępu (w przeglądarce
navigator.credentials.get
). Gdy użytkownik potwierdzi uwierzytelnianie przy użyciu klucza dostępu, wywołanie uwierzytelniania klucza dostępu zostanie zakończone i zwrócone dane logowania (PublicKeyCredential
). Dane logowania zawierają potwierdzenie uwierzytelniania.
- Zweryfikuj potwierdzenie uwierzytelniania.
- Jeśli potwierdzenie uwierzytelniania jest prawidłowe, uwierzytelnij użytkownika.
W sekcjach poniżej znajdziesz szczegółowe informacje na temat każdego kroku.
Utwórz wyzwanie
W praktyce wyzwanie to tablica losowych bajtów, reprezentowana jako obiekt ArrayBuffer
.
// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8
Aby mieć pewność, że wyzwanie spełni swoje zadanie, musisz:
- Dopilnuj, aby to samo wyzwanie nie było nigdy używane więcej niż raz. Generuj nowe wyzwanie przy każdej próbie logowania. Odrzuć test po każdej próbie logowania, niezależnie od tego, czy się udała czy nie. Odrzuć wyzwanie również po określonym czasie. Nigdy nie podejmuj tego samego wyzwania w odpowiedzi więcej niż raz.
- Upewnij się, że test zabezpieczający jest zabezpieczony kryptograficznie. Zadanie powinno być praktycznie niemożliwe do odgadnięcia. Do utworzenia kryptograficznie bezpiecznej testów zabezpieczających po stronie serwera najlepiej polegać zaufana biblioteka FIDO. Jeśli tworzysz własne wyzwania, skorzystaj z wbudowanych funkcji kryptograficznych dostępnych w Twoim stosie technologicznym lub poszukaj bibliotek przeznaczonych do zastosowań kryptograficznych. Przykładem może być iso-crypto w Node.js i secrets w Pythonie. Zgodnie ze specyfikacją wyzwanie musi mieć co najmniej 16 bajtów, aby zostało uznane za bezpieczne.
Po utworzeniu wyzwania zapisz je w sesji użytkownika, aby zweryfikować je później.
Utwórz opcje żądania danych logowania
Utwórz opcje żądania danych logowania w postaci obiektu publicKeyCredentialRequestOptions
.
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 generateAuthenticationOptions
.
Plik publicKeyCredentialRequestOptions
powinien zawierać wszystkie informacje wymagane do uwierzytelniania kluczem. Przekaż te informacje do funkcji w bibliotece po stronie serwera FIDO, która odpowiada za utworzenie obiektu publicKeyCredentialRequestOptions
.
Niektóre z pól publicKeyCredentialRequestOptions
mogą być stałymi. Inne powinny być dynamicznie zdefiniowane na serwerze:
rpId
: identyfikator RP, z którym mają być powiązane dane logowania, np.example.com
. Uwierzytelnianie uda się tylko wtedy, gdy podany tutaj identyfikator RP będzie zgodny z identyfikatorem RP powiązanym z danymi logowania. Aby wypełnić identyfikator RP, użyj tej samej wartości co identyfikator RP ustawiony wpublicKeyCredentialCreationOptions
podczas rejestracji danych logowania.challenge
: dane, które dostawca klucza dostępu podpisuje, aby udowodnić, że użytkownik posiada klucz dostępu w momencie żądania uwierzytelnienia. Sprawdź szczegóły w sekcji Tworzenie wyzwania.allowCredentials
: tablica akceptowanych danych uwierzytelniających podczas tego uwierzytelniania. Przekaż pustą tablicę, aby użytkownik mógł wybrać dostępny klucz dostępu z listy wyświetlanej przez przeglądarkę. Więcej informacji znajdziesz w artykułach Pobieranie wyzwania z serwera RP i Wykrywalne dane logowania.userVerification
: wskazuje, czy weryfikacja użytkownika przy użyciu blokady ekranu urządzenia jest „wymagana”, „preferowana” czy „odradzana”. Przeczytaj artykuł Pobieranie wyzwania z serwera RP.timeout
: czas (w milisekundach) potrzebny użytkownikowi na uwierzytelnienie. Kwota powinna być wystarczająca i krótsza niż w okresiechallenge
. Zalecana wartość domyślna to 5 minut, ale możesz ją wydłużyć – do 10 minut, co mieści się w zalecanym zakresie. Długie limity czasu sprawdzają się, jeśli spodziewasz się, że użytkownicy będą korzystać z przepływu pracy hybrydowego, który zwykle zajmuje trochę więcej czasu. Jeśli operacja przekroczy limit czasu, zostanie zgłoszonyNotAllowedError
.
Po utworzeniu pliku publicKeyCredentialRequestOptions
wyślij go do klienta.
![Parametr publicKeyCredentialCreationOptions wysyłany przez serwer](https://developers-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/auth-options.png?authuser=7&hl=pl)
challenge
odbywa się po stronie klienta.Przykładowy kod: tworzenie opcji żądania danych logowania
W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy opcje tworzenia żądań danych logowania do funkcji generateAuthenticationOptions
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
router.post('/signinRequest', csrfCheck, async (req, res) => {
// Ensure you nest 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 {
// Use the generateAuthenticationOptions function from SimpleWebAuthn
const options = await generateAuthenticationOptions({
rpID: process.env.HOSTNAME,
allowCredentials: [],
});
// Save the challenge in the user session
req.session.challenge = options.challenge;
return res.json(options);
} catch (e) {
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Weryfikacja i logowanie użytkownika
Jeśli navigator.credentials.get
zakończy się pomyślnie po stronie klienta, zwróci obiekt PublicKeyCredential
.
![Obiekt PublicKeyCredential wysłany przez serwer](https://developers-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/publicKeyCredential.png?authuser=7&hl=pl)
navigator.credentials.get
zwraca wartość PublicKeyCredential
.response
to AuthenticatorAssertionResponse
. Reprezentuje odpowiedź dostawcy klucza dostępu na instrukcję klienta dotyczącą utworzenia elementów niezbędnych do przeprowadzenia próby uwierzytelnienia za pomocą klucza dostępu w RP. Zawiera ona:
response.authenticatorData
iresponse.clientDataJSON
, np. podczas rejestracji klucza dostępu.response.signature
, który zawiera podpis nad tymi wartościami.
Wyślij obiekt PublicKeyCredential
do serwera.
Na serwerze wykonaj te czynności:
![Schemat bazy danych](https://developers-dot-devsite-v2-prod.appspot.com/identity/passkeys/developer-guides/server-authentication/images/database-schema-passkeys.png?authuser=7&hl=pl)
- Zbierz informacje potrzebne do zweryfikowania twierdzenia i uwierzytelnienia użytkownika:
- uzyskać oczekiwany test zapisany w sesji podczas generowania opcji uwierzytelniania;
- Uzyskaj oczekiwaną wartość origin i identyfikator RP.
- Sprawdź w bazie danych, kto jest użytkownikiem. W przypadku możliwych do znalezienia danych logowania nie wiadomo, kto wysyła żądanie uwierzytelniania. Możesz to sprawdzić na 2 sposoby:
- Opcja 1. Użyj
response.userHandle
w obiekciePublicKeyCredential
. W tabeli Użytkownicy znajdźpasskey_user_id
z wartościąuserHandle
. - Opcja 2. Użyj danych logowania
id
znajdujących się w obiekciePublicKeyCredential
. W tabeli Dane logowania klucza publicznego znajdź dane logowaniaid
, które odpowiadają danym logowaniaid
w obiekciePublicKeyCredential
. Następnie wyszukaj odpowiedniego użytkownika za pomocą klucza obcegopasskey_user_id
do tabeli Użytkownicy.
- Opcja 1. Użyj
- Znajdź w swojej bazie danych dane logowania klucza publicznego, które pasują do otrzymanego potwierdzenia uwierzytelniania. Aby to zrobić, w tabeli Dane logowania klucza publicznego znajdź dane logowania
id
zgodne z danymi logowaniaid
widocznymi w obiekciePublicKeyCredential
.
Zweryfikuj potwierdzenie uwierzytelniania. Przekaż ten etap weryfikacji do swojej biblioteki po stronie serwera FIDO, która zwykle udostępnia do tego celu funkcję narzędzia. Oferta SimpleWebAuthn, to na przykład
verifyAuthenticationResponse
. Więcej informacji znajdziesz w artykule Dodatek: weryfikacja odpowiedzi uwierzytelniającej.Usuń test niezależnie od tego, czy weryfikacja się udała, czy nie, aby zapobiec atakom metodą powtórzenia.
Zaloguj się jako użytkownik. Jeśli weryfikacja się powiodła, zaktualizuj informacje o sesji, aby oznaczyć użytkownika jako zalogowanego. Możesz też zwrócić do klienta obiekt
user
, aby frontend mógł korzystać z informacji powiązanych z nowo zalogowanym użytkownikiem.
Przykładowy kod: weryfikowanie i logowanie użytkownika
W naszych przykładach używamy biblioteki SimpleWebAuthn. Tutaj przekazujemy weryfikację odpowiedzi uwierzytelniającej funkcji verifyAuthenticationResponse
.
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';
router.post('/signinResponse', csrfCheck, async (req, res) => {
const response = req.body;
const expectedChallenge = req.session.challenge;
const expectedOrigin = getOrigin(req.get('User-Agent'));
const expectedRPID = process.env.HOSTNAME;
// 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 {
// Find the credential stored to the database by the credential ID
const cred = Credentials.findById(response.id);
if (!cred) {
throw new Error('Credential not found.');
}
// Find the user - Here alternatively we could look up the user directly
// in the Users table via userHandle
const user = Users.findByPasskeyUserId(cred.passkey_user_id);
if (!user) {
throw new Error('User not found.');
}
// Base64URL decode some values
const authenticator = {
credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
credentialID: isoBase64URL.toBuffer(cred.id),
transports: cred.transports,
};
// Verify the credential
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin,
expectedRPID,
authenticator,
requireUserVerification: false,
});
if (!verified) {
throw new Error('User verification failed.');
}
// Kill the challenge for this session.
delete req.session.challenge;
req.session.username = user.username;
req.session['signed-in'] = 'yes';
return res.json(user);
} catch (e) {
delete req.session.challenge;
console.error(e);
return res.status(400).json({ error: e.message });
}
});
Dodatek: weryfikacja odpowiedzi uwierzytelniającej
Weryfikacja odpowiedzi uwierzytelniającej obejmuje następujące kontrole:
- Upewnij się, że identyfikator RP jest zgodny z identyfikatorem w witrynie.
- Upewnij się, że źródło żądania jest zgodne z miejscem logowania w Twojej witrynie. W przypadku aplikacji na Androida zapoznaj się z artykułem Weryfikowanie pochodzenia.
- Sprawdź, czy urządzenie mogło wykonać zadanie.
- Sprawdź, czy podczas uwierzytelniania użytkownik spełnia wymagania określone przez Ciebie jako grupę z ograniczonym dostępem. Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga
uv
(zweryfikowany przez użytkownika) wauthenticatorData
ma wartośćtrue
. Sprawdź, czy flagaup
(obecny użytkownik) wauthenticatorData
ma wartośćtrue
, ponieważ obecność użytkownika jest zawsze wymagana w przypadku kluczy dostępu. - Sprawdź podpis. Aby zweryfikować podpis, musisz mieć:
- Podpis, który jest podpisanym wyzwaniem:
response.signature
- Klucz publiczny do weryfikacji podpisu.
- Pierwotnie podpisane dane. To są dane, których podpis ma zostać zweryfikowany.
- Algorytm kryptograficzny, który został użyty do utworzenia podpisu.
- Podpis, który jest podpisanym wyzwaniem:
Aby dowiedzieć się więcej o tych czynnościach, zajrzyj do kodu źródłowego aplikacji SimpleWebAuthn w domenie verifyAuthenticationResponse
lub zapoznaj się z pełną listą weryfikacji w specyfikacji.