OAuth2 et OpenID Connect pour les microservices et les applications publiques (Partie 2)
By WORMS David
20 nov. 2020
Ne ratez pas nos articles sur l'open source, le big data et les systèmes distribués, fréquence faible d’un email tous les deux mois.
En utilisant OAuth2 et OpenID Connect, il est important de comprendre comment se déroule le flux d’autorisation, qui appelle l’Authorization Server et comment stocker les tokens. De plus, les microservices et les applications clientes, telles que les applications mobiles et et SPA (Single Page Application), soulèvent la question de quels Flows (ou séquence) s’appliquent aux architectures OAuth2 modernes.
La partie 1, OAuth2 et OpenID Connect, une introduction douce et fonctionnelle se concentre sur l’intégration de votre première application avec un serveur OpenID Connect (Dex) et à la compréhension de l’Authorization Code Flow (pour flux d’autorisation par code) avec un server OAuth externe. Les stratégies Oauth et OpenID Connect sont compliquées et déroutantes, la lecture de cette première partie apportera de la lumière.
La partie 2, OAuth2 et OpenID Connect pour les microservices et les applications publiques, fournit une plongée approfondie dans la séquence OpenID Connect en décrivant, expliquant et illustrant chaque étape. Une fois terminés, vous pourrez les appliquer à vos applications clientes et publiques (mobile, SPA, …) sans avoir besoin d’outils supplémentaires.
Description du flux
Dans la partie 1 de l’article, nous avons utilisé Dex avec son application example pour se connecter avec un compte GitHub. Voici le déroulement de la séquence. Une application cliente nécessite que l’utilisateur soit authentifié. Depuis l’application cliente, l’utilisateur est redirigé vers le serveur d’autorisation. N’oubliez pas que le serveur d’autorisation est un serveur OpenID Connect qui est également un serveur OAuth2. Une fois authentifié, le consentement a lieu. L’utilisateur, nommé Resource Owner, autorise l’application à utiliser des ressources en son nom. L’utilisateur est redirigé vers l’application avec un code d’autorisation en direct. Le code d’autorisation est un code Nonce (No more than once, pas plus d’une fois). Il est échangé contre un token d’accès depuis l’application cliente.
Plongeons maintenant dans l’Authorization Code Flow.
Avant de commencer
Il est important d’avoir lu la partie 1 ou d’avoir une bonne compréhension d’OAuth ainsi qu’un serveur OpenID Connect opérationnel avant de poursuivre cet article et d’en reproduire les étapes.
Pour tester la procédure ci-dessous, il vous suffit d’avoir un serveur Dex opérationnel comme présenté dans la partie 1. Il n’est pas nécessaire de démarrer une application cliente.
Les serveurs OpenID Connect, également connus sous le nom de serveurs OAuth, fournissent un endpoint appelé Well-known URI Discovery Mechanism et disponible sous .well-known/openid-configuration
. Pour Dex, l’URL complète est http://127.0.0.1:5556/dex.well-known/openid-configuration
sur notre serveur local. Le résultat est ce document JSON :
{
"issuer": "http://127.0.0.1:5556/dex",
"authorization_endpoint": "http://127.0.0.1:5556/dex/auth",
"token_endpoint": "http://127.0.0.1:5556/dex/token",
"jwks_uri": "http://127.0.0.1:5556/dex/keys",
"userinfo_endpoint": "http://127.0.0.1:5556/dex/userinfo",
"device_authorization_endpoint": "http://127.0.0.1:5556/dex/device/code",
"grant_types_supported": [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code"
],
"response_types_supported": [
"code"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"code_challenge_methods_supported": [
"S256",
"plain"
],
"scopes_supported": [
"openid",
"email",
"groups",
"profile",
"offline_access"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic"
],
"claims_supported": [
"aud",
"email",
"email_verified",
"exp",
"iat",
"iss",
"locale",
"name",
"sub"
]
}
Il contient les différents endpoints exposés par Dex ainsi que plusieurs fonctionnalités disponibles et prises en charge.
Une application CLI pour tester le flux
J’ai écrit une petite application CLI pour Node.js publiée sur GitHub et NPM. Elle reproduit manuellement chaque étape du processus d’authentification. À chaque étape correspond un module Node.js à l’intérieur du dossier “lib” et nommé avec un numéro incrémentiel.
Vous pouvez exécuter l’application avec la commande npx openid-cli-usage
. Elle affiche :
NAME
openid-cli-usage - OAuth2 and OpenID Connect (OIDC) usage using the Authorization Code Grant.
SYNOPSIS
openid-cli-usage
OPTIONS
-h --help Display help information.
COMMANDS
redirect_url OAuth2 and OIDC usage - step 1 - redirect URL generation.
code_grant OAuth2 and OIDC usage - step 2 - authorization code grant.
refresh_token OAuth2 and OIDC usage - step 3 - refresh token grant.
user_info OAuth2 and OIDC usage - step 4 - user information.
jwt_verify OAuth2 and OIDC usage - step 5 - JWT verification.
help Display help information
EXAMPLES
openid-cli-usage --help Show this message.
openid-cli-usage help Show this message.
Note, il faut avoir Node.js installé sur votre système. Toutes les installations de Node.js sont fournies avec les commandes node
, npm
et npx
. npx
est un utilitaire pour lancer des packages node.js.
Il y a 5 commandes en plus de help
qui sont :
npx openid-cli-usage redirect_url
: étape 1 - génération de l’URLnpx openid-cli-usage code_grant
: étape 2 - Authorization Code Grantnpx openid-cli-usage refresh_token
: étape 3 - Refresh Token Grantnpx openid-cli-usage user_info
: étape 4 - informations utilisateurnpx openid-cli-usage jwt_verify
: étape 5 - vérification du JWT
À chaque commande correspond un module Node.js. Par exemple, la commande redirect_url
est implémentée dans le fichier ./lib/1.redirect_url.coffee
. Elle peut également être exécuté individuellement en clonant le repo et en exécutant npx coffee ./Lib/1.redirect_url.coffee
à l’intérieur. Au fait, nous sommes en 2020 et j’aime toujours écrire en CoffeeScript. Propre, simple et expressif.
Chaque module est construit avec le même modèle. Ils définissent une configuration interprétée par le package shell
et décrivant l’application CLI. La configuration comprend des descriptions, une liste d’options, ainsi que des fonctions handler
.
Les fonctions handler
sont là où le gros du travail est fait et c’est ce qui a été importé comme illustration de code dans cet article.
Étape 1 : génération de la Redirect URL
L’utilisateur est dans votre application cliente. Il n’est pas encore authentifié. Le client va le rediriger vers l’Authentication Server (AS). Pour cela, une requête spéciale doit être construite.
La commande redirect_url
, npx openid-cli-usage redirect_url --help
, ressemble à :
NAME
openid-cli-usage redirect_url - OAuth2 and OIDC usage - step 1 - generate URL
SYNOPSIS
openid-cli-usage redirect_url [redirect_url options]
OPTIONS for redirect_url
--authorization_endpoint Authorization endpoint. Required.
--client_id Client ID. Required.
-h --help Display help information.
--redirect_uri Redirect URI Required.
--scope No description yet for the scope option. Required.
OPTIONS for openid-cli-usage
-h --help Display help information.
EXAMPLES
openid-cli-usage redirect_url --help Show this message.
Les 4 paramètres sont obligatoires.
Le authorisation_endpoint
est l’url du serveur AS où l’utilisateur doit atterrir. Pour Dex, il est disponible sur http://127.0.0.1:5556/dex/auth. Vous pouvez interroger le endpoint de configuration OpenID pour découvrir sa valeur :
curl -s http://127.0.0.1:5556/dex/.well-known/openid-configuration \
| grep '"authorization_endpoint"'
"authorization_endpoint": "http://127.0.0.1:5556/dex/auth",
Le client_id
est enregistré dans l’Authentication Server, en l’occurence Dex.
Le redirect_uri
est l’URL de redirection de votre application cliente, par exemple http://my.great.app/callback
.
Le scope
est une liste de scopes valide pour OpenID. Le endpoint de configuration OpenID fourni les suivant par défaut :
{
"scopes_supported": [
"openid",
"email",
"groups",
"profile",
"offline_access"
]
}
Le scope openid
est obligatoire. Le scope email
est optionnel si vous souhaitez l’obtenir dans les informations de l’utilisateur. Le scope offline_access
est également important car c’est lui qui permet de fournir un refresh token. Nous détaillerons celui-ci plus tard.
Dans le module lib/1.redirect_url.coffee
, la fonction handler
est définie comme suivant :
crypto = require 'crypto'
base64URLEncode = (str) ->
str.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
sha256 = (buffer) ->
crypto
.createHash('sha256')
.update(buffer)
.digest()
handler = ({
params: { authorization_endpoint, client_id, redirect_uri, scope }
stdout
}) ->
code_verifier = base64URLEncode(crypto.randomBytes(32))
code_challenge = base64URLEncode(sha256(code_verifier))
url = [
"#{authorization_endpoint}?"
"client_id=#{client_id}&"
"scope=#{scope.join '%20'}&"
"response_type=code&"
"redirect_uri=#{redirect_uri}&"
"code_challenge=#{code_challenge}&"
"code_challenge_method=S256"
].join ''
data =
code_verifier: code_verifier
url: url
stdout.write JSON.stringify data, null, 2
stdout.write '\n\n'
La fonction imprime le code de vérification (verifier code) qui sont des octets générés aléatoirement puis encodés en base64 et l’URL pour rediriger l’utilisateur vers l’Authorization Server. Les fonctions base64URLEncode
et sha256
sont faciles à intégrer sur un client public comme un navigateur ou un environnement mobile.
L’URL est simplement construite. Il n’est pas nécessaire d’échapper les argument à l’exception de redirect_uri
mais ce n’est pas nécessaire dans mon cas.
Attention, l’URI de redirection doit correspondre à celle enregistrée dans le serveur OAuth.
Exécution de la commande :
npx openid-cli-usage redirect_url \
--authorization_endpoint http://127.0.0.1:5556/dex/auth \
--client_id example-app \
--redirect_uri http://127.0.0.1:5555/callback \
--scope openid \
--scope email \
--scope offline_access
La commande retourne :
{
"code_verifier": "jVc-dP1YsCFp6px0XKFHBMtM5lwfp2inbb9xE8iv3y8-MPwGQFUCCGlu_Ejsd5tuECu3lU",
"url": "http://127.0.0.1:5556/dex/auth?client_id=example-app&scope=openid%20email%20offline_access&response_type=code&redirect_uri=http://localhost:3002/auth/callback&code_challenge=I-bxiEvqV5NOveieEt2RWC1-pwknOg8UCa2FMi0Supg&code_challenge_method=S256"
}
Copiez collez l’URL dans votre navigateur.
Étape 2 : Authorization Code Grant
Vous êtes maintenant sur le serveur OAuth. En fonction de votre configuration et des connecteurs enregistrés, il vous sera proposé plusieurs fournisseurs de connexion. Sélectionnez-en un et terminez le processus d’authentification. Vous serez redirigé vers l’application cliente. Peu importe si l’application cliente n’est pas démarrée. En fait, c’est même mieux. Une fois les étapes de connexion et de consentement terminées, vous aller voir l’URL de callback avec les paramètres de requête supplémentaires code
et state
avant qu’une redirection ne puisse les faire disparaître de l’URL.
Le code
est nommé Authorization Code. Il s’agit d’un code temporaire renvoyé par l’Authorization Server au client qui l’échangera avec un token d’accès. C’est un Nonce (No more than once, pas plus d’une fois) et est de très courte durée, généralement autour de 30 secondes.
Notez que le token d’accès n’est pas renvoyé directement dans l’URL de rappel. Cette stratégie est appelé le Implicit Flow et elle ne doit plus être utilisée car elle n’est pas sécurisée. Placer le token dans une URL de redirection crée une grande surface d’attaque. Par exemple, elle laisse les token accessibles dans l’historique du navigateur. En outre, tous les plugins de votre navigateur et les librairies externe à moitié fiables et hébergées sur des CDN à moitié sérieux y ont accès. Au lieu de cela, il est recommandé d’utiliser PKCE, une petite extension de l’Authorization Code Flow dont la seule différence est qu’elle ne nécessite pas l’utilisation d’un secret client.
Au lieu de cela, le token d’accès est obtenu juste après avec une requête HTTP POST supplémentaire. Associé au code de vérification, le code d’autorisation valide un challenge. En cas de succès, les différents tokens, y compris le token d’accès, sont retournés.
Copiez la valeur de code
. Vous pouvez ignorer «state». Il s’agit simplement d’un moyen de renvoyer les informations de la page d’origine du client à la page de callback.
Nous utiliserons code
pour créer une requête POST et récupérer nos tokens. Voici comment le module lib/2.code_grant.coffee
l’implémente :
qs = require 'qs'
axios = require 'axios'
handler = ({
params: {
client_id, client_secret, token_endpoint,
redirect_uri, code_verifier, code
}
stdout
stderr
}) ->
try
{data} = await axios.post token_endpoint,
qs.stringify
grant_type: 'authorization_code'
client_id: "#{client_id}"
redirect_uri: "#{redirect_uri}"
client_secret: client_secret
code_verifier: "#{code_verifier}"
code: "#{code}"
stdout.write JSON.stringify data, null, 2
stdout.write '\n\n'
catch err
stderr.write JSON.stringify err.response.data, null, 2
stdout.write '\n\n'
Le package [axios
] (https://github.com/axios/axios) est un client HTTP. Nous utilisons le package qs
pour construire une chaîne de requête qui est envoyée comme contenu du body de la réquête POST.
Pour construire notre requête, nous avons besoin de l’ID client, du secret client et du redirect_uri correspondant à celui de la configuration Dex. Le endpoint du token peut être obtenu à partir du endpoint Well-known URI Discovery et équivaut à http://127.0.0.1:5556/dex/token
dans notre cas.
Ajustez les paramètres et exécutez la commande suivante :
npx openid-cli-usage code_grant \
--client_id exaple-app \
--client_secret ZXhhbXBsZS1hcHAtc2VjcmV0 \
--token_endpoint http://127.0.0.1:5556/dex/token \
--redirect_uri http://127.0.0.1:5555/callback \
--code_verifier jVc-dP1YsCFp6px0XKFHBMtM5lwfp2inbb9xE8iv3y8 \
--code ofupa4qe35oko4j7xzerrtyhp
Elle retourne en cas de succès :
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjgzYmI1ZTEyYmRlOTk3MWQ2ODgzMjU0MDA1NWI5ZjViN2NkZmIyYjYifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJ3ZWJ0ZWNoLWF1dGgiLCJleHAiOjE2MDU2ODkwODgsImlhdCI6MTYwNTYwMjY4OCwiYXRfaGFzaCI6ImJYLXpmSVlZZEtUaTE5Q1NNRmlGZkEiLCJlbWFpbCI6ImRhdmlkQGFkYWx0YXMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9.bIJYsGTCYypH9NYmxgX-KWjSs3Et7bEFAEkFlJKHvcXfYWxCAVBp0KZD2xUMTVXRsRHCjgsioyxuFqShmLu0Nt9Et5jQs8XieuTJTt4EplYt2q2SXveDM1xCpXLfMSTf5qbJKvKCxOo-fXsZXxYirEqA2wMa-0rsFvj8jyJGANe6iF7fbMHnnSmwGknQmMA7wT2S9J_0s53ommtbdAWFE8f8KyqjpzOugp3DRArwQzrViPeBpWqgHT3zMIZG_m4-LAHt5zJtk4SpUZuTG_MYamSMzmK0JVxmhXZm-KjM2FnT9UqX73qc74iBBn27VB1SnUhBpdxKjeHmXdZQg31ROA",
"token_type": "bearer",
"expires_in": 86399,
"refresh_token": "Chlhb2t4N20yaGVwcTJpMzY2Mm94cmhkdDJhEhl3aXp3MzJjYzdoajd6Nnhmd2V5amJ4czJo",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjgzYmI1ZTEyYmRlOTk3MWQ2ODgzMjU0MDA1NWI5ZjViN2NkZmIyYjYifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJ3ZWJ0ZWNoLWF1dGgiLCJleHAiOjE2MDU2ODkwODgsImlhdCI6MTYwNTYwMjY4OCwiYXRfaGFzaCI6ImhJTEhaaHJjNHdENlJZSzRLaVdMUlEiLCJlbWFpbCI6ImRhdmlkQGFkYWx0YXMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9.CU8zht_oqzCQ3-b9q9H7R2NbSN4V--uTvNUqvUpVFxKUfAC1J9Kc4RQYtnU-N0kJP4ZO-a4OCN31dDj-3hin1Wj3G2qoNeTQB6p3zveUYca_eEVI5cP1jcj-jUa4QNz-CCraWIoQwPdnqUjHiWY3kg-thEONvR6QFhrRMcP-YkDpFmgyjYqNE1iWOuZbRPi6b1TzWmiCQG2ucevmDE8XFv845f3h7-qFnj2wmkaBJ9gxyRyn_-sD-qfYlYzK9MwUToM5lIX5TLfuN4p5QVVqFLIdEDyTG3hFlk5LSu2dzimCgddeWbN1MJnVdjRQWc5Gpvi3qkXqSeWwGHyAdrj_LQ"
}
Et en cas d’erreur :
{
"error": "invalid_request",
"error_description": "Invalid or expired code parameter."
}
Application cliente publique et PKCE
Nous disposons maintenant d’un token d’accès, d’un token d’identification et même d’un token d’actualisation si le scope offline_access
a été fournie à l’origine. Il n’y a aucune raison technique pour laquelle nous n’aurions pas pu exécuter ce code dans le navigateur. Après tout, ce n’est que du JavaScript.
Il y a cependant deux mises en garde :
- L’ID secret doit être présent dans le navigateur ce qui signifie qu’il est désormais partagé entre chaque utilisateur, authentifié ou non. C’est là que PKCE vient à la rescousse.
- L’exécution du POST à partir du navigateur échouera pour des raisons de sécurité avec un message tel que
Origin http://localhost:8080 n'est pas autorisé par Access-Control-Allow-Origin
à moins que CORS est activé sur le serveur OpenID Connect.
PKCE est défini par la RFC 7636 et est une extension de l’Authorization Code Flow. Il se prononce “pixy” en anglais. Il remplace l’ Implicit Flow mentionné précédemment qui est beaucoup moins sécurisé et n’est plus recommandé. Il n’est même pas disponible avec Dex.
Pour activer PKCE dans Dex, commentez la clé secrète du client et activez la propriété public
de votre client :
staticClients:
- id: example-app
redirectURIs:
- 'http://127.0.0.1:5555/callback'
name: 'Example App'
# secret: ZXhhbXBsZS1hcHAtc2VjcmV0
public: true
Pour activer le CORS dans Dex, il faut mettre le paramètre allowedOrigins
à ['*']
dans la section web :
web:
http: 0.0.0.0:5556
allowedOrigins: ['*']
Redémarrez Dex puis répetez les étapes 1 et 2 en enlevant la propriété client_secret
:
npx openid-cli-usage code_grant \
--client_id example-app \
--token_endpoint http://127.0.0.1:5556/dex/token \
--redirect_uri http://127.0.0.1:5555/callback \
--code_verifier jVc-dP1YsCFp6px0XKFHBMtM5lwfp2inbb9xE8iv3y8 \
--code ofupa4qe35oko4j7xzerrtyhp
Echec de l’activation PKCE :
{
"error": "invalid_client",
"error_description": "Invalid client credentials."
}
Il n’y a plus de barrière technique. OpenID fonctionne maintenant dans votre client front-end publique, qu’il s’agisse d’une application mobile, d’une application SPA ou de toute autre chose.
Le reste de l’exemple part du principe que nous utilisons PKCE.
Refresh token
Le token d’accès est le seul token qui doit être envoyé à l’API lors de l’envoi de requêtes authentifiées. Vous ne partagerez jamais l’ID et le refresh token.
Le token d’accès a une courte période de validité. Si vous ne souhaitez pas que vos utilisateurs se connectent fréquemment, vous devez renvoyer le token de refresh avec le scope offline_access
.
Demander un nouveau token consiste à envoyer la bonne requête POST.
axios = require 'axios'
qs = require 'qs'
handler = ({
params: { client_id, client_secret, refresh_token, token_endpoint }
stdout
stderr
}) ->
try
{data} = await axios.post token_endpoint,
qs.stringify
grant_type: 'refresh_token'
client_id: client_id
client_secret: client_secret
refresh_token: refresh_token
stdout.write JSON.stringify data, null, 2
stdout.write '\n\n'
catch err
stderr.write JSON.stringify err.response.data, null, 2
stderr.write '\n\n'
Elle prends l’ID client, le client secret, sauf en cas d’utilisation du flux PCKE et le refresh token de l’utilisateur :
npx openid-cli-usage refresh_token \
--client_id example-app \
--token_endpoint http://127.0.0.1:5556/dex/token \
--refresh_token Chlhb2t4N20yaGVwcTJpMzY2Mm94cmhkdDJhEhl3aXp3MzJjYzdoajd6Nnhmd2V5amJ4czJo
Comme précédemment avec le Authorization Code, le résultat est :
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjAxNWMzYjNmYmE4YzkwNzVmYjhlMjcxOWZkMjNjOGU1YWE3Y2Q4MTQifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJleGFtcGxlLWFwcCIsImV4cCI6MTYwNTczNDQ2MiwiaWF0IjoxNjA1NjQ4MDYyLCJhdF9oYXNoIjoiam5fTV9SYndyMzU0NWRHakxRZWEyUSIsImVtYWlsIjoiZGF2aWRAYWRhbHRhcy5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.PmtqD50l3oJd4n1qAtgVwT6yimdFo3qlS1x_y0J9ZKEcfwPsgqnJJ6A9wbUIewhfuH9BCDnqhE-y6ZfrNi9ZIWQTETKZEx4LTg7vq5MKaWOMMEu_r1yOYmdHKsuOBXQko1vBdEjPtpSi7vCp4HR_gVwDfIe1KwnMPZjjcvvkr_PYMVj2_2RPWARB6tVczDTkBjTXTDvFXynVSM5mCihr_68ksat_dS6i5M1L7LRZAgvTeHVj0LrOnfE1hcEGQ5wME5m3diTRJbff_Q_UEhapsurTwrR4RRbaOIb6x-Oys26Ix7fdBNWucnOxmeBRUs6yTSdf1SUZHwkQxwsQrNx3_A",
"token_type": "bearer",
"expires_in": 86399,
"refresh_token": "ChlyZzZndnV6eDc2ZXVlZ2ZlazdibzZ4ZWluEhljcGJyYXZieGhlbXF0dWw0bXBsZmhoZ2I1",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjAxNWMzYjNmYmE4YzkwNzVmYjhlMjcxOWZkMjNjOGU1YWE3Y2Q4MTQifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJleGFtcGxlLWFwcCIsImV4cCI6MTYwNTczNDQ2MiwiaWF0IjoxNjA1NjQ4MDYyLCJhdF9oYXNoIjoidUF1aDVpWG5lVklCWV9FblNNek8xZyIsImVtYWlsIjoiZGF2aWRAYWRhbHRhcy5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0.JIMHsA2Mf1IPbUfjuWasxA_Xpng4tow3u_jiE2qnK7yd05_XMEOQi0KsQzTdk0Dl4M2uaQkSULU4lCx-ao4VrRsiSHBQ30NnT7AceHHp9h08DIeAuplnF0Ss8W3UPThAbwiAxl6G2PDXq5CUNGPrJ9d7mV3JE9lDv0QWjjycvsaAfAcC2ckjgYeLBl_mxI2BfZCT9dt2X7uyrPSH9HI4-r9mmeACZ3ybAAn_0TZ_1La5L94HfiS8eKzeNWgwToN0I52H1j7qrIzX44gFdK_6Xm07Ah6mg2vJJxS8ciJP4qyOjiHLqMuXz60JNtM6hZpl44jY7EaQCVsNsWFJUkVSgg"
}
Le refresh token est de type Nonce (No more than once, pas plus d’une fois). Si on tente de le réutiliser on obtient :
{
"error": "invalid_request",
"error_description": "Refresh token is invalid or has already been claimed by another client."
}
Vous devez réutiliser le refresh token généré par le dernier appel. La rotation du refresh token sur le client public garantit une période de validé relativement courtes aux access et refresh token. La surface d’attaque en est ainsi réduite.
Bearer authentication et information utilisateur
Avec la présence d’un token d’accès valide, l’utilisateur est connecté et chaque demande ultérieure inclura le JWT lui donnant accès à toutes les ressources autorisées avec le JWT.
Pour chaque appel HTTP, la requête doit inclure un “header” nommé “Authorization” avec une valeur commençant par “Bearer” suivi du token :
Authorization: Bearer <token>
C’est ce qu’on appelle une bearer authentication dans le sens où cela signifie “donner accès au porter (bearer) du token”.
Le protocole OpenID Connect enrichit le protocole OAuth de plusieurs manières, y compris l’ajout d’un endpoint pour récupérer les informations utilisateur. Le endpoint user information peut être obtenu à partir du endpoint Well-known URI Discovery. Dans notre cas : http://127.0.0.1:5556/dex/userinfo
dans notre cas.
La commande user_info
contacte l’Authorization Server et renvoye les informations utilisateur :
axios = require 'axios'
handler = ({
params: { access_token, userinfo_endpoint }
stdout
stderr
}) ->
try
{data} = await axios.get "#{userinfo_endpoint}",
headers: 'Authorization': "Bearer #{access_token}"
stdout.write JSON.stringify data, null, 2
stdout.write '\n\n'
catch err
stderr.write JSON.stringify err.response.data, null, 2
stderr.write '\n\n'
Elle prend en paramètre le endpoint d’information et l’access token :
npx openid-cli-usage user_info \
--access_token eyJhbGciOiJSUzI1NiIsImtpZCI6IjgzYmI1ZTEyYmRlOTk3MWQ2ODgzMjU0MDA1NWI5ZjViN2NkZmIyYjYifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJ3ZWJ0ZWNoLWF1dGgiLCJleHAiOjE2MDU2ODk4NDMsImlhdCI6MTYwNTYwMzQ0MywiYXRfaGFzaCI6IkNvcG92X01aOEo2Wmk2c0NwRTlPaHciLCJlbWFpbCI6ImRhdmlkQGFkYWx0YXMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9.A5Mz37rKLw8PbdB_9DJ6YGqEydvTe53a1Z8TMaNWUoaYz9tgFiQW_6gIJBX8ivmqoFVS-9ydbaTTomr64ZL6LtFtSl50jigJ5nBxpZv4_SXkCF0EphjoOmAvTX5HhCep_ig0QGwUamKGVzo5EeSqEK9jpH3nb2Hlt9AKjn4aShsWdrwiHz2FLHFdLlUfzSG113yDCvyoTP7JWONanSveLhDvEY3zlAlwY9auDVZqnnJsRatGbzWu1-gpAM9bZD6DgzMLnYyIaLH1yHtSgXOd748rTk4vOcvHRitSew_oZoVpcX17V0D2Fmk87tMKMnEgKARdcv5MKPH5YWpsZIkNbQ \
--userinfo_endpoint http://127.0.0.1:5556/dex/userinfo
Si cela fonctionne, la commande renvoi :
{
"iss": "http://127.0.0.1:5556/dex",
"sub": "CgU0Njg5NhIGZ2l0aHVi",
"aud": "example-app",
"exp": 1605689843,
"iat": 1605603443,
"at_hash": "Copov_MZ8J6Zi6sCpE9Ohw",
"email": "david@adaltas.com",
"email_verified": true
}
En cas d’échec :
{
"error": "invalid_request",
"error_description": "Refresh token is invalid or has already been claimed by another client."
}
Validation des JSON Web Token (JWT)
Nous avons utilisé la bearer authentication pour communiquer avec le serveur d’application mais comment l’utiliser avec notre API ?
Comme pour la récupération des informations utilisateur, le token doit être envoyé avec chaque demande authentifiée en tant que bearer token. Le serveur API obtient le token et vérifie l’identité présente à l’intérieur.
Les tokens d’accès et d’identification sont sérialisés en tant que JSON Web Tokens (JWT). Il se prononce «jot» en anglais. Un JWT est composé de 3 parties : l’en-tête, le payload et la signature. Pour valider les données présentes à l’intérieur, appelées la payload, le serveur d’API utilise la signature également présente à l’intérieur et les clés exposées par le serveur d’application.
Notez que le serveur de ressources est le terme OAuth 2.0 pour votre serveur API. Le serveur de ressources gère les demandes authentifiées une fois que l’application a obtenu un token.
Le serveur OpenID Connect expose plusieurs clés publiques. Les clés sont stockées au format JSON Web KEY (JWK). Le endpoint exposant ces clés dans Dex est situé sur http://127.0.0.1:5556/dex/keys
et est nommé jwks_uri
car il s’agit d’un JSON Web Key Set (JWKS, RFC 7517 section 5).
La fonction verify
du package jsonwebtoken
attend une clé publique au format PEM pour RSA. La conversion du JWK au RSA PEM est complexe. Dans le jwks-rsa
que nous avons utilisé, cela commence par le getSigningKeys
dans JwksClient
et passe à la fonction rsaPublicKeyToPEM
de utils
.
jwt = require 'jsonwebtoken'
jwksClient = require 'jwks-rsa'
handler = ({
params: { jwks_uri, token }
stdout
stderr
}) ->
try
# Extraction du header
header = JSON.parse Buffer.from(
token.split('.')[0], 'base64'
).toString('utf-8')
# Match du "kid" depuis le JWKS et obtention de la clé publique
{publicKey, rsaPublicKey} = await jwksClient
jwksUri: "#{jwks_uri}"
.getSigningKeyAsync header.kid
key = publicKey or rsaPublicKey
# Balidation du payload
payload = jwt.verify token, key
stdout.write JSON.stringify payload, null, 2
stdout.write '\n\n'
catch err
stderr.write err.message
stderr.write '\n\n'
L’ensemble du processus de vérification de l’identité de l’utilisateur est très rapide. L’une des principales forces de JWT est d’être autonome. Il lui suffit de récupérer la clé publique OAuth. Les certificats ne sont pas générés fréquemment. Lorsque les clés publiques sont mises en cache ou fournies par un autre moyen, aucune connexion réseau n’est impliquée. Il fonctionne également hors ligne au cas où le Resource Server n’a pas accès à l’Authorisation Server.
Pour vérifier la validité d’un jeton, utilisez la commande jwt_verify
:
npx openid-cli-usage jwt_verify \
--token eyJhbGciOiJSUzI1NiIsImtpZCI6IjgzYmI1ZTEyYmRlOTk3MWQ2ODgzMjU0MDA1NWI5ZjViN2NkZmIyYjYifQ.eyJpc3MiOiJodHRwOi8vMTI3LjAuMC4xOjU1NTYvZGV4Iiwic3ViIjoiQ2dVME5qZzVOaElHWjJsMGFIVmkiLCJhdWQiOiJ3ZWJ0ZWNoLWF1dGgiLCJleHAiOjE2MDU2ODk4NDMsImlhdCI6MTYwNTYwMzQ0MywiYXRfaGFzaCI6IkNvcG92X01aOEo2Wmk2c0NwRTlPaHciLCJlbWFpbCI6ImRhdmlkQGFkYWx0YXMuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWV9.A5Mz37rKLw8PbdB_9DJ6YGqEydvTe53a1Z8TMaNWUoaYz9tgFiQW_6gIJBX8ivmqoFVS-9ydbaTTomr64ZL6LtFtSl50jigJ5nBxpZv4_SXkCF0EphjoOmAvTX5HhCep_ig0QGwUamKGVzo5EeSqEK9jpH3nb2Hlt9AKjn4aShsWdrwiHz2FLHFdLlUfzSG113yDCvyoTP7JWONanSveLhDvEY3zlAlwY9auDVZqnnJsRatGbzWu1-gpAM9bZD6DgzMLnYyIaLH1yHtSgXOd748rTk4vOcvHRitSew_oZoVpcX17V0D2Fmk87tMKMnEgKARdcv5MKPH5YWpsZIkNbQ \
--jwks_uri http://127.0.0.1:5556/dex/keys
Si cela fonctionne, la commande renvoi :
{
"iss": "http://127.0.0.1:5556/dex",
"sub": "CgU0Njg5NhIGZ2l0aHVi",
"aud": "example-app",
"exp": 1605689843,
"iat": 1605603443,
"at_hash": "Copov_MZ8J6Zi6sCpE9Ohw",
"email": "david@adaltas.com",
"email_verified": true
}
En cas d’échec :
invalid signature
Conclusion
Quel voyage intéressant. Cela peut sembler compliqué, mais pour ceux d’entre nous qui connaissent Kerberos et GSSAPI, c’est tout de même beaucoup plus facile à comprendre. Alors que la partie 1 vous aide à comprendre ce que sont OAuth, OpenID et OIDC, la partie 2 vous a donné le pouvoir de reproduire les différentes étapes du processus. L’utilisation de HTTP, JSON et des technologies Web familières facilite la compréhension du protocole sous-jacent. Au final, il n’y a rien qu’un développeur web ne puisse faire lui-même, embarquant le flux OAuth dans son application mobile ou React par exemple.