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.
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 :
<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 :
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 » :
<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 » :
<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) :
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 » :
<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 :
<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 :
<
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 :
<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 :
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 :
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 » :
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 :
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 :
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 :
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 » :
<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 :
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 :
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) :
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
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.
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▲
pac4j : https://github.com/leleuj/pac4j
Mailings lists : https://groups.google.com/forum/?fromgroups#!forum/pac4j-users et https://groups.google.com/forum/?fromgroups#!forum/pac4j-dev
JIRA : https://pac4jos.atlassian.net/secure/Dashboard.jspa?selectPageId=10100
Site de présentation : http://www.pac4j.org/
Modules dédiés :
- Shiro : https://github.com/bujiio/buji-pac4j
- Play framework : https://github.com/leleuj/play-pac4j / https://github.com/leleuj/play-pac4j-scala-demo
- Ratpack : https://github.com/ratpack/ratpack/tree/master/ratpack-pac4j
- CAS : http://jasig.github.io/cas/current/integration/Delegate-Authentication.html
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
https://thierry-leriche-dessirier.developpez.com/#page_articlesTutoriels
Et suivez Thierry sur Twitter : @thierryleriche(https://twitter.com/thierryleriche)@thierryleriche