Autentykacja użytkowników
Dzięki wykorzystaniu sesji użytkownika potrafimy już powiązać kolejne żądania do naszej strony z działaniami pojedynczego użytkownika naszego serwisu. Teraz nauczmy się potwierdzać tożsamość użytkownika i przechowywać na jego temat informacje wykorzystując framework Django.
Autentykacja użytkownika
Termin autentykacja albo uwierzytelnianie (ang. authentication) oznacza potwierdzenie tożsamości uzytkownika. W świecie rzeczywistym w celu potwierdzenia czyjejś tożsamości używamy różnych narzędzi. Mogą być to:
- oficjalne dokumenty, np. dowód tożsamości, paszport, prawo jazdy, legitymacja
- numery urzędowe takie jak PESEL, NIP, itp.
- nadawane nam przez firmy inne identyfikatory: numer telefonu, numer klienta, etc.
W przypadku aplikacji internetowych mamy różne możliwości weryfikacji tożsamości użytkownika, przejżyjmy najpopularniejsze opcje.
Logowanie przy pomocy hasła
Najpopularniejszą metodą stosowaną w serwisach internetowych nadal jest logowanie przy pomocy hasła i loginu. Metoda ta pozwala uwierzytelniać użytkowników bez korzystania z żadnych dodatkowych usług, wystarczy nam nasz serwer, co jest dużym plusem.
Potencjalnym problemem tej metody jest możliwość łatwego utraty dostępu do konta Rozwiązaniem tego problemu może być użycie adresu e-mail jako loginu użytkownika
Metody bezhasłowe
W ostatnich latach wielokrotnie zapowiadano koniec ery stosowania haseł w celu uwierzytelniania użytkowników. Pomimo wielu starań, pojawiania się ciekawych i wygodnych metod alternatywnych, nadal przechowywanie haseł użytkownika jest praktycznie najprostszym rozwiązaniem do wdrożenia w swojej aplikacji. Pomimo to przyjrzyjmy się przykładowym metodom alternatywnym.
Logowanie przy pomocy jednorazowych linków
Ta metoda zakłada, że nasza aplikacja wygeneruje link, a następnie prześle je jakimś kanałem do użytkownika tak, aby ten mógł się zalogować na wybranym przez siebie urządzeniu. Kanałami komunikacyjnymi może być adres e-mail, numer telefonu czy konto na komunikatorze. Aby utrudnić przejęcie konta przez nieporządanych gości, linki powinny być jednorazowego użytku i powinny wygasać po stosunkowo krótkim czasie (np. 15 minut)
Wadami tego rozwiązania są:
- konieczność posiadania przez użytkownika e-maila, numeru telefonu, czy innego dostępu do wspieranych przez nas kanałów logowania
- konieczność integracji naszej aplikacji z systemami komunikacji wybranych kanałów
Może to się wydawać małym problemem, ale wysyłanie maili jest zaskakująco skomplikowanym zagadnieniem, a przy wyborze komunikacji SMS musimy wziąć pod uwagę np. obsługę numerów zagranicznych.
Logowanie przy pomocy innych serwisów
Wiele serwisów pozwala zalogować się swoim użytkownikom do innych aplikacji potwierdzając za nich ich tożsamość. Wielokrotnie pewnie korzystaliście z opcji “Zaloguj się przy pomocy…” i jest to wygodna opcja dla użytkowników popularnych serwisów.
Wadami tego rozwiązania są:
- Ograniczenie puli naszych użytkowników do użytkowników innego portalu
- Uzależnienia możliwości logowania do naszej aplikacji w zależności od statusu usług innych firm (np. co jeżeli nasz użytkownik zostanie zbanowany na serwisie, którego używał do logowania do naszej aplikacji?)
- Konieczności integracji z innymi serwisami internetowymi
Logowanie przy pomocy innych kluczy (klucze 2FA, biometria, kody jednorazowe, itp)
Dzięki istnieniu takich standardów jak WebAuthn logowanie się do serwisów internetowych przy pomocy kluczy fizycznych albo innych identyfikatorów użytkownika jest dziś realną możliwością. Czy jest to długo wyczekiwany zastępca haseł? Przyszłość pokaże…
Wadami tego rozwiązania są:
- Potencjalne powiązanie tożsamości z fizycznym obiektem, który można zgubić lub uszkodzić
Przechowywanie haseł
Jeżeli zdecydujemy się przechowywać hasła w naszej aplikacji, powinniśmy zadbać o ich bezpieczeństwo. W przypadku rażących zaniedbań grożą nam konsekwencje prawne, więc nie należy podchodzić do tego tematu lekko.
Na szczęście framework Django jak i wiele innych implementują najważniejsze zabezpieczenia w swoich bibliotekach, dlatego wystarczy, że skorzystamy z wbudowanych rozwiązań. Mimo to, dobrze jest wiedzieć jakie są obowiązujące dobre praktyki by móc samemu ocenić implementację bibliotek frameworka choćby po to, żeby wybrać inny, bezpieczniejszy.
Jak zawsze w kwestiach bezpieczeństwa odsyłam do materiałów organizacji zajmujących się stricte bezpieczeństwem aplikacji webowych, takich jak np. ten skrótowy poradnik organizacji OWASP oraz innych. Tutaj postaram się podsumować najważniejsze informacje z tej dziedziny.
Grzechy główne przechowywania haseł
Są lepsze i gorsze metody rozwiązywania problemów. Są też zwyczajnie błędne metody, takie jak te poniżej:
Przechowywanie haseł w otwartym tekście
NIGDY nie przechowuj haseł użytkowników otwartym tekstem. Bazy danych wyciekają. Backupy baz danych wyciekają. W twojej firmie mogą pracować nie do końca uczciwi ludzie. Poznanie hasła jednego z użytkowników nie może się sprowadzać do jednego zapytania do bazy danych.
Przechowywanie haseł w formie umożliwiającej odtworzenie hasła
Hasła użytkownika nie powinny być przechowywane zaszyfrowane, ponieważ gdzieś musi istnieć hasło do ich odszyfrowania. W przypadku wycieku bazy danych poznanie tego jednego głównego hasła służącego do odszyfrowania wszystkich innych staje się kluczem do całego królestwa. Bezpieczeństwo wszystkich użytkowników systemu nie powinno zależeć od znajomości jednej, stosunkowo krótkiej informacji.
Ograniczanie długości haseł i zbyt restrykcyjne zasady tworzenia i zmiany haseł
Podejście do tematu bezpieczeństwa haseł zmieniało się dramatycznie na przestrzeni lat, od rekomendowania użytkownikom haseł trudnych do zapamiętania i składających się z kombinacji wielu rodzajów znaków, po rekomendacje ustawiania długich, ale bardzo łatwych do zapamiętania dla ludzi haseł
W przypadku działów IT dużych korporacji nadal pokutują różne pomysły na temat tego, jakie jest “dobre” hasło, co prowadzi do powstawania długich “polityk” haseł użytkowników.
Takie podejście zazwyczaj ma efekt odwrotny od zamierzonego i ludzie zazwyczaj znajdują kreatywne sposoby na spełnienie tych zasad przy minimalnym wysiłku dla siebie samych. W takim przypadku ciężko zrzucać całą winę za luki bezpieczeństwa na użytkowników systemu.
Poprawne przechowywanie haseł
Haszowanie haseł
W celu przechowania hasła użytkownika chcemy użyć tzw. funkcji haszującej albo funkcji skrótu (ang. hash function). Polecam przeczytanie o tym, jak działają funkcje haszujące np. na Wikipedii, ale dla mniej zainteresowanych szybki skrót informacji:
Funkcja haszująca zamienia dowolny ciąg znaków (co w przypadku komputerów oznacza dowolnie wielką liczbę) na jedną liczbę (ciąg znaków) o określonej szerokości. Najważniejsze cechy funkcji haszujących to:
- Jednokierunkowość, czyli niemożność odtworzenia danych wejściowych na podstawie wyjścia
- Odporność na kolizje, czyli brak praktycznej możliwości wygenerowania alternatywnych danych wejściowych, które dadzą takie same wyniki jak wybrane dane wejściowe.
Z punktu widzenia bezpieczeństwa chcemy wybrać funkcję haszującą, która będzie wymagała relatywnie dużej ilości zasobów w celu obliczenia zahaszowanego hasła. Dzięki temu jeżeli nasza baza danych kiedykolwiek wycieknie i atakujący będą znali nasz algorytm liczenia zahashowanych haseł, nadal obliczenie ich będzie wymagało dużej ilości pracy. W najgorszym wypadku możemy użyć wielokrotnie tej samej funkcji, aby wymusić większe zużycie procesora potrzebne do weryfikacji jednego hasła.
Więc schemat postępowania po wybraniu funkcji haszującej, to zapisanie w bazie danych wyniku operacji:
hasz_hasła = funkcja_haszująca(hasło_użytkownika)
albo przy wielokrotnych obliczeniach, np.:
hasz_hasła = funkcja_haszująca(funkcja_haszująca(funkcja_haszująca(hasło_użytkownika)))
Solenie haseł
Aby jeszcze bardziej utrudnić życie potencjalnych hakerów czychających na bezpieczeństwo użytkowników naszego serwisu, możemy dodać kolejny krok do naszego przechowywania haseł.
Aby wyjaśnić, jak dodanie tego kroku wpływa na bezpieczeństwo użytkowników, wytłumaczmy jeden ze sposobów w jaki atakujący czasem zdobywają hasła użytkowników. W przypadku wycieku bazy danych naszego serwisu, atakujący poznają hasze haseł naszych użytkowników. Sama ta informacja jest bezużyteczna, bo haszem nie można się zalogować do serwisu, ani nie można odtworzyć na jego podstawie oryginalnego hasła. Ale jeżeli atakujący znają algorytm, którym obliczaliśmy nasze hasze, mogą stworzyć sobie swoją własną bazę danych haszy np. najpopularniejszych haseł, albo haseł stworzonych na podstawie słownika danego języka i porównać swoją bazę danych z haszami haseł naszych użytkowników. Oczywiście w ten sposób nie uda się znaleźć haseł wszystkich użytkowników, ale pewnie może być to spory odsetek.
W celu zapobiegnięcia takiemu atakowi, każdemu użytkownikowi możemy przypisać indywidualny, losowy ciąg znaków zwany “solą”, który następnie jest dodawany do hasła na etapie haszowania. Ponieważ ciągi są losowe i indywidualne, nie da się stworzyć uniwersalnej bazy danych z haszami, którą byłoby łatwo zestawić z haszami naszych użytkowników.
W związku z tym, potrzebujemy zapisać w bazie danych dwie rzeczy dla każdego użytkownika, zahaszowane hasło i sól użytą podczas haszowania
losowa_sól = random()
hasz_hasła = funkcja_haszująca(hasło_użytkownika + losowa_sól)
Dodatkowe zabezpieczenia
Powyższe dwie techniki to absolutny fundament bezpiecznego przechowywania haseł użytkownika. W celu poznania dodatkowych metod odsyłam do wspomnianej już wcześniej strony OWASP albo innych źródeł.