Najlepsze praktyki SPA dotyczące uwierzytelniania i zarządzania sesjami

307

Kiedy budujesz aplikacje w stylu SPA przy użyciu frameworków takich jak Angular, Ember, React itp., Co ludzie uważają za najlepsze praktyki uwierzytelniania i zarządzania sesjami? Mogę wymyślić kilka sposobów rozważenia rozwiązania problemu.

  1. Traktuj to nie inaczej niż uwierzytelnianie za pomocą zwykłej aplikacji internetowej, zakładając, że interfejs API i interfejs użytkownika mają tę samą domenę pochodzenia.

    Wymagałoby to prawdopodobnie posiadania pliku cookie sesji, przechowywania sesji po stronie serwera i prawdopodobnie pewnego punktu końcowego interfejsu API sesji, do którego uwierzytelniony internetowy interfejs użytkownika może trafić, aby uzyskać bieżące informacje o użytkowniku, aby pomóc w personalizacji, a nawet określeniu ról / zdolności po stronie klienta. Serwer nadal wymuszałby oczywiście zasady chroniące dostęp do danych, interfejs użytkownika używałby tych informacji do dostosowania sposobu działania.

  2. Traktuj to jak klienta zewnętrznego korzystającego z publicznego interfejsu API i uwierzytelnij się za pomocą systemu tokenów podobnego do OAuth. Ten mechanizm tokenu byłby używany przez interfejs użytkownika klienta do uwierzytelniania każdego żądania przesłanego do interfejsu API serwera.

Nie jestem zbytnio ekspertem, ale nr 1 wydaje się być całkowicie wystarczający w zdecydowanej większości przypadków, ale naprawdę chciałbym usłyszeć bardziej doświadczone opinie.

Chris Nicola
źródło
Perfer w ten sposób, stackoverflow.com/a/19820685/454252
allenhwkim

Odpowiedzi:

476

Pytanie to zostało poruszone w nieco innej formie, tutaj:

RESTful Authentication

Ale to rozwiązuje to po stronie serwera. Spójrzmy na to od strony klienta. Zanim to jednak zrobimy, istnieje ważne preludium:

JavaScript Crypto jest beznadziejny

Artykuł Matasano na ten temat jest znany, ale zawarte w nim lekcje są dość ważne:

https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2011/august/javascript-cryptography-consanted-harmful/

Podsumowując:

  • Atak typu man-in-the-middle może w prosty sposób zastąpić Twój kod kryptograficzny <script> function hash_algorithm(password){ lol_nope_send_it_to_me_instead(password); }</script>
  • Atak typu man-in-the-middle jest trywialny wobec strony, która obsługuje dowolny zasób przez połączenie inne niż SSL.
  • Gdy masz już SSL, i tak używasz prawdziwego szyfrowania.

I aby dodać własny wniosek:

  • Pomyślny atak XSS może spowodować, że osoba atakująca wykona kod w przeglądarce klienta, nawet jeśli używasz protokołu SSL - więc nawet jeśli walczysz o każdy właz, szyfrowanie przeglądarki może nadal zawieść, jeśli atakujący znajdzie sposób na wykonanie dowolny kod javascript w przeglądarce innej osoby.

To sprawia, że ​​wiele schematów uwierzytelniania RESTful jest niemożliwych lub niemądrych, jeśli zamierzasz używać klienta JavaScript. Spójrzmy!

Podstawowe uwierzytelnianie HTTP

Przede wszystkim uwierzytelnianie podstawowe HTTP. Najprostszy schemat: wystarczy podać nazwę i hasło przy każdym żądaniu.

To oczywiście wymaga protokołu SSL, ponieważ przekazujesz nazwę i hasło zakodowane w Base64 (odwracalnie) przy każdym żądaniu. Każdy, kto słucha przez linię, może w prosty sposób wyodrębnić nazwę użytkownika i hasło. Większość argumentów „Autoryzacja podstawowa jest niepewna” pochodzi z miejsca „Autoryzacja podstawowa przez HTTP”, co jest okropnym pomysłem.

Przeglądarka zapewnia wbudowaną obsługę uwierzytelniania podstawowego HTTP, ale jest brzydka jak grzech i prawdopodobnie nie należy jej używać w swojej aplikacji. Alternatywą jest jednak przechowywanie nazwy użytkownika i hasła w JavaScript.

To jest najbardziej RESTful rozwiązanie. Serwer nie wymaga żadnej wiedzy o stanie i uwierzytelnia każdą indywidualną interakcję z użytkownikiem. Niektórzy entuzjaści REST (głównie słowianie) twierdzą, że utrzymywanie dowolnego stanu jest herezją i będą pienieć na ustach, jeśli pomyślisz o jakiejkolwiek innej metodzie uwierzytelnienia. Istnieją teoretyczne korzyści z tego rodzaju zgodności ze standardami - jest obsługiwany przez Apache po wyjęciu z pudełka - możesz przechowywać swoje obiekty jako pliki w folderach chronionych plikami .htaccess, jeśli chcesz.

Problemem ? Buforujesz po stronie klienta nazwę użytkownika i hasło. Daje to evil.ru lepszy efekt - nawet najbardziej podstawowa słabość XSS może spowodować, że klient przesyła swoją nazwę użytkownika i hasło na zły serwer. Możesz spróbować zmniejszyć to ryzyko, mieszając i soląc hasło, ale pamiętaj: JavaScript Crypto jest beznadziejny . Możesz zmniejszyć to ryzyko, pozostawiając je do obsługi podstawowego uwierzytelniania przeglądarki, ale ... brzydkie jak grzech, jak wspomniano wcześniej.

HTTP Digest Auth

Czy możliwe jest uwierzytelnianie Digest w jQuery?

Bardziej „bezpieczne” uwierzytelnianie, jest to wyzwanie mieszania żądania / odpowiedzi. Z wyjątkiem JavaScript Crypto jest beznadziejny , więc działa tylko przez SSL i nadal musisz buforować nazwę użytkownika i hasło po stronie klienta, co czyni go bardziej skomplikowanym niż HTTP Basic Auth, ale nie jest bardziej bezpieczny .

Uwierzytelnianie zapytania z dodatkowymi parametrami podpisu.

Kolejne bardziej „bezpieczne” uwierzytelnianie, w którym szyfrujesz parametry za pomocą danych nonce i timing (w celu ochrony przed atakami powtarzającymi się i timing) i wysyłasz. Jednym z najlepszych przykładów tego jest protokół OAuth 1.0, który, o ile mi wiadomo, jest dość ciekawym sposobem na wdrożenie uwierzytelnienia na serwerze REST.

http://tools.ietf.org/html/rfc5849

Och, ale nie ma żadnych klientów OAuth 1.0 dla JavaScript. Dlaczego?

JavaScript Crypto jest beznadziejna , pamiętaj. JavaScript nie może uczestniczyć w OAuth 1.0 bez protokołu SSL, a nadal musisz przechowywać nazwę użytkownika i hasło klienta lokalnie - co stawia tę kategorię jako uwierzytelnianie szyfrowane - jest bardziej skomplikowane niż uwierzytelnianie podstawowe HTTP, ale nie jest bardziej bezpieczne .

Znak

Użytkownik wysyła nazwę użytkownika i hasło, aw zamian otrzymuje token, którego można użyć do uwierzytelnienia żądań.

Jest to nieznacznie bezpieczniejsze niż uwierzytelnianie podstawowe HTTP, ponieważ po zakończeniu transakcji nazwa użytkownika / hasło możesz odrzucić wrażliwe dane. Jest również mniej RESTful, ponieważ tokeny stanowią „stan” i komplikują implementację serwera.

SSL wciąż

Pociesz się jednak, że nadal musisz wysłać tę początkową nazwę użytkownika i hasło, aby otrzymać token. Wrażliwe informacje nadal dotykają Twojego skompromitowanego JavaScript.

Aby chronić dane uwierzytelniające użytkownika, nadal musisz powstrzymywać atakujących przed JavaScript, a także przesyłać nazwę użytkownika i hasło przez sieć. Wymagany SSL.

Wygaśnięcie tokena

Często egzekwowane są zasady dotyczące tokenów, takie jak „hej, gdy ten token jest za długi, odrzuć go i ponownie uwierzytelnij użytkownika”. lub „Jestem pewien, że jedynym adresem IP, który może używać tego tokena, jest XXX.XXX.XXX.XXX”. Wiele z tych zasad to całkiem dobre pomysły.

Ogień

Jednak użycie tokena Bez protokołu SSL jest nadal podatne na atak o nazwie „sidejacking”: http://codebutler.github.io/firesheep/

Atakujący nie uzyskuje poświadczeń użytkownika, ale nadal może udawać, że jest użytkownikiem, co może być dość złe.

tl; dr: Wysyłanie niezaszyfrowanych tokenów przez sieć oznacza, że ​​atakujący mogą łatwo zdobyć te tokeny i udawać, że są użytkownikiem. FireSheep to program, który sprawia, że ​​jest to bardzo łatwe.

Oddzielna, bezpieczniejsza strefa

Im większa aplikacja, którą uruchamiasz, tym trudniej jest absolutnie upewnić się, że nie będą w stanie wstrzyknąć kodu, który zmieni sposób przetwarzania wrażliwych danych. Czy całkowicie ufasz swojemu CDN? Twoi reklamodawcy? Twoja własna baza kodu?

Wspólne dla danych karty kredytowej, a rzadziej dla nazwy użytkownika i hasła - niektórzy realizatorzy przechowują „poufne dane” na innej stronie niż reszta aplikacji, strona, którą można ściśle kontrolować i zablokować najlepiej, jak to możliwe, najlepiej taką, która jest trudny do wyłudzenia użytkowników.

Cookie (oznacza po prostu Token)

Możliwe jest (i często) umieszczanie tokena uwierzytelniającego w pliku cookie. Nie zmienia to żadnych właściwości uwierzytelniania za pomocą tokena, jest to raczej wygoda. Wszystkie poprzednie argumenty nadal obowiązują.

Sesja (wciąż oznacza tylko Token)

Session Auth to po prostu uwierzytelnianie tokena, ale z kilkoma różnicami, które sprawiają, że wygląda to nieco inaczej:

  • Użytkownicy rozpoczynają od nieuwierzytelnionego tokena.
  • Backend utrzymuje obiekt „state”, który jest powiązany z tokenem użytkownika.
  • Token znajduje się w pliku cookie.
  • Środowisko aplikacji oddziela szczegóły od Ciebie.

Poza tym tak naprawdę nie różni się niczym od Token Auth.

Wędruje to jeszcze dalej od implementacji RESTful - z obiektami stanu idziesz dalej i dalej ścieżką zwykłego RPC na stanowym serwerze.

OAuth 2.0

OAuth 2.0 analizuje problem: „W jaki sposób oprogramowanie A zapewnia oprogramowaniu B dostęp do danych użytkownika X bez konieczności posiadania przez oprogramowanie B dostępu do danych logowania użytkownika X”.

Implementacja jest bardzo standardowym sposobem dla użytkownika, aby uzyskać token, a następnie dla usługi innej firmy, aby przejść „tak, ten użytkownik i ten token pasują, a teraz możesz uzyskać od nas niektóre ich dane”.

Zasadniczo jednak OAuth 2.0 to tylko protokół tokena. Wykazuje te same właściwości, co inne protokoły tokenów - nadal potrzebujesz SSL do ochrony tych tokenów - po prostu zmienia sposób ich generowania.

Są dwa sposoby, w jakie OAuth 2.0 może ci pomóc:

  • Udzielanie uwierzytelnienia / informacji innym osobom
  • Uzyskiwanie uwierzytelnienia / informacji od innych

Ale jeśli chodzi o to, po prostu ... używasz tokenów.

Powrót do twojego pytania

Pytanie, które zadajesz, brzmi: „czy powinienem przechowywać mój token w pliku cookie i pozwolić, aby automatyczne zarządzanie sesjami w moim środowisku zadbało o szczegóły, czy też powinienem przechowywać mój token w JavaScript i sam z nimi obchodzić?”

Odpowiedź brzmi: rób wszystko, co czyni cię szczęśliwym .

Jednak automatyczne zarządzanie sesjami polega na tym, że za kulisami dzieje się wiele magii. Często lepiej jest samemu kontrolować te szczegóły.

Mam 21 lat, więc SSL to tak

Inna odpowiedź brzmi: użyj https do wszystkiego, a bandyci ukradną hasła i tokeny użytkowników.

Curtis Lassam
źródło
3
Świetna odpowiedź. Doceniam równoważność między systemami uwierzytelniania tokenów a podstawowym uwierzytelnianiem plików cookie (które jest często wbudowane w środowisko sieciowe). Tego właśnie szukałem. Doceniam, że omawiasz tak wiele potencjalnych problemów. Twoje zdrowie!
Chris Nicola
11
Wiem, że minęło trochę czasu, ale zastanawiam się, czy należy to rozszerzyć o JWT? auth0.com/blog/2014/01/07/…
Chris Nicola,
14
It's also less RESTful, as tokens constitute "state and make the server implementation more complicated."REST Token (1) wymaga, aby serwer był bezstanowy. Token przechowywany po stronie klienta nie reprezentuje stanu w żaden znaczący sposób dla serwera. (2) Znacznie bardziej skomplikowany kod po stronie serwera nie ma nic wspólnego z RESTfulness.
zupa zupa
10
lol_nope_send_it_to_me_insteadPodobało mi się nazwa tej funkcji: D
Leo
6
Jedną z rzeczy, które wydają się przeoczyć: Pliki cookie są bezpieczne dla XSS, jeśli są oznaczone jako httpOnly, i można je dodatkowo zablokować za pomocą bezpiecznego i samesite. A obsługa plików cookie trwa już znacznie dłużej === bardziej zahartowana w bitwie. Poleganie na bezpieczeństwie tokenów na JS i lokalnym magazynie to gra głupców.
Martijn Pieters
57

Możesz zwiększyć bezpieczeństwo procesu uwierzytelniania, używając JWT (tokeny WWW JSON) i SSL / HTTPS.

Podstawowy identyfikator uwierzytelnienia / sesji można ukraść za pomocą:

  • Atak MITM (Man-In-The-Middle) - bez SSL / HTTPS
  • Intruz uzyskujący dostęp do komputera użytkownika
  • XSS

Korzystając z JWT, szyfrujesz dane uwierzytelniające użytkownika i zapisujesz je w kliencie, i wysyłasz je wraz z każdym żądaniem do API, gdzie serwer / API sprawdza token. Nie można go odszyfrować / odczytać bez klucza prywatnego (który serwer / interfejs API przechowuje potajemnie). Przeczytaj aktualizację .

Nowy (bezpieczniejszy) przepływ byłby:

Zaloguj sie

  • Użytkownik loguje się i wysyła dane logowania do interfejsu API (przez SSL / HTTPS)
  • Interfejs API odbiera dane logowania
  • Jeśli jest ważny:
    • Zarejestruj nową sesję w bazie danych Przeczytaj aktualizację
    • Zaszyfruj identyfikator użytkownika, identyfikator sesji, adres IP, znacznik czasu itp. W JWT za pomocą klucza prywatnego.
  • Interfejs API wysyła token JWT z powrotem do klienta (przez SSL / HTTPS)
  • Klient otrzymuje token JWT i przechowuje w localStorage / cookie

Każde żądanie do API

  • Użytkownik wysyła żądanie HTTP do API (przez SSL / HTTPS) z zapisanym tokenem JWT w nagłówku HTTP
  • Interfejs API odczytuje nagłówek HTTP i odszyfrowuje token JWT za pomocą klucza prywatnego
  • API sprawdza poprawność tokena JWT, dopasowuje adres IP z żądania HTTP do tego w tokenie JWT i sprawdza, czy sesja wygasła
  • Jeśli jest ważny:
    • Zwróć odpowiedź z żądaną treścią
  • Jeśli nieważne:
    • Wyjątek dotyczący rzucania (403/401)
    • Zgłoś wtargnięcie do systemu
    • Wyślij wiadomość e-mail z ostrzeżeniem do użytkownika.

Zaktualizowano 30.07.15:

Ładunek / roszczenia JWT można odczytać bez klucza prywatnego (tajnego) i przechowywanie go w localStorage nie jest bezpieczne. Przykro mi z powodu tych fałszywych oświadczeń. Wydaje się jednak, że pracują na standardzie JWE (JSON Web Encryption) .

Zaimplementowałem to, przechowując oświadczenia (userID, exp) w JWT, podpisałem go kluczem prywatnym (tajnym), o którym API / backend wie tylko i zapisałem jako bezpieczny plik cookie HttpOnly na kliencie. W ten sposób nie można go odczytać przez XSS i nie można nim manipulować, w przeciwnym razie JWT nie powiedzie się weryfikacja podpisu. Korzystając z bezpiecznego pliku cookie HttpOnly , upewniasz się, że plik cookie jest wysyłany tylko za pośrednictwem żądań HTTP (niedostępnych dla skryptu) i wysyłany tylko za pośrednictwem bezpiecznego połączenia (HTTPS).

Zaktualizowano 17.07.16:

JWT są z natury bezpaństwowcami. Oznacza to, że same unieważniają / wygasają. Dodając identyfikator SessionID do oświadczeń tokena, nadajesz mu status, ponieważ jego ważność nie zależy teraz tylko od weryfikacji podpisu i daty wygaśnięcia, ale także od stanu sesji na serwerze. Zaletą jest jednak to, że możesz łatwo unieważnić tokeny / sesje, czego nie było wcześniej w przypadku bezpaństwowych JWT.

Gaui
źródło
1
W końcu JWT jest nadal „tylko tokenem” z punktu widzenia bezpieczeństwa, jak sądzę. Serwer nadal może powiązać identyfikator użytkownika, adres IP, znacznik czasu itp. Z nieprzezroczystym tokenem sesji i nie byłby mniej lub bardziej bezpieczny niż JWT. Jednak bezstanowy charakter JWT ułatwia implementację.
James
1
@James JWT ma tę zaletę, że jest weryfikowalny i może przenosić kluczowe szczegóły. Jest to bardzo przydatne w różnych scenariuszach API, na przykład gdy wymagane jest uwierzytelnianie między domenami. Coś z sesji nie będzie tak dobre. Jest to także zdefiniowana (lub przynajmniej w toku) specyfikacja, która jest przydatna w implementacjach. Nie oznacza to, że jest lepsza niż jakakolwiek inna dobra implementacja tokena, ale jest dobrze zdefiniowana i wygodna.
Chris Nicola
1
@Chris Tak Zgadzam się ze wszystkimi twoimi punktami. Jednak przepływ opisany w powyższej odpowiedzi nie jest z natury bardziej bezpiecznym przepływem, jak twierdzono ze względu na zastosowanie JWT. Ponadto JWT nie można odwołać w schemacie opisanym powyżej, chyba że powiążesz identyfikator z JWT i zapiszesz stan na serwerze. W przeciwnym razie musisz albo regularnie otrzymywać nowy JWT, prosząc o nazwę użytkownika / hasło (słaba wygoda użytkownika), albo wydać JWT z bardzo długim okresem ważności (źle, jeśli token zostanie skradziony).
James
1
Moja odpowiedź nie jest w 100% poprawna, ponieważ JWT można faktycznie odszyfrować / odczytać bez klucza prywatnego (tajnego) i przechowywanie go w localStorage nie jest bezpieczne. Zaimplementowałem to, przechowując oświadczenia (userID, exp) w JWT, podpisałem go kluczem prywatnym (tajnym), o którym API / backend wie tylko i zapisałem jako plik cookie HttpOnly na kliencie. W ten sposób XSS nie może go odczytać. Ale musisz użyć HTTPS, ponieważ token może zostać skradziony podczas ataku MITM. Zaktualizuję moją odpowiedź, aby się nad tym zastanowić.
Gaui
1
@vsenko Plik cookie jest wysyłany przy każdym żądaniu klienta. Nie masz dostępu do pliku cookie z JS, jest on powiązany z każdym żądaniem HTTP od klienta do interfejsu API.
Gaui
7

Wybrałbym drugi system tokenów.

Czy wiesz o ember-auth lub ember-simple-auth ? Oboje używają systemu opartego na tokenach, takich jak stany ember-simple-auth:

Lekka i dyskretna biblioteka do implementacji uwierzytelniania opartego na tokenach w aplikacjach Ember.js. http://ember-simple-auth.simplabs.com

Mają zarządzanie sesjami i można je łatwo podłączyć do istniejących projektów.

Istnieje również przykładowa wersja ember-simple-auth Ember App Kit: działający przykład ember-app-kit używającej ember-simple-auth do uwierzytelniania OAuth2.

DelphiLynx
źródło