Jak zintegrować OAuth 2 ze swoim zapleczem Django/DRF bez popadania w szaleństwo
Opublikowany: 2022-03-11Wszyscy tam byliśmy. Pracujesz nad zapleczem API i jesteś zadowolony z tego, jak mu idzie. Udało Ci się niedawno ukończyć program o minimalnej opłacalności produktu (MVP), wszystkie testy przeszły pomyślnie i nie możesz się doczekać wprowadzenia nowych funkcji.
Następnie szef wysyła Ci e-mail: „Przy okazji, musimy pozwolić ludziom logować się przez Facebooka i Google; nie powinni tworzyć konta tylko w małej witrynie, takiej jak nasza”.
Świetnie. Pełzający zakres atakuje ponownie.
Dobrą wiadomością jest to, że OAuth 2 stał się standardem branżowym dla uwierzytelniania społecznościowego i zewnętrznego (używanego przez usługi takie jak Facebook, Google itp.), dzięki czemu możesz skupić się na zrozumieniu i wdrożeniu tego standardu w celu obsługi szerokiej gamy mediów społecznościowych. dostawcy uwierzytelniania.
Prawdopodobnie nie znasz OAuth 2; Nie byłem, kiedy mi się to przydarzyło.
Jako programista Pythona, Twój instynkt może prowadzić do pip, zalecanego narzędzia Python Package Index (PyPA) do instalowania pakietów Pythona. Złą wiadomością jest to, że pip wie o 278 pakietach obsługujących OAuth, z których 53 wyraźnie wspomina o Django. Warto poświęcić tydzień pracy na samo zbadanie opcji, nie mówiąc już o rozpoczęciu pisania kodu.
W tym samouczku dowiesz się, jak zintegrować OAuth 2 z Django lub Django Rest Framework przy użyciu Python Social Auth. Chociaż ten artykuł koncentruje się na Django REST Framework, możesz zastosować podane tutaj informacje, aby zaimplementować je w wielu innych popularnych frameworkach zaplecza.
Szybki przegląd przepływu OAuth 2
OAuth 2 został zaprojektowany od początku jako protokół uwierzytelniania internetowego. To nie jest to samo, co gdyby został zaprojektowany jako protokół uwierzytelniania sieciowego; zakłada, że dostępne są narzędzia, takie jak renderowanie HTML i przekierowania przeglądarki.
Jest to oczywiście przeszkoda dla interfejsu API opartego na JSON, ale można to obejść.
Przejdziesz przez ten proces tak, jakbyś pisał tradycyjną stronę internetową po stronie serwera.
Przepływ OAuth 2 po stronie serwera
Pierwszy krok odbywa się całkowicie poza przepływem aplikacji. Właściciel projektu musi zarejestrować Twoją aplikację u każdego dostawcy OAuth 2, dla którego potrzebujesz danych logowania.
Podczas tej rejestracji dostarczają dostawcy OAuth 2 identyfikator URI wywołania zwrotnego , pod którym Twoja aplikacja będzie dostępna do odbierania żądań. W zamian otrzymują klucz klienta i klucz klienta . Te tokeny są wymieniane podczas procesu uwierzytelniania w celu weryfikacji żądań logowania.
Tokeny odnoszą się do kodu serwera jako klienta. Host jest dostawcą OAuth 2. Nie są przeznaczone dla klientów Twojego API.
Przepływ rozpoczyna się, gdy aplikacja wygeneruje stronę zawierającą przycisk, taki jak „Zaloguj się przez Facebooka” lub „Zaloguj się przez Google+”. Zasadniczo nie są to nic innego jak proste linki, z których każdy wskazuje na adres URL podobny do następującego:
https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
(Uwaga: podziały wierszy wstawione do powyższego identyfikatora URI w celu zapewnienia czytelności).
Podałeś klucz klienta i identyfikator URI przekierowania, ale żadnych obiektów tajnych. W zamian poinformowałeś serwer, że chcesz otrzymać kod uwierzytelniający w odpowiedzi i uzyskać dostęp do zakresów „profil” i „e-mail”. Te zakresy definiują uprawnienia, których żądasz od użytkownika, i ograniczają autoryzację otrzymanego tokenu dostępu.
Po otrzymaniu przeglądarka użytkownika jest kierowana na dynamiczną stronę kontrolowaną przez dostawcę OAuth 2. Dostawca OAuth 2 przed kontynuowaniem sprawdza, czy identyfikator URI wywołania zwrotnego i klucz klienta są ze sobą zgodne. Jeśli tak, przepływ jest na krótko rozbieżny w zależności od tokenów sesji użytkownika.
Jeśli użytkownik nie jest aktualnie zalogowany do tej usługi, zostanie o to poproszony. Po zalogowaniu się użytkownikowi wyświetla się okno dialogowe z prośbą o zezwolenie aplikacji na zalogowanie się.
Zakładając, że użytkownik zatwierdzi, serwer OAuth 2 przekierowuje ich z powrotem do podanego identyfikatora URI wywołania zwrotnego, w tym kodu autoryzacji w parametrach zapytania: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
.
Kod autoryzacyjny to szybko wygasający, jednorazowy token; natychmiast po jego otrzymaniu serwer powinien się włączyć i wysłać kolejne żądanie do dostawcy OAuth 2, w tym zarówno kod autoryzacyjny, jak i klucz klienta:
POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET
Celem tego kodu autoryzacyjnego jest uwierzytelnienie powyższego żądania POST, ale ze względu na charakter przepływu, musi on zostać przekierowany przez system użytkownika. Jako taki jest z natury niepewny.
Ograniczenia dotyczące kodu autoryzacyjnego (tj. szybkie wygaśnięcie i możliwość jednorazowego użycia) mają na celu zminimalizowanie ryzyka związanego z przekazywaniem poświadczeń uwierzytelniających przez niezaufany system.
To wywołanie, wykonywane bezpośrednio z Twojego serwera na serwer dostawcy OAuth 2, jest kluczowym elementem procesu logowania po stronie serwera OAuth 2. Kontrolowanie połączenia oznacza, że wiesz, że połączenie jest zabezpieczone TLS, co pomaga chronić je przed atakami podsłuchowymi.
Dołączenie kodu autoryzacyjnego gwarantuje, że użytkownik wyraźnie wyraził zgodę. Dołączenie tajnego klucza klienta, który nigdy nie jest widoczny dla użytkowników, gwarantuje, że to żądanie nie pochodzi od jakiegoś wirusa lub złośliwego oprogramowania w systemie użytkownika, które przechwyciło kod autoryzacyjny.
Jeśli wszystko się zgadza, serwer zwraca token dostępu , za pomocą którego można wykonywać połączenia do tego dostawcy, będąc uwierzytelnionym jako użytkownik.
Po otrzymaniu tokena dostępu z serwera serwer ponownie przekierowuje przeglądarkę użytkownika na stronę docelową dla użytkowników, którzy właśnie się zalogowali. Często przechowuje się token dostępu w pamięci podręcznej sesji po stronie serwera, więc aby serwer mógł w razie potrzeby dzwonić do danego dostawcy usług społecznościowych.
Token dostępu nigdy nie powinien być udostępniany użytkownikowi!
Jest więcej szczegółów, w które moglibyśmy się zagłębić.
Na przykład Google zawiera token odświeżania , który przedłuża żywotność tokena dostępu, podczas gdy Facebook zapewnia punkt końcowy, w którym można wymienić krótkotrwałe tokeny dostępu na coś o dłuższej żywotności. Te szczegóły nie mają jednak dla nas znaczenia, ponieważ nie zamierzamy korzystać z tego przepływu.
Ten przepływ jest uciążliwy dla interfejsu API REST. Chociaż możesz poprosić klienta frontonu, aby wygenerował początkową stronę logowania, a back-end dostarczył adres URL wywołania zwrotnego, w końcu napotkasz problem. Chcesz przekierować użytkownika do strony docelowej frontonu po otrzymaniu tokena dostępu, a nie ma na to jasnego, zgodnego z REST sposobu.
Na szczęście dostępny jest inny przepływ OAuth 2, który w tym przypadku działa znacznie lepiej.
Przepływ OAuth 2 po stronie klienta
W tym przepływie front-end staje się odpowiedzialny za obsługę całego procesu OAuth 2. Generalnie przypomina to przepływ po stronie serwera, z ważnym wyjątkiem – front-endy działają na maszynach kontrolowanych przez użytkowników, więc nie można im powierzyć tajnego klucza klienta. Rozwiązaniem jest po prostu wyeliminowanie całego tego etapu procesu.
Pierwszym krokiem, podobnie jak w przepływie po stronie serwera, jest zarejestrowanie aplikacji.
W takim przypadku właściciel projektu nadal rejestruje aplikację, ale jako aplikację internetową. Dostawca OAuth 2 nadal będzie dostarczać klucz klienta , ale może nie udostępniać żadnego tajnego klucza klienta.
Fronton udostępnia użytkownikowi przycisk logowania społecznościowego, który kieruje do strony internetowej kontrolowanej przez dostawcę OAuth 2 i prosi naszą aplikację o pozwolenie na dostęp do niektórych aspektów profilu użytkownika.
Tym razem adres URL wygląda jednak trochę inaczej:
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
Zauważ, że parametr response_type
tym razem w adresie URL to token
.
A co z przekierowaniem URI?
Jest to po prostu dowolny adres w interfejsie, który jest przygotowany do odpowiedniej obsługi tokena dostępu.
W zależności od używanej biblioteki OAuth 2 front-end może faktycznie tymczasowo uruchomić serwer zdolny do akceptowania żądań HTTP na urządzeniu użytkownika; w takim przypadku adres URL przekierowania ma postać http://localhost:7862/callback/?token=TOKEN
.
Ponieważ serwer OAuth 2 zwraca przekierowanie HTTP po zaakceptowaniu przez użytkownika, a to przekierowanie jest przetwarzane przez przeglądarkę na urządzeniu użytkownika, ten adres jest interpretowany poprawnie, zapewniając dostęp frontonu do tokena.
Alternatywnie front-end może bezpośrednio zaimplementować odpowiednią stronę. Tak czy inaczej, front-end jest w tym momencie odpowiedzialny za parsowanie parametrów zapytania i przetwarzanie tokena dostępu.
Od tego momentu fronton może bezpośrednio wywoływać interfejs API dostawcy OAuth 2 przy użyciu tokena. Ale użytkownicy tak naprawdę tego nie chcą; chcą uwierzytelnionego dostępu do Twojego interfejsu API. Wszystko, co musi zapewnić back-end, to punkt końcowy, w którym fronton może wymienić token dostępu dostawcy społecznościowego na token, który zapewnia dostęp do Twojego interfejsu API.
Po co w ogóle na to pozwalać, biorąc pod uwagę, że dostarczanie tokenu dostępu do frontonu jest z natury mniej bezpieczne niż przepływ po stronie serwera?

Przepływ po stronie klienta umożliwia bardziej rygorystyczne rozdzielenie między interfejsem API REST zaplecza a frontonem skierowanym do użytkownika. Nic nie stoi na przeszkodzie, aby określić serwer zaplecza jako identyfikator URI przekierowania; efektem końcowym byłby pewien rodzaj przepływu hybrydowego.
Problem polega na tym, że serwer musi następnie wygenerować odpowiednią stronę dostępną dla użytkownika, a następnie w jakiś sposób przekazać kontrolę z powrotem do interfejsu użytkownika.
W nowoczesnych projektach często zdarza się, że ściśle oddziela się problemy między interfejsem użytkownika a zapleczem obsługującym całą logikę biznesową. Zwykle komunikują się za pośrednictwem dobrze zdefiniowanego interfejsu API JSON. Opisany powyżej przepływ hybrydowy zaciemnia jednak to oddzielenie obaw, zmuszając back-end do obsługi strony skierowanej do użytkownika, a następnie zaprojektuj przepływ, aby w jakiś sposób ręcznie sterować z powrotem do front-endu.
Zezwolenie front-endowi na obsługę tokena dostępu jest wygodną techniką, która zachowuje separację obaw. Nieco zwiększa ryzyko ze strony skompromitowanego klienta, ale ogólnie działa dobrze.
Ten przepływ może wydawać się skomplikowany dla front-endu i tak jest, jeśli wymagasz, aby zespół front-endowy rozwijał wszystko samodzielnie. Jednak zarówno Facebook, jak i Google udostępniają biblioteki, które umożliwiają front-endowi włączenie przycisków logowania, które obsługują cały proces przy minimalnej konfiguracji.
Oto przepis na wymianę tokenów na zapleczu.
W przepływie klienta back-end jest dość odizolowany od procesu OAuth 2. Nie daj się zwieść: to nie jest prosta praca. Będziesz chciał, aby obsługiwał co najmniej następujące funkcje.
- Wyślij co najmniej jedno żądanie do dostawcy OAuth 2, aby upewnić się, że token dostarczony przez fronton jest prawidłowy, a nie dowolny losowy ciąg.
- Gdy token jest prawidłowy, zwróć prawidłowy token dla swojego interfejsu API. W przeciwnym razie zwróć błąd informacyjny.
- Jeśli jest to nowy użytkownik, utwórz dla niego model
User
i odpowiednio go wypełnij. - Jeśli jest to użytkownik, dla którego istnieje już model
User
, dopasuj go według jego adresu e-mail, aby uzyskać dostęp do prawidłowego istniejącego konta zamiast tworzyć nowe dla logowania społecznościowego. - Zaktualizuj szczegóły profilu użytkownika na podstawie tego, co podał w mediach społecznościowych.
Dobrą wiadomością jest to, że implementacja całej tej funkcjonalności na zapleczu jest znacznie prostsza, niż można by się spodziewać.
Oto magia, jak sprawić, by to wszystko działało na zapleczu w zaledwie dwóch tuzinach linijek kodu. Zależy to od biblioteki Python Social Auth (odtąd PSA), więc musisz uwzględnić zarówno social-auth-core
jak i social-auth-app-django
w pliku requirements.txt
.
Musisz także skonfigurować bibliotekę zgodnie z dokumentacją tutaj. Zauważ, że dla jasności wyklucza to obsługę wyjątków.
Pełny kod tego przykładu można znaleźć tutaj.
@api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )
W ustawieniach jest jeszcze tylko trochę (pełny kod), a potem wszystko gotowe:
AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )
Dodaj mapowanie do tej funkcji w swoim urls.py
i gotowe!
Jak działa ta magia?
Python Social Auth to bardzo fajna, bardzo złożona maszyna. Doskonale radzi sobie z uwierzytelnianiem i dostępem do dowolnego z kilkudziesięciu dostawców uwierzytelniania społecznościowego i działa na większości popularnych frameworków internetowych Pythona, w tym Django, Flask, Pyramid, CherryPy i WebPy.
W większości powyższy kod jest bardzo standardowym widokiem opartym na funkcjach frameworku Django REST (DRF): nasłuchuje żądań POST na dowolnej ścieżce, na którą zmapujesz go w swoim urls.py
i zakładając, że wyślesz żądanie w oczekiwany format, a następnie pobiera obiekt User
lub None
.
Jeśli otrzymasz obiekt User
, jest to typ modelu, który został skonfigurowany w innym miejscu w projekcie, który może już istnieć lub nie. Firma PSA już zajęła się weryfikacją tokena, określeniem, czy istnieje dopasowanie użytkownika, utworzeniem użytkownika w razie potrzeby i aktualizacją danych użytkownika od dostawcy społecznościowego.
Dokładne szczegóły mapowania użytkownika z użytkownika dostawcy usług społecznościowych na użytkownika i powiązania z istniejącymi użytkownikami są określone przez zdefiniowaną powyżej SOCIAL_AUTH_PIPELINE
. Jest o wiele więcej do nauczenia się o tym, jak to wszystko działa, ale wykracza to poza zakres tego postu. Więcej na ten temat przeczytasz tutaj.
Kluczowym elementem magii jest dekorator @psa()
w widoku, który dodaje niektóre elementy do obiektu request
, który jest przekazywany do widoku. Najbardziej interesujący dla nas jest request.backend
(dla PSA backend to dowolny dostawca uwierzytelniania społecznościowego).
Odpowiedni back-end został dla nas wybrany i dołączony do obiektu request
na podstawie argumentu backend
widoku, który jest wypełniany przez sam adres URL.
Gdy masz już obiekt backend
w ręku, z przyjemnością uwierzytelnimy Cię przed tym dostawcą, biorąc pod uwagę Twój kod dostępu; to jest metoda do_auth
. To z kolei angażuje całość SOCIAL_AUTH_PIPELINE
z twojego pliku konfiguracyjnego.
Potok może zrobić kilka całkiem potężnych rzeczy, jeśli go rozszerzysz, chociaż już robi wszystko, czego potrzebujesz, z tylko domyślną, wbudowaną funkcjonalnością.
Potem wracamy do normalnego kodu DRF: jeśli masz poprawny obiekt User
, możesz bardzo łatwo zwrócić odpowiedni token API. Jeśli nie odzyskałeś prawidłowego obiektu User
, łatwo wygenerować błąd.
Jedną z wad tej techniki jest to, że chociaż stosunkowo łatwo jest zwracać błędy, jeśli się pojawią, trudno jest uzyskać dużo wglądu w to, co konkretnie poszło nie tak. PSA przełknie wszelkie szczegóły, które serwer mógł zwrócić na temat problemu.
Z drugiej strony, w naturze dobrze zaprojektowanych systemów uwierzytelniania leży dość nieprzejrzyste informowanie o źródłach błędów. Jeśli aplikacja kiedykolwiek powie użytkownikowi „Nieprawidłowe hasło” po próbie logowania, jest to równoznaczne z powiedzeniem „Gratulacje! Odgadłeś prawidłową nazwę użytkownika”.
Dlaczego po prostu nie rzucić własnej?
Jednym słowem: rozszerzalność. Bardzo niewielu dostawców społecznościowych OAuth 2 wymaga lub zwraca dokładnie te same informacje w swoich wywołaniach interfejsu API w dokładnie ten sam sposób. Istnieją jednak wszelkiego rodzaju przypadki specjalne i wyjątki.
Dodanie nowego dostawcy usług społecznościowych po skonfigurowaniu PSA to kwestia kilku linijek konfiguracji w plikach ustawień. Nie musisz w ogóle dostosowywać żadnego kodu. PSA streszcza to wszystko, dzięki czemu możesz skupić się na własnej aplikacji.
Jak u licha mam to przetestować?
Dobre pytanie! unittest.mock
nie nadaje się do wyśmiewania wywołań API ukrytych pod warstwą abstrakcji głęboko w bibliotece; samo odkrycie dokładnej ścieżki do kpin wymagałoby znacznego wysiłku.
Zamiast tego, ponieważ PSA jest zbudowany na bazie biblioteki Requests, używasz doskonałej biblioteki Responses do wyśmiewania dostawców na poziomie HTTP.
Pełne omówienie testowania wykracza poza zakres tego artykułu, ale próbka naszych testów znajduje się tutaj. Szczególnymi funkcjami, na które należy zwrócić uwagę, są SocialAuthTests
mocked
Niech PSA zajmie się podnoszeniem ciężarów.
Proces OAuth2 jest szczegółowy i skomplikowany z dużą nieodłączną złożonością. Na szczęście można ominąć większość tej złożoności, wprowadzając bibliotekę dedykowaną do obsługi tego w tak bezbolesny sposób, jak to tylko możliwe.
Python Social Auth wykonuje w tym świetną robotę. Zademonstrowaliśmy widok Django/DRF, który wykorzystuje niejawny przepływ OAuth2 po stronie klienta, aby uzyskać bezproblemowe tworzenie i dopasowywanie użytkowników w zaledwie 25 wierszach kodu. To nie jest zbyt nędzne.