Aller au contenu
English

OAuth et OIDC

  • Leçon 4
  • 50 min
  • Module 2 : Comprendre la connexion et les jetons

Ce que vous saurez faire

Confiez la vérification de l’identité à un service distinct. Ainsi, l’API de billets de soutien ne voit jamais de mot de passe. Découvrez les deux normes qui permettent cela : OAuth pour accéder à une API, et OpenID Connect pour se connecter. Lancez ensuite le fournisseur d’identité d’exercice du cours.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet après le module 1, dans ~/projects/ticket-api : python -m pytest affiche 154 passed (154 tests réussis).
  • PostgreSQL et Azurite en marche, avec les données des deux commerces chargées (python -m scripts.load).
  • Le dépôt d’exercices à côté du projet, dans ~/projects/nextia-learning-labs. Exécutez-y git pull pour obtenir les derniers instantanés.

Cette leçon ajoute le dossier idp/ et un paquet Python. Vous n’avez besoin ni de compte ni d’argent. Le fournisseur d’identité fonctionne sur votre ordinateur, et toutes les personnes qu’il contient sont fictives. Testé sur macOS (Apple silicon); les commandes Windows sont écrites, mais non testées.

Vous arrivez à un hôtel. À la réception, vous montrez votre passeport une seule fois. La personne à la réception le vérifie et vous donne une carte-clé en plastique. La carte ouvre la chambre 214 et la salle d’entraînement. Elle n’ouvre pas les autres chambres et cesse de fonctionner quand vous quittez l’hôtel.

La porte de la chambre 214 ne voit jamais votre passeport. Elle vérifie seulement la carte : vient-elle de cet hôtel, est-elle destinée à cette porte, et est-elle encore valide? Si vous perdez la carte, l’hôtel vous en donne une nouvelle, et votre passeport reste en sécurité.

Trois éléments rendent cela possible. Un seul endroit vérifie votre identité. Il vous donne un petit objet aux pouvoirs limités. Chaque porte vérifie cet objet, pas vos pièces d’identité.

Le problème : une clé qui ne dit rien et des mots de passe partout

Section intitulée « Le problème : une clé qui ne dit rien et des mots de passe partout »

Dans le module 1, l’API de billets avait une seule clé d’API partagée. Toutes les requêtes se ressemblaient, alors l’API ne pouvait pas distinguer Sam d’Ines. Les tentatives fictives dans Exemples de menaces se terminaient ainsi :

5 of 5 attempts got what they should not get.

La solution évidente est un formulaire de connexion dans l’API : demander un mot de passe à chaque personne et le vérifier. Kwame, l’architecte de solutions, énumère les coûts avant que quelqu’un commence à l’écrire :

  • L’API doit stocker un hachage de mot de passe pour chaque personne des deux commerces. Une fuite de cette table expose tous les comptes.
  • L’API doit prévoir la réinitialisation des mots de passe, un second facteur, le verrouillage après plusieurs échecs et la vérification des courriels. Chacune de ces fonctions de sécurité est facile à mal réaliser.
  • Chaque application qui appelle l’API doit demander le mot de passe : la page de soutien, un outil en ligne de commande, une future application mobile. Chacune voit le mot de passe et peut le divulguer.
  • Bramble Books a déjà ses propres comptes pour le personnel. Sa propriétaire, Ines, ne veut pas d’un deuxième mot de passe pour un outil de plus.

« Notre API doit répondre à trois questions pour chaque requête, dit Kwame. Elle ne devrait pas du tout gérer les mots de passe. »

L’idée : un fournisseur d’identité et des jetons

Section intitulée « L’idée : un fournisseur d’identité et des jetons »

Un fournisseur d’identité est la réception de l’hôtel. C’est un service distinct qui a une seule tâche : vérifier l’identité d’une personne. Il peut utiliser un mot de passe, un second facteur ou le compte du personnel du commerce. Après la vérification, il donne à l’application un jeton (token). Un jeton est un court texte qui indique qui s’est connecté, pour quelle API et jusqu’à quand. L’API vérifie le jeton, jamais le mot de passe.

Deux normes ouvertes décrivent ce fonctionnement. Votre API, le fournisseur et les applications les suivent. Vous pouvez donc changer de fournisseur plus tard sans modifier l’API.

OAuth 2.0 est une norme d’autorisation déléguée. « Déléguée » signifie qu’une personne permet à une application d’agir en son nom, avec des limites. L’application reçoit un jeton d’accès : la carte-clé. Le jeton d’accès vise une seule API, pour une courte durée et un ensemble limité d’actions. Le nom de l’API dans le jeton est son audience. Les actions limitées sont ses portées, par exemple tickets:read. OAuth ne dit pas qui est la personne. Il indique ce que l’application peut faire auprès de l’API.

OpenID Connect (OIDC) est une mince couche ajoutée à OAuth pour permettre la connexion. Avec OIDC, le fournisseur donne aussi à l’application un jeton d’identité. Il indique à l’application qui s’est connecté, par exemple « c’est usr-sam, connecté à 6 h 10 ». Le jeton d’identité est destiné à l’application, pas à l’API. Le nom vient d’OpenID, une ancienne norme de connexion, maintenant « connectée » à OAuth.

Chaque jeton a donc un seul destinataire. Le jeton d’identité est une note à l’application : « voici qui s’est connecté ». Le jeton d’accès est une carte-clé pour l’API : « cette application peut lire les billets de cette personne ». Ce que signifie un jeton montre pourquoi envoyer le mauvais jeton à l’API est une véritable erreur.

Voici les termes de l’hôtel et ceux d’OAuth côte à côte :

À l’hôtel Dans OAuth et OIDC Dans ce cours
La réception vérifie votre passeport Le fournisseur d’identité authentifie la personne Le fournisseur d’exercice à http://localhost:8400
Vous, la personne hébergée La personne (OAuth l’appelle le resource owner, propriétaire de la ressource) Sam, Ines, Camille…
L’application de réservation sur votre téléphone L’application (OAuth l’appelle le client) La page de soutien, un outil en ligne de commande
La carte-clé Le jeton d’accès Un jeton signé pour ticket-api
« Chambre 214 et salle d’entraînement » L’audience et les portées ticket-api; tickets:read tickets:write
La fiche d’arrivée avec votre nom Le jeton d’identité Un jeton signé pour l’application
La porte L’API (OAuth l’appelle le resource server, serveur de ressources) ticket-api
Un schéma avec quatre cases : Sam dans le navigateur, l'application de soutien, le fournisseur d'identité à http://localhost:8400 et ticket-api. Sam utilise l'application. Flèche 1 : Sam se connecte uniquement auprès du fournisseur d'identité, avec un mot de passe et un second facteur. Flèche 2 : le fournisseur donne à l'application un jeton d'identité destiné à l'application et un jeton d'accès destiné à ticket-api. Flèche 3 : l'application envoie le jeton d'accès à ticket-api dans l'en-tête Authorization : Bearer. Flèche 4, pointillée : ticket-api récupère les clés publiques du fournisseur, sans mot de passe et sans personne.
Les trois parties d'une connexion et les deux jetons. Le mot de passe va uniquement au fournisseur d'identité.
Description textuelle du diagramme

Lisez les numéros dans l’ordre. Seule la flèche 1 transporte un mot de passe. Il va au fournisseur d’identité, jamais à l’application ni à l’API. La flèche 2 transporte deux jetons avec deux destinataires différents. La flèche 3 montre ce que l’API reçoit à chaque requête : le jeton d’accès, dans l’en-tête Authorization (autorisation), après le mot Bearer (porteur). La flèche 4 est le seul lien entre l’API et le fournisseur. L’API récupère les clés publiques pour vérifier les jetons.

« Mais si l’API ne voit jamais le mot de passe, demande Amira, comment sait-elle qu’un jeton n’est pas inventé? » Le fournisseur signe chaque jeton avec une clé que lui seul possède. L’API vérifie la signature avec la clé publiée par le fournisseur. C’est le sujet de Lire un jeton : JWT, signatures et clés. Pour l’instant, faites confiance à la carte parce que l’hôtel l’a créée.

Chaque fournisseur OIDC publie un petit document JSON à une adresse fixe : /.well-known/openid-configuration. On l’appelle le document de découverte, car une application y découvre tout le reste. Voici une partie du document du fournisseur d’exercice :

{
"issuer": "http://localhost:8400",
"authorization_endpoint": "http://localhost:8400/authorize",
"token_endpoint": "http://localhost:8400/token",
"jwks_uri": "http://localhost:8400/jwks.json",
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid", "profile", "email", "offline_access",
"tickets:read", "tickets:write", "members:manage"]
}

Chaque ligne répond à une question d’une application ou d’une API :

  • issuer (émetteur) est le nom du fournisseur. Chaque jeton qu’il signe porte ce nom, et l’API accepte uniquement les jetons de cet émetteur.
  • authorization_endpoint (point de terminaison d’autorisation) est la page de connexion : la réception. Le navigateur s’y rend.
  • token_endpoint (point de terminaison des jetons) est l’endroit où une application échange un code à usage unique contre des jetons. Flux d’application l’utilise.
  • jwks_uri (adresse des clés publiques) indique où se trouvent les clés publiques. L’API les lit pour vérifier les signatures.
  • scopes_supported (portées prises en charge) énumère ce qu’une application peut demander. openid signifie « je veux aussi un jeton d’identité » : ce seul mot transforme OAuth en OIDC. Les trois dernières sont les portées propres à l’API de billets.

Un vrai fournisseur, comme Keycloak ou Microsoft Entra ID, publie le même document avec ses propres adresses. C’est pourquoi l’API a besoin d’un seul paramètre principal pour trouver un fournisseur : son émetteur.

Larkfield : lancer le fournisseur d’identité d’exercice

Section intitulée « Larkfield : lancer le fournisseur d’identité d’exercice »

Le projet contient un petit fournisseur d’identité d’exercice dans idp/, soit environ 400 lignes de FastAPI. C’est un simulacre (mock) : sa page de connexion énumère les personnes fictives et ne demande aucun mot de passe. Tout ce qui suit la connexion respecte les normes : le document de découverte, les clés, les codes et les jetons signés. La signature utilise PyJWT et le paquet cryptography, deux bibliothèques maintenues. Le fournisseur n’utilise aucune cryptographie écrite à la main.

Deux décisions façonnent les jetons de ce cours. D’abord, les portées : tickets:read, tickets:write et members:manage. Un script de production de rapports peut demander seulement tickets:read. Ensuite, le jeton ne contient ni organisation ni rôle. Camille fait partie du personnel de Larkfield et a un accès en lecture seule chez Bramble Books. Si son jeton disait « personnel », l’API le croirait pour les deux commerces. L’API lit donc l’organisation dans le chemin de la requête et le rôle dans sa propre table d’adhésions. Le module 4 construit cette vérification.

  1. Dans ~/projects/ticket-api, avec l’environnement virtuel actif, copiez le fournisseur et les nouveaux fichiers de dépendances depuis l’instantané end-of-m02 :

    Fenêtre de terminal
    S=../nextia-learning-labs/C23/snapshots/end-of-m02
    cp -R "$S/idp" idp
    for f in requirements.txt requirements-run.txt requirements-lock.txt .gitignore; do cp "$S/$f" "$f"; done

    Les fichiers de dépendances ajoutent un paquet, PyJWT[crypto]==2.15.1 (licence MIT). Le fichier .gitignore ajoute .idp/ et .tokens/ : deux dossiers contenant des secrets que Git ne doit jamais voir.

  2. Installez le nouveau paquet :

    Fenêtre de terminal
    python -m pip install -r requirements.txt

    Attendez-vous à voir Successfully installed PyJWT-2.15.1 (PyJWT-2.15.1 installé avec succès) vers la fin.

  3. Configurez le fournisseur. Cette commande crée une clé de signature et les secrets de deux applications :

    Fenêtre de terminal
    python -m idp init
    Made a signing key (My9O_SxNpIg4Ff10) and the clients in /Users/you/projects/ticket-api/.idp.
    Wrote the client secrets of help-desk-web and export-worker to /Users/you/projects/ticket-api/.env.
    Wrote a new SESSION_KEY (the API encrypts its sessions' tokens with it).
    The provider keeps only their SHA-256. Never commit .env or .idp/.

    Votre identifiant de clé entre crochets est différent : init crée une nouvelle clé sur chaque ordinateur. Les noms de dossiers sont aussi propres à votre ordinateur.

  4. Démarrez le fournisseur. Il fonctionne jusqu’à ce que vous l’arrêtiez, alors gardez ce terminal ouvert :

    Fenêtre de terminal
    python -m idp
    Practice identity provider: http://localhost:8400 (made-up people, no passwords)
  5. Ouvrez un deuxième terminal, allez dans le dossier du projet et activez l’environnement virtuel. Demandez au fournisseur son document de découverte :

    Fenêtre de terminal
    curl -s http://localhost:8400/.well-known/openid-configuration | python -m json.tool

    Comparez-le au petit exemple ci-dessus. Il contient aussi userinfo_endpoint (point de terminaison des renseignements utilisateur), revocation_endpoint (point de terminaison de révocation) et end_session_endpoint (point de terminaison de fin de session), utilisés dans le module 3.

  6. Énumérez les personnes fictives :

    Fenêtre de terminal
    python -m idp users
    usr-grace Grace grace@larkfield.example
    usr-sam Sam sam@larkfield.example
    usr-omar Omar omar@larkfield.example
    usr-ines Ines ines@bramble.example
    usr-camille Camille camille@larkfield.example
    usr-tomas Tomás tomas@larkfield.example
    usr-kwame Kwame kwame@larkfield.example

    La première colonne est l’identifiant utilisateur que chaque jeton portera. C’est le même identifiant que dans la table users du module 1.

  7. Ouvrez la page de connexion dans votre navigateur. Une application construit cette adresse; pour l’instant, copiez-la :

    http://localhost:8400/authorize?response_type=code&client_id=ticket-cli&redirect_uri=http%3A%2F%2F127.0.0.1%3A8765%2Fcallback&scope=openid%20tickets%3Aread&state=practice&code_challenge=LZsOqzOw8X64BP0LymcAokLEhiwPdKsXzgRUwiuBNM8&code_challenge_method=S256

    Regardez la page, mais ne choisissez pas encore de personne. La prochaine leçon explique chaque partie de cette adresse.

La page de connexion du fournisseur d'exercice dans un navigateur. Le titre indique Sign in to Ticket command line (se connecter à Ticket command line). Un paragraphe indique qu'il s'agit du fournisseur d'identité d'exercice du cours, qu'il ne demande pas de mot de passe et qu'il faut choisir une personne fictive. La ligne suivante indique que l'application demande : openid tickets:read. En dessous, sept boutons affichent Grace, Sam, Omar, Ines, Camille, Tomás et Kwame, chacun avec une adresse courriel.
La page de connexion du fournisseur d'exercice. Un vrai fournisseur demande ici un mot de passe, et souvent un second facteur. L'application ne voit jamais ce que la personne saisit sur cette page. L’image est en anglais.

La page nomme l’application (« Ticket command line », outil de billets en ligne de commande) et les portées qu’elle demande. Un vrai fournisseur affiche les mêmes renseignements et demande souvent à la personne de donner son accord. C’est la partie « déléguée » d’OAuth : la personne voit ce que l’application pourra faire.

Quand utiliser un fournisseur d’identité, quand s’en passer et quels sont les coûts

Section intitulée « Quand utiliser un fournisseur d’identité, quand s’en passer et quels sont les coûts »

Utilisez un fournisseur d’identité dès que des personnes se connectent à votre API : personnel, clientèle ou les deux. Le fournisseur fait le travail difficile et risqué une seule fois, pour toutes les applications. Un fournisseur peut aussi faire confiance à un autre fournisseur. Le personnel de Bramble pourrait donc, plus tard, se connecter avec ses comptes de librairie. Le module 3 relie l’API à Keycloak, un fournisseur gratuit qui fonctionne dans Docker, et examine les paramètres de Microsoft Entra ID.

Vous n’en avez pas besoin pour un programme qui appelle un autre programme, sans personne entre les deux. La clé d’API de l’API de billets pour /v1/classify reste, comme dans Créer et déployer votre première API. Même là, un fournisseur peut aider : Flux d’application donne à un service en arrière-plan son propre jeton.

Les coûts sont réels. Le fournisseur est une nouvelle dépendance : s’il est en panne, personne ne peut se connecter. Vous devez le configurer avec soin. Une mauvaise adresse de redirection ou audience est une faille de sécurité. Un fournisseur hébergé peut coûter de l’argent pour chaque utilisateur. Enfin, le fournisseur d’exercice sert uniquement à apprendre : il n’a pas de mots de passe et garde ses codes en mémoire.

À vous de jouer : demandez une portée qui n’existe pas

Section intitulée « À vous de jouer : demandez une portée qui n’existe pas »

Changez un élément dans l’adresse de l’étape 7. Faites d’abord une prédiction : que répond le fournisseur si l’application demande aussi tickets:delete? Remplacez la partie des portées par scope=openid%20tickets%3Aread%20tickets%3Adelete, puis ouvrez l’adresse.

Afficher la réponse

Le fournisseur n’affiche aucune page de connexion. Il répond par une erreur :

{"error":"invalid_scope","error_description":"Scopes not allowed for this application: tickets:delete"}

Le fournisseur connaît les portées que chaque application inscrite peut demander. ticket-cli peut demander les trois portées de l’API de billets, et tickets:delete n’en fait pas partie. Une application ne peut donc pas s’accorder plus de pouvoirs en les demandant. La personne ne voit jamais de page qui les propose.

Erreur courante

Un deuxième fournisseur, ou un autre programme, sur le port 8400

Symptôme : python -m idp affiche sa première ligne et s’arrête aussitôt :

Practice identity provider: http://localhost:8400 (made-up people, no passwords)
ERROR: [Errno 48] error while attempting to bind on address ('127.0.0.1', 8400): address already in use

Le numéro est 48 sur macOS. Linux et Windows affichent d’autres numéros pour la même erreur.

Cause : un autre programme écoute déjà sur le port 8400. Souvent, c’est le fournisseur lui-même, qui tourne encore dans un terminal oublié.

Diagnostic : trouvez le programme qui utilise le port. Sur macOS et Linux : lsof -i :8400. Sur Windows : Get-NetTCPConnection -LocalPort 8400 affiche l’identifiant du processus dans OwningProcess (processus propriétaire).

Solution :

  1. Si c’est votre propre fournisseur, utilisez-le ou arrêtez-le avec Ctrl+C dans son terminal.
  2. Si un autre programme a besoin du port, changez celui du fournisseur. Définissez IDP_PORT=8401 et IDP_ISSUER=http://localhost:8401 dans .env.
  3. Ajoutez aussi OIDC_ISSUER=http://localhost:8401 à .env. Dans la prochaine leçon, scripts.login trouve le fournisseur grâce à ce paramètre. L’API le fait aussi, à partir de Ce que signifie un jeton. L’émetteur est le nom du fournisseur dans chaque jeton, et l’API refuse les jetons portant un autre nom.

Une erreur semblable au premier démarrage : The provider has no keys yet. Run: python -m idp init (le fournisseur n’a pas encore de clés; exécutez la commande indiquée). Exécutez init (initialiser) dans le dossier du projet, puis redémarrez le fournisseur.

Vérifiez votre compréhension

Sam se connecte à la page du service de soutien. Quel jeton indique à l'application de soutien qui s'est connecté? Quel jeton l'application envoie-t-elle à ticket-api? Pourquoi ne pas utiliser un seul jeton pour les deux?

Afficher la réponse

Le jeton d’identité indique à l’application qui s’est connecté; il fait partie d’OpenID Connect. L’application envoie le jeton d’accès à ticket-api; il fait partie d’OAuth. Un jeton servant aux deux aurait deux destinataires. L’audience d’un jeton désigne son unique destinataire. L’API peut ainsi refuser un jeton destiné à quelqu’un d’autre. Cela empêche un jeton pour une autre API, ou un jeton d’identité, de donner accès à cette API.

Vérifiez votre compréhension

Tomás dit : « OAuth est une norme de connexion, donc le jeton d'accès indique à l'application qui est la personne. » Qu'est-ce qui est faux?

Afficher la réponse

OAuth concerne l’accès : le jeton d’accès indique ce qu’une application peut faire auprès d’une API. Il est destiné à l’API, et l’application ne devrait pas le lire. La connexion relève d’OpenID Connect, une couche ajoutée à OAuth. La portée openid demande un jeton d’identité, destiné à l’application, qui indique qui s’est connecté.

Vérifiez votre compréhension

Pourquoi le jeton d'accès du cours ne contient-il ni organisation ni rôle, même si Camille a un rôle dans deux boutiques?

Afficher la réponse

Parce qu’un rôle appartient à une adhésion, pas à une personne. Camille fait partie du personnel de Larkfield et a un accès en lecture seule chez Bramble Books. Un jeton indiquant « personnel » lui donnerait aussi les droits du personnel chez Bramble Books. Le fournisseur ne connaît pas non plus les membres des boutiques. À chaque requête, l’API obtient donc l’organisation du chemin de la requête et le rôle de sa propre table d’adhésions.

Récapitulatif

  • Un fournisseur d’identité vérifie qui est une personne et émet des jetons. L’API ne voit jamais de mot de passe.
  • OAuth 2.0 donne à une application un jeton d’accès limité pour une API : la carte d’accès. Son audience désigne l’API; ses portées désignent les actions permises.
  • OpenID Connect ajoute la connexion : la portée openid demande un jeton d’identité, qui indique à l’application qui s’est connecté.
  • Le document de découverte, à /.well-known/openid-configuration, répertorie l’émetteur, la page de connexion, le point de terminaison des jetons et les clés.
  • Le fournisseur d’exercice s’exécute avec python -m idp init et python -m idp à l’adresse http://localhost:8400. Le jeton ne contient ni organisation ni rôle : l’API les lit dans ses propres tables.
  • Ensuite : connectez-vous étape par étape et découvrez pourquoi le code dans l’adresse du navigateur ne sert à rien à un voleur.

Leçon suivante : Flux d'application