OpenId Connect (OIDC pour les intimes) est un standard d'identification qui vient se positionner au dessus d'OAuth 2.0. Je vous propose de découvrir les mécanismes de base de ce protocole et les avantages qu'il apporte à vos applications.
Le premier d'entre eux est le principe de délégation de l'authentification des utilisateurs : avec OpenId Connect, cette responsabilité est confiée à un service indépendant et sort complètement du périmètre de l'application. Celle-ci utilise simplement le protocole qui lui permet de s'assurer que l'utilisateur est authentifié, mais quels moyens ont été utilisés pour l'identification ou bien la façon dont sont stockés les comptes ne sont pas de son ressort. Donc exit le développement des formulaires de login et l'intégration au LDAP dans votre code, première bonne nouvelle. Pour aller plus loin, c'est également le service OIDC qui est en charge d'imposer ou de proposer les niveaux de sécurité, avec par exemple l'utilisation d'un mode deux facteurs, du SMS OTP, un formulaire de login de base, un capteur biométrique, etc.
Comme ce service est indépendant de l'application, il peut de facto être transverse et faciliter le principe d'identification unique dans un système d'information (SSO). En conjugant ce point avec le précédent, charge au service OIDC de procéder à l'intégration de protocoles plus complexes à mettre en oeuvre comme Kerberos.
Conçu sur la base des services web, OpenId Connect a sa place tant sur Internet que dans l'informatique d'entreprise.
JWT
Je vous propose de commencer cette introduction dans le désordre et d'exposer dès maintenant la finalité : le sésame produit.
Une fois l'authentification effectuée, le principe de base est que l'application soit capable de façon autonome de faire confiance à une requête soumise. Cette partie repose sur un autre standard : JSON Web Token, nom de code RFC 7519. Un JWT est un JSON accompagné d'une signature et de la référence à la clef qui permet de vérifier la signature. Le tout est encodé en Base64 et les 3 parties sont séparées par des points. Elles sont assemblées comme suit : la référence à la clef, le JSON et ensuite la signature.
(Cliquer sur l'image pour l'agrandir)
JWT spécifie un ensemble de champs normalisés qu'un jeton doit contenir (domaine de sécurité, date d'émission, date d'expiration, etc.), mais c'est un standard extensible qui permet d'ajouter des champs personnalisés. Dans l'exemple précédent, un access token généré par un serveur Keycloak est analysé en ligne par le site jwt.io, on peut voir que le jeton porte des informations concernant l'utilisateur (nom, prénom, mail, etc.) mais également les rôles. De plus, la signature permet aux applications de faire confiance au JWT car elle ne peut être forgée que grâce à la clef privée correspondante qui est détenue par le service OIDC et elle permet de s'assurer que le JSON n'a pas été modifié depuis la signature. Cela signifie également que l'application est en capacité de connaître le compte applicatif correspondant à une requête sans avoir à interroger un autre service externe. JWT permet ainsi d'allier scalabilité et sécurité dans la propagation de l'identité de service en service.
Notez qu'un JWT porte une date d'expiration, ce qui signifie qu'il sera invalide à cette date, qu'il ait été utilisé ou non ; contrairement à une session d'application web qui expire habituellement après une durée d'inactivité (session timeout).
Maintenant que nous avons présenté JWT, revenons à OpenID Connect.
Initialisation d'OIDC
La première tâche qu'une application doit accomplir est la découverte de la configuration du service OIDC. Pour cela, le standard prévoit que le fournisseur doive exposer un service web qui donne l'ensemble de la configuration d'OpenId Connect, .well-known/openid-configuration
. Voici par exemple la configuration du service OpenId Connect de Google : https://accounts.google.com/.well-known/openid-configuration :
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://www.googleapis.com/oauth2/v4/token",
"userinfo_endpoint": "https://www.googleapis.com/oauth2/v3/userinfo",
"revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"response_types_supported": [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
"none"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"email",
"profile"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"client_secret_basic"
],
"claims_supported": [
"aud",
"email",
"email_verified",
"exp",
"family_name",
"given_name",
"iat",
"iss",
"locale",
"name",
"picture",
"sub"
],
"code_challenge_methods_supported": [
"plain",
"S256"
]
}
Nous ne rentrerons pas dans le détail de toute la configuration, focalisons-nous simplement sur l'essentiel.
Nous avons expliqué précédemment que le JWT comprenait une référence vers une clef, donc il est nécessaire de connaître l'ensemble des clefs qui peuvent être utilisées pour valider la signature des jetons et cette information est portée par le champ jwks_uri
. JWK (JSON Web Key) est un standard qui détaille la façon de décrire des clefs de chiffrement dans un contexte JSON. Si vous naviguez vers l'URL produite dans ce champs de la configuration, voici un exemple de ce que vous pouvez trouver :
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "4f71eaa1927ae6b1faf4521930bc0d4544e141fb",
"n": "1WOYoHRxv3VloUBI9QhIUBXve35OiyH-hgCmCHq4pljj5uOnK-wZsE53SjarvHYHpoVDUldy-pn-Ec13nq1AqCzwgbYBKSEh6RVmznq5U3m2_Zo3ETjODGhMCTV1KDPGFuOsumrcxr4pzo0AWLztgTCjSJXwalK8w0oNkF55HGLTq8LJ9RAPrSj4X9_hHKQgQ4Z0s4hHy2ZSKLJTFCI8DZBERYk6NcuZQ2Wsez6a9juP8a7_AZ8I3zCNQs6mcSYw2DsPDj6D3du66--omNiKsuRqQaj739v8waPR71ahNYEihpEi5iKGOuG1RZMkOyPhBSqjaBupojy7VagKnKNhYQ",
"e": "AQAB"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "eae56bf96b2e15d4c895b6d59b429c16bffaff4a",
"n": "p_edvCmKzp3IaOV52ofpntZFKift04BwrdmS-ajGFQIYn6nSuyUSs6g5Hz7Xe3o2NKVpwS5VGK6msRqXjnAuz1RRlk6AQwYHHDJjfaHCV70_KVipfnhdrzjWtmrhI3hUSqzq7McCJakexiatT6mNGK4CICVquJX3X2-bga4uOanRMq0b2aMz5VmIUG5zBAuQNAzAOURqU_g58WGFrAU8l3kRKwyw5vKIp-l9bZ73zYAvf-Z21JKF7bJIfV6K7nzcBv4F3mC60l-Rfis-wmiUDQr37lHlEcwZG6CESQeo0j43i1fYD4a1hvprZMXYvw3Xl2UqIgV3TtsEPmTJJZixjw",
"e": "AQAB"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "347909f8a798196057ac66dfba5c26cc7757184d",
"n": "k9nYHNLTq99qapEXET9yKZ710GSPNbqdVqGLasSk56K4D8DEbarBUDLUT077fS9h2GszKARqN4njPsgPqAn5p4szCGMCT2Pi5oqjlhcYOMLtQabMwq4c12sQWowk4ThBYgOSIXAG1i9mVuUf0knuQ28ko9OadALaSm8IrPlcLdF2TMnR3zAeiVKOzQmJGD5veoYtFLZthWUZ2N773Yo0Bx0mIsGHM9Tn0Dko9Z02ygcmROF4HqtxhOtbbZHsmtAhpp4ED3c2R-eXsoHwMboj8A_yVSWeuoGrUTYW_01PMllUf9xVuGbgI7UbkyeMBn6KEiAUMRJ1hsbR4IZ9rRLx4Q",
"e": "AQAB"
},
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "7e3d8087655edd15c2e427b806ed91354ddde8d5",
"n": "r8uc9nSjwAKPfREpFaYvFtKXciA9Oa6Q4m01BGt6nshCWBb752J6ec13ne939ML_njv5B2Bd2kPgFj0ams9_79TxHpto6Za_Hfp-qg9mr5YGVWFug2hiT8nc5DFVaMwDcWdonJjODANBBqYa6GOmsohQsWIVjsfsJ5jBWKALykUjb3MhWEVeGzFBbw6sqdIe7Q_mVm5hm2qbO-eKKX7AGgHKytc7LQKpVwcRSr8PqL2kXnrKuf1GEzyaWsOKmRNaRfCMA4pQeC9J-wqLP85glnCnUMOXZ-Zy1IdfgxWB6Rad9iUXkA_9Ejm8Qp1Ydh0q_WRj_TsGTy2e9TdFmdsLKQ",
"e": "AQAB"
}
]
}
Grâce à ces informations, les clients pourront reconstruire le référentiel de clefs publiques utilisables pour valider les signatures. Attention, ce jeu de clef n'est pas fixe dans le temps (principe de rotation) et si un jeton se présente avec un identifiant de clef (kid
) inconnu, alors il est sûrement temps de reconsulter le JWKS pour mettre à jour le jeu.
L'application collecte également l'URL vers laquelle rediriger les utilisateurs non connectés, authorization_endpoint
, et le service web qui délivre les jetons, token_endpoint
.
L'accès à ces informations est public.
Séquence d'authentification
L'application semble maintenant capable de qualifier l'identité de l'émetteur d'une requête mais il reste un point essentiel : procéder à l'authentification de l'utilisateur et prélever le jeton. Nous allons détailler cette séquence dans le cadre d'une application web. Elle se déroule grossièrement comme un coup de billard en trois bandes :
- L'application reçoit une requête sans token ou avec un token invalide (expiré), elle redirige l'utilisateur vers l'URL d'authentification du service OpenId Connect.
- Le service OIDC procède à l'identification de l'utilisateur. Dans la plupart des cas, cela va se résumer à afficher une mire de login, cependant tous les rafinements en la matière sont envisageables : authentifications par deux facteurs (OTP, clef USB de sécurité), biométrie, Kerberos si l'utilisateur est dans un domaine AD, etc.
- Si l'identification se déroule sans accroc, le service OIDC fournit un code et redirige l'utilisateur vers l'application.
- L'application présente ce code au service OIDC pour obtenir un jeton au nom de l'utilisateur qui s'est connecté.
- L'application fournit ce token au client qui peut maintenant le présenter dans les prochains appels.
Ce sont les grandes lignes, mais attardons-nous un peu sur les détails :
(Cliquer sur l'image pour l'agrandir)
Notons que dans ce schéma, si l'application doit être connue du service d'authentification grâce à un couple client_id
+ client_secret
, l'URI de redirection doit également être référencée dans le service OpenId Connect pour des raisons de sécurité. Cette URI de redirection est une callback qui est envoyée à l'utilisateur si l'identification s'est bien passée. Dans l'étape 2
, seul le client_id
est découvert.
Le callback fourni par l'application à l'étape 2
est accompagné d'un état, qui représente l'état que l'application voudra restaurer lorsque l'utilisateur se sera identifié. Usuellement, il s'agit d'une URL de l'application, mais pas forcément.
A l'étape 12
, le service OIDC délivre non pas un JWT mais jusqu'à 3 jetons :
id_token
: porte l'identification de l'utilisateur.access_token
: utilisé pour interagir avec les API.refresh_token
: lorsqu'unaccess token
est arrivé à expiration, il est possible d'en redemander un nouveau en présentant unrefresh_token
. Evidemment ce dernier a une durée de vie plus importante et son utilisation permet de ne pas redemander à l'utilisateur de se réauthentifier. Notez que les services OpenId Connect ne proposent pas tous un service de renouvellement des jetons.
Vous remarquerez que le service OIDC ne délivre pas directement de jetons au client mais qu'il fournit un code qui permet de réclamer les jetons. Ce code a une durée de validité très courte et c'est ensuite à l'application de récupérer les JWT (étape 9
) en présentant le client_id
, le client_secret
, le code et la redirect_uri
. Cela permet à l'application de décider ce qu'elle va faire de ces jetons : elle peut tous les transmettre directement au client ou bien décider de ne transmettre que l'id_token
et l'access_token
et de conserver privé le refresh_token
. Il y a effectivement débat concernant ce dernier car si le jeton d'accès est compromis, il n'offre à un assaillant un sésame qui ne sera valide que durant la durée de vie du JWT et ses appels seront rejetés ensuite ; en revanche si le jeton de renouvellement est intercepté, alors l'assaillant pourra interroger l'application pendant la durée de vie de l'access_token
, puis le renouveler pour continuer à consommer les services de l'application.
Notez également qu'en identifiant de cette façon les utilisateurs, les crédentiels ne transitent jamais par l'application, ce qui présente plusieurs avantages :
- Lorsque vous utilisez une application en ligne qui propose de vous authentifier avec votre compte Google, votre mot de passe n'est pas transmis à un tiers auquel vous ne faites pas forcément confiance.
- Cela permet également de réduire la surface d'attaque de vos applications puisque l'identification de vos utilisateurs est concentrée en un seul point spécialisé et sécurisé, comparé à une implémentation de l'authentification propre à chacune de vos applications.
Cet article vous a présenté l'utilisation d'OpenId Connect dans une configuration, mais ce n'est évidemment pas la seule et le standard propose bien plus d'options et de raffinements.
Au Sujet de l'Auteur
Brice Leporini est un développeur sénior qui totalise plus de 20 ans d’expérience sur différentes technologies dont une bonne partie focalisée sur l’ecosystème Java et les architectures n-tiers. Freelance depuis 2006, son activité actuelle oscille entre le coaching technique d’équipes de jeunes geeks, les travaux d’amélioration de performance, les études préalables et le développement (of course!). Retrouvez-le sur Twitter à @blep.