Loupe

OpenId Connect et OAuth : comment choisir son flow de connexion ?

Dans des précédents articles, nous avons abordé comment sécuriser une API avec OpenIddict. Dans cet article, je vais détailler les différents flows possibles de connexion dans le cadre d'OpenId Connect. Je pensais que cela serait un petit article mais après coup, il ya quand même pas mal de lecture...

Dans la suite de l'article, nous utiliserons cette terminologie :

  • L'application cliente (AC) souhaitant accéder à une ressource sécurisée.
  • Le serveur d'Authorization (SA) permettant d'authentifier l'utilisateur.
  • Le serveur possédant la ressource (SR).

Je vais présenter plusieurs concepts dans cet article :

  • Le processus de connexion "Authorization code flow",
  • Le processus de connexion "Implicit flow",
  • Le processus de connexion "Ressource Owned Password Credential",
  • Le processus de connexion "Client Credential",
  • PKCE (à prononcer pixy comme dit dans la doc)

Authorization Code Flow

Processus de connexion

Dans ce mode, l'utilisateur doit se connecter sur une mire de connexion du serveur d'Authorization qui retourne un code à l'application cliente. Cette dernière pourra alors échanger ce code auprès du serveur d'authorization afin d'obtenir un jeton d'accès.

Le processus suivant est alors mis en place :

  1. L'AC affiche à l'utilisateur une page de connexion hébergée sur le SA. Il fournit en paramètre de l'URL les informations obligatoires : client_id, scope demandé, url de callback et un state (aussi appelé nonce). On utilise ici un endpoint spécifique appelé "Authorization endpoint" dans la littérature.
  2. Le SA affiche alors une page de connexion à l'utilisateur où il lui demande de se connecter et d'autoriser l'accès aux ressources lui appartenant pour le périmètre donné à l'AC.
  3. Une fois que l'utilisateur accepte, le SA appelle l'url de callback (indiquée lors de l'appel initial) avec comme paramètre (GET) un code d'Authorization.
  4. L'AC interprète ce code et appelle le SA en demandant d'échanger ce code contre un access_token. S'il s'agit d'une application privée, elle doit absolument s'identifier (utiliser le client_id / client_secret) pour faire cet échange. On utilise ici un endpoint spécifique appelé "Token endpoint" dans la littérature.
  5. Le SA valide ce code et retourne un jeton d'accès et aussi le nonce initialement passé en paramètre.
  6. L'AC valide alors que le nonce reçu correspond bien à celui transmis initialement, sinon il faut considérer le jeton comme invalide.

Capture d’écran 2019-01-15 à 11.59.30.png

Pour quels types d'application ?

Ce processus est souvent recommandée pour les "applications privées" étant pas en mesure de stocker de manière sécurisée un client_secret, mais rien n'empêche dans la spécification de l'utiliser pour une application publique (sans utilisation de client_secret donc). Aussi, ce workflow est dédié aux applications ne pouvant pas faire confiance à la couche applicative gérant la partie "callback". Imaginons par exemple que l'url de callback utilisée soit une URL de type "protocole" déclenchant l'ouverture d'une application, rien ne garantit qu'une application espionne ne peut pas intercepter un éventuel jeton d'accès en implémentant elle-même le protocole utilisé.

Voici quelques exemples d'applications pouvant utiliser ce mode de connexion :

  • Application serveur : il s'agit du mode préféré de connexion car il est possible de stocker de manière sécurisé le client_secret.
  • Application SPA : pas de problème non plus pour ce mode qui est une étape de plus par rapport au mode "implicit" décrit ci-après. Attention, vous le verrez sur l'internet mondial, il n'y a pas de consensus général sur la possibilité d'échanger un code contre un token par une application "publique" et beaucoup de fournisseur d'identité l'empêche. L'utilisation de PKCE (plus d'infos plus tard dans cet article) est alors recommandée.
  • Application native : même chose que pour une application SPA, à la différence près que le mode 'implicit" n'est pas une option pour application native (du fait de la non présence du navigateur web en guise d'intermédiaire).

Comment gérer la péremption du jeton d'accès ?

Il est possible d'obtenir un jeton de rafraîchissement dans ce mode mais il faudra s'assurer de le conserver de manière sécurisé car il donne un accès à votre API.

Pour tous les cas où cela ne peut pas être garanti, il faudra faire repasser l'utilisateur par le processus complet de connexion. Par contre, si l'utilisateur est encore connecté (par exemple s'il a coché une case "se souvenir de moi" sur la page de connexion) et que le périmètre demandé n'a pas changé, alors cela peut être fait de manière assez transparente pour lui en utilisant une iframe / webview cachée qui appellera déclenchera de manière naturelle l'appel de l'url de callback.

À quoi ressemble un appel ?

Dans ce mode, l'appel correspond notamment à l'URL d'affichage de la mire de connexion. On remarquera notamment que l'on demande une réponse de type token :

GET /authorize?
    response_type=code
    &client_id=monClientId
    &redirect_uri=https%3A%2F%2Furl.callback%2Fcb
    &scope=openid%20profile
    &state=kikouLolMdr

Je vous invite à lire la spécification pour y trouver une liste complète des paramètres possibles : https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth

Implicit Flow

Processus de connexion

Ce mode est très similaire au précédent, à la différence près que vous pouvez faire confiance à la couche applicative effectuant la partie callback. Cela est par exemple le cas d'une application Web : on peut faire confiance au Browser pour intercepter la demande d'affichage d'une URL utilisant le protocole HTTP (mais si, je vous dis, on peut !).

Attention cependant, le groupe de travail IETF sur Oauth recommande depuis peu de ne pas utiliser ce mode de connexion (source) :

  In order to avoid these issues, Clients SHOULD NOT use the implicit
   grant and any other response type causing the authorization server to
   issue an access token in the authorization response.

Dans tous les cas, le processus mis en place est le suivant :

  1. L'AC affiche à l'utilisateur une page de connexion hébergée sur le SA. Il fournit en paramètre de l'URL les informations obligatoires : client_id, scope demandés et url de callback.
  2. Le SA affiche la page de connexion à l'utilisateur où il lui demande de se connecter et d'autoriser l'accès aux ressources lui appartenant pour le périmètre donné à l'AC.
  3. Une fois que l'utilisateur accepte, le SA appelle l'url de callback (indiquée lors de l'appel initial) avec comme paramètre (GET) le jeton d'accès.
  4. L'AC interprète l'URL de callback et y trouve avec joie le jeton d'accès. Dans ce mode, le jeton est fourni sous la forme d'un fragment de l'url (derrière un # donc), plutôt que dans un paramètre GET, car dans cette configuration aucun appel réseau n'est fait avec le jeton d'accès visible dans l'URL (les fragments ne sont pas envoyés par les navigateurs web aux serveurs) bien que restant visible/exploitable par l'AC.

Capture d’écran 2019-01-17 à 19.08.28.png

Pour quels types d'application ?

Principalement pour toutes les applications dont le mécanise de "callback" est garanti comme sûr : les applications SPA principalement.

Comment gérer la péremption du jeton d'accès ?

Exactement de la même manière que pour le code flow.

À quoi ressemble un appel ?

Très similaire à celle vue précédemment mais on demande une réponse de type "token" :

GET /authorize?
    response_type=token
    &client_id=monClientId
    &redirect_uri=https%3A%2F%2Furl.callback%2Fcb
    &scope=openid%20profile
    &state=kikouLolMdr
  Host: infinitesquare.com

Resource Owner Password Credentials / Password flow

Ce mode permet à l'AC d'héberger elle même le formulaire de connexion pour le présenter à l'utilisateur. Cela est très intéressant et permet de proposer à l'utilisateur une expérience plus agréable :

  • Le formulaire est complètement intégré (d'un point de vue design) à l'application,
  • Il n'y a pas de navigation dans un browser "qui le sort du contexte où il est",
  • Il n'a qu'à cliquer sur un bouton pour se connecter.

Processus de connexion

Ce processus est alors mis en place :

  1. L'AC affiche à l'utilisateur une page de connexion.
  2. L'AC demande un jeton d'accès en utilisant le login/mot de passe donné par l'utilisateur. Ici c'est l'endpoint donnant des tokens qui est utilisé en POST : il n'y a donc pas de mot de passe qui transite en clair dans une URL. (Merci pour la précision Kévin !).

Capture d’écran 2019-01-17 à 19.10.36.png

Pour quels types d'application ?

En réalité il s'agit surtout d'un processus de connexion mis en place pour migrer des applications utilisant l'ancienne connexion HTTP basic vers OAuth et n'est pas forcément le mode de connexion le plus recommandé.

En effet il possède plusieurs défauts à prendre en compte :

  • Le login / mot de passe de l'utilisateur va être dans les mains de l'AC et transiter "en clair" dans les paramètres envoyés au SA.
  • Il est facile dans ce contexte de créer une application "fake" qui demande à l'utilisateur son login / mot de passe car il a l'habitude de le faire dans l'AC. 
  • On fait confiance à l'application cliente pour présenter de manière correcte le périmètre demandé à l'utilisateur.
  • On ne peut pas mettre de SSO car la connexion se passe à chaque fois sur l'application client (et non pas sur un seul SA).
  • Son acronyme ROPC est quand même vachement dur à prononcer.
  • etc.

Néanmoins, il reste intéressant à mettre en place dans certains contextes, notamment s'il s'agit d'une application "confidentielle" (pas visible sur l'internet mondial) avec une seule application cliente (pas besoin de SSO), et où le besoin d'expérience utilisateur de haute voltige est très fort.

À quoi ressemble un appel ?

L'appel ici est unique pour demander le jeton d'accès :

GET /authorize?
    grant_type=password
    &client_id=monClientId
    &scope=openid%20profile
    &username=jonathan
    &password=motDePasse

Client Credentials

Ce processus est dédié à des applications sans notion d'utilisateur. Je suis déjà rentré en détail sur la mise en place avec OpenIddict dans un autre article de blog.

Processus de connexion

Ce processus n'a pas besoin de demander à l'utilisateur de se connecter est donc très simple :) :

  1. L'AC demande un jeton d'accès en utilisant le client_id/client_secret. Ici encore c'est l'endpoint donnant des tokens qui est utilisé en POST : il n'y a donc pas de mot de passe qui transite en clair dans une URL. 

Capture d’écran 2019-01-17 à 19.09.40.png

Pour quels types d'application ?

Pour toutes les applications en mesure de stocker de manière sécurisée un client_secret. Cela est donc réservé de manière générale aux applications "serveur".

À quoi ressemble un appel ?

GET /authorize?
    grant_type=client_credentials
    &client_id=monClientId
    &scope=openid%20profile
    &client_id=jonathan
    &client_secret=motDePasse

PKCE

PKCE signifie Proof Key for Code Exchange et se prononce "pixy".

Ce n'est pas un processus de connexion à proprement parler mais c'est une "surcouche" à l'Authorization code flow qui permet d'éviter les problèmes liés à l'interception du codeIl est vivement recommandé d'utiliser PKCE avec ce mode de connexion. En effet, si une application malveillante réussit à intercepter le code, elle peut l'échanger facilement contre un jeton d'accès et utiliser l'API au nom de l'utilisateur. Cela est d'ailleurs très bien schématisé dans la documentation officielle :

Capture d’écran 2019-01-15 à 14.18.07.png

 

PKCE propose ainsi deux choses :

  • Dans le premier appel de demande de code : indiquer au SA le hash d'un code de vérification et la méthode de hashage utilisé. Le code de vérification doit avoir un minimum de 43 caractères et un maximum de 128 caractères non réservés. Il existe deux méthodes de cryptage : plain ou sha256 qui doit être préféré si cela est possible.
  • Dans l'appel permettant d'échanger le code contre un jeton d'accès, indiquer le code de vérification en clair. Le SA vérifie alors que le jeton initial correspond à celui donné au second appel et ne donne un jeton que si cela est le cas.

Ainsi, même en interceptant le code, il devient difficile de l'échanger contre un jeton d'accès.

Un appel pour une demande de code ressemble alors à celui vu pour Authorization Code Flow avec en plus les paramètres code_challenge et code_challenge_method : 

GET /authorize?
    response_type=code
    &client_id=monClientId
    &redirect_uri=https%3A%2F%2Furl.callback%2Fcb
    &scope=openid%20profile
    &state=kikouLolMdr
    &code_challenge=vBcZBGqBEcQdekdkedkJNDLEJjnde678HD
    &code_challenge_method=S256

Plus d'infos dans la doc officielle.

Happy coding :)

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus