Gérer l’authentification dans une Architecture Moderne - Partie 1

Introduction

Quel que soit le type d’application que vous mettez en place, la sécurité et en particulier l’authentification de vos utilisateurs est probablement un de vos points d’attention majeur.

Les architectures modernes peuvent prendre plusieurs formes et différents niveaux de complexité. Mais dans le cadre de cet article sur l’authentification, c'est le caractère distribué de ces architectures qui va nous préoccuper :

  • qu’il s’agisse du découpage d’une unique application en plusieurs micro-services
  • ou d’un éco-système de services Cloud consommant leurs APIs respectives
  • ou de l’ensemble des applications et services d’un riche Système d’Information
  • ou de tout cela à la fois !

Toutefois, chacune de vos architectures aura ses propres spécificités, opportunités et contraintes :

  • capacités natives de votre plateforme d’exécution
  • choix d’externaliser ou non certains aspects
  • besoins de scalabilité
  • besoins de collaboration et d’interopérabilité
  • confiance avec vos partenaires

Il est donc difficile de fournir une solution unique clef en main à ce sujet. Mais les problématiques et points d’attention seront eux similaires et l’objet de ce billet est d’essayer de faire le tour des concepts à prendre en compte.

Ce premier billet va se focaliser sur les problématiques apparaissant au sein d’une application distribuée.
Un deuxième billet étendra la réflexion lorsque des échanges avec des applications tierces sont nécessaires.

L’architecture de la World Company comme base de réflexion

Je vais tenter d’illustrer une partie de ces concepts au travers d’une première architecture fictive :

Blog-Authent-init

Cette architecture représente l’écosystème dans lequel la “World Company” évolue pour le moment.
Elle a choisi de mettre une nouvelle application à disposition de ses utilisateurs :

  • elle est disponible sous la forme d’une application SPA (Single Page Application) et d’une application mobile Native effectuant des appels de services sur sa plateforme.
  • une architecture micro-service a été mise en place pour implémenter le backend
    • chaque micro-service a besoin de connaître l’identité de l’utilisateur final qui le sollicite directement ou indirectement.
  • ces micro-services sont exposés sur Internet via une API Gateway.
  • cette gateway s’appuie ici sur un module Authorization Server distinct : son rôle est d’émettre les tokens d’accès nécessaires à l’appel des services via la gateway
  • l’aspect authentification proprement dit est géré par plusieurs briques :
    • l’Identity Provider interne historique est toujours en place et le SI interne n’est couplé qu’avec lui.
    • mais il délègue maintenant l’authentification à la nouvelle plateforme Cloud du moment : un service tiers nommé “Auth For Everyone” qui facilite l’accès de leurs utilisateurs en leur proposant un Single Sign On avec d’autres services.

Au travers de cette architecture fictive, on peut identifier un certain nombre de préoccupations concernant l’authentification des appelants :

  • la délégation de l’authentification
  • l’authentification propement dite
  • le maintien du contexte d’authentification
    • qui passe ici par l’obtention d’un token d’accès
  • la propagation de l’identité de l’utilisateur

Creusons un peu ces différentes préoccupations et voyons un certain nombre des alternatives s’offrant à nous.

Authentification de l’utilisateur

Délégation d’authentification

Rares sont maintenant les applications qui ne délèguent pas l’authentification de l’utilisateur final à un serveur d’identité ( Identity Provider / IdP ) : qu’il soit interne ou externe au SI.
On gagne en particulier en sécurité en limitant le nombre de systèmes ayant accès aux credentials de l’utilisateur (mot de passe ou autre).

Une relation de confiance forte doit être établie avec l’Identity Provider. Pour établir cette confiance il vous faudra aussi considérer la fiabilité de l’IdP en termes de sécurité.

Les protocoles utilisables pour effectuer cette délégation sont :

  • OpenId Connect (OIdC)
    • voire une simple authentification via OAuth2 (cf encart ci-dessous)
  • SAML 2 ( via le Web Browser SSO Profile ) :
  • on peut encore rencontrer de plus anciens protocoles : CAS par exemple

On notera que ces protocoles sont le plus souvent utilisés dans le cadre d’un service de Single Sign On (SSO) : offrir la possibilité à l’utilisateur de ne se connecter qu’une seule fois pour l’utilisation de différents services (potentiellement n’ayant pas de liens entre eux) constitue un autre critère de choix intéressant.

Dans notre architecture fictive, cela correspond aux protocoles potentiellement utilisables :

  • entre l’Authorization Server de la World Company avec son Identity Provider
  • entre l’Identity Provider de la World Company avec Auth For Everyone

Il n’est pas nécessaire que ce soit le même pour ces deux délégations.
Dans tous les cas, l’application cliente de l’utilisateur sera mis à contribution pour effectuer cette délégation qui est toujours basée sur des redirections HTTP pour ces protocoles.

Voici des exemples de solutions permettant de mettre en place un Identity Provider capable de faire du SAML et du OpenId Connect :

  • Home made : basé sur Spring Security
  • Identity Provider : Keycloak, WSO2 IS, ...
  • Solutions Saas : Auth0, Okta, Google, ...

Ces solutions sont aussi capables pour la plupart de déléguer l’authentification à un service tiers sur ces mêmes protocoles.

OpenId Connect et OAuth2

 
OAuth2, comme nous verrons plus en détail dans la 2ème partie de ce billet, est un protocole de délégation d’autorisation. Toutefois, avant la mise en place de OpenId Connect, il a aussi souvent été utilisé de manière à récupérer l’identité de l’utilisateur, et donc de fait pour faire de l'authentification (Spring Security 4 en particulier ne propose qu’une authentification via OAuth2, Spring Security 5 va pallier cela en supportant vraiment OpenId Connect)
Malheureusement, dans certains cas aux limites, cela peut poser des problèmes de sécurité ( le lecteur intéressé pourra lire Why is it a bad idea to use plain oauth2 for authentication? et
The problem with OAuth for Authentication)

OpenId Connect est une extension de OAuth2 qui apportent les garanties de sécurité nécessaires à son usage en tant que protocole d’authentification.
OpenId Connect peut être utilisé uniquement pour la partie Authentification (OAuth2 est alors un détail d’implémentation) ou conjointement avec les capacités de délégation d’autorisation de OAuth2

L’authentification proprement dite

L’authentification proprement dite est réalisée lorsque l’utilisateur prouve son identité auprès de l’IdP en bout de chaîne et ce quel que soit le protocole de délégation utilisée.
A vous de choisir la solution ou le fournisseur vous garantissant le bon niveau de sécurité.

On retrouvera souvent les méthodes suivantes :

  • login/mot de passe
  • Kerberos (au sein de votre réseau interne)
  • authentification PKI
  • two-factor authentication (via l’envoi d’un SMS par exemple)

Fédération d’identité et Discovery

Dans notre architecture fictive, l’Identity Provider interne de la World Company peut paraître inutile puisqu’il délègue l’authentification à un Identity Provider externe.

Son premier intérêt est de découpler l’ensemble du SI de l’Identity Provider externe.

Mais cette architecture offre aussi d’autres avantages :

  • L’ IdP interne pourra choisir de déléguer l’authentification à différents Identity Providers selon le contexte :
    • Soit en demandant explicitement à l’utilisateur : “Log With [Facebook|Google] ...”.
    • Soit automatiquement suivant un critère disponible au moment de la connection (l’IP, le précédent choix de l’utilisateur stocké dans un cookie, ...).
    • Ce processus s’appelle l’IdP Discovery.
  • Il pourra alors faire de la fédération d’identité : c’est à dire fournir un identifiant unique aux utilisateurs quelque soit l’IdP qu’ils utilisent.
  • Il pourra aussi gérer des informations sur l’utilisateur qui ne sont pas disponibles ou accessibles auprès du service externe ; ou qui sont spécifiques à la World Company.

Maintien du contexte d’authentification

Une fois l’utilisateur authentifié, il est nécessaire de maintenir ce contexte d’authentification pour l’ensemble des invocations de services qui vont suivre.

Notre architecture d’exemple illustre déjà un parti pris pour cette problématique via l’usage d’un Authorisation Server. Mais avant de se restreindre à ce choix, plusieurs stratégies sont envisageables suivant le contexte précis :

  • le classique cookie de session (propageant un identifiant de session associé à un état stocké côté serveur) reste très bien sécurisé (s’il est bien Secure et HttpOnly) et peut encore rendre service suivant vos contraintes.
    (Il sera néanmoins peu adapté à une application mobile)

  • un token JWT custom permet de mettre en place une approche stateless (c’est à dire sans état stocké côté serveur) pour cette problématique
    Quentin a justement posté un billet sur le sujet récemment : Preuve d’authentification avec JWT

  • l’usage d’un access token OAuth2; qui sera surtout orienté appel de service, répond aussi à ce besoin :
    La présence de l’access token dans chaque requête permettra au système appelé (nommé Resource Server dans le jargon OAuth2) d’obtenir l’identité utilisé lors de l’obtention de ce token.

    C’est l’approche illustrée dans notre architecture fictive. Dans son état actuel, l’usage de l’access token est limité aux clients sous le contrôle de la World Company (la SPA et l’application mobile).
    Nous étudierons OAuth2 plus en détail dans le prochain billet : notons d'ors et déjà qu’il propose plusieurs méthodes (“grant_type”) pour obtenir un access token :

    • la SPA utilisera le grant_type “implicit”.
    • tandis que l’application mobile utilisera une variante du grant_type “authorization code” utilisant PKCE ( voir aussi les best practice de l' IETF).

    Notons que suivant l’implémentation de l’Authorization Server (AS) cette approche peut être stateless ou bien nécessiter le stockage d’un état sur l’AS.

Quelle que soit l’approche, dans notre architecture, ce sera avant tout à la Gateway de s’assurer que chaque appel de service comporte les éléments nécessaires à garantir l’identité de l’appelant (sans parler de certains aspects relatifs aux autorisations). Ce qui ne présuppose pas du mode de propagation de cette identité aux services sous-jacents.

OAuth2 et JWT

 
Suivant l’implémentation de l’Authorization Server, un token OAuth2 peut ou non suivre le format JWT : c’est un détail d’implémentation qui ne doit être connu que de l’Authorization Server et souvent mais pas obligatoirement des services appelés. Pour l’application appelante (le “client”) ce token doit rester opaque.

JWT (ou plus exactement l’ensemble des spécifications JOSE ) est un format d’encodage et de signature d‘informations qui peut être utilisé dans différents cas d’usage.
Il sera en particulier adapté à une simple propagation d’informations signées. Et dans ce cas, le format du token fait parti de l’interface entre les parties concernées.

Propagation de l’identité

Notre architecture fictive comporte deux besoins de propagations de l’identité de l’utilisateur au sein des modules de la World Company :

  • de la gateway vers les micro-services
  • de micro-service à micro-service

Pour le moment, nous restons au sein du SI et donc la plupart des composants peuvent être considérés de confiance (du moins nous n’allons pas trop challenger cette assertion dans cette première partie).

On peut identifier plusieurs façons de propager l’identité dans ces conditions :

  • le partage du jeton d’accès initial (OAuth2 ou JWT) : le jeton utilisé en entrée du système.
    Techniquement, il s’agit d’associer le token à chaque requête aval de la même façon qu’il a été reçu en amont : dans un header Authorization, de type Bearer généralement.

    L’ “audience” du jeton doit alors être l’ensemble du système et pas uniquement un micro-service en particulier. En effet, un Authorization Server pouvant émettre des jetons à destination de différents systèmes, l’audience permet de garantir qu’ils ont bien été émis pour des services cibles spécifiques : cette audience doit être vérifiée par le service appelé avant de prendre acte de l’identité de l’utilisateur (ou des autres informations associées au token).

    Dans le cas de OAuth2, cette méthode implique un couplage avec les spécificités de l’Autorization Server : en particulier sur le mode de validation du token (et le format des informations associées).

  • la création d’un token JWT interne dédié à la propagation à l’intérieur du SI :
    L’identité est transmise dans un token JWT spécifique signé par la gateway qui est ensuite passé de micro-service en micro-service via un header HTTP.

    La propagation de l’identité est alors agnostique au(x) protocole(s) d’authentification et/ou de maintien du contexte d’authentification avec l’utilisateur final. Les micro-services n’ont besoin que de faire confiance à la signature de la gateway.
    Un autre avantage est que la gateway peut enrichir le token d’informations supplémentaires non disponibles dans le token d’origine (des rôles, d’autre informations sur l’utilisateur, … ) : à utiliser avec discernement mais potentiellement très puissant.
    Ce jeton n’a besoin d’être valide que le temps du traitement de la requête : cela permet d’améliorer la sécurité : en cas de vol/détournement de ce token, il ne sera utilisable que dans un bref délai.

  • propagation applicative sans signature particulière :
    Si le service appelant s’authentifie lui-même auprès de l’appelé (cf “Appels de service à service” dans la 2ème partie de ce post) : l’identité de l’utilisateur final peut être transmise de point à point via un header ad hoc ou dans la payload applicative sans avoir besoin d’une signature.
    Dans les faits, on change le contexte de sécurité (dont les autorisations) : l’appel s’effectue alors sous l’identité technique du service appelant. A charge de la logique métier d’extraire l’identité de l’utilisateur final et de l’utiliser pour ces propres besoins.

Implication de la propagation

Attention à la propagation des autorisations de l’utilisateur (ou plutôt du token) :

  • les tokens d’accès sont généralement associés à un ensemble d’autorisations : qu’elles soient propagées au sein du token lui-même ou obtenues via un autre mécanisme.
  • or propager ces autorisations au cours d’appels en cascade ne répond pas toujours au besoin : il est en effet parfois nécessaire de permettre à un micro-service d’appeler un autre micro-service avec des droits plus élevés que ceux de l’utilisateur.
  • différentes approches sont possibles pour répondre à ce point, mais la dernière proposition ci-dessus, où l’appel est effectué sous l’identité du service appelant, répond bien à cette contrainte.

Conclusion

Au cours de ce billet, nous avons essayé d’identifier les différentes alternatives s’offrant à nous pour implémenter toute la chaîne nous permettant d’authentifier l’utilisateur puis de maintenir et propager son identité au sein d’une architecture fictive non triviale.

Les pistes proposées sont classiques dans les architectures actuelles, mais pas forcément exhaustives. D’autres approches restent envisageables suivant les spécificités. Restez ouvert et pragmatique aux opportunités, mais comme souvent en sécurité, soyez prudent et humble, en essayant de vous poser le maximum de questions. J’espère que ce billet et le suivant vous aideront dans cette démarche.

Dans la deuxième partie, je vous parlerai des problématiques associées aux échanges avec des tiers, où nous remettrons en particulier en cause certaines des approches proposées.

Author image
Fort de plus de 20 ans d'expérience autour de l'écosystème JavaEE, Fabien accompagne ses clients dans la conception et le cadrage de leurs projets.