Ajouter une authentification oauth2 à votre API FastAPI
- 2021-07-20
- Publié par : Christophe DELEUZE
- Catégorie : FastAPI
OAuth2 est, vous l’aurez deviné, la version 2 du protocole (appelé aussi framework) OAuth. Ce protocole permet à des applications tierces d’obtenir un accès limité à un service disponible via HTTP par le biais d’une autorisation préalable du détenteur des ressources.
De nos jours, les Systèmes d’Information doivent être flexibles afin de répondre aux enjeux de l’économie digitale. Cette ouverture n’est cependant pas sans conséquences et la sécurisation du droit d’accès aux ressources exposées devient, plus que jamais, critique pour l’ensemble des entreprises : c’est ici qu’intervient le « protocole de délégation d’autorisation » OAuth2.
Sans OAuth2, pour donner un accès à une application tierce, il faut y stocker en clair les identifiants de l’utilisateur. Cette pratique entraîne de nombreux risques en cas de compromission, de plus il est impossible de limiter les droits de l’application de façon standardisée. Par ailleurs, identifier l’entité souhaitant accéder aux ressources s’avère compliqué, ouvrant donc la porte aux abus (i.e. Identity Spoofing) et laissant l’utilisateur et le Système d’Informations sans défense.
L’utilisation de OAuth2 permet de mettre en place une délégation d’autorisation pour accorder à une application tierce un accès limité sur une ressource, avec l’accord du propriétaire de celle-ci. Il n’y a alors plus besoin de stocker les identifiants de l’utilisateur dans les applications tierces.
OAuth2 : les coulisses
OAuth2 repose sur des échanges entre 4 acteurs. L’utilisateur, ici nommé Resource Owner, est capable d’accorder l’accès à la ressource pour une application nommée Client.
L’Authorization Server occupe le rôle central au sein du protocole, il est chargé d’authentifier le Resource Owner et de délivrer son autorisation sous la forme d’un jeton appelé access token. Le Resource Server quant à lui correspond au serveur où sont stockées les ressources protégées.
Le protocole adresse des situations de sécurité différenciées en fonction de 2 types de clients :
- Un client confidentiel est une application ou un service back-end installé dans une zone sécurisée. Ainsi, ses identifiants et tokens ne sont pas exposés au Resource Owner.
- Un client public est généralement installé et exécuté sur la machine du Resource Owner ou exécuté par le navigateur (front-end), il est donc possible pour les personnes et les programmes ayant accès à la machine d’accéder aux identifiants et aux tokens du client.
Un client confidentiel voit son accès aux ressources protégées facilité. Cela nécessite cependant qu’il soit capable de garantir la confidentialité de ses identifiants et des tokens délivrés. Lors de leur enregistrement auprès de l’Authorization Server, tous les clients reçoivent un client id ainsi qu’un client secret pour les clients confidentiels.
De l‘autorisation à l’identification
A la différence de l’identification qui consiste à donner son identité, l’authentification prouve cette identité à travers différents moyens tels que le mot de passe, la biométrie, etc.
Token : Le sauf-conduit 2.0
L’access token est l’élément utilisé pour accéder à une ressource protégée. Accordé par le Resource Owner et délivré par l’Authorization Server, il permet d’obtenir les privilèges accordés au client (i.e. scope) et la durée de vie de cette autorisation. Il est généralement de courte durée.
Le refresh token est l’élément utilisé pour obtenir un nouvel access token, sans interaction avec l’utilisateur. Il n’est délivré qu’aux clients confidentiels. Son utilisation nécessite uniquement l’authentification du client et non plus celle du Resource Owner. Grâce au refresh token, l’utilisateur n’est pas contraint de s’authentifier régulièrement, améliorant ainsi son expérience.
Il existe deux implémentations possibles de tokens : les tokens classiques, aussi appelés opaques, et les self-contained tokens.
Les tokens classiques (opaques)
Les tokens classiques sont des chaines de caractères inintelligibles pour l’utilisateur. Seuls les Authorization Servers sont capables de les exploiter (comprendre ici, de les associer à un scope). Ces tokens nécessitent la mise en place d’une cinématique d’échange entre l’Authorization Server, qui délivre les tokens, et le Resource Server qui les reçoit. En effet, le Resource Server est incapable de déterminer si l’access token est valable pour la requête du client.
Le Resource Server effectue un appel vers l’Authorization Server avec le token. Celui-ci retourne alors les informations représentées par ce token parmi lesquels le scope, la validité et l’identité du client à laquelle il a été délivré. Cela permet au Resource Server de valider ou non la requête du client.
Les self-contained tokens
Les tokens self-contained sont quant à eux porteurs de l’information. Ils sont généralement signés, peuvent être chiffrés et ne nécessitent pas de faire appel à l‘Authorization Server pour être validés. La signature de l’Authorization Server permet de prouver l’intégrité des self-contained tokens. En effet, en chiffrant le condensat du token avec sa clé privée, l’Authorization Server permet à n’importe quel détenteur de sa clé publique, de vérifier que le token reçu correspond bien au token envoyé et qu’il n’a pas été modifié en transit. Dans ce cas de figure, la mise en place d’une révocation de token est soumise à des contraintes, car l’Authorization Server doit être en mesure de faire part de la révocation à tous les acteurs. Dans les faits, la durée de vie des self-contained tokens est très courte pour répondre à cette contrainte.
Ce qu'il faut retenir de Oauth2
Avant d’attaquer dans le vif du sujet avec FastAPI, résumons ce qu’il faut retenir de OAuth2 :
- Resource Owner : Entité (personne, machine…) qui possède les droits sur la ressource ;
- Client : L’application (mobile, web…) qui veut accéder aux ressources protégées ;
- Authorization Server : Serveur qui authentifie le Client et qui lui délivre des Tokens ;
- Resource Server : Serveur contenant la ressource protégée à laquelle le Client veut accéder.
Pourquoi OAuth2 avec FastAPI ?
Un besoin courant dans la conception d’API REST consiste à fournir des services dont certains sont publics et d’autres privés. Les premiers sont accessibles à n’importe qui sans identification ou authentification préalable, tandis que l’utilisation des seconds est restreinte à des utilisateurs authentifiés ayant les bons droits d’utilisation. La question de la sécurité, notamment pour toute la partie privée de l’API, est un point central à ne pas négliger. Il existe de nombreuses façons de gérer la sécurité, l’authentification et l’autorisation. Et c’est normalement un sujet complexe et “difficile”. Dans de nombreux frameworks et systèmes, la simple gestion de la sécurité et de l’authentification demande beaucoup d’efforts et de code (dans de nombreux cas, cela peut représenter 50 % ou plus de tout le code écrit). Heureusement pour nous, FastAPI fournit plusieurs outils pour nous aider à gérer la sécurité facilement, rapidement, de manière standard, sans avoir à étudier et à apprendre toutes les spécifications de sécurité.
Toutefois, avant de rentrer dans les détails, regardons ce que préconise, en termes de sécurité, la spécification OpenAPI sur laquelle repose FastAPI :
- Schémas d’authentification HTTP ;
- Clés API dans les en-têtes (headers), cookies, ou chaînes de requête ;
- OAuth2 ;
- OpenID.
Il ressort de cette spécification que les protocoles recommandés pour l’authentification sont OAuth2 et OpenAPI (qui repose aussi sur OAuth2). C’est pour cela que nous allons implémenter OAuth2 et l’ensemble des outils fournis par FastAPI vont nous permettre de rapidement mettre en place le protocole OAuth2.
A titre informatif, le protocole OAuth2 est implémenté par Facebook, Google, Twitter, Git-hub et vous vous en servez à chaque fois que vous vous connectez sur l’une de leurs plateformes.
Toujours à titre informatif, OpenID Connect, qui est aussi basé sur OAuth2, étend simplement OAuth2 en spécifiant certaines choses qui sont relativement ambigües dans OAuth2, pour essayer de le rendre plus interopérable. Par exemple, la connexion à Google utilise le protocole OpenID Connect (qui utilise en arrière-plan OAuth2). Toutefois, la connexion à Facebook ne prend pas en charge OpenID Connect. Facebook ayant sa propre implémentation d’OAuth2.
Au regard de ces informations, connaître OAuth2 et savoir l’implémenter vous permettra de facilement faire la même chose avec OpenID Connect si vous en avez besoin.
Comment est utilisé la spécification OpenID dans FastAPI pour tout ce qui concerne la sécurité
Encore une fois, c’est la spécification OpenAPI qui va nous donner les éléments de réponse nécessaires à notre compréhension. OpenAPI a un moyen de définir plusieurs « schémas » de sécurité. En les utilisant, nous pouvons profiter de tous ces outils standardisés ainsi que de leur documentation interactive. OpenAPI définit les schémas de sécurité suivants :
apiKey: une clé spécifique à l’application qui peut provenir de :- Un paramètre de requête ;
- Un en-tête ;
- Un cookie.
http: systèmes d’authentification HTTP standard, notamment :bearer: un en-têteAuthorizationavec une valeur deBearerplus un token (jeton). Ceci est hérité d’OAuth2 ;- Authentification de base en HTTP ;
- HTTP Digest, etc.
oauth2: toutes les façons OAuth2 de gérer la sécurité (appelées « flux »).- Plusieurs de ces flux sont appropriés pour créer un fournisseur d’authentification OAuth 2.0 (comme Google, Facebook, Twitter, Git-hub, etc.) :
implicit;clientCredentials;authorizationCode.
- Mais il existe un “flux” spécifique qui peut parfaitement être utilisé pour gérer directement l’authentification dans la même application :
password : certains prochains chapitrent en couvriront des exemples.
openIdConnect: a un moyen de définir comment découvrir automatiquement les données d’authentification OAuth2.- Cette découverte automatique est ce qui est défini dans la spécification OpenID Connect.
- Plusieurs de ces flux sont appropriés pour créer un fournisseur d’authentification OAuth 2.0 (comme Google, Facebook, Twitter, Git-hub, etc.) :
Outils fournis par FastAPI
FastAPI fournit plusieurs outils pour chacun de ces schémas de sécurité dans le module fastapi.security qui simplifient l’utilisation de ces mécanismes de sécurité. Dans les prochains sections, nous allons voir comment sécuriser votre API avec un système authentification OAuth2 à l’aide des outils fournis par FastAPI et nous verrons aussi comment il s’intègre automatiquement dans le système de documentation interactive.
Premiers pas avec OAuth2 et FastAPI
FastAPI fournit plusieurs outils pour chacun de ces schémas de sécurité dans le module fastapi.security qui simplifient l’utilisation de ces mécanismes de sécurité. Dans les prochains sections, nous allons voir comment sécuriser votre API avec un système authentification OAuth2 à l’aide des outils fournis par FastAPI et nous verrons aussi comment il s’intègre automatiquement dans le système de documentation interactive.
Prérequis
Avant de commencer, nous devons satisfaire une dépendance qui n’est pas présente nativement dans FastAPI : python-multipart. En effet, OAuth2 utilise des multipart/form-data (données de formulaire) pour envoyer à l’API le nom d’utilisateur et le mot de passe et FastAPI ne sait pas les gérer nativement. Dans notre cas, python-multipart va nous permettre d’ajouter la fonctionnalité manquante à notre API.
Pour l’installer :
pip install python-multipart
Premiers pas
Imaginons que vous ayez votre API (backend) dans un domaine différent de celui de votre interface (frontend), si vous voulez avoir un moyen pour que le frontend puisse s’authentifier avec le backend, en utilisant un nom d’utilisateur (username) et un mot de passe (password) il faudra utiliser OAuth2 pour construire cela avec FastAPI.
Commençons avec un cas simple, copiez l’exemple ci-dessous dans un fichier api.py :
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
return {"token": token}
Puis exécuter le serveur :
uvicorn api:app --reload
Ensuite, en accédant à L’URL http://127.0.0.1:8000/docs vous devriez voir cela :
Notez la présence d’un petit verrou dans le coin supérieur droit sur lequel vous pouvez cliquer. Si vous cliquez dessus, cela ouvrira un petit formulaire d’authentification où vous pourrez taper un username et password (ainsi que d’autres champs optionnels). Pour le moment, peu importe ce que vous taperez dans le formulaire, cela ne fonctionnera pas encore, mais on y arrivera.
L’interface que vous voyez n’est bien sûr pas le frontend pour les utilisateurs finaux, mais c’est un excellent outil généré automatiquement qui sert pour documenter de manière interactive toutes vos API. Il peut être utilisé par l’équipe frontend (qui peut aussi être vous-même). Il peut être utilisé par des applications et des systèmes tiers. Et il peut également être utilisé par vous-même pour déboguer, vérifier et tester la même application.
La cinématique avec password
La cinématique (flux) avec password est l’un des moyens définis dans OAuth2, pour gérer la sécurité et l’authentification. OAuth2 a été conçu pour que le backend ou l’API puisse être indépendant du serveur qui authentifie l’utilisateur. Mais dans notre cas et à des fins de démonstration, nous utiliserons la même application FastAPI pour gérer l’API et l’authentification.
Alors, examinons notre étude cas de ce point de vue simplifié :
- L’utilisateur tape le
usernameetpassworddans le frontend, et appuie surEnter. - Le frontend (exécuté dans le navigateur de l’utilisateur) envoie le
usernameet lepasswordà une URL spécifique dans notre API (déclarée avectokenUrl="token"). - L’API vérifie le
usernameet lepassword, et répond avec un token (jeton).- Un token est juste une chaîne de caractère avec du contenu réutilisable plus tard pour authentifier cet utilisateur.
- Normalement, un token est configuré pour expirer après un certain temps.
- Ainsi, l’utilisateur devra se reconnecter ultérieurement.
- Et si le jeton est volé, le risque est moindre. Ce n’est pas comme une clé permanente qui fonctionnera pour toujours (dans la plupart des cas).
- Le frontend stocke ce token temporairement quelque part.
- L’utilisateur clique dans le frontend pour accéder à une autre section de l’application Web frontend.
- L’interface doit récupérer des données supplémentaires de l’API.
- Mais il a besoin d’une authentification pour ce point de terminaison spécifique.
- Ainsi, pour s’authentifier avec notre API, il envoie un header (en-tête)
AuthorizationBearerplus le token. - Si le jeton contient
foobar, le contenu de l’en-têteAuthorizationserait :Bearer foobar.
Le module OAuth2PasswordBearer de FastAPI
FastAPI fournit plusieurs outils, à différents niveaux d’abstraction, pour implémenter les fonctionnalités de sécurité que nous n’avons pas encore déployées.
Dans cet exemple, nous allons utiliser OAuth2, avec un password, en utilisant un token Bearer. Nous allons le faire à l’aide de la classe OAuth2PasswordBearer qui nous a été fournie par FastAPI. Pour information, bien qu’un token Bearer ne soit pas l’unique option mise à notre disposition, dans la plupart des cas d’utilisation, il répondra parfaitement à nos besoins.
Lorsque nous créons une instance de la classe OAuth2PasswordBearer, nous passons le paramètre tokenUrl. Ce paramètre contient l’URL que le client (l’interface s’exécutant dans le navigateur de l’utilisateur) utilisera pour envoyer le username et password afin d’obtenir un token (jeton).
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
return {"token": token}
Ici, tokenUrl="token" fait référence à une URL relative au token et que nous n’avons pas encore créée. Comme il s’agit d’une URL relative, elle équivaut à /token.
Étant donné que nous utilisons une URL relative, si votre API était située à https://example.com/, elle ferait référence à https://example.com/token. Mais si votre API était située à https://example.com/api/v1/, alors elle ferait référence à https://example.com/api/v1/token.
Toutefois, ce paramètre ne déclenche pas l’opération de création d’un chemin avec comme point de terminaison /, mais déclare que l’URL /token sera celle que le client devra utiliser pour obtenir son token. De plus, ces informations sont utilisées dans OpenAPI et dans le système de documentation interactive de l’API.
Vous noterez aussi que le nom du paramètre tokenUrl est écrit en CamelCase alors qu’en python, nous nous serions plutôt attendus à du snake_case ce qui aurait dû donner : token_url. C’est parce que le style standard imposé par la spécification OpenAPI pour les paramètres est le CamelCase.
La variable oauth2_scheme qui nous avons créé est une instance de OAuth2PasswordBearer, mais c’est aussi un callable qui peut être appelé ainsi : oauth2_scheme(some, parameters) et qui peut être utilisé avec Depends.
Dans l’exemple précédent, oauth2_scheme a été utilisé comme étant une dépendance d’une requête GET à l’aide de Depends:
async def read_items(token: str = Depends(oauth2_scheme)):
Cette dépendance fournira un str qui sera affecté au paramètre token de la fonction d’opération de chemin.
En complément, FastAPI saura qu’il peut utiliser cette dépendance pour définir un “schéma de sécurité” dans le schéma OpenAPI (et les documentations automatiques de l’API).
Détails techniques : FastAPI sait qu’il peut utiliser la classe OAuth2PasswordBearer (déclarée dans une dépendance) pour définir le schéma de sécurité dans OpenAPI, car il hérite de fastapi.security.oauth2.OAuth2, qui à son tour hérite de fastapi.security.base.SecurityBase. Tous les outils de sécurité qui s’intègrent à OpenAPI héritent de SecurityBase, c’est ainsi que FastAPI sait comment les intégrer dans OpenAPI.
Le fonctionnement en détail
Après avoir reçu une requête, FastAPI ira chercher dans l’en-tête Authorization la valeur de Bearer et la renverra sous forme de chaine de caractère. S’il ne voit pas d’en-tête Authorization ou si la valeur de Bearer n’a pas de token, il répondra directement avec un code erreur 401 (UNAUTHORIZED). Vous n’avez pas besoin de vérifier vous-même si le token existe pour renvoyer une erreur. Vous pouvez être sûr que si votre fonction est exécutée, c’est qu’un str aura été attribué au paramètre token.
Voici ce que cela donne via la documentation interactive :
Nous ne vérifions pas encore la validité du token, mais c’est déjà un début.
Créer un modèle utilisateur
Maintenant que nous avons mis en place le flux OAuth2, il ne nous reste plus qu’à associer le token que nous transportons avec un utilisateur. Tout d’abord, créons un modèle d’utilisateur Pydantic.
from pydantic import BaseModel
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
Créer une dépendance get_current_user
Dans un premier temps, il nous faut décoder le token pour obtenir l’utilisateur. Cela se fera à l’aide d’une fonction get_current_user.
Rappelez-vous que les dépendances peuvent avoir des sous-dépendances. Ainsi, get_current_user aura une dépendance avec le même oauth2_scheme que nous avons créé auparavant.
De la même manière que nous le faisions précédemment dans l’opération de chemin directement, notre nouvelle dépendance get_current_user recevra un token en tant que str de la sous-dépendance oauth2_scheme.
Ainsi, pour obtenir le détail de l’utilisateur courant, il nous suffira d’utiliser la dépendance get_current_user au lieu de oauth2_scheme.
Enfin, get_current_user utilisera une (fausse) fonction utilitaire que nous avons créé, qui prend un token en tant que str et renvoie notre modèle Pydantic User :
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com", full_name="John Doe")
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
return user
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
Notez que nous déclarons le type de current_user comme modèle Pydantic User. Vous vous souvenez peut-être que les corps des requêtes sont également déclarés avec des modèles Pydantic. Toutefois, FastAPI ne fera pas la confusion, car nous utilisons Depends. La façon dont ce système de dépendances est conçu nous permet d’avoir différentes dépendances qui renvoient toutes un modèle User. Nous ne sommes pas limités à n’avoir qu’une seule dépendance pouvant renvoyer ce type de données. Vous pouvez maintenant obtenir l’utilisateur actuel directement dans les fonctions d’opération de chemin et gérer les mécanismes de sécurité au niveau de l’injection de dépendance, à l’aide de Depends. Et vous pouvez utiliser n’importe quel modèle ou donnée pour les exigences de sécurité (dans notre cas, un modèle Pydantic User).
Mais vous n’êtes pas limité à l’utilisation d’un modèle de données, d’une classe ou d’un type spécifique. Vous voulez avoir un id, un email et ne pas en avoir username dans votre modèle ? Aucun problème, vous pouvez utiliser ces mêmes outils. Vous voulez juste avoir un str ? Ou juste un dict ? Ou une instance de modèle de classe de base de données directement ? Tout fonctionne de la même manière. En fait, vous n’avez pas d’utilisateurs qui se connectent à votre application, mais des robots, des bots ou d’autres systèmes, qui n’ont qu’un jeton d’accès ? Encore une fois, tout fonctionne de la même manière.
Utilisez simplement n’importe quel type de modèle, n’importe quel type de classe, n’importe quel type de base de données dont vous avez besoin pour votre application. FastAPI couvrira votre besoin grâce à son système d’injection de dépendance.
Voici le code complet de ce que nous venons d’étudier :
from typing import Optional
from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
def fake_decode_token(token):
return User(username=token + "fakedecoded", email="john@example.com", full_name="John Doe")
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
return user
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
Cet exemple peut sembler verbeux. Gardez à l’esprit que nous mélangeons la sécurité, les fonctions utilitaires des modèles de données et les opérations de chemin dans le même fichier. Les éléments d’injection de sécurité et de dépendance sont écrits une seule et unique fois. Vous pouvez les rendre aussi complexes que vous le souhaitez et vous pouvez avoir des milliers de points de terminaison ( opérations de chemin ) en utilisant le même système de sécurité. Et tous (ou toute partie d’entre eux que vous souhaitez) peuvent tirer parti de la réutilisation de ces dépendances ou de toute autre dépendance que vous créez.
Et l’appel de toutes ces opérations de chemin peut être résumé à ces simples 3 lignes :
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
OAuth2 simple avec username et password
Maintenant, ajoutons les pièces manquantes pour avoir un flux de sécurité complet.
Obtenir le username et password
Nous allons utiliser les outils de sécurité fournis par FastAPI pour obtenir le username et password d’un utilisateur.
OAuth2 spécifie que lors de l’utilisation du “flux avec mot de passe” (que nous utilisons), le client/utilisateur doit envoyer un username et un password en tant que données de formulaire.
Attention, la spécification OAuth2 nous impose de nommer l’utilisateur username et son mot de passe password, donc user-name ou email ne fonctionneront pas. Mais ne vous inquiétez pas, cela ne vous empêche pas de le montrer comme vous le souhaitez à vos utilisateurs finaux dans le frontend. Et vos modèles de base de données peuvent utiliser n’importe quel autre nom. Toutefois, pour l’étape d’authentification, nous devons utiliser ces noms pour être compatibles avec la spécification (et pouvoir, par exemple, utiliser le système de documentation automatique qui est intégré à l’API).
La spécification indique également que le username et password doivent être envoyés en tant que données de formulaire (donc, pas de JSON ici).
scope
La spécification indique également que le client peut envoyer un autre champ de formulaire nommé scope. Bien que le nom du champ de formulaire soit scope (au singulier), mais il s’agit en fait d’une longue chaîne avec des “portées” séparées par des espaces. Chaque “portée” est juste une chaîne (sans espaces).
Ils sont normalement utilisés pour déclarer des autorisations de sécurité spécifiques, par exemple :
users:readouusers:writesont des exemples courants.instagram_basicest utilisé par Facebook / Instagram.https://www.googleapis.com/auth/driveest utilisé par Google.
Dans OAuth2, une “portée” est seulement une chaîne qui déclare une autorisation spécifique requise. Peu importe s’il a d’autres caractères comme : ou s’il s’agit d’une URL. Ces détails sont spécifiques à la mise en œuvre. Pour OAuth2, ce ne sont que des chaînes.
Code pour obtenir le username et password
Utilisons maintenant les outils fournis par FastAPI pour obtenir le username et le password.
OAuth2PasswordRequestForm
Tout d’abord, importez OAuth2PasswordRequestForm et utilisez-le comme dépendance avec Depends avec comme point de terminaison /token :
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
OAuth2PasswordRequestForm est une dépendance de classe qui déclare un corps de formulaire avec :
- Le
username; - Le
password; - Un champ optionnel
scopesous forme de grande chaîne, composé de chaînes séparées par des espaces ; - Un champ optionnel
grant_type; - Un champ optionnel
client_id(nous n’en avons pas besoin pour notre exemple) ; - Un champ optionnel
client_secret(nous n’en avons pas besoin pour notre exemple).
Attention, la spécification OAuth2 nécessite en fait un champ grant_type avec une valeur à taille fixe pour le password, mais OAuth2PasswordRequestForm ne l’applique pas. Si vous devez l’appliquer, utilisezOAuth2PasswordRequestFormStrict à la place de OAuth2PasswordRequestForm.
Ensuite, nous récupérons les données utilisateur de la (fausse) base de données, en utilisant le champ username du formulaire. Si l’utilisateur n’existe pas, nous renvoyons une erreur disant “nom d’utilisateur ou mot de passe incorrect”. Pour générer l’erreur, nous utilisons l’exception HTTPException.
Vérifiez le mot de passe
À ce stade, nous avons les données utilisateur de notre base de données, mais nous n’avons pas vérifié le mot de passe. Mettons d’abord ces données dans le modèle Pydantic UserInDB.
Vous ne devez jamais enregistrer de mots de passe en clair, nous utiliserons donc le (faux) système de hachage de mot de passe. Si les mots de passe ne correspondent pas, nous renvoyons la même erreur que précédemment : “nom d’utilisateur ou mot de passe incorrect”. Hashing signifie : convertir du contenu (un mot de passe dans ce cas) en une séquence d’octets (juste une chaîne) qui ressemble à du charabia. Chaque fois que vous passez exactement le même contenu (exactement le même mot de passe), vous obtenez exactement le même charabia. Mais vous ne pouvez pas convertir le charabia en mot de passe.
Le hashage de mot de passe, une étape indispensable
Si votre base de données est volée, le voleur n’aura pas les mots de passe en clair de vos utilisateurs, seulement les hachages. Ainsi, le voleur ne pourra pas essayer d’utiliser ces mêmes mots de passe dans un autre système (comme de nombreux utilisateurs utilisent le même mot de passe-partout, ce serait dangereux).
Pour finir, la réponse du point de terminaison doit être un objet JSON contenant le token. Il devrait y avoir un token_type. Dans notre cas, comme nous utilisons des tokens Bearer, le type du token doit être bearer. Et il devrait avoir un access_token, avec une chaîne contenant notre token d’accès.
Pour cet exemple simple, nous allons être complètement non sécurisés et renvoyer le même username que le token.
Mettre à jour les dépendances
Nous allons maintenant mettre à jour nos dépendances. Nous voulons obtenir le current_user seul si cet utilisateur est actif. Ainsi, nous créons une dépendance supplémentaire get_current_active_user qui à son tour utilise get_current_user comme dépendance.
Ces deux dépendances renverront simplement une erreur HTTP si l’utilisateur n’existe pas ou s’il est inactif. Ainsi, dans notre point de terminaison, nous n’obtiendrons un utilisateur que si l’utilisateur existe, a été correctement authentifié et est actif.
Voici le code complet avec la mise à jour des dépendances :
from typing import Optional
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "fakehashedsecret",
"disabled": False,
},
"alice": {
"username": "alice",
"full_name": "Alice Wonderson",
"email": "alice@example.com",
"hashed_password": "fakehashedsecret2",
"disabled": True,
},
}
app = FastAPI()
def fake_hash_password(password: str):
return "fakehashed" + password
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: Optional[str] = None
full_name: Optional[str] = None
disabled: Optional[bool] = None
class UserInDB(User):
hashed_password: str
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def fake_decode_token(token):
# This doesn't provide any security at all
# Check the next version
user = get_user(fake_users_db, token)
return user
async def get_current_user(token: str = Depends(oauth2_scheme)):
user = fake_decode_token(token)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_active_user(current_user: User = Depends(get_current_user)):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@app.post("/token")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
user_dict = fake_users_db.get(form_data.username)
if not user_dict:
raise HTTPException(status_code=400, detail="Incorrect username or password")
user = UserInDB(**user_dict)
hashed_password = fake_hash_password(form_data.password)
if not hashed_password == user.hashed_password:
raise HTTPException(status_code=400, detail="Incorrect username or password")
return {"access_token": user.username, "token_type": "bearer"}
@app.get("/users/me")
async def read_users_me(current_user: User = Depends(get_current_active_user)):
return current_user
Test en réel
Ouvrir la documentation interactive : http://127.0.0.1:8000/docs.
Cliquer sur le bouton “Authorize”.
Utiliser les identifiants :
User: johndoe
Password: secret
Après avoir été authentifié dans le système, vous verrez le message suivant :
Utilisez maintenant une requête GET avec le chemin /users/me.
Vous obtiendrez les données de votre utilisateur tel que :
{
"username": "johndoe",
"email": "johndoe@example.com",
"full_name": "John Doe",
"disabled": false,
"hashed_password": "fakehashedsecret"
}
Maintenant, si vous cliquez sur l’icône de verrouillage et que vous vous déconnectez, puis si vous réessayez la même opération, vous obtiendrez l’erreur HTTP 401 suivante :
{
"detail": "Not authenticated"
}
Essayez maintenant avec un utilisateur inactif, authentifiez-vous avec :
User: alice
Password: secret2
Et essayez d’utiliser une requête GET avec le chemin /users/me.
Vous obtiendrez l’erreur “utilisateur inactif” :
{
"detail": "Inactive user"
}
Le mot de la fin
Vous disposez désormais des outils nécessaires pour mettre en place un système de sécurité complet basé sur un username et un password pour votre API.
Grâce à ces outils, vous pouvez rendre le système de sécurité compatible avec n’importe quelle base de données et avec n’importe quel utilisateur ou modèle de données. Le seul détail manquant est qu’il n’est pas encore réellement “sécurisé”.