Cum să integrezi OAuth 2 în back-end-ul tău Django/DRF fără a înnebuni
Publicat: 2022-03-11Am fost cu toții acolo. Lucrezi la back-end-ul API și ești mulțumit de modul în care merge. Ați finalizat recent produsul minim viabil (MVP), toate testele au trecut și așteptați cu nerăbdare să implementați câteva funcții noi.
Apoi șeful îți trimite un e-mail: „Apropo, trebuie să lăsăm oamenii să se conecteze prin Facebook și Google; nu ar trebui să-și creeze un cont doar pentru un site mic ca al nostru.”
Grozav. Scope creep lovește din nou.
Vestea bună este că OAuth 2 a apărut ca standardul industriei pentru autentificarea socială și a terților (utilizat de servicii precum Facebook, Google etc.), astfel încât să vă puteți concentra pe înțelegerea și implementarea acelui standard pentru a susține o gamă largă de rețele sociale. furnizorii de autentificare.
Probabil că nu sunteți familiarizat cu OAuth 2; Nu eram, când mi s-a întâmplat asta.
În calitate de dezvoltator Python, instinctul tău poate duce la pip, instrumentul recomandat Python Package Index (PyPA) pentru instalarea pachetelor Python. Vestea proastă este că pip știe despre 278 de pachete care se ocupă de OAuth – dintre care 53 menționează în mod specific Django. Este o săptămână de muncă doar pentru a cerceta opțiunile, nu contează să începeți să scrieți cod.
În acest tutorial, veți învăța cum să integrați OAuth 2 în cadrul Django sau Django Rest folosind Python Social Auth. Deși acest articol se concentrează pe cadrul Django REST, puteți aplica informațiile furnizate aici pentru a le implementa într-o varietate de alte cadre back-end comune.
O privire de ansamblu rapidă a fluxului OAuth 2
OAuth 2 a fost conceput de la început ca un protocol de autentificare web. Acesta nu este chiar la fel ca și cum ar fi fost proiectat ca un protocol de autentificare nete; presupune că instrumente precum randarea HTML și redirecționările browser sunt disponibile pentru dvs.
Acest lucru este, evident, o piedică pentru un API bazat pe JSON, dar puteți rezolva acest lucru.
Veți parcurge procesul ca și cum ați scrie un site web tradițional, pe partea de server.
Fluxul OAuth 2 pe partea serverului
Primul pas are loc în întregime în afara fluxului aplicației. Proprietarul proiectului trebuie să vă înregistreze aplicația la fiecare furnizor OAuth 2 pentru care aveți nevoie de autentificare.
În timpul acestei înregistrări, ei furnizează furnizorului OAuth 2 un URI de apel invers , la care aplicația dvs. va fi disponibilă pentru a primi solicitări. În schimb, ei primesc o cheie de client și un secret de client. Aceste jetoane sunt schimbate în timpul procesului de autentificare pentru a valida cererile de conectare.
Tokenurile se referă la codul serverului dvs. ca client. Gazda este furnizorul OAuth 2. Ele nu sunt destinate clienților dvs. API.
Fluxul începe atunci când aplicația dvs. generează o pagină care include un buton, cum ar fi „Conectați-vă cu Facebook” sau „Conectați-vă cu Google+”. În principiu, acestea nu sunt altceva decât simple linkuri, fiecare dintre acestea indicând o adresă URL ca următoarea:
https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
(Notă: întreruperile de linie au fost introduse în URI-ul de mai sus pentru lizibilitate.)
Ați furnizat cheia dvs. de client și URI de redirecționare, dar fără secrete. În schimb, i-ați spus serverului că doriți un cod de autentificare ca răspuns și acces la ambele domenii „profil” și „e-mail”. Aceste domenii definesc permisiunile pe care le solicitați de la utilizator și limitează autorizarea jetonului de acces pe care îl primiți.
La primire, browserul utilizatorului este direcționat către o pagină dinamică controlată de furnizorul OAuth 2. Furnizorul OAuth 2 verifică dacă URI-ul de apel invers și cheia client se potrivesc reciproc înainte de a continua. Dacă o fac, fluxul diverge pentru scurt timp în funcție de jetoanele de sesiune ale utilizatorului.
Dacă utilizatorul nu este în prezent conectat la acel serviciu, i se va solicita să facă acest lucru. Odată ce s-au conectat, utilizatorului i se prezintă un dialog care solicită permisiunea de a permite aplicației dvs. să se autentifice.
Presupunând că utilizatorul aprobă, serverul OAuth 2 îi redirecționează apoi către URI-ul de apel invers pe care l-ați furnizat, inclusiv un cod de autorizare în parametrii de interogare: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
.
Codul de autorizare este un token de unică folosință cu expirare rapidă; imediat după primirea acestuia, serverul dvs. ar trebui să se întoarcă și să facă o altă solicitare furnizorului OAuth 2, incluzând atât codul de autorizare, cât și secretul clientului:
POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET
Scopul acestui cod de autorizare este autentificarea cererii POST de mai sus, dar din cauza naturii fluxului, aceasta trebuie direcționată prin sistemul utilizatorului. Ca atare, este în mod inerent nesigur.
Restricțiile privind codul de autorizare (adică expiră rapid și poate fi folosit doar o singură dată) sunt acolo pentru a atenua riscul inerent de a trece o acreditare de autentificare printr-un sistem care nu este de încredere.
Acest apel, efectuat direct de pe serverul dvs. către serverul furnizorului OAuth 2, este componenta cheie a procesului de conectare la nivelul serverului OAuth 2. Controlul apelului înseamnă că știți că apelul este securizat prin TLS, contribuind astfel la protejarea acestuia împotriva atacurilor de interceptare.
Includerea codului de autorizare asigură că utilizatorul și-a acordat în mod explicit consimțământul. Includerea secretului clientului, care nu este niciodată vizibil pentru utilizatorii dvs., asigură că această solicitare nu provine de la un virus sau malware din sistemul utilizatorului, care a interceptat codul de autorizare.
Dacă totul se potrivește, serverul returnează un token de acces , cu care puteți efectua apeluri către acel furnizor în timp ce sunteți autentificat ca utilizator.
Odată ce ați primit jetonul de acces de la server, serverul dvs. redirecționează apoi browserul utilizatorului încă o dată către pagina de destinație pentru utilizatorii care tocmai s-au conectat. Este obișnuit să păstrați jetonul de acces în memoria cache a sesiunii de pe serverul utilizatorului, așa că serverul poate efectua apeluri către furnizorul social dat ori de câte ori este necesar.
Jetonul de acces nu ar trebui să fie niciodată disponibil utilizatorului!
Sunt mai multe detalii în care ne-am putea scufunda.
De exemplu, Google include un jeton de reîmprospătare care prelungește durata de viață a jetonului dvs. de acces, în timp ce Facebook oferă un punct final la care puteți schimba simboluri de acces de scurtă durată cu ceva cu viață mai lungă. Aceste detalii nu contează totuși pentru noi, pentru că nu vom folosi acest flux.
Acest flux este greoi pentru un API REST. Deși ați putea solicita clientului front-end să genereze pagina de conectare inițială și ca back-end-ul să furnizeze o adresă URL de apel invers, în cele din urmă veți întâlni o problemă. Doriți să redirecționați utilizatorul către pagina de destinație a front-end-ului odată ce ați primit indicativul de acces și nu există o modalitate clară, RESTful de a face acest lucru.
Din fericire, există un alt flux OAuth 2 disponibil, care funcționează mult mai bine în acest caz.
Fluxul OAuth 2 la nivelul clientului
În acest flux, front-end-ul devine responsabil pentru gestionarea întregului proces OAuth 2. În general, seamănă cu fluxul de pe partea serverului, cu o excepție importantă – front-end-urile trăiesc pe mașini pe care utilizatorii le controlează, astfel încât nu li se poate încredința secretul clientului. Soluția este pur și simplu eliminarea întregului pas al procesului.
Primul pas, ca în fluxul de pe partea serverului, este înregistrarea aplicației.
În acest caz, proprietarul proiectului încă înregistrează aplicația, dar ca aplicație web. Furnizorul OAuth 2 va furniza în continuare o cheie de client , dar este posibil să nu furnizeze niciun secret de client.
Front-end-ul oferă utilizatorului un buton de conectare socială, care direcționează către o pagină web controlată de furnizorul OAuth 2 și solicită permisiunea aplicației noastre de a accesa anumite aspecte ale profilului utilizatorului.
URL-ul arată puțin diferit de data aceasta, totuși:
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
Rețineți că de data aceasta parametrul response_type
din URL este token
.
Deci, cum rămâne cu URI-ul de redirecționare?
Aceasta este pur și simplu orice adresă de pe front-end care este pregătită să gestioneze în mod corespunzător jetonul de acces.
În funcție de biblioteca OAuth 2 utilizată, front-end-ul poate rula temporar un server capabil să accepte solicitări HTTP pe dispozitivul utilizatorului; în acest caz, adresa URL de redirecționare are forma http://localhost:7862/callback/?token=TOKEN
.
Deoarece serverul OAuth 2 returnează o redirecționare HTTP după ce utilizatorul a acceptat, iar această redirecționare este procesată de browser pe dispozitivul utilizatorului, această adresă este interpretată corect, oferind acces front-end la token.
Alternativ, front-end-ul poate implementa direct o pagină adecvată. În orice caz, front-end-ul este responsabil, în acest moment, pentru analizarea parametrilor de interogare și procesarea jetonului de acces.
Din acest moment, front-end-ul poate apela direct API-ul furnizorului OAuth 2 folosind simbolul. Dar utilizatorii nu prea doresc asta; doresc acces autentificat la API-ul dvs. Tot ce trebuie să ofere back-end-ul este un punct final la care front-end-ul poate schimba un token de acces al furnizorului social cu un token care acordă acces la API-ul tău.
De ce să permiteți acest lucru, având în vedere că furnizarea jetonului de acces la front-end este în mod inerent mai puțin sigură decât fluxul de pe partea serverului?

Fluxul din partea clientului permite o separare mai strictă între un API REST back-end și un front-end orientat către utilizator. Nimic nu vă împiedică strict să specificați serverul back-end ca URI de redirecționare; efectul final ar fi un fel de flux hibrid.
Problema este că serverul trebuie să genereze apoi o pagină adecvată pentru utilizator și apoi controlul manual înapoi la front-end într-un fel.
Este obișnuit în proiectele moderne să se separe strict preocupările între interfața de utilizare front-end și back-end-ul care se ocupă de toată logica de afaceri. De obicei, comunică printr-un API JSON bine definit. Fluxul hibrid descris mai sus încurcă totuși această separare a preocupărilor, forțând back-end-ul să servească atât o pagină orientată către utilizator, cât și apoi să creeze un flux pentru a controla manual înapoi la front-end.
Permiterea front-end-ului să gestioneze jetonul de acces este o tehnică convenabilă care păstrează separarea preocupărilor. Crește oarecum riscul de la un client compromis, dar funcționează bine, în general.
Acest flux poate părea complicat pentru front-end și este, dacă ai nevoie ca echipa de front-end să dezvolte totul pe cont propriu. Cu toate acestea, atât Facebook, cât și Google oferă biblioteci care permit front-end-ului să includă butoane de conectare care gestionează întregul proces cu o configurație minimă.
Iată o rețetă pentru schimbul de jetoane pe back-end.
Sub fluxul de client, back-end-ul este destul de izolat de procesul OAuth 2. Nu vă lăsați induși în eroare: aceasta nu este o muncă simplă. Veți dori ca acesta să accepte cel puțin următoarele funcționalități.
- Trimiteți cel puțin o solicitare furnizorului OAuth 2, doar pentru a vă asigura că simbolul furnizat de front-end este valid, nu un șir arbitrar aleatoriu.
- Când simbolul este valid, returnați un simbol valid pentru API-ul dvs. În caz contrar, returnați o eroare informativă.
- Dacă acesta este un utilizator nou, creați un model de
User
pentru acesta și completați-l corespunzător. - Dacă acesta este un utilizator pentru care există deja un model de
User
, potriviți-l după adresa de e-mail, astfel încât să obțină acces la contul existent corect în loc să creeze unul nou pentru autentificarea socială. - Actualizați detaliile profilului utilizatorului în funcție de ceea ce a furnizat pe rețelele sociale.
Vestea bună este că implementarea tuturor acestor funcționalități pe back-end este mult mai simplă decât v-ați aștepta.
Iată magia despre cum să faci toate acestea să funcționeze pe back-end în doar două duzini de linii de cod. Aceasta depinde de biblioteca Python Social Auth („PSA” de acum înainte), așa că va trebui să includeți atât social-auth-core
cât și social-auth-app-django
în requirements.txt
.
De asemenea, va trebui să configurați biblioteca așa cum este documentat aici. Rețineți că acest lucru exclude gestionarea excepțiilor pentru claritate.
Codul complet pentru acest exemplu poate fi găsit aici.
@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, )
Mai este puțin mai mult care trebuie să intre în setările tale (codul complet), iar apoi ești gata:
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', )
Adăugați o mapare la această funcție în urls.py
și gata!
Cum funcționează această magie?
Python Social Auth este o piesă foarte cool, foarte complexă. Este foarte fericit să gestioneze autentificarea și accesul la oricare dintre câteva zeci de furnizori de autentificare socială și funcționează pe cele mai populare cadre web Python, inclusiv Django, Flask, Pyramid, CherryPy și WebPy.
În cea mai mare parte, codul de mai sus este o vizualizare bazată pe funcții Django REST framework (DRF): ascultă cererile POST pe orice cale pe care îl mapați în urls.py
și, presupunând că îi trimiteți o solicitare în formatul la care se așteaptă, apoi vă obține un obiect User
sau None
.
Dacă obțineți un obiect User
, acesta este de tipul de model pe care l-ați configurat în altă parte în proiect, care poate sau nu să fi existat deja. PSA s-a ocupat deja de validarea jetonului, de a identifica dacă există sau nu o potrivire de utilizator, de a crea un utilizator dacă este necesar și de a actualiza detaliile utilizatorului de la furnizorul de servicii sociale.
Detaliile exacte despre modul în care un utilizator este mapat de la utilizatorul furnizorului de servicii sociale la al dvs. și asociat cu utilizatorii existenți, sunt specificate de SOCIAL_AUTH_PIPELINE
definit mai sus. Mai sunt multe de învățat despre cum funcționează toate acestea, dar este în afara domeniului de aplicare al acestei postări. Puteți citi mai multe despre el aici.
Elementul cheie de magie este decoratorul @psa()
de pe vizualizare, care adaugă câțiva membri la obiectul de request
care este transmis în vizualizarea ta. Cel mai interesant pentru noi este request.backend
(pentru PSA, un backend este orice furnizor de autentificare socială).
Backend-ul adecvat a fost ales pentru noi și atașat obiectului de request
pe baza argumentului backend
-ului pentru vizualizare, care este populat de adresa URL în sine.
Odată ce ai în mână obiectul backend
, este foarte fericit să te autentifice împotriva acelui furnizor, având în vedere codul tău de acces; aceasta este metoda do_auth
. Aceasta, la rândul său, angajează întregul SOCIAL_AUTH_PIPELINE
din fișierul dvs. de configurare.
Conducta poate face lucruri destul de puternice dacă o extindeți, deși face deja tot ceea ce aveți nevoie cu nimic altceva decât cu funcționalitatea implicită, încorporată.
După aceea, a revenit la codul DRF normal: dacă aveți un obiect User
valid, puteți returna foarte ușor un token API corespunzător. Dacă nu ați primit înapoi un obiect User
valid, este ușor să generați o eroare.
Un dezavantaj al acestei tehnici este că, deși este relativ simplu să returnezi erori în cazul în care apar, este dificil să obții multe informații despre ce anume a mers prost. PSA înghite orice detalii pe care serverul le-ar fi putut returna despre care a fost problema.
Apoi, din nou, este în natura sistemelor de autentificare bine concepute să fie destul de opace în ceea ce privește sursele de eroare. Dacă o aplicație îi spune vreodată unui utilizator „Parolă nevalidă” după o încercare de conectare, aceasta echivalează cu a spune „Felicitări! Ați ghicit un nume de utilizator valid.”
De ce să nu rulezi pe al tău?
Într-un cuvânt: extensibilitate. Foarte puțini furnizori de servicii sociale OAuth 2 solicită sau returnează exact aceleași informații în apelurile lor API, exact în același mod. Totuși, există tot felul de cazuri speciale și excepții.
Adăugarea unui nou furnizor de servicii sociale după ce ați configurat deja un PSA este o chestiune de câteva linii de configurare în fișierele dvs. de setări. Nu trebuie să ajustați deloc niciun cod. PSA retrage toate acestea, astfel încât să vă puteți concentra asupra propriei aplicații.
Cum naiba testez asta?
Buna intrebare! unittest.mock
nu este potrivit pentru a batjocori apelurile API îngropate sub un strat de abstractizare adânc în interiorul unei biblioteci; doar descoperirea căii precise spre batjocura ar necesita un efort substanțial.
În schimb, deoarece PSA este construit deasupra bibliotecii de solicitări, utilizați excelenta bibliotecă de răspunsuri pentru a bate joc de furnizori la nivel HTTP.
O discuție completă despre testare depășește scopul acestui articol, dar un eșantion din testele noastre sunt incluse aici. Funcții speciale de remarcat sunt managerul de context mocked
și clasa SocialAuthTests
.
Lasă PSA să facă treaba grea.
Procesul OAuth2 este detaliat și complicat, cu multă complexitate inerentă. Din fericire, este posibil să ocoliți o mare parte din această complexitate adăugând o bibliotecă dedicată gestionării acesteia într-un mod cât mai nedureros posibil.
Python Social Auth face o treabă grozavă în acest sens. Am demonstrat o vizualizare Django/DRF care utilizează fluxul implicit OAuth2 la nivelul clientului pentru a obține crearea și potrivirea fără probleme a utilizatorilor în doar 25 de linii de cod. Nu e prea ponosit.