Unieważnianie tokenów internetowych JSON

420

W przypadku nowego projektu node.js, nad którym pracuję, zastanawiam się nad przejściem z sesji opartej na plikach cookie (mam na myśli przechowywanie identyfikatora w magazynie kluczy i wartości zawierającym sesje użytkownika w przeglądarce użytkownika) do sesji opartej na tokenach (bez magazynu kluczy i wartości) przy użyciu tokenów WWW JSON (jwt).

Projekt jest grą wykorzystującą socket.io - posiadanie sesji tokena byłoby przydatne w takim scenariuszu, w którym w jednej sesji będzie wiele kanałów komunikacji (web i socket.io)

W jaki sposób można zapewnić unieważnienie tokena / sesji z serwera przy użyciu metody jwt?

Chciałem również zrozumieć, na jakie częste (lub nietypowe) pułapki / ataki powinienem uważać przy tego rodzaju paradygmacie. Na przykład, jeśli ten paradygmat jest podatny na te same / różne rodzaje ataków co podejście oparte na przechowywaniu sesji / plikach cookie.

Powiedzmy, że mam następujące (dostosowane z tego i tego ):

Logowanie do sklepu sesyjnego:

app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        // Create session token
        var token= createSessionToken();

        // Add to a key-value database
        KeyValueStore.add({token: {userid: profile.id, expiresInMinutes: 60}});

        // The client should save this session token in a cookie
        response.json({sessionToken: token});
    });
}

Logowanie tokena:

var jwt = require('jsonwebtoken');
app.get('/login', function(request, response) {
    var user = {username: request.body.username, password: request.body.password };
    // Validate somehow
    validate(user, function(isValid, profile) {
        var token = jwt.sign(profile, 'My Super Secret', {expiresInMinutes: 60});
        response.json({token: token});
    });
}

-

Wylogowanie (lub unieważnienie) dla podejścia Session Store wymagałoby aktualizacji bazy danych KeyValueStore o podany token.

Wydaje się, że taki mechanizm nie istniałby w podejściu opartym na tokenach, ponieważ sam token zawierałby informacje, które normalnie istniałyby w magazynie klucz-wartość.

funseiki
źródło
1
Jeśli używasz pakietu „express-jwt”, możesz spojrzeć na tę isRevokedopcję lub spróbować replikować tę samą funkcjonalność. github.com/auth0/express-jwt#revoked-tokens
Signus
1
Rozważ użycie krótkiego czasu wygaśnięcia tokena dostępu i użycie tokenu odświeżania z długim okresem ważności, aby umożliwić sprawdzenie statusu dostępu użytkownika w bazie danych (czarna lista). auth0.com/blog/…
Rohmer
inną opcją byłoby dołączenie adresu IP w ładunku podczas generowania tokena jwt i sprawdzanie zapisanego adresu IP w porównaniu z przychodzącym żądaniem dla tego samego adresu IP. np .: req.connection.remoteAddress w nodeJs. Są dostawcy usług internetowych, którzy nie wydają statycznego adresu IP na klienta, myślę, że nie będzie to problemem, dopóki klient nie połączy się ponownie z Internetem.
Gihan Sandaru

Odpowiedzi:

391

Ja również badam to pytanie i chociaż żaden z poniższych pomysłów nie jest kompletnym rozwiązaniem, mogą one pomóc innym wykluczyć pomysły lub dostarczyć dalsze.

1) Po prostu usuń token z klienta

Oczywiście nie ma to żadnego wpływu na bezpieczeństwo po stronie serwera, ale powstrzymuje atakującego poprzez usunięcie tokena z istnienia (tzn. Musiałby go skradzić przed wylogowaniem).

2) Utwórz czarną listę tokenów

Możesz przechowywać nieprawidłowe tokeny do ich pierwotnej daty wygaśnięcia i porównywać je z przychodzącymi żądaniami. Wydaje się, że to przede wszystkim neguje powód przejścia na token, ponieważ trzeba będzie dotykać bazy danych dla każdego żądania. Rozmiar pamięci prawdopodobnie byłby mniejszy, ponieważ wystarczyłoby przechować tokeny, które znajdowały się między czasem wylogowania a czasem wygaśnięcia (jest to przeczucie i zdecydowanie zależy od kontekstu).

3) Po prostu skróć czas ważności tokena i często go zmieniaj

Jeśli dotrzymasz czasu wygaśnięcia tokena w wystarczająco krótkich odstępach czasu, a działający klient będzie śledził i żądał aktualizacji w razie potrzeby, numer 1 działałby skutecznie jako kompletny system wylogowania. Problem z tą metodą polega na tym, że uniemożliwia to zalogowanie użytkownika między zamknięciami kodu klienta (w zależności od tego, jak długo upływa interwał wygasania).

Plany awaryjne

Jeśli kiedykolwiek zdarzyła się sytuacja wyjątkowa lub token użytkownika został przejęty, jedną z rzeczy, które możesz zrobić, jest zezwolenie użytkownikowi na zmianę identyfikatora odnośnika użytkownika za pomocą jego poświadczeń logowania. Spowodowałoby to unieważnienie wszystkich powiązanych tokenów, ponieważ nie można już znaleźć powiązanego użytkownika.

Chciałem również zauważyć, że dobrym pomysłem jest dołączenie do tokena ostatniej daty logowania, abyś mógł wymusić ponowne zalogowanie po pewnym czasie.

Jeśli chodzi o podobieństwa / różnice w odniesieniu do ataków przy użyciu tokenów, ten post dotyczy pytania: https://github.com/dentarg/blog/blob/master/_posts/2014-01-07-angularjs-authentication-w--cookies -vs-token.markdown

Matt Way
źródło
3
Doskonałe podejście. Moją odwagą byłoby wykonanie kombinacji wszystkich 3 i / lub żądanie nowego tokena po każdych „n” żądaniach (w przeciwieństwie do timera). Używamy redis do przechowywania obiektów w pamięci i moglibyśmy z łatwością użyć tego dla przypadku nr 2, a wtedy opóźnienie spadłoby ZNACZNIE.
Aaron Wagner
2
Ten kodujący post z horrorem oferuje kilka porad: utrzymuj sesje zawierające pliki cookie (lub tokeny) krótkie, ale czyń je niewidocznymi dla użytkownika - co wydaje się być zgodne z punktem 3. Moje własne przeczucie (być może dlatego, że jest bardziej tradycyjne) polega na tym, że token (lub jego skrót) działa jak klucz do białej bazy danych sesji (podobnej do # 2)
funseiki 18.04.2014
7
Artykuł jest dobrze napisany i stanowi rozwiniętą wersję 2)powyższego. Chociaż działa dobrze, osobiście nie widzę dużej różnicy w stosunku do tradycyjnych sklepów z sesjami. Wydaje mi się, że zapotrzebowanie na miejsce byłoby niższe, ale nadal potrzebujesz bazy danych. Największym urokiem JWT było dla mnie to, że w ogóle nie korzystałem z bazy danych do sesji.
Matt Way
210
Powszechnym podejściem do unieważniania tokenów, gdy użytkownik zmienia swoje hasło, jest podpisanie tokena hashem swojego hasła. Dlatego jeśli hasło się zmieni, wszelkie poprzednie tokeny automatycznie nie weryfikują się. Można to rozszerzyć na wylogowanie, włączając czas ostatniego wylogowania do rekordu użytkownika i używając kombinacji czasu ostatniego wylogowania i skrótu hasła do podpisania tokena. Wymaga to sprawdzenia DB za każdym razem, gdy trzeba zweryfikować podpis tokena, ale prawdopodobnie i tak szukasz użytkownika.
Travis Terry,
4
Czarną listę można usprawnić, utrzymując ją w pamięci, dzięki czemu DB wystarczy tylko w celu zarejestrowania unieważnień i usunięcia wygasłych unieważnień oraz odczytu tylko przy uruchomieniu serwera. W architekturze równoważenia obciążenia czarna lista w pamięci może sondować DB w krótkich odstępach czasu, takich jak 10s, ograniczając ekspozycję unieważnionych tokenów. Te podejścia pozwalają serwerowi na dalsze uwierzytelnianie żądań bez dostępu do bazy danych na żądanie.
Joe Lapp
85

Pomysły zamieszczone powyżej są dobre, ale bardzo prostym i łatwym sposobem na unieważnienie wszystkich istniejących JWT jest po prostu zmiana tajemnicy.

Jeśli Twój serwer tworzy JWT, podpisuje go tajnym kluczem (JWS), a następnie wysyła go do klienta, wystarczy zmienić klucz tajny, unieważniając wszystkie istniejące tokeny i wymagając od wszystkich użytkowników uzyskania nowego tokena w celu uwierzytelnienia, ponieważ ich stary token nagle traci ważność zgodnie z do serwera.

Nie wymaga żadnych zmian w rzeczywistej zawartości tokena (lub identyfikatora wyszukiwania).

Oczywiście działa to tylko w nagłych przypadkach, gdy chciałeś, aby wygasły wszystkie istniejące tokeny, dla wygaśnięcia tokena wymagane jest jedno z powyższych rozwiązań (takie jak krótki czas wygaśnięcia tokena lub unieważnienie przechowywanego klucza w tokenie).

Andy
źródło
9
Myślę, że to podejście nie jest idealne. Chociaż działa i jest z pewnością prosty, wyobraź sobie przypadek, w którym używasz klucza publicznego - nie chcesz iść i odtwarzać tego klucza za każdym razem, gdy chcesz unieważnić pojedynczy token.
Signus
1
@KijanaWoodard, para kluczy publiczny / prywatny może być używana do sprawdzania poprawności podpisu jako efektywnego klucza tajnego w algorytmie RS256. W pokazanym tutaj przykładzie wspomina o zmianie tajemnicy, aby unieważnić JWT. Można tego dokonać albo a) wprowadzając fałszywy klucz pub, który nie pasuje do podpisu, albo b) generując nowy klucz pub. W tej sytuacji jest to mniej niż idealne.
Signus
1
@Signus - gotcha. nie używając klucza publicznego jako tajnego, ale inni mogą polegać na kluczu publicznym w celu weryfikacji podpisu.
Kijana Woodard,
8
To bardzo złe rozwiązanie. Głównym powodem korzystania z JWT jest jego bezpaństwowość i skalowanie. Korzystanie z dynamicznego sekretu wprowadza stan. Jeśli usługa jest skupiona w wielu węzłach, trzeba będzie zsynchronizować klucz tajny za każdym razem, gdy zostanie wydany nowy token. Będziesz musiał przechowywać sekrety w bazie danych lub innej zewnętrznej usłudze, która polegałaby na ponownym wynalezieniu uwierzytelniania opartego na plikach cookie
Tuomas Toivonen
5
@TuomasToivonen, ale musisz podpisać JWT za pomocą klucza tajnego i mieć możliwość weryfikacji JWT za pomocą tego samego klucza tajnego. Musisz więc przechowywać sekret w chronionych zasobach. Jeśli tajemnica zostanie naruszona, musisz ją zmienić i rozpowszechnić tę zmianę w każdym ze swoich węzłów. Dostawcy usług hostingowych z klastrowaniem / skalowaniem zwykle umożliwiają przechowywanie tajemnic w ich usługach, aby ich dystrybucja była łatwa i niezawodna.
Rohmer,
67

Jest to przede wszystkim długi komentarz wspierający odpowiedź @mattway i oparty na niej

Dany:

Niektóre z innych proponowanych rozwiązań na tej stronie zalecają odwiedzanie magazynu danych na każde żądanie. Jeśli trafisz do głównego magazynu danych, aby sprawdzić poprawność każdego żądania uwierzytelnienia, nie widzę powodu, aby używać JWT zamiast innych ustalonych mechanizmów uwierzytelniania tokenów. Zasadniczo uczyniłeś JWT stanowym, zamiast bezpaństwowego, jeśli za każdym razem odwiedzasz magazyn danych.

(Jeśli Twoja witryna otrzyma dużą liczbę nieautoryzowanych żądań, JWT odmówi ich nie trafiając do magazynu danych, co jest pomocne. Prawdopodobnie istnieją inne podobne przypadki użycia).

Dany:

Prawdziwie bezpaństwowego uwierzytelnienia JWT nie można uzyskać dla typowej aplikacji internetowej w świecie rzeczywistym, ponieważ bezstanowy JWT nie ma możliwości zapewnienia natychmiastowej i bezpiecznej obsługi następujących ważnych przypadków użycia:

Konto użytkownika zostało usunięte / zablokowane / zawieszone.

Hasło użytkownika zostało zmienione.

Role lub uprawnienia użytkownika są zmieniane.

Użytkownik jest wylogowany przez administratora.

Wszelkie inne krytyczne dane aplikacji w tokenie JWT są zmieniane przez administratora witryny.

W takich przypadkach nie można czekać na wygaśnięcie tokena. Unieważnienie tokena musi nastąpić natychmiast. Ponadto nie można ufać klientowi, że nie zachowa i nie użyje kopii starego tokena, czy to ze złośliwym zamiarem, czy nie.

Dlatego: Myślę, że odpowiedź z @ matt-way, nr 2 TokenBlackList, byłaby najbardziej wydajnym sposobem na dodanie wymaganego stanu do uwierzytelnienia opartego na JWT.

Masz czarną listę, która przechowuje te tokeny, dopóki nie upłynie ich data ważności. Lista tokenów będzie dość niewielka w porównaniu z całkowitą liczbą użytkowników, ponieważ musi ona utrzymywać tokeny z czarnej listy aż do wygaśnięcia. Wdrożyłbym, umieszczając unieważnione tokeny w redis, memcached lub innym magazynie danych w pamięci, który obsługuje ustawianie czasu ważności klucza.

Nadal musisz zadzwonić do bazy danych w pamięci dla każdego żądania uwierzytelnienia, które przechodzi wstępne uwierzytelnianie JWT, ale nie musisz tam przechowywać kluczy dla całego zestawu użytkowników. (Co może, ale nie musi być wielką sprawą dla danej witryny).

Ed J
źródło
15
Nie zgadzam się z twoją odpowiedzią. Uderzenie w bazę danych nie powoduje stanu; przechowuje stan na backend. JWT nie został utworzony, abyś nie musiał trafiać do bazy danych przy każdym żądaniu. Każda duża aplikacja korzystająca z JWT jest wspierana przez bazę danych. JWT rozwiązuje zupełnie inny problem. en.wikipedia.org/wiki/Stateless_protocol
Julian
6
@Julian, czy mógłbyś to trochę rozwinąć? Który problem tak naprawdę rozwiązuje JWT?
zero01alpha
8
@ zero01alpha Uwierzytelnianie: jest to najczęstszy scenariusz korzystania z JWT. Po zalogowaniu się każde kolejne żądanie będzie zawierać JWT, umożliwiając mu dostęp do tras, usług i zasobów, które są dozwolone za pomocą tego tokena. Wymiana informacji: Tokeny sieciowe JSON to dobry sposób bezpiecznego przesyłania informacji między stronami. Ponieważ podpisy JWT mogą być podpisane, możesz być pewien, że nadawcy są tymi, za których się podają. Zobacz jwt.io/introduction
Julian
7
@Julian Nie zgadzam się z twoją niezgodą :) JWT rozwiązuje problem (w przypadku usług) konieczności uzyskania dostępu do scentralizowanego podmiotu zapewniającego informacje autoryzacyjne dla dowolnego klienta. Więc zamiast usługi A i usługi B trzeba uzyskać dostęp do jakiegoś zasobu, aby dowiedzieć się, czy klient X ma uprawnienia do zrobienia czegoś, usługi A i B otrzymują token od X, który potwierdza jego uprawnienia (najczęściej wydawane przez trzecią osobę) przyjęcie). W każdym razie JWT jest narzędziem, które pomaga uniknąć stanu wspólnego między usługami w systemie, szczególnie gdy są one kontrolowane przez więcej niż jednego dostawcę usług.
LIvanov
1
Również z jwt.io/introduction If the JWT contains the necessary data, the need to query the database for certain operations may be reduced, though this may not always be the case.
giantas
43

Zapisałbym numer wersji JWT w modelu użytkownika. Nowe tokeny jwt ustawiłyby na to swoją wersję.

Podczas sprawdzania poprawności pliku JWT wystarczy sprawdzić, czy ma on numer wersji równy bieżącej wersji pliku JWT użytkownika.

Za każdym razem, gdy chcesz unieważnić stare JWTS, po prostu podbij numer wersji JWT użytkowników.

DaftMonk
źródło
15
To ciekawy pomysł, jedyną rzeczą jest miejsce przechowywania wersji, ponieważ w ramach tokena jest to, że jest on bezstanowy i nie musi korzystać z bazy danych. Wersja zakodowana utrudniłaby zderzenie, a numer wersji w bazie danych negowałby niektóre korzyści płynące z używania tokenów.
Stephen Smith
13
Prawdopodobnie przechowujesz już identyfikator użytkownika w tokenie, a następnie odpytujesz bazę danych, aby sprawdzić, czy użytkownik istnieje / ma uprawnienia dostępu do punktu końcowego interfejsu API. Więc nie robisz żadnych dodatkowych zapytań db, porównując numer wersji tokena jwt z numerem użytkownika.
DaftMonk
5
Nie powinienem chyba mówić, ponieważ istnieje wiele sytuacji, w których można użyć tokenów z walidacjami, które w ogóle nie dotykają bazy danych. Myślę jednak, że w tym przypadku trudno go uniknąć.
DaftMonk
11
Co się stanie, jeśli użytkownik zaloguje się z wielu urządzeń? Czy należy używać jednego tokena na wszystkich, czy też logowanie powinno unieważnić wszystkie poprzednie?
meeDamian,
10
Zgadzam się z @SergioCorrea To sprawiłoby, że JWT byłby prawie tak samo stanowy jak każdy inny mechanizm uwierzytelniania tokena.
Ed J
40

Jeszcze tego nie próbowałem i wykorzystuje wiele informacji na podstawie niektórych innych odpowiedzi. Złożoność polega na tym, aby uniknąć wywołania składnicy danych po stronie serwera na żądanie informacji użytkownika. Większość innych rozwiązań wymaga wyszukiwania bazy danych na żądanie do magazynu sesji użytkownika. W niektórych scenariuszach jest to w porządku, ale zostało to stworzone, aby uniknąć takich wywołań i sprawić, by wymagany stan serwera był bardzo mały. Ostatecznie odtworzysz sesję po stronie serwera, jednak niewielką, aby zapewnić wszystkie funkcje wymuszania unieważnienia. Ale jeśli chcesz to zrobić, oto sedno:

Cele:

  • Ogranicz użycie magazynu danych (bez stanu).
  • Możliwość wymuszenia wylogowania wszystkich użytkowników.
  • Możliwość wymuszenia wylogowania dowolnej osoby w dowolnym momencie.
  • Możliwość żądania ponownego wprowadzenia hasła po upływie określonego czasu.
  • Możliwość współpracy z wieloma klientami.
  • Możliwość wymuszenia ponownego logowania, gdy użytkownik kliknie wyloguj się z określonego klienta. (Aby zapobiec „cofnięciu” tokena klienta po odejściu użytkownika - dodatkowe informacje można znaleźć w komentarzach)

Rozwiązanie:

  • Użyj krótkotrwałych (<5m) tokenów dostępu w połączeniu z dłuższym (kilka godzin) tokenem odświeżania przechowywanym przez klienta .
  • Każde żądanie sprawdza ważność daty ważności tokenu uwierzytelniania lub odświeżania.
  • Po wygaśnięciu tokena dostępu klient używa tokenu odświeżania do odświeżania tokena dostępu.
  • Podczas sprawdzania tokena odświeżania serwer sprawdza małą czarną listę identyfikatorów użytkowników - jeśli zostanie znaleziony, odrzuć żądanie odświeżenia.
  • Jeśli klient nie ma prawidłowego (nie wygasłego) tokenu odświeżania lub uwierzytelnienia, użytkownik musi się zalogować ponownie, ponieważ wszystkie inne żądania zostaną odrzucone.
  • Na żądanie logowania sprawdź magazyn danych użytkownika pod kątem blokady.
  • Po wylogowaniu - dodaj tego użytkownika do czarnej listy sesji, aby musiał się ponownie zalogować. Trzeba będzie przechowywać dodatkowe informacje, aby nie wylogować ich ze wszystkich urządzeń w środowisku wielu urządzeń, ale można to zrobić, dodając pole urządzenia do czarna lista użytkowników.
  • Aby wymusić ponowne wejście po upływie x czasu - zachowaj datę ostatniego logowania w tokenie uwierzytelnienia i sprawdź ją na żądanie.
  • Aby wymusić wylogowanie wszystkich użytkowników - zresetuj klucz skrótu tokena.

Wymaga to utrzymania czarnej listy (stanu) na serwerze, przy założeniu, że tabela użytkowników zawiera informacje o zablokowanych użytkownikach. Nieprawidłowa czarna lista sesji - to lista identyfikatorów użytkowników. Ta czarna lista jest sprawdzana tylko podczas żądania tokenu odświeżania. Wpisy są wymagane, aby żyć na nim tak długo, jak token odświeżania TTL. Po wygaśnięciu tokena odświeżania użytkownik będzie musiał się ponownie zalogować.

Cons:

  • Nadal wymagane jest sprawdzenie magazynu danych na żądanie tokenu odświeżania.
  • Nieprawidłowe tokeny mogą nadal działać dla TTL tokena dostępu.

Plusy:

  • Zapewnia pożądaną funkcjonalność.
  • Akcja odświeżania tokena jest ukryta przed użytkownikiem podczas normalnej pracy.
  • Wymagane tylko, aby wykonać wyszukiwanie magazynu danych dla żądań odświeżenia zamiast każdego żądania. tj. 1 co 15 minut zamiast 1 na sekundę.
  • Minimalizuje stan serwera do bardzo małej czarnej listy.

Dzięki temu rozwiązaniu magazyn danych w pamięci, taki jak Reddis, nie jest potrzebny, przynajmniej nie dla informacji o użytkowniku, ponieważ użytkownik wykonuje połączenie db co 15 minut. W przypadku korzystania z funkcji Reddis przechowywanie prawidłowej / nieprawidłowej listy sesji byłoby bardzo szybkim i prostszym rozwiązaniem. Nie ma potrzeby tokena odświeżania. Każdy token uwierzytelnienia miałby identyfikator sesji i identyfikator urządzenia, mogą one być przechowywane w tabeli reddis podczas tworzenia i unieważniane w razie potrzeby. Następnie będą sprawdzane przy każdym żądaniu i odrzucane, gdy są nieważne.

Ashtonian
źródło
Co ze scenariuszem, w którym jedna osoba wstaje z komputera, aby umożliwić innej osobie korzystanie z tego samego komputera? Pierwsza osoba wyloguje się i oczekuje, że wylogowanie natychmiast zablokuje drugą osobę. Jeśli druga osoba jest przeciętnym użytkownikiem, klient może łatwo zablokować użytkownika, usuwając token. Ale jeśli drugi użytkownik ma umiejętności hakowania, ma czas na odzyskanie wciąż ważnego tokena w celu uwierzytelnienia się jako pierwszy użytkownik. Wydaje się, że nie można w żaden sposób uniknąć konieczności natychmiastowego unieważnienia tokenów, bez opóźnień.
Joe Lapp
5
Możesz też usunąć swój JWT z sesji / lokalnego magazynu lub pliku cookie.
Kamil Kiełczewski,
1
Dzięki @Ashtonian. Po przeprowadzeniu rozległych badań porzuciłem JWT. O ile nie podejmiesz nadzwyczajnych starań, aby zabezpieczyć tajny klucz, lub jeśli nie delegujesz do bezpiecznej implementacji protokołu OAuth, JWT są znacznie bardziej narażone na atak niż zwykłe sesje. Zobacz mój pełny raport: by.jtl.xyz/2016/06/the-unspoken-vulnerability-of-jwts.html
Joe Lapp
2
Korzystanie z tokena odświeżania jest kluczem do umożliwienia umieszczenia na czarnej liście. Świetne wyjaśnienie: auth0.com/blog/…
Rohmer
1
To wydaje mi się najlepszą odpowiedzią, ponieważ łączy w sobie krótkotrwały token dostępu z długowiecznym tokenem odświeżania, który można umieścić na czarnej liście. Po wylogowaniu klient powinien usunąć token dostępu, aby drugi użytkownik nie mógł uzyskać dostępu (nawet jeśli token dostępu pozostanie ważny jeszcze przez kilka minut po wylogowaniu). @Joe Lapp mówi, że haker (drugi użytkownik) otrzymuje token dostępu nawet po jego usunięciu. W jaki sposób?
M3RS
14

Podejście, które rozważałem, to zawsze mieć iat(wyemitowaną w) wartość w JWT. Następnie, gdy użytkownik się wyloguje, zapisz ten znacznik czasu w rekordzie użytkownika. Podczas sprawdzania poprawności JWT wystarczy porównać z datą iatostatniego wylogowania. Jeśli iatjest starszy, to nie jest poprawny. Tak, musisz przejść do bazy danych, ale i tak zawsze ściągam rekord użytkownika, jeśli JWT jest w inny sposób poprawny.

Główną wadą, którą dostrzegam, jest to, że wyloguje ich ze wszystkich sesji, jeśli są w wielu przeglądarkach lub mają klienta mobilnego.

Może to być również niezły mechanizm unieważniania wszystkich JWT w systemie. Część kontroli może być w stosunku do globalnego znacznika czasu ostatniego ważnego iatczasu.

Brack Mo
źródło
1
Dobra myśl! Aby rozwiązać problem „jednego urządzenia”, należy uczynić to funkcją awaryjną, a nie wylogowaniem. Zapisz datę w rekordzie użytkownika, która unieważnia wszystkie tokeny wydane przed nim. Coś jak token_valid_afterlub coś. Niesamowite!
OneHoopyFrood
1
Hej @OneHoopyFrood masz przykładowy kod, który pomoże mi lepiej zrozumieć ten pomysł? Doceniam twoją pomoc!
alexventuraio
2
Podobnie jak wszystkie inne proponowane rozwiązania, to wymaga wyszukiwania w bazie danych, co jest przyczyną tego pytania, ponieważ unikanie tego wyszukiwania jest tutaj najważniejsze! (Wydajność, skalowalność). W normalnych okolicznościach nie potrzebujesz wyszukiwania DB, aby mieć dane użytkownika, już dostałeś je od klienta.
Rob Evans,
9

Jestem tu trochę spóźniony, ale myślę, że mam przyzwoite rozwiązanie.

Mam kolumnę „last_password_change” w mojej bazie danych, która przechowuje datę i godzinę ostatniej zmiany hasła. Przechowuję również datę / godzinę wydania w JWT. Podczas sprawdzania poprawności tokena sprawdzam, czy hasło zostało zmienione po wydaniu tokena i czy był to token, który został odrzucony, mimo że jeszcze nie wygasł.

Matas Kairaitis
źródło
1
Jak odrzucić token? Czy możesz pokazać krótki przykładowy kod?
alexventuraio
1
if (jwt.issue_date < user.last_pw_change) { /* not valid, redirect to login */}
Vanuan
15
Wymaga wyszukiwania db!
Rob Evans,
5

Możesz mieć pole „last_key_used” na swoim DB w dokumencie / rekordzie użytkownika.

Gdy użytkownik zaloguje się z użytkownikiem i przejdzie, wygeneruj nowy ciąg losowy, zapisz go w polu last_key_used i dodaj do ładunku podczas podpisywania tokena.

Gdy użytkownik zaloguje się przy użyciu tokena, sprawdź parametr last_key_used w bazie danych, aby pasował do tego z tokena.

Następnie, gdy użytkownik na przykład wylogowuje się lub jeśli chcesz unieważnić token, po prostu zmień to pole „last_key_used” na inną losową wartość, a wszelkie kolejne kontrole zakończą się niepowodzeniem, w ten sposób zmuszając użytkownika do zalogowania się z użytkownikiem i przekazania ponownie.

NickVarcha
źródło
Oto rozwiązanie, które rozważałem, ale ma ono następujące wady: (1) albo sprawdzasz bazę danych przy każdym żądaniu, aby sprawdzić losowość (unieważniasz powód używania tokenów zamiast sesji), albo jesteś sprawdzanie tylko okresowo po wygaśnięciu tokenu odświeżania (zapobiegając natychmiastowemu wylogowaniu użytkowników lub natychmiastowemu zakończeniu sesji); oraz (2) wylogowanie wylogowuje użytkownika ze wszystkich przeglądarek i wszystkich urządzeń (co nie jest tradycyjnie oczekiwanym zachowaniem).
Joe Lapp
Nie musisz zmieniać klucza, gdy użytkownik się wylogowuje, tylko wtedy, gdy zmieni swoje hasło lub - jeśli je podasz - gdy zdecydują się wylogować ze wszystkich urządzeń
NickVarcha
3

Prowadź taką listę w pamięci

user_id   revoke_tokens_issued_before
-------------------------------------
123       2018-07-02T15:55:33
567       2018-07-01T12:34:21

Jeśli twoje tokeny wygasną w ciągu tygodnia, wyczyść lub zignoruj ​​starsze rekordy. Przechowuj także tylko najnowszy zapis każdego użytkownika. Rozmiar listy zależy od tego, jak długo przechowujesz tokeny i jak często użytkownicy odwołują swoje tokeny. Używaj db tylko wtedy, gdy tabela się zmienia. Załaduj tabelę do pamięci podczas uruchamiania aplikacji.

Eduardo
źródło
2
Większość zakładów produkcyjnych działa na więcej niż jednym serwerze, więc to rozwiązanie nie będzie działać. Dodanie pamięci podręcznej Redis lub podobnej pamięci podręcznej międzyprocesowej znacznie komplikuje system i często powoduje więcej problemów niż rozwiązań.
user2555515
@ user2555515 wszystkie serwery mogą być synchronizowane z bazą danych. Wybór należy do bazy danych za każdym razem, czy nie. Możesz powiedzieć, jakie to przynosi problemy.
Eduardo,
3

------------------------ Trochę za późno na tę odpowiedź, ale być może pomoże komuś ------------- -----------

Po stronie klienta najłatwiej jest usunąć token z pamięci przeglądarki.

Ale co jeśli chcesz zniszczyć token na serwerze węzłów?

Problem z pakietem JWT polega na tym, że nie zapewnia on żadnej metody ani sposobu na zniszczenie tokena. Możesz użyć różnych metod w odniesieniu do JWT, które są wymienione powyżej. Ale tutaj idę z JWT-Redis.

Aby więc zniszczyć token na serwerze, możesz użyć pakietu jwt-redis zamiast JWT

Ta biblioteka (jwt-redis) całkowicie powtarza całą funkcjonalność biblioteki jsonwebtoken, z jednym ważnym dodatkiem. Jwt-redis pozwala przechowywać etykietę tokena w redis w celu weryfikacji ważności. Brak etykiety tokena w redis powoduje, że token jest nieprawidłowy. Aby zniszczyć token w jwt-redis, istnieje metoda niszczenia

działa w ten sposób:

1) Zainstaluj jwt-redis z npm

2) Aby utworzyć -

var redis = require('redis');
var JWTR =  require('jwt-redis').default;
var redisClient = redis.createClient();
var jwtr = new JWTR(redisClient);

jwtr.sign(payload, secret)
    .then((token)=>{
            // your code
    })
    .catch((error)=>{
            // error handling
    });

3) Aby zweryfikować -

jwtr.verify(token, secret);

4) Aby zniszczyć -

jwtr.destroy(token)

Uwaga : możesz podać datę wygaśnięcia podczas logowania tokena w taki sam sposób, jak w JWT.

Może to komuś pomoże

Aman Kumar Gupta
źródło
2

Dlaczego po prostu nie użyć oświadczenia jti (nonce) i zapisać go na liście jako pole rekordu użytkownika (zależne od bazy danych, ale przynajmniej lista rozdzielona przecinkami jest w porządku)? Nie ma potrzeby oddzielnego wyszukiwania, ponieważ inni zauważyli prawdopodobnie, że i tak chcesz uzyskać rekord użytkownika, dzięki czemu możesz mieć wiele prawidłowych tokenów dla różnych instancji klienta („wyloguj się wszędzie” może zresetować listę do pustej)

davidkomer
źródło
Tak to. Może utworzyć relację jeden do wielu między tabelą użytkownika a nową tabelą (sesyjną), aby można było przechowywać metadane wraz z oświadczeniami jti.
Peter Lada,
2
  1. Daj 1 dzień ważności tokenów
  2. Utrzymuj codzienną czarną listę.
  3. Umieść unieważnione tokeny / wylogowania na czarnej liście

Aby sprawdzić poprawność tokena, najpierw sprawdź czas wygaśnięcia tokena, a następnie czarną listę, jeśli token nie wygasł.

W przypadku długich sesji powinien istnieć mechanizm wydłużania czasu wygaśnięcia tokena.

Ebru Yener
źródło
4
odłóż żetony na czarną listę, a wtedy pojawi się twoja bezpaństwowość
Kerem Baydoğan
2

Późno na imprezę, MOJE dwa centy podano poniżej po kilku badaniach. Podczas wylogowywania upewnij się, że dzieją się następujące rzeczy ...

Wyczyść pamięć / sesję klienta

Zaktualizuj datę i godzinę ostatniego logowania do tabeli użytkowników i datę i godzinę wylogowania, ilekroć nastąpi odpowiednio logowanie lub wylogowanie. Dlatego data zalogowania zawsze powinna być większa niż data wylogowania (lub pozostaw datę wylogowania zerową, jeśli bieżący status to logowanie i jeszcze się nie wylogowano)

Jest to o wiele prostsze niż regularne utrzymywanie dodatkowej tabeli czarnej listy i czyszczenie. Obsługa wielu urządzeń wymaga dodatkowej tabeli do zalogowania, daty wylogowania z dodatkowymi szczegółami, takimi jak dane systemu operacyjnego lub klienta.

Shamseer
źródło
2

Unikalny ciąg na użytkownika i ciąg globalny zsumowany

służyć jako tajna część JWT, umożliwiając indywidualne i globalne unieważnienie tokena. Maksymalna elastyczność kosztem wyszukiwania / odczytu bazy danych podczas uwierzytelniania żądania. Również łatwe do buforowania, ponieważ rzadko się zmieniają.

Oto przykład:

HEADER:ALGORITHM & TOKEN TYPE

{
  "alg": "HS256",
  "typ": "JWT"
}
PAYLOAD:DATA

{
  "sub": "1234567890",
  "some": "data",
  "iat": 1516239022
}
VERIFY SIGNATURE

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload), 
  HMACSHA256('perUserString'+'globalString')
)

where HMACSHA256 is your local crypto sha256
  nodejs 
    import sha256 from 'crypto-js/sha256';
    sha256(message);

na przykład użycie patrz https://jwt.io (nie jestem pewien, czy obsługują dynamiczne 256-bitowe sekrety)

Mark Essel
źródło
1
Wystarczyłoby trochę więcej szczegółów
gigas
2
@giantas, myślę, że Mark miał na myśli część podpisu. Zamiast tego, używając tylko jednego klucza do podpisania JWT, połącz go z kluczem unikalnym dla każdego klienta. Dlatego jeśli chcesz unieważnić całą sesję użytkownika, po prostu zmień klucz dla tego użytkownika, a jeśli unieważnisz całą sesję w systemie, po prostu zmień ten pojedynczy globalny klucz.
Tommy Aria Pradana
1

Zrobiłem to w następujący sposób:

  1. Wygeneruj a unique hash, a następnie zapisz go w Redis i JWT . Można to nazwać sesją
    • Będziemy także przechowywać liczbę żądań złożonych przez konkretny JWT - Za każdym razem, gdy jwt jest wysyłany do serwera, zwiększamy liczbę całkowitą żądań . (jest to opcjonalne)

Tak więc, gdy użytkownik się loguje, tworzony jest unikalny skrót, przechowywany w redis i wstrzykiwany do JWT .

Gdy użytkownik spróbuje odwiedzić chroniony punkt końcowy, pobierzesz unikatowy skrót sesji z JWT , prześlesz zapytanie ponownie i zobaczysz, czy jest on zgodny!

Możemy to rozszerzyć i uczynić nasz JWT jeszcze bardziej bezpiecznym, oto jak:

Każde X żądań konkretnego JWT , generujemy nową unikalną sesję, przechowujemy ją w naszym JWT , a następnie umieszczamy na czarnej liście poprzednią.

Oznacza to, że JWT ciągle się zmienia i przestaje nieświeże JWT „s posiekany, kradzieży lub coś innego.

James111
źródło
1
Możesz haszować sam token i przechowywać tę wartość w redis, zamiast wstrzykiwać nowy hash do tokena.
Frug
Sprawdź także audi jtiroszczenia w JWT, jesteś na właściwej ścieżce.
Peter Lada,
1

Jeśli chcesz mieć możliwość odwołania tokenów użytkownika, możesz śledzić wszystkie wydane tokeny na swoim DB i sprawdzić, czy są prawidłowe (istnieją) w tabeli podobnej do sesji. Minusem jest to, że trafisz DB na każde żądanie.

Nie próbowałem tego, ale sugeruję następującą metodę, aby zezwolić na odwołanie tokena przy jednoczesnym ograniczeniu trafień DB do minimum -

Aby obniżyć wskaźnik kontroli bazy danych, podziel wszystkie wydane tokeny JWT na X grup zgodnie z pewnym deterministycznym powiązaniem (np. 10 grup na pierwszą cyfrę identyfikatora użytkownika).

Każdy token JWT będzie zawierał identyfikator grupy i znacznik czasu utworzony podczas tworzenia tokena. na przykład,{ "group_id": 1, "timestamp": 1551861473716 }

Serwer będzie przechowywać w pamięci wszystkie identyfikatory grup, a każda grupa będzie miała znacznik czasu wskazujący, kiedy było ostatnie zdarzenie wylogowania użytkownika należącego do tej grupy. na przykład,{ "group1": 1551861473714, "group2": 1551861487293, ... }

Żądania z tokenem JWT, które mają starszy znacznik czasowy grupy, zostaną sprawdzone pod kątem ważności (trafienie w bazie danych), a jeśli są prawidłowe, zostanie wydany nowy token JWT ze świeżym znacznikiem czasu do przyszłego użytku przez klienta. Jeśli znacznik czasu grupy tokena jest nowszy, ufamy JWT (bez trafienia DB).

Więc -

  1. Sprawdzamy poprawność tokena JWT przy użyciu bazy danych tylko wtedy, gdy token ma stary znacznik czasu grupy, podczas gdy przyszłe żądania nie zostaną sprawdzone, dopóki ktoś w grupie użytkownika się nie wyloguje.
  2. Korzystamy z grup, aby ograniczyć liczbę zmian znaczników czasu (powiedzmy, że użytkownik loguje się i wylogowuje, jakby nie było jutra - wpłynie tylko na ograniczoną liczbę użytkowników zamiast wszystkich)
  3. Ograniczamy liczbę grup, aby ograniczyć liczbę znaczników czasu przechowywanych w pamięci
  4. Unieważnienie tokena to pestka - po prostu usuń go z tabeli sesji i wygeneruj nowy znacznik czasu dla grupy użytkowników.
Arik
źródło
Ta sama lista może być przechowywana w pamięci (aplikacja dla c #) i wyeliminowałoby to potrzebę uderzania bazy danych dla każdego żądania. Listę można załadować z
bazy
1

Jeśli akceptowalna jest opcja „wyloguj się ze wszystkich urządzeń” (w większości przypadków tak jest):

  • Dodaj pole wersji tokena do rekordu użytkownika.
  • Dodaj wartość w tym polu do oświadczeń przechowywanych w JWT.
  • Zwiększaj wersję za każdym razem, gdy użytkownik się wylogowuje.
  • Podczas sprawdzania poprawności tokena porównaj jego oświadczenie o wersji z wersją zapisaną w rekordzie użytkownika i odrzuć, jeśli nie jest taki sam.

W większości przypadków wymagana jest wycieczka db w celu uzyskania rekordu użytkownika, więc nie powoduje to dużego obciążenia procesu sprawdzania poprawności. W przeciwieństwie do utrzymywania czarnej listy, gdzie obciążenie bazy danych jest znaczne ze względu na konieczność użycia złączenia lub osobnego wywołania, czyszczenia starych rekordów i tak dalej.

użytkownik2555515
źródło
0

Mam zamiar odpowiedzieć, jeśli musimy zapewnić wylogowanie z funkcji wszystkich urządzeń, gdy korzystamy z JWT. W tym podejściu zostaną wykorzystane wyszukiwania bazy danych dla każdego żądania. Ponieważ potrzebujemy stanu bezpieczeństwa trwałości nawet w przypadku awarii serwera. W tabeli użytkowników będziemy mieć dwie kolumny

  1. LastValidTime (domyślnie: czas utworzenia)
  2. Zalogowany (domyślnie: prawda)

Ilekroć pojawi się prośba o wylogowanie użytkownika, zaktualizujemy LastValidTime do bieżącej godziny i Zalogowany na false. Jeśli pojawi się prośba o zalogowanie, nie zmienimy LastValidTime, ale zalogowany zostanie ustawiony na true.

Kiedy tworzymy JWT, będziemy mieć czas utworzenia JWT w ładunku. Po autoryzacji usługi sprawdzimy 3 warunki

  1. Czy JWT jest ważny
  2. Czy czas tworzenia ładunku JWT jest dłuższy niż LastValidTime użytkownika
  3. Czy użytkownik jest zalogowany?

Zobaczmy praktyczny scenariusz.

Użytkownik X ma dwa urządzenia A, B. Zalogował się na naszym serwerze o godzinie 19:00, używając urządzenia A i urządzenia B. (powiedzmy, że czas wygaśnięcia JWT wynosi 12 godzin). Zarówno A, jak i B mają JWT z createTime: 19:00

O 21:00 stracił swoje urządzenie B. Natychmiast wylogował się z urządzenia A. Oznacza to, że teraz wpis użytkownika X bazy danych ma LastValidTime jako „ThatDate: 9: 00: xx: xxx” i zalogowany jako „false”.

O 9:30 Mr.Tief próbuje zalogować się przy użyciu urządzenia B. Sprawdzimy bazę danych, nawet jeśli zalogowany jest fałszywy, więc nie pozwolimy.

O godzinie 10 Mr.X loguje się ze swojego urządzenia A. Teraz urządzenie A ma JWT z utworzonym czasem: 22.00. Teraz zalogowana baza danych jest ustawiona na „prawda”

O 22:30 Mr.Thief próbuje się zalogować. Mimo że zalogowany jest prawdziwy. LastValidTime ma godzinę 21:00 w bazie danych, ale JWT B stworzył czas jako 19:00. Więc nie będzie miał dostępu do usługi. Korzystając z urządzenia B bez hasła, nie może użyć już utworzonego JWT po wylogowaniu jednego urządzenia.

Tharsanan
źródło
0

Rozwiązanie IAM, takie jak Keycloak (nad którym pracowałem), zapewnia punkt końcowy odwołania tokena

Punkt końcowy odwołania tokenu /realms/{realm-name}/protocol/openid-connect/revoke

Jeśli chcesz po prostu wylogować użytkownika (lub użytkownika), możesz również wywołać punkt końcowy (to po prostu unieważniłoby Tokeny). Ponownie, w przypadku Keycloak, Strona ufająca musi tylko wywołać punkt końcowy

/realms/{realm-name}/protocol/openid-connect/logout

Link w przypadku, jeśli chcesz dowiedzieć się więcej

Subbu Mahadevan
źródło
-1

Wydaje się to naprawdę trudne do rozwiązania bez sprawdzania bazy danych przy każdej weryfikacji tokena. Alternatywą, o której mogę myśleć, jest utrzymywanie czarnej listy unieważnionych tokenów po stronie serwera; który powinien być aktualizowany w bazie danych za każdym razem, gdy nastąpi zmiana, aby zachować zmiany między restartami, zmuszając serwer do sprawdzenia bazy danych po ponownym uruchomieniu w celu załadowania bieżącej czarnej listy.

Ale jeśli trzymasz go w pamięci serwera (rodzaj globalnej zmiennej), to nie będzie skalowalny na wielu serwerach, jeśli używasz więcej niż jednego, więc w takim przypadku możesz zachować go na wspólnej pamięci podręcznej Redis, która powinna być skonfigurowany do przechowywania danych gdzieś (baza danych? system plików?) na wypadek, gdyby trzeba go było zrestartować, a za każdym razem, gdy nowy serwer jest podkręcany, musi subskrybować pamięć podręczną Redis.

Alternatywnie do czarnej listy, korzystając z tego samego rozwiązania, możesz to zrobić za pomocą skrótu zapisanego w opcji redis na sesję, jak wskazuje ta druga odpowiedź (nie jestem pewien, czy byłoby to bardziej wydajne przy logowaniu wielu użytkowników).

Czy to brzmi okropnie skomplikowane? robi mi to!

Oświadczenie: Nie korzystałem z Redis.

Jose PV
źródło
-1

Jeśli używasz axios lub podobnej biblioteki żądania HTTP opartej na obietnicach, możesz po prostu zniszczyć token na interfejsie wewnątrz .then()części. Zostanie uruchomiony w części odpowiedzi .then () po wykonaniu przez użytkownika tej funkcji (kod wyniku z punktu końcowego serwera musi być w porządku, 200). Gdy użytkownik kliknie tę trasę podczas wyszukiwania danych, jeśli pole bazy danych user_enabledjest fałszywe, spowoduje to zniszczenie tokena, a użytkownik zostanie natychmiast wylogowany i nie będzie miał dostępu do chronionych tras / stron. Nie musimy czekać na wygaśnięcie tokena, gdy użytkownik jest zalogowany na stałe.

function searchForData() {   // front-end js function, user searches for the data
    // protected route, token that is sent along http request for verification
    var validToken = 'Bearer ' + whereYouStoredToken; // token stored in the browser 

    // route will trigger destroying token when user clicks and executes this func
    axios.post('/my-data', {headers: {'Authorization': validToken}})
     .then((response) => {
   // If Admin set user_enabled in the db as false, we destroy token in the browser localStorage
       if (response.data.user_enabled === false) {  // user_enabled is field in the db
           window.localStorage.clear();  // we destroy token and other credentials
       }  
    });
     .catch((e) => {
       console.log(e);
    });
}
Dan
źródło
-3

Właśnie zapisuję token w tabeli użytkowników, podczas logowania użytkownika zaktualizuję nowy token, a gdy autoryzacja będzie równa aktualnemu jwt użytkownika.

Myślę, że nie jest to najlepsze rozwiązanie, ale dla mnie to działa.

Vo Manh Kien
źródło
2
Oczywiście, że nie jest najlepszy! Każdy, kto ma dostęp do bazy danych, może łatwo podszyć się pod dowolnego użytkownika.
user2555515
1
@ user2555515 To rozwiązanie działa dobrze, jeśli token przechowywany w bazie danych jest szyfrowany, podobnie jak każde hasło przechowywane w bazie danych. Istnieje różnica między Stateless JWTi Stateful JWT(co jest bardzo podobne do sesji). Stateful JWTmoże czerpać korzyści z utrzymywania białej listy tokenów.
TheDarkIn1978