OAuth2 et OpenID Connect pour les microservices et les applications publiques (Partie 2)

OAuth2 et OpenID Connect pour les microservices et les applications publiques (Partie 2)

Vous appréciez notre travail......nous recrutons !

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’URL
  • npx openid-cli-usage code_grant  : étape 2 - Authorization Code Grant
  • npx openid-cli-usage refresh_token : étape 3 - Refresh Token Grant
  • npx openid-cli-usage user_info  : étape 4 - informations utilisateur
  • npx 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.

Partagez cet article

Canada - Maroc - France

Nous sommes une équipe passionnée par l'Open Source, le Big Data et les technologies associées telles que le Cloud, le Data Engineering, la Data Science le DevOps…

Nous fournissons à nos clients un savoir faire reconnu sur la manière d'utiliser les technologies pour convertir leurs cas d'usage en projets exploités en production, sur la façon de réduire les coûts et d'accélérer les livraisons de nouvelles fonctionnalités.

Si vous appréciez la qualité de nos publications, nous vous invitons à nous contacter en vue de coopérer ensemble.

Support Ukrain