Omówienie
Oto ogólny przegląd najważniejszych kroków związanych z uwierzytelnianiem za pomocą klucza dostępu:
- Zdefiniuj test zabezpieczający 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 (
navigator.credentials.get
w przeglądarce). 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 o poszczególnych krokach.
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:
- Upewnij się, że to samo wyzwanie nie jest używane więcej niż raz. Wygeneruj nowe zadanie przy każdej próbie logowania. Odrzuć test zabezpieczający po każdej próbie zalogowania się niezależnie od tego, czy się udało. Możesz też odrzucić wyzwanie po określonym czasie. Nigdy nie podejmuj tego samego wyzwania w odpowiedzi więcej niż raz.
- Upewnij się, że test jest zabezpieczony kryptograficznie. Wyzwanie powinno być praktycznie niemożliwe do odgadnięcia. Aby utworzyć kryptograficznie bezpieczną bibliotekę wyzwania po stronie serwera, najlepiej jest korzystać z zaufanej biblioteki FIDO po stronie serwera. Jeśli zamiast tego tworzysz własne wyzwania, skorzystaj z wbudowanych funkcji kryptograficznych dostępnych w Twoim stosie technologicznym lub poszukaj bibliotek zaprojektowanych na potrzeby zastosowań kryptograficznych. Mogą to być na przykład iso-crypto w Node.js lub obiekty tajne w Pythonie. Zgodnie ze specyfikacją test zabezpieczający musi mieć co najmniej 16 bajtów, aby został uznany za bezpieczny.
Po utworzeniu wyzwania zapisz je w sesji użytkownika, aby móc je później zweryfikować.
Opcje tworzenia żądania danych logowania
Tworzenie opcji żądań danych logowania jako obiektu publicKeyCredentialRequestOptions
.
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. generateAuthenticationOptions
.
W polu publicKeyCredentialRequestOptions
powinny znajdować się wszystkie informacje potrzebne do uwierzytelnienia za pomocą klucza dostępu. Przekaż te informacje do funkcji w bibliotece FIDO po stronie serwera, która odpowiada za utworzenie obiektu publicKeyCredentialRequestOptions
.
Niektóre z publicKeyCredentialRequestOptions
' mogą być stałe. Inne powinny być dynamicznie zdefiniowane na serwerze:
rpId
: z którym identyfikatorem grupy objętej ograniczeniami, z którym dane logowania mają być powiązane, np.example.com
. Uwierzytelnianie się powiedzie tylko wtedy, gdy podany tutaj identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem RP powiązanym z danymi uwierzytelniającymi. Aby uzupełnić identyfikator grupy objętej ograniczeniami, użyj tej samej wartości co identyfikator RP ustawiony wpublicKeyCredentialCreationOptions
podczas rejestracji danych logowania.challenge
: dane, które dostawca klucza podpisuje, aby potwierdzić, że użytkownik posiada klucz dostępu w momencie wysłania prośby o uwierzytelnienie. Sprawdź szczegóły w sekcji Utwórz wyzwanie.allowCredentials
: tablica z danymi uwierzytelniającymi akceptowalnymi 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 Szczegółowe omówienie danych logowania z możliwością wykrywania.userVerification
: wskazuje, czy weryfikacja użytkownika przy użyciu blokady ekranu urządzenia jest „wymagana”, „preferowana”. czy „odradzane”. Przeczytaj artykuł Pobieranie wyzwania z serwera RP.timeout
: czas (w milisekundach) potrzebny użytkownikowi na ukończenie uwierzytelniania. Powinien być odpowiednio obszerny i krótszy niż cykl życiachallenge
. Zalecana wartość domyślna to 5 minut, ale możesz ją zwiększyć do maksymalnie 10 minut, czyli nadal mieści się w zalecanym zakresie. Długie limity czasu są przydatne, jeśli spodziewasz się, że użytkownicy będą korzystać z hybrydowego przepływu pracy, który zwykle trwa nieco dłużej. W przypadku przekroczenia limitu czasu operacji jest zgłaszany błądNotAllowedError
.
Po utworzeniu zadania publicKeyCredentialRequestOptions
wyślij go do klienta.
Przykładowy kod: opcje żądania utworzenia danych logowania
W przykładach używamy biblioteki SimpleWebAuthn. Przekazujemy tutaj możliwość tworzenia opcji żą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 });
}
});
Potwierdź i zaloguj użytkownika
Gdy usługa navigator.credentials.get
zostanie pomyślnie rozwiązana na kliencie, zwraca obiekt PublicKeyCredential
.
response
to AuthenticatorAssertionResponse
. Reprezentuje odpowiedź dostawcy klucza dostępu na instrukcję klienta dotyczącą utworzenia elementów niezbędnych do uwierzytelnienia za pomocą klucza dostępu w RP. Zawiera ona:
response.authenticatorData
orazresponse.clientDataJSON
, np. na etapie rejestracji klucza dostępu.response.signature
, który zawiera podpis nad tymi wartościami.
Wyślij obiekt PublicKeyCredential
na serwer.
Na serwerze wykonaj te czynności:
- Zbierz informacje potrzebne do zweryfikowania potwierdzenia i uwierzytelnienia użytkownika:
- Wyświetlenie oczekiwanego testu zabezpieczającego zapisanego w sesji podczas generowania opcji uwierzytelniania.
- Uzyskaj oczekiwany pochodzenie i identyfikator RP.
- Znajdź w bazie danych tego użytkownika. W przypadku możliwych do znalezienia danych logowania nie wiadomo, kto jest użytkownikiem wysyłającym żądanie uwierzytelnienia. Możesz to sprawdzić na 2 sposoby:
- Opcja 1. Użyj
response.userHandle
w obiekciePublicKeyCredential
. W tabeli Użytkownicy poszukaj wartościpasskey_user_id
odpowiadającej wartościuserHandle
. - Opcja 2. Użyj danych logowania
id
obecnych w obiekciePublicKeyCredential
. W tabeli Dane logowania klucza publicznego znajdź dane logowaniaid
, które są zgodne z danymiid
dostępnymi w obiekciePublicKeyCredential
. Następnie znajdź odpowiedniego użytkownika, używając klucza obcegopasskey_user_id
w tabeli Users (Użytkownicy).
- Opcja 1. Użyj
- Znajdź w swojej bazie danych informacje o danych logowania klucza publicznego zgodne z otrzymanym potwierdzeniem uwierzytelniania. Aby to zrobić, w tabeli Dane logowania klucza publicznego znajdź dane logowania
id
, które są zgodne z danymiid
danymi w obiekciePublicKeyCredential
.
Sprawdź potwierdzenie uwierzytelniania. Przekaż ten etap weryfikacji do biblioteki FIDO po stronie serwera, która zwykle oferuje funkcję narzędziową do tego celu. SimpleWebAuthn oferuje np.
verifyAuthenticationResponse
. Więcej informacji o tym, co dzieje się za maskami, znajdziesz w Załączniku: weryfikacja odpowiedzi na potrzeby uwierzytelniania.Usuń test zabezpieczający bez względu na to, czy weryfikacja się uda, czy nie, aby zapobiec atakom metodą powtórzenia.
Zaloguj użytkownika. Jeśli weryfikacja się udała, zaktualizuj informacje o sesji, by oznaczyć użytkownika jako zalogowanego. Możesz też zwrócić klientowi obiekt
user
, aby frontend mógł użyć informacji powiązanych z nowo zalogowanym użytkownikiem.
Przykładowy kod: weryfikacja i logowanie użytkownika
W przykładach używamy biblioteki SimpleWebAuthn. W tym przypadku przekazujemy weryfikację odpowiedzi uwierzytelniania jej 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 });
}
});
Załącznik: weryfikacja odpowiedzi na uwierzytelnianie
Weryfikacja odpowiedzi uwierzytelniania obejmuje te procesy:
- Upewnij się, że identyfikator grupy objętej ograniczeniami jest zgodny z identyfikatorem Twojej witryny.
- Upewnij się, że źródło żądania jest zgodne ze źródłem logowania w Twojej witrynie. W przypadku aplikacji na Androida przeczytaj artykuł Weryfikowanie pochodzenia.
- Sprawdź, czy urządzenie było w stanie wyświetlić podane przez Ciebie wyzwanie.
- Sprawdź, czy podczas uwierzytelniania użytkownik spełniał wymagania określone przez Ciebie jako RP. Jeśli wymagasz weryfikacji użytkownika, upewnij się, że flaga
uv
(zweryfikowane przez użytkownika) w systemieauthenticatorData
ma wartośćtrue
. Sprawdź, czy flagaup
(obecny użytkownik) w systemieauthenticatorData
ma wartośćtrue
, ponieważ w przypadku kluczy dostępu obecność użytkownika jest zawsze wymagana. - Sprawdź podpis. Aby potwierdzić podpis, potrzebne będą:
- Podpis, który jest podpisanym wyzwaniem:
response.signature
- klucz publiczny do weryfikacji podpisu,
- Oryginalne podpisane dane. Są to dane, których podpis ma zostać zweryfikowany.
- algorytm kryptograficzny użyty do utworzenia podpisu,
- Podpis, który jest podpisanym wyzwaniem:
Aby dowiedzieć się więcej o tych krokach, zapoznaj się z kodem źródłowym witryny verifyAuthenticationResponse
SimpleWebAuthn lub zapoznaj się z pełną listą weryfikacji w specyfikacji.