Voire comment intégré l’authentification OAuth 2.0 à Liferay pour accéder aux service d’Office 365.
Office 365 est aujourd’hui de plus en plus utilisé en entreprise. L’intégration d’office 365 avec un portail devient un requis pour ajouter des fonctionnalités telles que l’enregistrement d’un événement dans son agenda, la création d’un répertoire de contact depuis Azure AD ou afficher des messages depuis Teams. La documentation de Microsoft explique bien comment utiliser le Microsoft Graph API avec plusieurs technologies, mais quand il est temps de le faire fonctionner avec une application web java, ça se complexifie légèrement. Ajoutez un portail Liferay et ça devient périlleux. La partie la plus complexe est de faire l’authentification avec Oauth 2.0.
Il y a plusieurs éléments à combiner pour faire fonctionner le tout correctement pour vos utilisateurs :
ScribeJava est conçu pour rendre OAuth 2.0 facile. La première chose à faire est de créer un service qui soit configuré pour notre application.
1 2 3 4 5 6 | OAuth20Service azureAuthService = new ServiceBuilder(configuration.apiKey()) .withScope("openid offline_access Calendars.ReadWrite") .apiKey(configuration.apiKey()) .apiSecret(configuration.apiSecret()) .callback(authentication.getCallBackURL()) .build(MicrosoftAzureActiveDirectory20Api.instance()); |
configuration est l’objet de configuration qui contient les informations de notre application Azure
Le scope contient les droits d’accès qui sont demandés au Microsoft graph api, openid est requis pour faire l’authentification, offline_access donne accès à l’api sans que l’utilisateur ne soit présent, cela nous permet surtout d’obtenir un refresh token qui nous permettra de réduire le nombre de redirections subi par l’utilisateur à son retour.
Le callbackURL est l’URL utilisée pour rediriger l’utilisateur vers votre application après son authentification, nous allons voir comment l’utiliser plus loin.
Le service azureAuthService comporte trois appels intéressant que nous utiliserons.
Obtenir l’URL d’autorisation:
1 2 3 4 5 | Map<String, String> params = new HashMap<>(); params.put("response_mode","form_post"); params.put("state",backURL); params.put("prompt",prompt); String authorizationUrl = authService.getAuthorizationUrl(params); |
Nous utilisons le response_mode = form_post pour que le retour vers le callbackURL de notre application soit fait avec une requête POST. C’est plus sécuritaire qu’avec une requête GET et surtout plus propre pour l’utilisateur.
Le state sera retournée telle quelle à notre callbackUrl, on l’utilisera pour déterminer vers quelle page rediriger l’utilisateur une fois l’authentification complétée.
Le prompt nous permet de configurer l’invite de connexion Microsoft, nous verrons l’utilité bientôt.
Avec l’authorizationUrl en main, nous pouvons envoyer les utilisateurs vers celle-ci et nous recevrons ensuite un id_token lorsqu’ils seront authentifiés.
Obtenir l’access token:
1 | OAuth2AccessToken accessToken = authService.getAccessToken(id_token); |
Le ID_token que l’ont reçoit doit ensuite être validé et utilisé pour obtenir un accessToken. C’est celui-ci qui nous permettra de signer nos prochaines requêtes au Graph API.
Obtenir un nouvel access token quand il expire:
1 | OAuth2AccessToken accessToken = authService.refreshAccessToken(refreshToken); |
L’access token a normalement une durée de vie de moins d’une heure. Avec un refresh token, on peut obtenir un nouvel access token sans avoir à retourner l’utilisateur vers l’authorizationUrl à nouveau. Le refresh token est normalement valide pour au moins un mois, mais il peut être révoqué depuis Azure.
L’étape suivante est d’intégrer le processus de redirection nécessaire à l’authentification de l’utilisateur. Il y a une bonne explication du processus dans la documentation de Microsoft. Pour résumer, nous devons :
Pour pouvoir gérer l’authentification dans une application web, nous devons avoir un accès complet à la requête. L’utilisation d’un filtre de servlet est parfaite pour cela. À noter que ce filtre pourrait facilement être modifié pour une utilisation hors Liferay. Voici le filtre utilisé pour écouter sur le callbackURL :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | protected void processFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { try { String code = ParamUtil.get(request, "code", ""); O365Authentication authentication = authenticationService.getAuthentication(request); if(Validator.isNull(code)) { String backURL = ParamUtil.getString(request, AuthenticationService.BACK_URL_PARAM, "/"); // step 0 - test if user is already authenticated if(authenticationService.isConnected(authentication, authenticatedServiceTracker.getScope())){ String contextUrl = HttpUtil.decodeURL(backURL); response.sendRedirect(contextUrl); return; } // Step 1 - redirection to o365 authentication String error = ParamUtil.getString(request, "error"); String prompt = AuthenticationService.PROMPT_NONE; switch (error) { case "login_required": case "interaction_required": prompt = AuthenticationService.PROMPT_LOGIN; break; case "consent_required": prompt = AuthenticationService.PROMPT_CONSENT; break; case "access_denied": // the user refuse to give access, we return it to the portal page String state = ParamUtil.getString(request, "state"); String contextUrl = HttpUtil.decodeURL(state); LOG.debug("Send user to " + contextUrl); response.sendRedirect(contextUrl); return; } String authenticationURL = authenticationService.getAuthenticationURL(authentication, backURL, prompt); LOG.debug("Send user to "+authenticationURL); response.sendRedirect(authenticationURL); } else { // Step 2 - token validation LOG.debug("Received id_token: "+code); authenticationService.validateIdToken(authentication, code); String state = ParamUtil.getString(request, "state"); String contextUrl = HttpUtil.decodeURL(state); LOG.debug("Send user to "+contextUrl); response.sendRedirect(contextUrl); } } catch (Exception e) { throw new PortalException("Cannot authenticate user to o365", e); } } |
Le paramètre code contiens l’id_token une fois que l’utilisateur est authentifié avec succès.
Nous utilisons un seul filtre pour gérer le processus entier. Pour pouvoir démarrer le processus d’authentification, nous devons donc envoyer l’utilisateur vers ce filtre. Comme aucun code ne sera reçu à ce moment, l’étape 0 sera enclenchée.
Pour que l’authentification soit la moins intrusive possible pour l’utilisateur, on commence par vérifier si nous avons un accessToken valide. La méthode authenticationService.isConnected permet de faire cette vérification et utilisera automatiquement le refreshToken si requis afin d’obtenir un nouvel accessToken.
Si nous avons un accessToken valide, nous redirigeons directement l’utilisateur vers sa page cible, sinon, nous continuons à l’étape 1.
Pour authentifier l’utilisateur, nous devons le rediriger vers l’authenticationURL. Cette URL est configurée en fonction de la situation de l’utilisateur actuel. Dans le meilleur des cas, l’utilisateur est déjà connecté à Office 365, ce qui est fortement probable dans un environnement d’entreprise. Nous essayons donc d’authentifier l’utilisateur sans interface du côté de Microsoft (promt=none), cela n’affichera qu’une page blanche en attendant qu’il soit redirigé vers notre application. Si cela échoue, nous recevrons une erreur interaction_required, nous pouvons donc rediriger à nouveau l’utilisateur en demandant cette fois l’invite de connexion (prompt=login), dans ce cas, l’utilisateur verra une page blanche pendant un court moment puis l’interface de connexion de Microsoft. À l’exception des changements d’URL, il ne devrait pas voir qu’il est redirigé 4 fois. Il reste deux cas d’erreur particuliers : consent_required, si nous demandons des accès que l’utilisateur n’a jamais approuvé, dans ce cas, nous le redirigeons vers l’authenticationURL avec le paramètre prompt = conscent, l’interface de Microsoft affichera directement le formulaire d’approbation des permissions sans passer par celui d’authentification. Le second cas d’erreur est access_denied, celui-ci survient si l’utilisateur, ou son administrateur, refuse d’approuver les permissions demandées. L’application ne peut pas corriger d’elle-même cette erreur, il faudra en informer l’utilisateur. Dans ce cas, nous redirigeons l’utilisateur à sa page cible et les erreurs seront gérées à partir des points d’utilisation de l’API.
La seconde étape survient une fois que l’utilisateur s’est complètement authentifié et a accepté toutes les permissions demandées. On demande donc la validation du token ce qui sauvegardera les informations d’authentification pour une utilisation future. On peut maintenant rediriger l’utilisateur à sa page cible et utiliser les différentes API disponibles.
Le SDK MsGraph pour java est bien documenté. La seule partie manquante est comment intégrer l’authentification provenant de ScribeJava à la librairie MsGraph. Comme indiqué, nous devons écrire un AuthenticationProvider qui sera utilisé pour signer chacune des requêtes envoyées depuis la librairie. Voici l’implémentation du AuthenticationProvider:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | final class ScribejavaAuthenticationProvider implements IAuthenticationProvider { private final O365Authentication authentication; private static final String AUTHORIZATION_HEADER_NAME = "Authorization"; private static final String OAUTH_BEARER_PREFIX = "bearer "; ScribejavaAuthenticationProvider(O365Authentication authentication) { if(authentication == null || authentication.getAccessToken()==null){ throw new RuntimeException("User need to be logged in Office 365 before calling the API." ); } this.authentication = authentication; } @Override public void authenticateRequest(IHttpRequest request) { OAuth2AccessToken accessToken = (OAuth2AccessToken) authentication.getAccessToken(); request.addHeader(AUTHORIZATION_HEADER_NAME, OAUTH_BEARER_PREFIX + accessToken.getAccessToken()); } } |
Nous pouvons ensuite ajouter un événement à notre agenda:
1 2 3 4 5 6 7 | IAuthenticationProvider authenticationProvider = new ScribejavaAuthenticationProvider(authentication); IGraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider(authenticationProvider).buildClient(); Event event = new Event(); // I let you create the event as you need graphClient.me().events().buildRequest().post(event); |
J’ai montré comment mettre en place une authentification à office 365 via Oauth 2.0 à partir d’une application web Java. Cette base peut permettre d’accéder à différents services basés sur OAuth 2.0. Bien que certains outils de Liferay aient été utilisés dans les exemples, ils peuvent facilement être remplacés par des librairies plus génériques si votre cas d’utilisation n’est pas intégré dans un portail Liferay. Le code fonctionnel complet est disponible ici : https://github.com/savoirfairelinux/office-365-integration
Comme indiqué au début, intégrer ceci à Liferay comporte certains défis supplémentaires. On regardera ceux-ci dans le prochain article.
Récemment, notre département en Intelligence Artificielle a livré une preuve de concept d’un robot autonome utilisé pour explorer et cartographier des espaces industriels et résidentiels inconnus tout en identifiant les différents types de sols. Le robot (à l’exception de son châssis Roomba) est entièrement construit à partir de composants matériels et logiciels open-source (OS). Le […]
Cet article survient suite à la réalisation, par notre équipe Plateformes d’intégration et Intelligence Artificielle, d’un site informationnel pour l’un de nos clients, une grande entreprise canadienne de télécommunication et de médias, en utilisant Liferay 7 (la dernière version du portail Liferay). Alexis, développeur front-end, vous partage son expérience sur ce projet afin de vous […]
Le Thumbnail Generator vise à améliorer la génération de thumbnails, proposée par Liferay. Ce plugin a été créé au cours d’un projet nécessitant la présence d’un très grand nombre de thumbnails de dimensions précises, afin d’optimiser au maximum les temps de chargement des pages Web. En effet, Liferay offre seulement deux thumbnails lors du chargement […]
Premier diffuseur de ressources francophones en sciences humaines et sociales en Amérique du Nord, Érudit s’est refait une beauté en 2017. Fruit d’un travail d’un an et demi, cette refonte technique et visuelle offre de nouvelles fonctionnalités, auxquelles notre expert Python Morgan Aubert s’est fait un grand plaisir de contribuer. En accès libre Avant d’aborder […]