Czy serwer może być klientem i resource serwerem jednocześnie?

0

Cześć Wszystkim,

Na pytanie z tematu wątku (myślę) znam odpowiedź, jeden serwis może być OAuth2 Clientem i Resource Serverem. Pytanie czy jest to poprawne podejście.

Trochę kontekstu.
Robię 4fun projekt template, który posłuży jako szybki bootstrap do budowy aplikacji webowych opartych na React i Spring Boot. W założenia wpisałem sobie, że taki template to prosty UI z formularzami do logowania/rejestracji za pomocą kredek jak i zewnętrznych IDP, oraz zabezpieczenie routeów po stronie frontu i endpointów po stronie backendu.

Teraz problem, który chce rozwiązać i jak sobie to wyobrażam.
Na początku stwierdziłem, że mój pojedynczy serwer będzie zarówno klientem jak i resource serverem. Nawet zacząłem iść w tym kierunku, widziałbym to tak, że na froncie pod przyciskiem logowania za pomocą zewnętrznego IDP np. Google kryje się zwykły link do backendu z konkretną końcówką. Użytkownik jest przekierowany do zewnętrznego IDP i wraca z informacją czy uwierzytelnienie się powiodło. W Spring Security przechodzimy do Success Handlera, który mi wygeneruje JWT w claimsach umieszczam token, z którym użytkownik wrócił od zewnętrznego dostawcy. Użytkownik wraca skąd przyszedł i zapisuje się JWT. Potem przy każdym strzale na backend JWT jest walidowany, myślałem, też żeby odpytać ext IDP o poprawność tokenu, z którym wrócił użytkownik, i którego zapisaliśmy w claimsach (overkill?).
Plus, minus takbym widział high-level flow.
Jednak coraz bardziej mam przekonanie, że klientem OAuth2 powinien być front, a backend jako resource server potwierdzać poprawność tokenu, z którym przychodzi użytkownik. W tym podejściu backend nie musi być dostępny dla świata, np. mogę mieć dwa kontenery, które są w jednej sieci wirtualnej ale tylko front jest wystawiony na zewnątrz. W pierwszym podejściu backend musiałby być dostępny dla świata, aby móc przekierować usera z poziomu backendu do zewnętrznego dostawcy tożsamości.

Stąd też następujące pytania:

  1. Jakie podejście jest lepsze i dlaczego? Jeżeli odpowiedzią jest "To zależy", to od czego zależy?
  2. Jeżeli Client i Resource Server na backendzie to jak zarządzać JWT? Czytałem, że token, który wydaje zewnętrzny IDP nie powinien być używany po stronie backendu, stąd też pomysł żeby generować nowy JWT a w claimsach zawrzeć ten od IDP w celu podwójnej walidacji.
  3. Jeżeli Client i Resource Server na backendzie to czy przedstawione flow jest w porządku? Może ktoś zna jakieś przykładowe repo.
  4. Jeżeli Client na Frontendzie, to czy są rzeczy, na które trzeba zwrócić uwagę z powodu, że tutaj jest to publiczny klient, który nie przechowuje żadnych secretów.

Link do repo: https://github.com/malochak/react-spring-tailwind-auth

2

Na wstępie chciałbym napisać, że musisz wejść w kod źródłowy springa aby zrozumieć jak to działa. W innym przypadku będziesz wynajdował koło na nowo, kleił kod aby się odpalał, a potem okaże się, że słabo to działa. Niestety, kiedyś zostałem wrzucony do projektu, gdzie ktoś zaimplementował własne tokeny, i nazwał je JWT, chociaż nimi nie były, takie tak security by obscurity, które kopało nas w ...

Dobra, teraz czas na konkrety:

  • W OAuth2/OpenID Connect wyróżniamy 4 role:
    • User - czyli najczęściej człowiek, fizyczny byt
    • Client - aplikacja, która w imieniu user-a wykonuje akcje, w tym przypadku aplikacja frontendowa
    • Resource server - zasób, czyli w tym przypadku aplikacja backendowa z REST Api, do której uderza client
    • Authorization server - wystawca tokenów JWT, do którego uderza resource server w celu sprawdzenia uprawnień, może nim być np. Google, Facebook, Keycloak
  • Aby aplikacja springowa działała jako resource server to dodajemy do niej zależność spring-boot-starter-oauth2-resource-server
  • Aby aplikacja springowa integrowała się z zewnętrznym authorization server to dodajemy do niej zależność spring-boot-starter-oauth2-client
  • Widzę, że w springu pojawiła się nowa zależność spring-boot-starter-oauth2-authorization-server, niestety jest to dla mnie coś nowego i jeszcze nie miałem okazji tego zbadać, proponuję Tobie przyjrzeć się temu
  • JWT powinny być podpisane, jakakolwiek zmiana zawartości tokena jest przez to niemożliwa.
  • JWT posiadają pole iss (issuer), jest to wystawca tokena, czyli authorization server, jeśli Google go wystawia, to siebie tam wpisuje, jeśli Ty wystawiasz token, to wpisujesz swój identyfikator systemu (lub odnośnik do niego).
  • Jeden resource server może być zintegrowany z wieloma authorization server-ami, to po polu iss backend rozróżnia, kto wystawił token i który authorization server ma się odpytać.
  • JWT wystawione przez authorization server wracają do resource server, tam resource server może odczytywać claims, stosować odpowiednie walidacje do uprawnień, ale same tokeny są niezmienne i przesyła je w odpowiedzi client-owi, a ten klient w kolejnych żądanich używa tego samego tokenu (nagłówek Authorization: Bearer <token>)
  • Można łączyć ze sobą różne role w jednej aplikacji, często resource server jest łączony z authorization server

Podsumowując próbujesz osiągnąć coś hybrydowego, chcesz użyć zewnętrznego authorization server (Google) z jego użytkownikami, a także rejestrować własnych użytkowników. W drugim przypadku chcesz utworzyć własny authorization server. Po udanej rejestracji użytkownika powinieneś stworzyć własny token z odpowiednią wartością pola iss oraz go podpisać własnym kluczem. Dobrym pomysłem będzie przyjrzenie się klasie JwtIssuerAuthenticationManagerResolver oraz poszukanie artykułów i filmików, gdzie prezentacje prowadzi autor implementacji OAuth2 w springu Josh Cummings.
Możesz też wystawić własny authorization server, np. Keycloak i zintegrować się z nim. Keycloak posiada także także swoje REST api, za pomocą którego możesz utworzyć nowych userów zamiast zapisywać ich we własnej bazie danych.

0
ivo napisał(a):

Podsumowując próbujesz osiągnąć coś hybrydowego, chcesz użyć zewnętrznego authorization server (Google) z jego użytkownikami, a także rejestrować własnych użytkowników. W drugim przypadku chcesz utworzyć własny authorization server. Po udanej rejestracji użytkownika powinieneś stworzyć własny token z odpowiednią wartością pola iss oraz go podpisać własnym kluczem. Dobrym pomysłem będzie przyjrzenie się klasie JwtIssuerAuthenticationManagerResolver oraz poszukanie artykułów i filmików, gdzie prezentacje prowadzi autor implementacji OAuth2 w springu Josh Cummings.
Możesz też wystawić własny authorization server, np. Keycloak i zintegrować się z nim. Keycloak posiada także także swoje REST api, za pomocą którego możesz utworzyć nowych userów zamiast zapisywać ich we własnej bazie danych.

Dziękuje za odpowiedź. Pytaniem pozostaje dalej, czy user ma być przekierowany do zewnętrznego IDP (Authorization Servera) z poziomu frontu, czy backendu?
Jeżeli mam użytkownika, który się uwierzytelnia z pomocą loginu i hasła to wiadomo, że uderza na backend w celu zweryfikowania poprawności tej pary. Jeżeli mamy użytkownika, który rejestrował się z pomocą Google jako zewnętrznego IDP, to czy klientem jest tutaj front, czy backend? Skąd użytkownik będzie przekierowany, żeby Auth Server (w tym przypadku Google) wystawił ten token, który będzie weryfikowany z poziomu backendu (Resource Servera)

1

W Twoim przypadku w kontekście oauth2 Twój frontend zawsze jest klientem, w kontekście relacji pomiędzy usługami (czyli poza kontekstem oauth2) Twój backend jest klientem dla serwisu Google. Stąd też nie należy mieszać tych dwóch znaczenia słowa klient, bo to zależy od kontekstu.
Dawno nie bawiłem się oauth2, więc mogę w czymś się mylić, ale postaram się odpowiedzieć:
Czy przekierowanie do authorization server powinno być z poziomu backendu czy frontendu? To zależy jak ustawisz w authorization server. O ile pamiętam jeśli konfigurujemy Keycloak jako authorization server to można było wybrać rodzaj klienta (klient w znaczeniu usługa, która łączy się z serwisem). Jeden rodzaj to public, a drugi to confidential. Pierwszy służy do frontendów renderowanych po stronie przeglądarki (Angular, React), a drugi do usług backendowych. W pierwszym flow przekierowań jest zarządzanych przez kod frontendu, a w drugim przez kod backendu.
O ile pamiętam ze względu na bezpieczeństwo preferowana jest druga opcja, gdzie należy oprogramować tak logikę na frontendzie, aby obsługiwała przekierowania zwracane przez backend. Wtedy authorization server będzie komunikował się z Twoim backendem, a backend będzie zarządzał flow w kontekscie oauth2. A od strony backendu prawdopodobnie nic nie trzeba bedzie oprogramowywać, wystarczy odpowiednia konfiguracja w application.yml.
Proponuję Tobie poszerzyć wiedzę teoretyczną nt. oauth2 oraz openid connect. Mimo że to są dwa różne terminy często są zamiennie używane, w zależności kto opisuje dane zagadnienie.
Kolejna sprawa to taka, że jeśli piszesz sobie własny template aplikacji to głupio byłoby, abyś zrobił coś co potem będziesz powielał w swoich aplikacjach, a będzie to zrobione w sposób niezgodny z ogólnymi zasadami bezpieczeństwa. Dlatego podstawy teoretyczne są tutaj bardzo potrzebne. Głupio będzie np. jak napiszesz aplikację frontendową w Reakcie i wkompilujesz w nią swój client-id i client-secret. A takie rzeczy zdarzają się, jak ludzie nie rozróżniają typu klienta w Keycloak pomiędzy public a confidential.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.