I. Introduction

Sur un site web, les utilisateurs ont généralement besoin de s'authentifier. Plutôt que d'utiliser une authentification locale propre au site, les utilisateurs souhaitent de plus en plus utiliser leur compte Facebook, Twitter ou Google…

La délégation d'authentification est devenue une problématique récurrente. Pourtant, chaque fournisseur d'identité (protocole OAuth pour Facebook, OpenId pour Google, etc.) utilise des solutions et des bibliothèques différentes, plus ou moins complexes, obligeant les développeurs à tout réapprendre tout le temps.

La bibliothèque PAC4j résout ce problème en proposant une solution simple et cohérente pour tous les fournisseurs d'identité, protocoles et frameworks de la Machine virtuelle Java (JVM).

Cet écosystème se compose :

  • d'un ensemble de modules (pac4j-core, pac4j-oauth, pac4j-openid…) implémentant les algorithmes et les comportements des différents protocoles et fournisseurs d'identité : le développeur devra choisir les modules adéquats pour le(s) fournisseur(s) d'identité souhaité(s) ;
  • d'un ensemble d'implémentations dédiées à différents frameworks de la JVM : j2e-pac4j pour une simple application JEE, spring-security-pac4j pour une application Spring-Security, buji-pac4j pour une application Shiro, play-pac4j pour le framework Play… Le développeur devra choisir la bibliothèque adaptée à son application ;
  • d'un ensemble d'applications Web de démonstration (pour JEE, Spring-Security, Shiro, Play, etc.) démontrant les principaux cas d'utilisation.
Image non disponible

Pour le moment, PAC4J supporte cinq protocoles et dix-huit fournisseurs d'identité :

  • OAuth : Facebook, GitHub, Google, LinkedIn, Twitter, Yahoo, Windows Live, WordPress, DropBox, PayPal, Vk.com, Foursquare, un wrapper OAuth pour le serveur CAS ;
  • CAS : serveurs CAS ;
  • OpenID : Google ;
  • SAML : IdP SAML 2 ;
  • HTTP : authentification par formulaire ou par basic auth.

II. Protégez votre application JEE

La première étape pour intégrer l'authentification Facebook dans une application J2E est d'ajouter des dépendances :

  • à pac4j-oauth (protocole utilisé par Facebook) ;
  • à j2e-pac4j (puisque c'est une application J2E).

Pour Maven par exemple, cela revient à ajouter les dépendances suivantes dans le fichier pom.xml :

pom.xml
Sélectionnez

<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>j2e-pac4j</artifactId>
    <version>1.0.2</version>
</dependency>
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-oauth</artifactId>
    <version>1.5.0</version>
</dependency>

La deuxième étape consiste à définir, pour chaque fournisseur d'identité, le client adéquat permettant de « jouer » l'authentification. Pour Facebook, Twitter et Google (fournisseurs OAuth), il faut créer une application OAuth auprès de chacun de ces fournisseurs pour pouvoir s'authentifier. Pour chaque application OAuth, une clé et un « secret » sont fournis par le fournisseur d'identité. Pour cela, on créera une classe dédiée :

MyClientsFactory.java
Sélectionnez

public class MyClientsFactory implements ClientsFactory {
    @Override
    public Clients build() {
        final FacebookClient facebookClient = new FacebookClient("fbKey", "fbSecret");
        final TwitterClient twitterClient = new TwitterClient("twKey", "twSecret");
        final Google2Client googleClient = new Google2Client("gooKey", "gooSecret");
        return new Clients("http://localhost:8080/callback", facebookClient, twitterClient, googleClient);
    }
}

Dans cet exemple, l'URL « http://localhost:8080/callback » est l'URL de callback de l'application utilisée par les fournisseurs d'identité pour terminer le process d'authentification.

Pour protéger les URL commençant par « /facebook » et déclencher l'authentification avec Facebook, il faut définir un filtre « RequiresAuthenticationFilter » dans le fichier « web.xml » :

web.xml
Sélectionnez

<filter>
    <filter-name>FacebookFilter</filter-name>
    <filter-class>org.pac4j.j2e.filter.RequiresAuthenticationFilter</filter-class>
    <init-param>
        <param-name>clientsFactory</param-name>
           <param-value>org.leleuj.config.MyClientsFactory</param-value>
    </init-param>
    <init-param>
           <param-name>clientName</param-name>
           <param-value>FacebookClient</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>FacebookFilter</filter-name>
    <url-pattern>/facebook/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

Dans l'exemple, l'application va intercepter toutes les requêtes dont l'URL commencera par « /facebook/ » et les faire passer par le filtre indiqué. Ce filtre pourra réaliser diverses opérations, comme laisser passer (si l'utilisateur est déjà authentifié) ou rediriger vers une page de connexion.

Pour que le processus d'authentification fonctionne (en définissant l'URL de callback), il est nécessaire de définir le filtre « CallbackFilter » dans le fichier « web.xml » :

web.xml
Sélectionnez

<filter>
    <filter-name>CallbackFilter</filter-name>
    <filter-class>org.pac4j.j2e.filter.CallbackFilter</filter-class>
    <init-param>
          <param-name>clientsFactory</param-name>
           <param-value>org.leleuj.config.MyClientsFactory</param-value>
    </init-param>
    <init-param>
          <param-name>defaultUrl</param-name>
           <param-value>/</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CallbackFilter</filter-name>
    <url-pattern>/callback</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

Il faut bien voir que l'utilisateur peut déclencher volontairement ou non l'authentification. Il la déclenche involontairement lorsqu'il tente d'accéder à une partie protégée du site. Il peut aussi la déclencher, volontairement cette fois, lorsqu'il clique sur le bouton/lien « Log in ». D'ailleurs, sur de nombreux sites, comme sur les webmails (Gmail, Hotmail, etc.), c'est souvent la seule action possible.

Si on veut explicitement déclencher l'authentification (pour Facebook par exemple), il suffit de calculer l'URL de redirection appropriée (vers Facebook) :

Calcul de la redirection
Sélectionnez

FacebookClient facebookClient = (FacebookClient) clients.findClient("FacebookClient");
String redirectionUrl = facebookClient.getRedirectAction(context, false, false).getLocation();

III. Protégez votre application Spring-Security

La bibliothèque Spring-Security est certainement l'une des plus populaires pour gérer la sécurité de son site. Dans cette section, nous allons voir comment mixer PAC4j et Spring-Security, car le fonctionnement est un peu spécifique.

La première étape pour intégrer l'authentification Facebook dans une application Spring-Security est là encore d'ajouter les dépendances correspondantes :

  • à pac4j-oauth (protocole utilisé par Facebook) ;
  • à spring-security-pac4j (puisque c'est une application Spring Security).

Pour Maven par exemple, il faut donc ajouter les dépendances suivantes dans le fichier « pom.xml » :

pom.xml
Sélectionnez

<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>spring-security-pac4j</artifactId>
    <version>1.2.2</version>
</dependency>
<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-oauth</artifactId>
    <version>1.5.0</version>
</dependency>

Dans une application Spring-Security, les clients sont définis dans le contexte Spring :

beans.xml
Sélectionnez

<bean id="facebookClient" class="org.pac4j.oauth.client.FacebookClient">
    <property name="key" value="fbKey" />
    <property name="secret" value="fbSecret" />
</bean>
<bean id="twitterClient" class="org.pac4j.oauth.client.TwitterClient">
    <property name="key" value="twKey" />
    <property name="secret" value="twSecret" />
</bean>
<bean id="googleClient" class="org.pac4j.oauth.client.Google2Client">
    <property name="key" value="gooKey" />
    <property name="secret" value="gooSecret" />
</bean>
<bean id="clients" class="org.pac4j.core.client.Clients">
    <property name="callbackUrl" value="http://localhost:8080/callback" />
    <property name="clients">
          <list>
               <ref bean="facebookClient" />
               <ref bean="twitterClient" />
               <ref bean="googleClient" />
           </list>
    </property>
</bean>

Pour protéger les URL commençant par « /facebook », une section « security:http » doit être définie avec l'entry point et le filtre adéquats dans le contexte de sécurité Spring :

beans.xml
Sélectionnez

<security:http entry-point-ref="facebookEntryPoint">
    <security:custom-filter after="CAS_FILTER" ref="clientFilter" />
    <security:intercept-url pattern="/facebook/**"     access="IS_AUTHENTICATED_FULLY" />
</security:http>
<bean id="facebookEntryPoint"     class="org.pac4j.springframework.security.web.ClientAuthenticationEntryPoint">
    <property name="client" ref="facebookClient" />
</bean>

Pour que le process d'authentification fonctionne (en définissant l'URL de callback), il est nécessaire de définir le filtre « ClientAuthenticationFilter » et le provider « ClientAuthenticationProvider » dans le contexte de sécurité Spring :

beans.xml
Sélectionnez

<bean id="clientFilter" class="org.pac4j.springframework.security.web.ClientAuthenticationFilter">
    <constructor-arg value="/callback"/>
    <property name="clients" ref="clients" />
    <property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="clientProvider" class="org.pac4j.springframework.security.authentication.ClientAuthenticationProvider">
    <property name="clients" ref="clients" />
</bean>

IV. Protégez vos autres applications

Vous pouvez également protéger vos applications Shiro, Play framework ou ratpack de manière similaire à ce qui a été fait pour JEE et Spring-Security en utilisant les bibliothèques dédiées.

Le serveur CAS utilise également la librairie pac4j via le module cas-server-support-pac4j pour déléguer l'authentification.

Si vous souhaitez utiliser un fournisseur d'identité qui n'existe pas encore pour pac4j, vous pouvez le développer vous-même. Ou si vous utilisez un framework Java pour lequel il n'existe aucune implémentation utilisable de pac4j, vous pouvez également la créer vous-même. Tous les protocoles implémentés par pac4j suivent la cinématique suivante :

Image non disponible

Lorsque l'utilisateur accède à une application protégée, 1) il est redirigé vers le fournisseur d'identité et s'y authentifie, 2) il est redirigé vers l'application avec des informations spécifiques et 3) l'application utilise ces informations spécifiques pour valider l'authentification de l'utilisateur et récupérer son profil.

Cette cinématique est représentée par l'interface « Client » qui doit être implémentée par tous les clients :

Clien.java
Sélectionnez

public interface Client<C extends Credentials, U extends UserProfile > {

    void redirect(WebContext context, boolean protectedTarget, boolean ajaxRequest) throws RequiresHttpAction;

    C getCredentials(WebContext context) throws RequiresHttpAction;

    U getUserProfile(C credentials, WebContext context);
}

Donc tous les clients comme Facebook, Twitter, Google, etc. (définis par les classes FacebookClient, TwitterClient, Google2Client, etc.) implémentent cette interface. En fait, il existe une hiérarchie complète de classes sous l'implémentation mère « org.pac4j.core.client.BaseClient » :

Image non disponible

Pour démarrer le processus d'authentification pour Facebook par exemple, la méthode « redirect » du client FacebookClient doit être appelée pour rediriger l'utilisateur vers le fournisseur d'identité afin de l'authentifier. Après une authentification réussie, l'utilisateur est redirigé vers l'URL de callback de l'application sur laquelle la méthode « getCredentials » doit être appelée afin de récupérer les informations spécifiques (« credentials ») renvoyées par le fournisseur d'identité. On a différents types de « credentials » suivant les protocoles :

Image non disponible

Avec ces « credentials », on récupère finalement le profil de l'utilisateur authentifié en appelant la méthode « getUserProfile » du client. Là aussi, on a une hiérarchie complète de profils suivant les différents clients :

Image non disponible

Tous les profils utilisateurs héritent de la classe « org.pac4j.core.profil.CommonProfile », qui regroupe un ensemble de méthodes habituelles : getEmail, getFirstName, getLastName…

Un profil utilisateur est défini par un identifiant, une liste d'attributs, des rôles, des permissions et un statut « remember-me ». Ces attributs sont récupérés du fournisseur d'identité et peuvent être définis précisément en implémentant l'interface « org.pac4j.core.profile.AttributesDefinition » qui donne les conversions à effectuer pour chacun des attributs récupérés. Par exemple, voici la définition d'attributs pour Facebook :

Pour Facebook
Sélectionnez

public class FacebookAttributesDefinition extends OAuthAttributesDefinition {
    
    public static final String NAME = "name";
    public static final String FIRST_NAME = "first_name";
    public static final String MIDDLE_NAME = "middle_name";
    public static final String LAST_NAME = "last_name";
    public static final String GENDER = "gender";
    public static final String LOCALE = "locale";
    public static final String LANGUAGES = "languages";

    ...

    public FacebookAttributesDefinition() {
        final String[] names = new String[] { 
            NAME, FIRST_NAME, MIDDLE_NAME, LAST_NAME, LINK, USERNAME, THIRD_PARTY_ID, 
            BIO, EMAIL, POLITICAL, QUOTES, RELIGION, WEBSITE };

        for (final String name : names) {
            addAttribute(name, Converters.stringConverter);
        }
        addAttribute(TIMEZONE, Converters.integerConverter);
        addAttribute(VERIFIED, Converters.booleanConverter);
        addAttribute(GENDER, Converters.genderConverter);
        addAttribute(LOCALE, Converters.localeConverter);
        addAttribute(UPDATED_TIME, Converters.dateConverter);
        addAttribute(BIRTHDAY, FacebookConverters.birthdayConverter);
        addAttribute(RELATIONSHIP_STATUS, FacebookConverters.relationshipStatusConverter);
        addAttribute(LANGUAGES, FacebookConverters.listObjectConverter);

        ...

Ainsi, pour un nouveau fournisseur d'identité, il vous faut créer votre client « MonFournisseurClient », gérant des informations spécifiques « MonFournisseurCredentials » et renvoyant un profil utilisateur « MonFournisseurProfile »…

Si aucun fournisseur d'identité ne vous manque, mais que vous n'avez pas d'implémentation PAC4J pour votre framework, vous pouvez créer vous-même votre bibliothèque « monframework-pac4j ».

Cette bibliothèque sera basée sur la dernière version du module « pac4j-core » qui est le socle commun (indépendant des protocoles) de PAC4J. Pour Maven par exemple, cela donnerait l'ajout suivant dans le fichier « pom.xml » :

pom.xml
Sélectionnez

<dependency>
    <groupId>org.pac4j</groupId>
    <artifactId>pac4j-core</artifactId>
    <version>1.5.0</version>
</dependency>

Vous devrez utiliser la classe « org.pac4j.core.client.Clients » (notez le « s » à la fin), qui permet de regrouper plusieurs clients sur une même URL de callback (sinon il vous faudrait plusieurs URL de callback : /callbackFacebook, /callbackTwitter, /callbackGoogle… ce qui complexifierait grandement les choses).

La bibliothèque pac4j interagit avec les requêtes/réponses HTTP via l'abstraction/interface « org.pac4j.core.context.WebContext » qui représente le contexte web courant. Une implémentation pour JEE existe via la classe « org.pac4j.core.context.J2EContext » mais vous serez peut-être amené à créer la vôtre, par exemple :

  • le contexte « io.buji.pac4j.ShiroWebContext » dédié au framework de securité Shiro ;
  • le contexte « org.pac4j.play.java.JavaWebContext » pour le framework Play en langage Java.

Ainsi, en utilisant la classe « Clients », le contexte web approprié et la méthode « redirect » du client choisi, vous pouvez sécuriser une URL et rediriger l'utilisateur vers le fournisseur d'identité pour qu'il s'authentifie.

La sécurisation d'une URL dépend évidemment des possibilités offertes par le framework. Dans le cas de JEE, le filtre « RequiresAuthenticationFilter » assure ce travail :

 
Sélectionnez

protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response, final HttpSession session, final FilterChain chain) throws IOException, ServletException {

    final CommonProfile profile = UserUtils.getProfile(request);
    logger.debug("profile : {}", profile);

    // profile not null, already authenticated -> access
    if (profile != null) {
        chain.doFilter(request, response);

    } else {
        // no authentication tried -> redirect to provider
        // keep the current url
        String requestedUrl = request.getRequestURL().toString();
        String queryString = request.getQueryString();
        if (CommonHelper.isNotBlank(queryString)) {
            requestedUrl += "?" + queryString;
        }
        logger.debug("requestedUrl : {}", requestedUrl);
        session.setAttribute(ORIGINAL_REQUESTED_URL, requestedUrl);

        // compute and perform the redirection
        final WebContext context = new J2EContext(request, response);
        Client<Credentials, CommonProfile> client = ClientsConfiguration.getClients().findClient(this.clientName);

        try {
            client.redirect(context, true, false);
        } catch (RequiresHttpAction e) {
            logger.debug("extra HTTP action required : {}", e.getCode());
        }
    }
}

Dans le cas de Spring-Security, c'est l'entry point « ClientAuthenticationEntryPoint » (déclenché lorsqu'une authentification est nécessaire) qui joue ce rôle :

 
Sélectionnez

public final class ClientAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {

    private Client<Credentials, UserProfile> client;

    public void commence(final HttpServletRequest request, final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException {
        logger.debug("client : {}", this.client);
        final WebContext context = new J2EContext(request, response);

        try {
            this.client.redirect(context, true, false);
        } catch (final RequiresHttpAction e) {
            logger.debug("extra HTTP action required : {}", e.getCode());
        }
    }

Après une authentification réussie, l'utilisateur est redirigé sur l'URL de callback de l'application où l'implémentation de pac4j adéquate doit récupérer les « credentials » et le profil utilisateur en utilisant les Clients et les méthodes « getCredentials » et « getUserProfile ».

Dans le cas de JEE, c'est le filtre « CallbackFilter » qui effectue ces tâches et initialise ainsi le contexte de sécurité (avec le profil de l'utilisateur) :

 
Sélectionnez

public class CallbackFilter extends ClientsConfigFilter {
    
    protected void internalFilter(final HttpServletRequest request, final HttpServletResponse response, 
                        final HttpSession session, final FilterChain chain) throws IOException,    ServletException {
        
        final WebContext context = new J2EContext(request, response);
        final Client client = ClientsConfiguration.getClients().findClient(context);
        logger.debug("client : {}", client);
        
        final Credentials credentials;
        try {
            credentials = client.getCredentials(context);
        } catch (final RequiresHttpAction e) {
            logger.debug("extra HTTP action required : {}", e.getCode());
            return;
        }
        logger.debug("credentials : {}", credentials);
        
        // get user profile
        final CommonProfile profile = (CommonProfile) client.getUserProfile(credentials,     context);
        logger.debug("profile : {}", profile);
        
        if (profile != null) {
            // only save profile when it's not null
            UserUtils.setProfile(session, profile);
        }
        
        final String requestedUrl = (String) session.getAttribute(RequiresAuthenticationFilter.ORIGINAL_REQUESTED_URL);
        logger.debug("requestedUrl : {}", requestedUrl);
        if (CommonHelper.isNotBlank(requestedUrl)) {
            response.sendRedirect(requestedUrl);
        } else {
            response.sendRedirect(this.defaultUrl);
        }
    }
}

Dans le cas de Spring-Security, la récupération des « credentials » et du profil utilisateur s'effectue en deux temps via le filtre approprié et le provider adéquat.

Pour créer votre propre implémentation de pac4j pour votre framework, vous devrez donc fournir les mécanismes nécessaires pour protéger des URL et terminer le processus d'authentification.

V. Conclusion

C'est souvent la croix et la bannière de sécuriser une application Web à l'aide des protocoles à la mode (OAuth, OpenId, etc.) et d'autant plus lorsqu'on en utilise plusieurs. Or les fournisseurs comme Google, Linked'in, Yahoo (et bien d'autres) ne rendent pas cette tâche si facile, malgré leurs efforts.

Simplifiez-vous donc la vie et utilisez pac4j pour gérer toutes vos authentifications sur toutes vos plateformes.

Alors ? A-t-on réussi le pari de la connexion OAuth, OpenId, etc. en moins de cinq minutes ? À condition d'avoir tous les identifiants et les URL sous la main, je pense sincèrement que c'est jouable. Mais la balle est dans votre camp. À vous de nous dire si c'est gagné.

Dans tous les cas, l'équipe qui développe PAC4J est très réactive et répond volontiers aux questions. N'hésitez donc pas à consulter le site de la bibliothèque et, si besoin, à soumettre un ticket.

Vos retours nous aident à améliorer nos publications. N'hésitez donc pas à commenter cet article sur le forum : 7 commentaires Donner une note à l'article (5)

VI. Remerciements

D'abord j'adresse mes remerciements à l'équipe de Pac4J, pour avoir développé une bibliothèque aussi utile et pour la maintenir. Je n'oublie pas tous les contributeurs qui participent à son amélioration, notamment pour l'ajout de nouveaux protocoles.

Je voudrais aussi remercier Jérôme Leleu qui a très largement contribué à l'écriture de cet article.

Image non disponible

Plus spécifiquement en ce qui concerne cet article, je tiens à remercier l'équipe de Developpez.com et plus particulièrement Fabien.

VII. Annexes

VII-A. Liens

VII-B. À propos des auteurs

Jérôme est responsable technique chez SFR, Chairman du projet open source CAS, créateur d'une bibliothèque cliente de sécurité unifiée pour les protocoles OAuth, CAS, OpenID et HTTP : http://www.pac4j.org

Retrouvez La page de Thierry et ses autres articles sur Developpez.com à l'adresse
http://thierry-leriche-dessirier.developpez.com/#page_articlesTutoriels

Et suivez Thierry sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)@thierryleriche