Nous allons maintenant travailler sur la partie la plus importante de notre formulaire de connexion : nous allons ajouter une authentification OAuth à l'application web.
Mettez en place un formulaire de connexion pour travailler avec OAuth 2.0 et OIDC
Retrouvez moi dans la démonstration suivante pour configuration une connexion avec OAuth 2.0 et GitHub :
Vous avez du pain sur la planche, mais vous pouvez le faire ! Vous avez déjà créé un formulaire avec la méthode formLogin()
et une authentification in-memory.
Vous avez également eu recours à l'annotation @Configuration, aux classes SecurityFilterChain et HttpSecurity pour votre formulaire de connexion basique avec Spring Security. Il n’y a pas beaucoup de différences avec la manière dont vous commencerez votre formulaire de connexion avec les nouveaux paramètres de sécurité.
Pour utiliser notre application web client avec une connexion GitHub, vous aurez besoin de vous enregistrer sur GitHub pour obtenir une client id et un client secret. Ces deux appellations font référence aux traditionnels nom d’utilisateur et mot de passe pour votre application web ; c'est ce qui vous permet de vous connecter au serveur d’autorisation de GitHub avec OAuth 2.0.
Si tout fonctionne, vous obtiendrez vos propres client id et client secret. Mettez-les de côté pour plus tard.
Une fois que vous avez obtenu vos client id et client secret, vous pouvez les renseigner votre fichier application.properties sous le dossier src/java/resources. Cela dirigera directement les utilisateurs OAuth vers le serveur d’autorisation approprié.
Pour définir GitHub en tant que page de connexion OAuth 2.0, vous pouvez copier-coller les lignes ci-dessous dans votre fichier application.properties, mais assurez-vous de remplacer client id et client secret par vos propres informations de connexion générées par GitHub !
spring.security.oauth2.client.registration.github.client-id=<your client ID>
spring.security.oauth2.client.registration.github.client-secret=<client-secret>
Une fois ces lignes collées, votre fichier application.properties devrait ressembler à ça :
Configurez OAuth 2.0 dans votre page de connexion
Maintenant, ajoutons manuellement la connexion par défaut de OAuth 2.0 à votre page. Vous avez simplement à ajouter la méthode oauth2login(Customizer.withDefaults())
à votre chaîne de filtres de sécurité dans votre fichier SpringSecurityConfig.java.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.authorizeHttpRequests(auth -> {
auth.requestMatchers("/admin").hasRole("ADMIN");
auth.requestMatchers("/user").hasRole("USER");
auth.anyRequest().authenticated();
}).formLogin(Customizer.withDefaults()).oauth2Login(Customizer.withDefaults()).build();
}
Ajoutons une page d'accueil pour GitHub à votre contrôleur, à l'aide de la méthode getGithub()
:
package com.openclassrooms.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@GetMapping("/user")
public String getUser() {
return "Welcome, User";
}
@GetMapping("/admin")
public String getAdmin() {
return "Welcome, Admin";
}
@GetMapping("/")
public String getGitHub() {
return "Welcome, GitHub user";
}
}
À présent, la mention “Welcome, GitHub user” (Bienvenue, utilisateur GitHub) devrait apparaître après l’authentification avec GitHuB lors de la consultation de la page http://localhost:8080/.
Connectez-vous avec OAuth 2.0
Eh bien… voilà qui est intéressant :
En supplément du formulaire de connexion Spring Security, vous disposez à présent du formulaire de connexion par défaut de OAuth 2.0.
Maintenant, cliquez sur le lien du serveur d’autorisation de GitHub.
Cette page va vous rediriger vers un lien spécifique de votre application web. J’ai intitulé le mien OAuth2OpenClassrooms lorsque j’ai enregistré mon application web sur GitHub. Connectez-vous à l’aide de vos identifiants GitHub.
Vous devriez arriver sur la page d’accueil.
Explorez le Principal User
Personnalisons votre mot de bienvenue avec le nom d’utilisateur.
Comment s’y prendre ?
Commencez par jeter un œil aux informations transmises par GitHub, et voyez ce que vous pouvez extraire.
Les ressources protégées peuvent être, par exemple, des informations spécifiques concernant l’utilisateur et votre token d’accès. Les ressources non protégées peuvent être votre nom d’utilisateur, dans le but de le rendre disponible plus facilement. Dans GitHub, les ressources non protégées détiennent un grand nombre d’informations. Voyons ça !
Définissons un paramètre de type Principal
et de nom user
dans la méthode getGithub()
. Généralement, on peut récupérer le nom d'utilisateur à partir d'un objetPrincipal
en utilisant la méthode user.getName()
.
Cela devrait vous indiquer Welcome, <my GitHub username>.
public String getGithub(Principal user){
return "Welcome, " + user.getName();
}
Ça n'a pas l’air de fonctionner… Une donnée numérique apparaît :
Voyons ce qu’on peut extraire de l'objet Principal user
.
Ajoutez une ligne à la méthode getGithub()
pour vérifier ce qui nous a été envoyé via l’objet Principal user
. Pour ce faire, renvoyez l’objet Principal user
converti en String
grâce à la méthode user.toString()
.
Vous êtes sûr que le nom d’utilisateur va s’afficher avecuser.toString()
?
C’est ce qu’on va vérifier.
public String getGithub(Principal user){
return user.toString();
}
Cette fois-ci, la page d’accueil de getGitHub a l’air complètement différente. Regardez ça :
L’interface montre les informations du profil d’utilisateur que j’ai autorisées lorsque je me suis connecté à mon application GitHub OAuth 2.0 au cours de l’authentification. C’est le scope. Vous vous souvenez ? Il s’agit d'informations de profil en lecture seule dont l’application web client dispose, suite à votre autorisation.
Obtenez des données protégées depuis le Principal user
Je souhaite afficher mon nom complet ainsi que mon adresse mail. J’ai aussi envie de savoir à quoi ressemble mon token d’accès.
De plus, je veux que cette méthode soit directement prête à l’utilisation pour d’autres fournisseurs d'identité OAuth 2.0, et pas uniquement GitHub.
Il faut tout d’abord supprimer l’intégralité de la méthode getGithub()
de la classe LoginController.java. Vous pouvez le faire car vous allez travailler avec des ressources protégées, comme le token d’accès, et l’ID Base64. La classe d’authentification ne sera pas utile à la saisie de ces tokens protégés JWT.
Vous avez besoin d’une nouvelle méthode afin que votre classe LoginController fonctionne pour la connexion avec OAuth 2.0 mais aussi pour la connexion avec le formulaire.
Appelez cette méthode getUserInfo()
pour lui attribuer un nom générique qui s’applique à plusieurs fournisseurs d'identité, et non uniquement à GitHub. Le Principal reste votre unique paramètre, contenant les informations de l’utilisateur, envoyé par les serveurs d’autorisation.
@GetMapping("/*")
public String getUserInfo(Principal user) {
}
Ici, Il va vous falloir trouver un moyen de mettre les attributs des utilisateurs en mémoire et ensuite les récupérer. Il existe plusieurs manières de travailler avec le contenu d’une chaîne de caractères.
En l'occurrence, j’utilise la classe StringBuffer avec une instance nomméeuserinfo
, car c’est une manière propre de créer des données avec des chaînes de caractères, et d’y ajouter d’autres attributs d’utilisateur.
public String getUserInfo(Principal user) {
StringBuffer userInfo= new StringBuffer();
return userInfo.toString();
}
C’est le moment d’ajouter votre nom intégral, votre adresse mail et les informations de votre token d’accès à votre StringBuffer
dans la méthodegetUserInfo()
.
Pour cela, mettez en place deux autres méthodes ( getUsernamePasswordLoginInfo()
et getOAuth2LoginInfo()
) pour obtenir ces informations. Celles-ci vont retourner un objet de type StringBuffer
.
Pour la première méthode getUsernamePasswordLoginInfo()
, la classe UsernamePasswordAuthenticationToken permettra de récupérer le nom de l’utilisateur, après avoir vérifié que le token est authentifié, en utilisant la méthode getPrincipal()
.
Ensuite, cette information sera ajoutée à l’instance du StringBuffer nommée usernameInfo
.
Vous pouvez ajouter cette méthode sous la méthode getUserInfo()
.
private StringBuffer getUsernamePasswordLoginInfo(Principal user)
{
StringBuffer usernameInfo = new StringBuffer();
UsernamePasswordAuthenticationToken token = ((UsernamePasswordAuthenticationToken) user);
if(token.isAuthenticated()){
User u = (User) token.getPrincipal();
usernameInfo.append("Welcome, " + u.getUsername());
}
else{
usernameInfo.append("NA");
}
return usernameInfo;
}
Lorsque vous vous connecterez avec le formulaire Spring Security, c’est ce précédent code qui s'exécutera.
Dans l’autre méthode nommée getOauth2LoginInfo()
, déclarez une variable intitulée authToken de type OAuth2AuthenticationToken et castez l’objet Principal user
dans cette nouvelle variable.
La classe OAuth2AuthenticationToken contient des méthodes à utiliser pour des ressources protégées, comme celles contenues dans l’objet user
.
private StringBuffer getOauth2LoginInfo(Principal user){
StringBuffer protectedInfo = new StringBuffer();
OAuth2AuthenticationToken authToken = ((OAuth2AuthenticationToken) user);
}
Grâce à la classe OAuth2AuthenticationToken, l’application client a la permission d’accéder à davantage de ressources protégées, comme le token d‘accès.
Mais avant cela, créez une variable privée pour votre classe LoginController de type OAuth2AuthorizedClientService, et intitulez-la authorizedClientService
:
private final OAuth2AuthorizedClientService authorizedClientService;
Créez un constructeur public pour votre classe LoginController pour l’injection de la dépendance :
public LoginController(OAuth2AuthorizedClientService authorizedClientService) {
this.authorizedClientService = authorizedClientService;
}
Revenez dans la méthode getOauth2LoginInfo()
et récupérez l’instance de l’objet OAuth2AuthorizedClient. Vous remarquerez que la méthode loadAuthorizedClient retourne le client qui correspond à l’ID et au nom du principal transmis en paramètre.
Pour le moment, la méthode renvoie protectedInfo
, qui ne contient aucune donnée.
private StringBuffer getOauth2LoginInfo(Principal user){
StringBuffer protectedInfo = new StringBuffer();
OAuth2AuthenticationToken authToken = ((OAuth2AuthenticationToken) user);
OAuth2AuthorizedClient authClient = this.authorizedClientService.loadAuthorizedClient(authToken.getAuthorizedClientRegistrationId(), authToken.getName());
return protectedInfo;
}
De retour dans la méthode getUserInfo()
, il faut extraire les informations souhaitées de l’objet Principal user grâce aux 2 méthodes précédemment créées puis les ajouter dans le StringBuffer.
public String getUserInfo(Principal user) {
StringBuffer userInfo= new StringBuffer();
if(user instanceof UsernamePasswordAuthenticationToken){
userInfo.append(getUsernamePasswordLoginInfo(user));
} else if(user instanceof OAuth2AuthenticationToken){
userInfo.append(getOauth2LoginInfo(user));
}
return userInfo.toString();
}
Désormais, vous disposez des informations nécessaires pour aller plus loin et afficher le nom complet, l’adresse mail et le token d’accès dans le cas d’une connexion OAuth2.
Utilisez une variable HashMap userAttributes
pour récupérer l'ensemble des attributs.
Revenez à votre méthode getOauth2LoginInfo()
, et ajoutez dans la variable protectedInfo
les informations de l’utilisateur lors de sa connexion. Le nom et le mail sont des attributs qui sont extraits du principal. Tandis que pour le token d’accès, ajoutez-le en utilisant les méthodes getAccessToken()
et getTokenValue()
.
La variable userToken
se voit attribuer la valeur du token d’accès. Elle contiendra donc la chaîne de caractères encodés du token d’accès. Tout cela est ensuite ajouté à protectedInfo
.
private StringBuffer getOauth2LoginInfo(Principal user){
StringBuffer protectedInfo = new StringBuffer();
OAuth2AuthenticationToken authToken = ((OAuth2AuthenticationToken) user);
OAuth2AuthorizedClient authClient = this.authorizedClientService.loadAuthorizedClient(authToken.getAuthorizedClientRegistrationId(), authToken.getName());
if(authToken.isAuthenticated()){
Map<String,Object> userAttributes = ((DefaultOAuth2User) authToken.getPrincipal()).getAttributes();
String userToken = authClient.getAccessToken().getTokenValue();
protectedInfo.append("Welcome, " + userAttributes.get("name")+"<br><br>");
protectedInfo.append("e-mail: " + userAttributes.get("email")+"<br><br>");
protectedInfo.append("Access Token: " + userToken+"<br><br>");
}
else{
protectedInfo.append("NA");
}
return protectedInfo;
}
Serai-je en mesure de voir ces informations ?
Voici une capture d’écran de mes résultats :
Mais pourquoi l’adresse mail n'apparaît pas ? 😭
Lorsque vous procédez à l'extraction d’informations depuis l’objet Principal user
, vous avez des informations protégées et non protégées.
Vous pouvez avoir recours à certaines classes, comme principal et authentification, uniquement pour extraire des informations non protégées.
Les classes capables d’extraire les informations protégées disposent de couches d’abstraction, et requièrent davantage de droits. GitHub garde votre adresse mail dans la partie protégée. La seule manière de l’extraire, c’est grâce à l'ID token avec le claim openid email
dans le scope.
Cela nous amène à OpenID Connect !
Utilisez OpenID Connect avec Google
Pourquoi est-ce essentiel d’utiliser OpenID Connect pour une authentification ?
Google est une excellente ressource pour OAuth 2.0, et dispose d’un support performant pour OpenID Connect.
Dans le formulaire, créez un nom pour votre application web client OAuth 2.0.
Afin de travailler sur votre localhost:8080
, mettez en place la configuration suivante :
Restrictions
Sauvegardez vos client ID et client secret, afin de saisir les propriétés Google dans votre fichier application.properties, sous les propriétés de GitHub.
spring.security.oauth2.client.registration.google.client-id=<clientid>.apps.googleusercontent.com
spring.security.oauth2.client.registration.google.client-secret=<clientsecret>
La clé pour ajouter OpenID Connect à votre requête originale dans le serveur de connexion est le scope pour openid
, ainsi que les claims profil
et email
.
Heureusement, ce scope et ces claims sont définis par défaut dans notre configuration de sécurité !
Ces derniers vont être récupérés avec le token ID.
À présent, vous devez ajouter la récupération du token ID dans la classe LoginController.java. Cette procédure contribue au fonctionnement du scope openid
.
Heureusement, Spring Security va nous aider. Dans le prototype de la méthode getUserInfo() on peut injecter un objet OidcUser grâce à l’annotation @AuthenticationPrincipal.
@GetMapping("/*")
public String getUserInfo(Principal user, @AuthenticationPrincipal OidcUser oidcUser) {
StringBuffer userInfo = new StringBuffer();
if (user instanceof UsernamePasswordAuthenticationToken) {
userInfo.append(getUsernamePasswordLoginInfo(user));
} else if (user instanceof OAuth2AuthenticationToken) {
userInfo.append(getOAuth2LoginInfo(user, oidcUser));
}
return userInfo.toString();
}
Puis transmettez cet objet OidcUser à la méthode getOauth2LoginInfo()
.
Maintenant que vous avez renvoyé votre OidcUser à votre méthode getOauth2LoginInfo()
, vous allez ajouter du code pour que celle-ci sache quoi en faire.
Commencez avec la classe OidcIdToken.
Créez une variable idToken
de type OidcIdToken pour contenir votre token ID. Ce dernier sera assigné via la fonction getIdToken()
de l’OidcUser passé en paramètre.
OidcIdToken idToken = oidcUser.getIdToken();
À présent, ajoutez le Token ID dans protectedInfo
. Le Token ID contient les informations des claims (autorisations) que vous avez requises via le scope.
Avec openid
requis par défaut, des claims supplémentaires ont été ajoutées pour profil vérifié et e-mail, afin d’authentifier l’utilisateur.
En utilisant le code ci-dessous, vous obtenez un HashMap, avec vos claims que vous pouvez récupérer en ayant recours à la méthode getClaims()
spécifiée dans la classe OidcIdToken
.
if(oidcUser != null) {
OidcIdToken idToken = oidcUser.getIdToken();
if(idToken != null) {
protectedInfo.append("idToken value: " + idToken.getTokenValue()+"<br><br>");
protectedInfo.append("Token mapped values <br><br>");
Map<String, Object> claims = idToken.getClaims();
for (String key : claims.keySet()) {
protectedInfo.append(" " + key + ": " + claims.get(key)+"<br>");
}
}
}
J’espère que vous vous êtes amusé ! Il existe une myriade de manières de mettre en place cette page en toute sécurité, en ayant recours à une autorisation et à une authentification basées sur des tokens.
En résumé
Vous avez suivi les étapes suivantes pour créer un formulaire de connexion OAuth 2.0 avec OIDC :
Configurer la connexion par défaut OAuth 2.0 dans la chaîne de filtres de sécurité.
Enregistrer votre application web avec GitHub et Google, pour avoir un client ID et un client secret.
Ajouter la configuration GitHub dans le fichier application.properties.
Extraire les informations du principal user détails et du token d’accès depuis GitHub.
Configurer OIDC avec GitHub dans le fichier application.properties.
Extraire le principal, le token d’accès, et le ID Token depuis Google.
Décoder l’ID Token pour extraire les informations du claim.
Vous avez ajouté de super options pour autoriser les utilisateurs à accéder à votre application web, mais comment s’assurer que les utilisateurs lambda n’aient pas accès aux pages qu’ils ne sont pas autorisés à consulter ?
C’est important, il est donc judicieux de mettre en place des tests pour vous assurer que cela fonctionne ! C'est ce que nous allons voir dans le prochain chapitre.