Come integrare OAuth 2 nel tuo back-end Django/DRF senza impazzire

Pubblicato: 2022-03-11

Ci siamo stati tutti. Stai lavorando sul back-end dell'API e sei soddisfatto di come sta andando. Hai recentemente completato il prodotto minimo vitale (MVP), i test stanno tutti superando e non vedi l'ora di implementare alcune nuove funzionalità.

Poi il capo ti manda una mail: “A proposito, dobbiamo permettere alle persone di accedere tramite Facebook e Google; non dovrebbero creare un account solo per un piccolo sito come il nostro.

Grande. Scope creep colpisce ancora.

La buona notizia è che OAuth 2 è emerso come lo standard del settore per l'autenticazione social e di terze parti (utilizzato da servizi come Facebook, Google, ecc.), quindi puoi concentrarti sulla comprensione e sull'implementazione di tale standard per supportare un'ampia gamma di social fornitori di autenticazione.

È probabile che tu non abbia familiarità con OAuth 2; Non lo ero, quando è successo a me.

Integra OAuth 2 nel tuo back-end Django/DRF

Come sviluppatore Python, il tuo istinto potrebbe portarti a pip, lo strumento consigliato Python Package Index (PyPA) per l'installazione di pacchetti Python. La cattiva notizia è che pip conosce 278 pacchetti che trattano OAuth, 53 dei quali menzionano specificamente Django. È una settimana di lavoro solo per ricercare le opzioni, non importa iniziare a scrivere codice.

In questo tutorial imparerai come integrare OAuth 2 nel tuo Django o Django Rest Framework usando Python Social Auth. Sebbene questo articolo si concentri sul Django REST Framework, puoi applicare le informazioni fornite qui per implementare lo stesso in una varietà di altri framework back-end comuni.

Una rapida panoramica del flusso OAuth 2

OAuth 2 è stato progettato fin dall'inizio come protocollo di autenticazione web. Questo non è proprio come se fosse stato progettato come protocollo di autenticazione di rete; presuppone che siano disponibili strumenti come il rendering HTML e i reindirizzamenti del browser.

Questo è ovviamente una sorta di ostacolo per un'API basata su JSON, ma puoi aggirare questo problema.

Passerai attraverso il processo come se stessi scrivendo un sito Web lato server tradizionale.

Il flusso OAuth 2 lato server

Il primo passaggio avviene completamente al di fuori del flusso dell'applicazione. Il proprietario del progetto deve registrare la tua domanda con ogni provider OAuth 2 per cui hai bisogno di login.

Durante questa registrazione, forniscono al provider OAuth 2 un URI di richiamata , in cui la tua applicazione sarà disponibile per ricevere richieste. In cambio, ricevono una chiave client e un segreto client . Questi token vengono scambiati durante il processo di autenticazione per convalidare le richieste di accesso.

I token si riferiscono al codice del tuo server come client. L'host è il provider OAuth 2. Non sono pensati per i clienti della tua API.

Il flusso inizia quando l'applicazione genera una pagina che include un pulsante, ad esempio "Accedi con Facebook" o "Accedi con Google+". Fondamentalmente, questi non sono altro che semplici collegamenti, ognuno dei quali punta a un URL come il seguente:

 https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

(Nota: interruzioni di riga inserite nell'URI sopra per la leggibilità.)

Hai fornito la chiave client e l'URI di reindirizzamento, ma nessun segreto. In cambio, hai detto al server che desideri un codice di autenticazione in risposta e l'accesso a entrambi gli ambiti "profilo" e "e-mail". Questi ambiti definiscono le autorizzazioni richieste all'utente e limitano l'autorizzazione del token di accesso ricevuto.

Al ricevimento, il browser dell'utente viene indirizzato a una pagina dinamica controllata dal provider OAuth 2. Il provider OAuth 2 verifica che l'URI di callback e la chiave client corrispondano tra loro prima di procedere. In tal caso, il flusso diverge brevemente a seconda dei token di sessione dell'utente.

Se l'utente non è attualmente connesso a quel servizio, verrà richiesto di farlo. Dopo aver effettuato l'accesso, all'utente viene presentata una finestra di dialogo che richiede l'autorizzazione per consentire all'applicazione di accedere.

Supponendo che l'utente approvi, il server OAuth 2 lo reindirizza all'URI di callback fornito, incluso un codice di autorizzazione nei parametri della query: GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE .

Il codice di autorizzazione è un token monouso a scadenza rapida; immediatamente dopo la sua ricezione, il tuo server dovrebbe girarsi e fare un'altra richiesta al provider OAuth 2, includendo sia il codice di autorizzazione che il tuo segreto client:

 POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET

Lo scopo di questo codice di autorizzazione è di autenticare la richiesta POST di cui sopra, ma a causa della natura del flusso, deve essere instradato attraverso il sistema dell'utente. In quanto tale, è intrinsecamente insicuro.

Le restrizioni sul codice di autorizzazione (ovvero che scada rapidamente e possa essere utilizzato una sola volta) servono a mitigare il rischio intrinseco di passare una credenziale di autenticazione attraverso un sistema non attendibile.

Questa chiamata, effettuata direttamente dal tuo server al server del provider OAuth 2, è il componente chiave del processo di accesso lato server di OAuth 2. Controllare la chiamata significa sapere che la chiamata è protetta da TLS, contribuendo così a proteggerla dagli attacchi di intercettazione.

L'inclusione del codice di autorizzazione garantisce che l'utente abbia espressamente concesso il consenso. Includere il client secret, che non è mai visibile ai tuoi utenti, garantisce che questa richiesta non provenga da virus o malware nel sistema dell'utente, che hanno intercettato il codice di autorizzazione.

Se tutto corrisponde, il server restituisce un token di accesso , con il quale puoi effettuare chiamate a quel provider mentre sei autenticato come utente.

Dopo aver ricevuto il token di accesso dal server, il tuo server reindirizza nuovamente il browser dell'utente alla pagina di destinazione per gli utenti che hanno appena effettuato l'accesso. È comune conservare il token di accesso nella cache della sessione lato server dell'utente, quindi che il server può effettuare chiamate al provider sociale specificato ogni volta che è necessario.

Il token di accesso non dovrebbe mai essere messo a disposizione dell'utente!

Ci sono più dettagli in cui potremmo approfondire.

Ad esempio, Google include un token di aggiornamento che prolunga la vita del tuo token di accesso mentre Facebook fornisce un endpoint in cui puoi scambiare token di accesso di breve durata con qualcosa di più lungo. Questi dettagli non ci interessano, però, perché non utilizzeremo questo flusso.

Questo flusso è ingombrante per un'API REST. Sebbene tu possa fare in modo che il client front-end generi la pagina di accesso iniziale e che il back-end fornisca un URL di callback, alla fine ti imbatterai in un problema. Vuoi reindirizzare l'utente alla pagina di destinazione del front-end una volta ricevuto il token di accesso e non esiste un modo chiaro e RESTful per farlo.

Fortunatamente, è disponibile un altro flusso OAuth 2, che in questo caso funziona molto meglio.

Il flusso OAuth 2 lato client

In questo flusso, il front-end diventa responsabile della gestione dell'intero processo OAuth 2. Somiglia generalmente al flusso lato server, con un'importante eccezione: i front-end risiedono su macchine controllate dagli utenti, quindi non possono essere affidati il ​​segreto del client. La soluzione è semplicemente eliminare l'intera fase del processo.

Il primo passaggio, come nel flusso lato server, è la registrazione dell'applicazione.

In questo caso, il proprietario del progetto registra comunque l'applicazione, ma come applicazione web. Il provider OAuth 2 fornirà comunque una chiave client , ma potrebbe non fornire alcun segreto client.

Il front-end fornisce all'utente un pulsante di accesso social, che indirizza a una pagina Web controllata dal provider OAuth 2 e richiede l'autorizzazione alla nostra applicazione per accedere a determinati aspetti del profilo dell'utente.

L'URL ha un aspetto leggermente diverso questa volta:

 https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email

Si noti che il parametro response_type questa volta nell'URL è token .

Quindi che dire dell'URI di reindirizzamento?

Questo è semplicemente un qualsiasi indirizzo sul front-end preparato per gestire il token di accesso in modo appropriato.

A seconda della libreria OAuth 2 in uso, il front-end può effettivamente eseguire temporaneamente un server in grado di accettare richieste HTTP sul dispositivo dell'utente; in tal caso, l'URL di reindirizzamento ha il formato http://localhost:7862/callback/?token=TOKEN .

Poiché il server OAuth 2 restituisce un reindirizzamento HTTP dopo che l'utente ha accettato e questo reindirizzamento viene elaborato dal browser sul dispositivo dell'utente, questo indirizzo viene interpretato correttamente, fornendo al front-end l'accesso al token.

In alternativa, il front-end può implementare direttamente una pagina appropriata. In ogni caso, il front-end è responsabile, a questo punto, dell'analisi dei parametri della query e dell'elaborazione del token di accesso.

Da questo momento in poi, il front-end può chiamare direttamente l'API del provider OAuth 2 utilizzando il token. Ma gli utenti non lo vogliono davvero; vogliono l'accesso autenticato alla tua API. Tutto ciò che il back-end deve fornire è un endpoint in cui il front-end può scambiare il token di accesso di un provider sociale con un token che concede l'accesso alla tua API.

Perché permetterlo, dato che fornire il token di accesso al front-end è intrinsecamente meno sicuro del flusso lato server?

Il flusso lato client consente una separazione più rigorosa tra un'API REST back-end e un front-end rivolto all'utente. Non c'è nulla che ti impedisca di specificare il tuo server back-end come URI di reindirizzamento; l'effetto finale sarebbe una sorta di flusso ibrido.

Il problema è che il server deve quindi generare una pagina appropriata rivolta all'utente e quindi restituire il controllo al front-end in qualche modo.

È comune nei progetti moderni separare rigorosamente le preoccupazioni tra l'interfaccia utente front-end e il back-end che gestisce tutta la logica aziendale. In genere comunicano tramite un'API JSON ben definita. Il flusso ibrido sopra descritto, tuttavia, confonde quella separazione delle preoccupazioni, costringendo il back-end a servire entrambi una pagina rivolta all'utente e quindi a progettare un flusso per in qualche modo riportare il controllo al front-end.

Consentire al front-end di gestire il token di accesso è una tecnica utile che mantiene la separazione delle preoccupazioni. Aumenta in qualche modo il rischio di un client compromesso, ma funziona bene, in generale.

Questo flusso può sembrare complicato per il front-end e lo è, se hai bisogno che il team front-end sviluppi tutto da solo. Tuttavia, sia Facebook che Google forniscono librerie che consentono al front-end di includere pulsanti di accesso che gestiscono l'intero processo con una configurazione minima.

Ecco una ricetta per lo scambio di token sul back-end.

Sotto il flusso del client, il back-end è piuttosto isolato dal processo OAuth 2. Non fatevi ingannare: questo non è un lavoro semplice. Ti consigliamo di supportare almeno le seguenti funzionalità.

  • Invia almeno una richiesta al provider OAuth 2, solo per assicurarti che il token fornito dal front-end fosse valido, non una stringa casuale arbitraria.
  • Quando il token è valido, restituisci un token valido per la tua API. In caso contrario, restituire un errore informativo.
  • Se si tratta di un nuovo utente, crea un modello User per lui e compilalo in modo appropriato.
  • Se si tratta di un utente per il quale esiste già un modello User , abbinalo al suo indirizzo email, in modo che acceda all'account esistente corretto invece di crearne uno nuovo per l'accesso social.
  • Aggiorna i dettagli del profilo dell'utente in base a ciò che ha fornito sui social media.

La buona notizia è che implementare tutte queste funzionalità sul back-end è molto più semplice di quanto ci si possa aspettare.

Ecco la magia su come far funzionare tutto questo sul back-end in sole due dozzine di righe di codice. Questo dipende dalla libreria Python Social Auth ("PSA" d'ora in poi), quindi dovrai includere sia social-auth-core che social-auth-app-django nel tuo requirements.txt .

Dovrai anche configurare la libreria come documentato qui. Si noti che ciò esclude la gestione di alcune eccezioni per chiarezza.

Il codice completo per questo esempio può essere trovato qui.

 @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, )

C'è solo un po' di più che deve andare nelle tue impostazioni (codice completo), e poi sei pronto:

 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', )

Aggiungi una mappatura a questa funzione nel tuo urls.py e sei pronto!

Come funziona quella magia?

Python Social Auth è un macchinario molto interessante e molto complesso. È perfettamente felice di gestire l'autenticazione e l'accesso a una qualsiasi delle diverse dozzine di provider di autenticazione sociale e funziona sui framework Web Python più popolari, inclusi Django, Flask, Pyramid, CherryPy e WebPy.

Per la maggior parte, il codice sopra è una vista basata sulla funzione Django REST Framework (DRF) molto standard: ascolta le richieste POST su qualunque percorso lo mappi nel tuo urls.py e, supponendo che tu gli invii una richiesta nel formato che si aspetta, quindi ti ottiene un oggetto User o None .

Se ottieni un oggetto User , è del tipo di modello che hai configurato altrove nel tuo progetto, che potrebbe essere già esistito o meno. PSA si è già occupata della convalida del token, dell'identificazione dell'esistenza o meno di una corrispondenza utente, della creazione di un utente se necessario e dell'aggiornamento dei dettagli dell'utente dal social provider.

I dettagli esatti di come un utente viene mappato dall'utente del social provider al tuo e associato agli utenti esistenti sono specificati dalla SOCIAL_AUTH_PIPELINE definita sopra. C'è molto altro da imparare su come funziona, ma non rientra nell'ambito di questo post. Puoi leggere di più a riguardo qui.

Il pezzo chiave della magia è il decoratore @psa() sulla vista, che aggiunge alcuni membri all'oggetto request che viene passato alla vista. Il più interessante per noi è request.backend (per PSA, un backend è qualsiasi provider di autenticazione sociale).

Il back-end appropriato è stato scelto per noi e aggiunto all'oggetto request in base all'argomento backend -end della vista, che viene popolata dall'URL stesso.

Una volta che hai in mano l'oggetto backend -end, è perfettamente felice di autenticarti contro quel provider, dato il tuo codice di accesso; questo è il metodo do_auth . Questo, a sua volta, impegna l'intero SOCIAL_AUTH_PIPELINE dal tuo file di configurazione.

La pipeline può fare alcune cose piuttosto potenti se la estendi, anche se fa già tutto ciò di cui hai bisogno con nient'altro che la sua funzionalità predefinita integrata.

Dopodiché, si torna al normale codice DRF: se hai un oggetto User valido, puoi facilmente restituire un token API appropriato. Se non hai recuperato un oggetto User valido, è facile generare un errore.

Uno svantaggio di questa tecnica è che mentre è relativamente semplice restituire gli errori se si verificano, è difficile ottenere molte informazioni su ciò che è andato storto in modo specifico. PSA ingoia tutti i dettagli che il server potrebbe aver restituito su quale fosse il problema.

Inoltre, è nella natura di sistemi di autenticazione ben progettati essere abbastanza opachi sulle fonti di errore. Se un'applicazione dice a un utente "Password non valida" dopo un tentativo di accesso, equivale a dire "Congratulazioni! Hai indovinato un nome utente valido.

Perché non arrotolare il tuo?

In una parola: estensibilità. Pochissimi provider di social OAuth 2 richiedono o restituiscono esattamente le stesse informazioni nelle loro chiamate API esattamente nello stesso modo. Ci sono tutti i tipi di casi speciali ed eccezioni però.

L'aggiunta di un nuovo social provider dopo aver già impostato un PSA è una questione di poche righe di configurazione nei file delle impostazioni. Non è necessario modificare alcun codice. PSA astrae tutto ciò, in modo che tu possa concentrarti sulla tua applicazione.

Come diavolo faccio a testarlo?

Buona domanda! unittest.mock non è adatto per deridere le chiamate API sepolte sotto uno strato di astrazione nel profondo di una libreria; solo scoprire il percorso preciso per deridere richiederebbe uno sforzo notevole.

Invece, poiché PSA è basato sulla libreria Richieste, usi l'eccellente libreria Responses per prendere in giro i provider a livello HTTP.

Una discussione completa sui test va oltre lo scopo di questo articolo, ma un esempio dei nostri test è incluso qui. Funzioni particolari da notare ci sono il mocked gestore di contesto e la classe SocialAuthTests .

Lascia che PSA faccia il lavoro pesante.

Il processo OAuth2 è dettagliato e complicato con molta complessità intrinseca. Fortunatamente, è possibile aggirare gran parte di questa complessità introducendo una libreria dedicata a gestirla nel modo più indolore possibile.

Python Social Auth fa un ottimo lavoro in questo. Abbiamo dimostrato una vista Django/DRF che utilizza il flusso OAuth2 implicito lato client per ottenere la creazione e la corrispondenza degli utenti senza interruzioni in sole 25 righe di codice. Non è troppo malandato.