Comment intégrer OAuth 2 dans votre back-end Django/DRF sans devenir fou

Publié: 2022-03-11

Nous y avons tous été. Vous travaillez sur le back-end de l'API et vous êtes satisfait de la façon dont cela se passe. Vous avez récemment terminé le produit minimal viable (MVP), les tests sont tous réussis et vous avez hâte de mettre en œuvre de nouvelles fonctionnalités.

Ensuite, le patron vous envoie un e-mail : "Au fait, nous devons permettre aux gens de se connecter via Facebook et Google ; ils ne devraient pas avoir à créer un compte juste pour un petit site comme le nôtre.

Génial. Le fluage de la portée frappe à nouveau.

La bonne nouvelle est qu'OAuth 2 est devenu la norme de l'industrie pour l'authentification sociale et tierce (utilisée par des services tels que Facebook, Google, etc.) afin que vous puissiez vous concentrer sur la compréhension et la mise en œuvre de cette norme pour prendre en charge un large éventail de réseaux sociaux. fournisseurs d'authentification.

Il est probable que vous ne connaissiez pas OAuth 2 ; Je ne l'étais pas, quand cela m'est arrivé.

Intégrez OAuth 2 dans votre back-end Django/DRF

En tant que développeur Python, votre instinct peut vous conduire à pip, l'outil recommandé Python Package Index (PyPA) pour l'installation de packages Python. La mauvaise nouvelle est que pip connaît environ 278 packages traitant d'OAuth, dont 53 mentionnent spécifiquement Django. C'est une semaine de travail juste pour rechercher les options, sans parler de commencer à écrire du code.

Dans ce didacticiel, vous apprendrez à intégrer OAuth 2 dans votre Django ou Django Rest Framework à l'aide de Python Social Auth. Bien que cet article se concentre sur le Django REST Framework, vous pouvez appliquer les informations fournies ici pour implémenter la même chose dans une variété d'autres frameworks principaux courants.

Un aperçu rapide du flux OAuth 2

OAuth 2 a été conçu dès le départ comme un protocole d'authentification Web. Ce n'est pas tout à fait la même chose que s'il avait été conçu comme un protocole d'authentification Internet ; il suppose que des outils tels que le rendu HTML et les redirections de navigateur sont à votre disposition.

C'est évidemment un obstacle pour une API basée sur JSON, mais vous pouvez contourner ce problème.

Vous suivrez le processus comme si vous écriviez un site Web traditionnel côté serveur.

Le flux OAuth 2 côté serveur

La première étape se déroule entièrement en dehors du flux d'application. Le propriétaire du projet doit enregistrer votre application auprès de chaque fournisseur OAuth 2 pour lequel vous avez besoin de connexions.

Lors de cet enregistrement, ils fournissent au fournisseur OAuth 2 un URI de rappel , sur lequel votre application sera disponible pour recevoir des requêtes. En échange, ils reçoivent une clé client et un secret client . Ces jetons sont échangés lors du processus d'authentification pour valider les demandes de connexion.

Les jetons font référence à votre code serveur en tant que client. L'hôte est le fournisseur OAuth 2. Ils ne sont pas destinés aux clients de votre API.

Le flux commence lorsque votre application génère une page qui inclut un bouton, comme "Se connecter avec Facebook" ou "Se connecter avec Google+". Fondamentalement, ce ne sont rien d'autre que de simples liens, chacun pointant vers une URL comme celle-ci :

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

(Remarque : sauts de ligne insérés dans l'URI ci-dessus pour plus de lisibilité.)

Vous avez fourni votre clé client et votre URI de redirection, mais aucun secret. En échange, vous avez indiqué au serveur que vous souhaitiez un code d'authentification en réponse et l'accès aux champs d'application "profil" et "e-mail". Ces étendues définissent les autorisations que vous demandez à l'utilisateur et limitent l'autorisation du jeton d'accès que vous recevez.

Dès réception, le navigateur de l'utilisateur est dirigé vers une page dynamique contrôlée par le fournisseur OAuth 2. Le fournisseur OAuth 2 vérifie que l'URI de rappel et la clé client correspondent avant de continuer. Si tel est le cas, le flux diverge brièvement en fonction des jetons de session de l'utilisateur.

Si l'utilisateur n'est pas actuellement connecté à ce service, il sera invité à le faire. Une fois connecté, l'utilisateur se voit présenter une boîte de dialogue demandant l'autorisation d'autoriser votre application à se connecter.

En supposant que l'utilisateur approuve, le serveur OAuth 2 le redirige ensuite vers l'URI de rappel que vous avez fourni, en incluant un code d'autorisation dans les paramètres de requête : GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE .

Le code d'autorisation est un jeton à usage unique et à expiration rapide ; immédiatement après sa réception, votre serveur doit faire demi-tour et faire une autre demande au fournisseur OAuth 2, incluant à la fois le code d'autorisation et votre secret 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

Le but de ce code d'autorisation est d'authentifier la demande POST ci-dessus, mais en raison de la nature du flux, il doit être acheminé via le système de l'utilisateur. En tant que tel, il est intrinsèquement peu sûr.

Les restrictions sur le code d'autorisation (c'est-à-dire qu'il expire rapidement et ne peut être utilisé qu'une seule fois) sont là pour atténuer le risque inhérent de transmettre un justificatif d'authentification via un système non fiable.

Cet appel, effectué directement de votre serveur au serveur du fournisseur OAuth 2, est le composant clé du processus de connexion côté serveur OAuth 2. Le contrôle de l'appel signifie que vous savez que l'appel est sécurisé par TLS, ce qui contribue à le protéger contre les attaques d'écoute électronique.

L'inclusion du code d'autorisation garantit que l'utilisateur a explicitement donné son consentement. L'inclusion du secret client, qui n'est jamais visible par vos utilisateurs, garantit que cette demande ne provient pas d'un virus ou d'un logiciel malveillant sur le système de l'utilisateur, qui a intercepté le code d'autorisation.

Si tout correspond, le serveur renvoie un jeton d'accès , avec lequel vous pouvez effectuer des appels vers ce fournisseur tout en étant authentifié en tant qu'utilisateur.

Une fois que vous avez reçu le jeton d'accès du serveur, votre serveur redirige à nouveau le navigateur de l'utilisateur vers la page d'accueil des utilisateurs qui viennent de se connecter. Il est courant de conserver le jeton d'accès dans le cache de session côté serveur de l'utilisateur, donc que le serveur peut appeler le fournisseur social donné chaque fois que nécessaire.

Le jeton d'accès ne doit jamais être mis à la disposition de l'utilisateur !

Il y a plus de détails dans lesquels nous pourrions plonger.

Par exemple, Google inclut un jeton d'actualisation qui prolonge la durée de vie de votre jeton d'accès, tandis que Facebook fournit un point de terminaison sur lequel vous pouvez échanger des jetons d'accès de courte durée contre quelque chose de plus long. Ces détails nous importent peu, cependant, car nous n'allons pas utiliser ce flux.

Ce flux est lourd pour une API REST. Bien que vous puissiez faire en sorte que le client frontal génère la page de connexion initiale et que le back-end fournisse une URL de rappel, vous rencontrerez éventuellement un problème. Vous souhaitez rediriger l'utilisateur vers la page de destination du front-end une fois que vous avez reçu le jeton d'accès, et il n'y a pas de moyen clair et RESTful de le faire.

Heureusement, il existe un autre flux OAuth 2 disponible, qui fonctionne beaucoup mieux dans ce cas.

Le flux OAuth 2 côté client

Dans ce flux, le frontal devient responsable de la gestion de l'ensemble du processus OAuth 2. Il ressemble généralement au flux côté serveur, à une exception près : les frontaux vivent sur des machines que les utilisateurs contrôlent, de sorte qu'ils ne peuvent pas se voir confier le secret du client. La solution consiste simplement à éliminer toute cette étape du processus.

La première étape, comme dans le flux côté serveur, consiste à enregistrer l'application.

Dans ce cas, le propriétaire du projet enregistre toujours l'application, mais en tant qu'application Web. Le fournisseur OAuth 2 fournira toujours une clé client , mais ne fournira peut-être aucun secret client.

Le frontal fournit à l'utilisateur un bouton de connexion sociale, qui dirige vers une page Web contrôlée par le fournisseur OAuth 2 et demande l'autorisation pour notre application d'accéder à certains aspects du profil de l'utilisateur.

L'URL est un peu différente cette fois-ci :

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

Notez que le paramètre response_type cette fois dans l'URL est token .

Qu'en est-il de l'URI de redirection ?

Il s'agit simplement de n'importe quelle adresse sur le front-end qui est prête à gérer le jeton d'accès de manière appropriée.

Selon la bibliothèque OAuth 2 utilisée, le frontal peut en fait exécuter temporairement un serveur capable d'accepter les requêtes HTTP sur l'appareil de l'utilisateur ; dans ce cas, l'URL de redirection est de la forme http://localhost:7862/callback/?token=TOKEN .

Étant donné que le serveur OAuth 2 renvoie une redirection HTTP après que l'utilisateur a accepté, et que cette redirection est traitée par le navigateur sur l'appareil de l'utilisateur, cette adresse est interprétée correctement, donnant l'accès frontal au jeton.

Alternativement, le frontal peut implémenter directement une page appropriée. Dans tous les cas, le frontal est responsable, à ce stade, de l'analyse des paramètres de requête et du traitement du jeton d'accès.

À partir de ce moment, le frontal peut appeler directement l'API du fournisseur OAuth 2 à l'aide du jeton. Mais les utilisateurs ne veulent pas vraiment cela ; ils veulent un accès authentifié à votre API. Tout ce que le back-end doit fournir est un point de terminaison sur lequel le front-end peut échanger le jeton d'accès d'un fournisseur social contre un jeton qui accorde l'accès à votre API.

Pourquoi autoriser cela, étant donné que la fourniture du jeton d'accès au front-end est intrinsèquement moins sécurisée que le flux côté serveur ?

Le flux côté client permet une séparation plus stricte entre une API REST back-end et un front-end orienté utilisateur. Rien ne vous empêche strictement de spécifier votre serveur principal comme URI de redirection ; l'effet final serait une sorte de flux hybride.

Le problème est que le serveur doit ensuite générer une page appropriée destinée à l'utilisateur, puis remettre le contrôle au frontal d'une manière ou d'une autre.

Il est courant dans les projets modernes de séparer strictement les préoccupations entre l'interface utilisateur frontale et le back-end qui gère toute la logique métier. Ils communiquent généralement via une API JSON bien définie. Le flux hybride décrit ci-dessus brouille cette séparation des préoccupations, obligeant le back-end à servir à la fois une page destinée à l'utilisateur, puis à concevoir un flux pour en quelque sorte remettre le contrôle au front-end.

Autoriser le frontal à gérer le jeton d'accès est une technique rapide qui conserve la séparation des préoccupations. Cela augmente quelque peu le risque d'un client compromis, mais cela fonctionne bien, en général.

Ce flux peut sembler compliqué pour le front-end, et il l'est si vous demandez à l'équipe front-end de tout développer par elle-même. Cependant, Facebook et Google fournissent des bibliothèques qui permettent au frontal d'inclure des boutons de connexion qui gèrent l'ensemble du processus avec une configuration minimale.

Voici une recette pour l'échange de jetons sur le back-end.

Sous le flux client, le back-end est assez isolé du processus OAuth 2. Ne vous y trompez pas : ce n'est pas un travail simple. Vous voudrez qu'il prenne en charge au moins les fonctionnalités suivantes.

  • Envoyez au moins une requête au fournisseur OAuth 2, juste pour vous assurer que le jeton fourni par le frontal était valide, et non une chaîne aléatoire arbitraire.
  • Lorsque le jeton est valide, renvoyez un jeton valide pour votre API. Sinon, renvoie une erreur informative.
  • S'il s'agit d'un nouvel utilisateur, créez un modèle d' User pour lui et remplissez-le de manière appropriée.
  • S'il s'agit d'un utilisateur pour lequel un modèle User existe déjà, faites-le correspondre par son adresse e-mail, afin qu'il ait accès au bon compte existant au lieu d'en créer un nouveau pour la connexion sociale.
  • Mettez à jour les détails du profil de l'utilisateur en fonction de ce qu'il a fourni sur les réseaux sociaux.

La bonne nouvelle est que la mise en œuvre de toutes ces fonctionnalités sur le back-end est beaucoup plus simple que vous ne le pensez.

Voici la magie pour faire fonctionner tout cela sur le back-end en seulement deux douzaines de lignes de code. Cela dépend de la bibliothèque Python Social Auth ("PSA" désormais), vous devrez donc inclure à la fois social-auth-core et social-auth-app-django dans votre requirements.txt .

Vous devrez également configurer la bibliothèque comme indiqué ici. Notez que cela exclut la gestion des exceptions pour plus de clarté.

Le code complet de cet exemple peut être trouvé ici.

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

Il y a juste un peu plus qui doit aller dans vos paramètres (code complet), et puis vous êtes prêt :

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

Ajoutez un mappage à cette fonction dans votre urls.py et vous êtes prêt !

Comment fonctionne cette magie ?

Python Social Auth est une machine très cool et très complexe. Il est parfaitement heureux de gérer l'authentification et l'accès à l'un des plusieurs dizaines de fournisseurs d'authentification sociale, et il fonctionne sur les frameworks Web Python les plus populaires, notamment Django, Flask, Pyramid, CherryPy et WebPy.

Pour l'essentiel, le code ci-dessus est une vue très standard basée sur la fonction Django REST framework (DRF) : il écoute les requêtes POST sur le chemin vers lequel vous le mappez dans votre urls.py et, en supposant que vous lui envoyiez une requête dans le format qu'il attend, il vous obtient alors un objet User , ou None .

Si vous obtenez un objet User , il s'agit du type de modèle que vous avez configuré ailleurs dans votre projet, qui peut déjà exister ou non. PSA s'est déjà occupé de valider le jeton, d'identifier s'il existait ou non une correspondance d'utilisateur, de créer un utilisateur si nécessaire et de mettre à jour les détails de l'utilisateur auprès du fournisseur social.

Les détails exacts de la façon dont un utilisateur est mappé de l'utilisateur du fournisseur social au vôtre, et associé aux utilisateurs existants, sont spécifiés par le SOCIAL_AUTH_PIPELINE défini ci-dessus. Il y a beaucoup plus à apprendre sur le fonctionnement de tout cela, mais cela sort du cadre de cet article. Vous pouvez en savoir plus ici.

La clé de la magie est le décorateur @psa() sur la vue, qui ajoute des membres à l'objet de request qui est transmis à votre vue. Le plus intéressant pour nous est request.backend (pour PSA, un backend est tout fournisseur d'authentification sociale).

Le backend approprié a été choisi pour nous et ajouté à l'objet de request en fonction de l'argument backend de la vue, qui est rempli par l'URL elle-même.

Une fois que vous avez l'objet backend en main, il est parfaitement heureux de vous authentifier auprès de ce fournisseur, étant donné votre code d'accès ; c'est la méthode do_auth . Ceci, à son tour, engage l'intégralité du SOCIAL_AUTH_PIPELINE à partir de votre fichier de configuration.

Le pipeline peut faire des choses assez puissantes si vous l'étendez, bien qu'il fasse déjà tout ce dont vous avez besoin avec rien d'autre que sa fonctionnalité intégrée par défaut.

Après cela, on revient au code DRF normal : si vous avez un objet User valide, vous pouvez très facilement renvoyer un jeton API approprié. Si vous n'avez pas récupéré un objet User valide, il est facile de générer une erreur.

L'un des inconvénients de cette technique est que, bien qu'il soit relativement simple de renvoyer des erreurs si elles se produisent, il est difficile d'avoir une meilleure idée de ce qui s'est spécifiquement passé. PSA avale tous les détails que le serveur aurait pu renvoyer sur le problème.

Là encore, il est dans la nature des systèmes d'authentification bien conçus d'être assez opaques sur les sources d'erreur. Si jamais une application indique à un utilisateur « Mot de passe invalide » après une tentative de connexion, cela revient à dire « Félicitations ! Vous avez deviné un nom d'utilisateur valide.

Pourquoi ne pas simplement rouler le vôtre ?

En un mot : extensibilité. Très peu de fournisseurs sociaux OAuth 2 exigent ou renvoient exactement les mêmes informations dans leurs appels d'API de la même manière. Il existe cependant toutes sortes de cas particuliers et d'exceptions.

L'ajout d'un nouveau fournisseur social une fois que vous avez déjà configuré un PSA est une question de quelques lignes de configuration dans vos fichiers de paramètres. Vous n'avez pas du tout besoin d'ajuster de code. PSA résume tout cela, afin que vous puissiez vous concentrer sur votre propre application.

Comment diable puis-je tester cela ?

Bonne question! unittest.mock n'est pas bien adapté pour se moquer des appels d'API enfouis sous une couche d'abstraction au plus profond d'une bibliothèque ; le simple fait de découvrir le chemin précis pour se moquer demanderait des efforts substantiels.

Au lieu de cela, comme PSA est construit au-dessus de la bibliothèque Requests, vous utilisez l'excellente bibliothèque Responses pour simuler les fournisseurs au niveau HTTP.

Une discussion complète sur les tests dépasse le cadre de cet article, mais un échantillon de nos tests est inclus ici. Les fonctions particulières à noter sont le gestionnaire de contexte SocialAuthTests mocked

Laissez PSA faire le gros du travail.

Le processus OAuth2 est détaillé et compliqué avec beaucoup de complexité inhérente. Heureusement, il est possible de contourner une grande partie de cette complexité en introduisant une bibliothèque dédiée à sa gestion de la manière la plus indolore possible.

Python Social Auth fait un excellent travail à cet égard. Nous avons présenté une vue Django/DRF qui utilise le flux OAuth2 implicite côté client pour obtenir une création et une correspondance transparentes des utilisateurs en seulement 25 lignes de code. Ce n'est pas trop minable.