TutorielGuides

Application web Spring Boot avec OpenID Connect

Sur cette page

Une application web classique, dont les pages sont produites par le serveur (Spring MVC, Thymeleaf, JSF…), est un client OpenID Connect confidentiel : le serveur garde un secret, échange lui-même le code d’autorisation contre les jetons et les conserve dans la session. Le navigateur ne voit jamais ni le secret, ni le jeton d’accès, ni le jeton de rafraîchissement : il ne garde qu’un cookie de session (le jeton d’identité ne passe par lui qu’une fois, à la déconnexion). C’est la différence avec les applications Angular et React, clients publics qui reçoivent les jetons dans le navigateur.

Ce tutoriel configure le client dans TOSIAM, puis une application Spring Boot qui se connecte avec Spring Security (oauth2Login), affiche l’utilisateur et ses revendications, et se déconnecte de TOSIAM. Le code est dans le dépôt tosiam-samples, dossier tosiam-spring-web.

texte
Navigateur ──① /oauth2/authorization/tosiam──▶ Application (localhost:8084) ──② redirection──▶ TOSIAM /authorize (connexion)
          ◀──────────────────────────────────── ③ code ◀──────────────────────────────────────────
Application ──④ code + secret + code_verifier──▶ TOSIAM /access_token ──▶ jeton d'identité, d'accès, de rafraîchissement
Application ──⑤ jeton d'accès──▶ TOSIAM /userinfo            (les jetons restent dans la session du serveur)

Étape 1 : le client de l’application#

Dans Clients, Créer un agent ouvre l’assistant. À l’étape Identité, donnez l’identifiant spring-web-client et l’URI de redirection http://localhost:8084/login/oauth2/code/tosiam, l’adresse où Spring Security reçoit le code (/login/oauth2/code/ suivi du nom de l’enregistrement, tosiam ici).

Assistant de création d'agent, étape Identité : identifiant spring-web-client et URI de redirection http://localhost:8084/login/oauth2/code/tosiam Assistant de création d'agent, étape Identité : identifiant spring-web-client et URI de redirection http://localhost:8084/login/oauth2/code/tosiam
Identifiant du client (1) et URI de redirection (2).

À l’étape Sécurité :

Assistant de création d'agent, étape Sécurité : type Confidentiel, mot de passe et méthode client_secret_basic, types d'autorisation Authorization Code et Refresh Token, scopes openid et profile Assistant de création d'agent, étape Sécurité : type Confidentiel, mot de passe et méthode client_secret_basic, types d'autorisation Authorization Code et Refresh Token, scopes openid et profile
Type de client Confidentiel (1), secret et méthode client_secret_basic (2), types d’autorisation (3) et scopes (4).
  • Type de client : Confidentiel.
  • Mot de passe de l’agent : le secret du client (spring-web-secret pour ce test). La méthode client_secret_basic envoie l’identifiant et le secret dans l’en-tête Authorization de l’appel au point des jetons.
  • Types d’autorisation : Authorization Code et Refresh Token, proposés par défaut.
  • Scopes : openid et profile, proposés par défaut.

Créez le client, puis vérifiez sa fiche. Onglet Général :

Fiche du client spring-web-client, onglet Général : type de client Confidentiel, URI de redirection, scopes openid et profile Fiche du client spring-web-client, onglet Général : type de client Confidentiel, URI de redirection, scopes openid et profile
Type de client (1), URI de redirection (2) et scopes (3).

Onglet Avancé :

Fiche du client spring-web-client, onglet Avancé : méthode d'authentification client_secret_basic, PKCE obligatoire et consentement implicite activés Fiche du client spring-web-client, onglet Avancé : méthode d'authentification client_secret_basic, PKCE obligatoire et consentement implicite activés
Méthode d’authentification (1), PKCE obligatoire (2) et consentement implicite (3).
  • Méthode d’authentification au point d’accès des jetons : client_secret_basic.
  • PKCE obligatoire (paramètre code_challenge requis) : activé. PKCE n’est pas réservé aux clients publics : il rend inutilisable un code d’autorisation intercepté, même par quelqu’un qui connaîtrait le secret. Spring Security l’envoie si on le lui demande (étape 2).
  • Consentement implicite : activé, pour que l’application de votre organisation ne demande pas son accord à l’utilisateur (le fournisseur doit autoriser les clients à passer outre le consentement).

Onglet OpenID Connect, URI de redirection après déconnexion : http://localhost:8084/, l’adresse où TOSIAM renvoie l’utilisateur après la déconnexion.

Fiche du client spring-web-client, onglet OpenID Connect : URI de redirection après déconnexion http://localhost:8084/ Fiche du client spring-web-client, onglet OpenID Connect : URI de redirection après déconnexion http://localhost:8084/
URI de redirection après déconnexion.

Enfin, onglet Signature et chiffrement, algorithme de signature du jeton d’identité RS256 : Spring Security le vérifie avec les clés publiques du fournisseur (jwks_uri). Aucune configuration CORS n’est nécessaire : le navigateur n’appelle jamais TOSIAM en JavaScript, seul le serveur parle au point des jetons.

Étape 2 : l’application Spring Boot#

L’application dépend de trois starters : spring-boot-starter-oauth2-client (client OpenID Connect), spring-boot-starter-web et spring-boot-starter-thymeleaf pour les pages, plus thymeleaf-extras-springsecurity6.

Le client se déclare dans application.yml. Avec issuer-uri, Spring Security lit au démarrage le document de découverte de TOSIAM (/.well-known/openid-configuration) : points d’accès, clés publiques et adresse de déconnexion.

yaml· app/src/main/resources/application.yml
server:
  port: 8084
  servlet:
    session:
      cookie:
        # Cookies ignore the port: every sample on localhost would otherwise share JSESSIONID
        name: TOSIAM_WEB_SESSION
        same-site: lax

spring:
  security:
    oauth2:
      client:
        registration:
          tosiam:
            client-id: spring-web-client
            # Demonstration secret: set SPRING_WEB_SECRET outside a local test
            client-secret: ${SPRING_WEB_SECRET:spring-web-secret}
            client-authentication-method: client_secret_basic
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            scope: openid,profile
            client-name: TOSIAM
        provider:
          tosiam:
            issuer-uri: http://localhost:8080/tosiam/oauth2/ref

Le cookie de session porte un nom propre à l’application : les cookies ne tiennent pas compte du port, et deux applications de localhost qui gardent le nom JSESSIONID s’écrasent mutuellement leur session.

La configuration de sécurité ajoute deux choses au comportement par défaut d’oauth2Login : PKCE, que Spring Security n’envoie d’office que pour les clients publics, et la déconnexion de TOSIAM.

java· app/src/main/java/org/tosit/samples/web/SecurityConfig.java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, ClientRegistrationRepository registrations)
        throws Exception {
    // PKCE for a confidential client too: the code is useless to whoever intercepts it
    DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(
            registrations, "/oauth2/authorization");
    resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());

    // Sign out of TOSIAM as well: end_session_endpoint with id_token_hint, then back to the home page
    OidcClientInitiatedLogoutSuccessHandler logoutSuccess = new OidcClientInitiatedLogoutSuccessHandler(
            registrations);
    logoutSuccess.setPostLogoutRedirectUri("{baseUrl}/");

    http
            .authorizeHttpRequests(authorize -> authorize
                    .requestMatchers("/", "/error", "/style.css").permitAll()
                    .anyRequest().authenticated())
            .oauth2Login(login -> login
                    // A custom login page turns off the page Spring Security generates, which would
                    // otherwise answer the failure URL itself ("Please sign in", "Invalid credentials")
                    .loginPage("/oauth2/authorization/tosiam")
                    .authorizationEndpoint(endpoint -> endpoint.authorizationRequestResolver(resolver))
                    .failureUrl("/?erreur=1"))
            .logout(logout -> logout.logoutSuccessHandler(logoutSuccess))
            .exceptionHandling(exceptions -> exceptions.accessDeniedHandler(expiredSessionLogout()));
    return http.build();
}
  • Une page protégée (/profil) demandée sans session redirige vers /oauth2/authorization/tosiam, qui redirige vers TOSIAM avec state, nonce et le code_challenge PKCE.
  • Au retour, Spring Security échange le code (avec le secret et le code_verifier), vérifie le jeton d’identité (signature, émetteur, audience, nonce, expiration), appelle le point userinfo, puis renvoie l’utilisateur sur la page demandée.
  • La déconnexion est un POST /logout protégé par le jeton CSRF : un simple lien ne suffit pas, il faut un formulaire.
  • En cas d’échec de la connexion (secret refusé, jeton invalide), l’accueil affiche le message de Spring Security (/?erreur=1), précédé d’une explication pour les cas les plus fréquents. loginPage est indispensable : sans lui, Spring Security génère sa propre page de connexion et c’est elle, en anglais (« Please sign in », « Invalid credentials »), qui répond à l’adresse d’échec.
  • Un formulaire de déconnexion resté ouvert après l’expiration de la session de l’application porte un jeton CSRF périmé. Au lieu d’une page 403, expiredSessionLogout() renvoie sur l’accueil avec une explication (/?session-expiree=1) ; un utilisateur encore connecté qui envoie la déconnexion sans jeton reçoit toujours un refus 403.

Étape 3 : les pages#

Le contrôleur reçoit l’utilisateur connecté (OidcUser) et le client autorisé (OAuth2AuthorizedClient), qui détient les jetons. La page de profil montre chaque revendication avec sa source, et seulement l’expiration et les scopes des jetons, jamais leur valeur :

java· app/src/main/java/org/tosit/samples/web/PagesController.java
@GetMapping("/profil")
String profile(@AuthenticationPrincipal OidcUser user,
        @RegisteredOAuth2AuthorizedClient("tosiam") OAuth2AuthorizedClient client, Model model) {
    model.addAttribute("user", user);
    model.addAttribute("claims", claims(user));
    // The tokens stay in the server session: the page only shows their expiry and scopes
    model.addAttribute("accessTokenExpiresAt", date(client.getAccessToken().getExpiresAt()));
    model.addAttribute("scopes", new TreeSet<>(client.getAccessToken().getScopes()));
    model.addAttribute("refreshToken", client.getRefreshToken() != null);
    model.addAttribute("idTokenExpiresAt", date(user.getIdToken().getExpiresAt()));
    return "profil";
}

user.getIdToken().getClaims() donne les revendications du jeton d’identité, user.getUserInfo().getClaims() celles du point userinfo ; user.getClaims() les fusionne. Avec les réglages par défaut du fournisseur, le nom et le prénom ne viennent que de userinfo (voir Le jeton d’identité OpenID Connect).

Quand le jeton d’accès a expiré, @RegisteredOAuth2AuthorizedClient en obtient un nouveau avec le jeton de rafraîchissement avant d’appeler la méthode : une application qui appelle une API avec ce jeton n’a rien d’autre à faire.

Étape 4 : lancer et tester#

bash
cd tosiam-spring-web
./up.sh

Ouvrez http://localhost:8084.

Accueil de l'application web Spring Boot, bouton Se connecter Accueil de l'application web Spring Boot, bouton Se connecter
L’application avant connexion.

Se connecter mène à la page de connexion de TOSIAM. Connectez-vous avec dduck / Donald-Duck-2026.

Page de connexion TOSIAM : nom d'utilisateur et mot de passe Page de connexion TOSIAM : nom d'utilisateur et mot de passe
La page de connexion de TOSIAM.

De retour sur l’application, l’accueil affiche l’utilisateur ; Voir le profil montre ses revendications et les jetons gardés par le serveur.

Accueil de l'application connectée : Connecté en tant que Donald (dduck), boutons Voir le profil et Se déconnecter Accueil de l'application connectée : Connecté en tant que Donald (dduck), boutons Voir le profil et Se déconnecter
L’application après connexion.
Page Profil : revendications du jeton d'identité et de userinfo avec leur source, expiration des jetons, scopes openid et profile, jeton de rafraîchissement présent Page Profil : revendications du jeton d'identité et de userinfo avec leur source, expiration des jetons, scopes openid et profile, jeton de rafraîchissement présent
Les revendications et leur source ; les jetons restent sur le serveur.

Les échanges : les trois premiers passent par le navigateur (onglet Réseau des outils de développement) ; les deux derniers partent du serveur de l’application et n’y apparaissent pas.

Faites défiler le tableau
RequêteQui l’envoieContenu
GET /oauth2/authorization/tosiamnavigateur → applicationdémarre la connexion
GET /tosiam/oauth2/ref/authorizenavigateur → TOSIAMresponse_type=code, state, nonce, code_challenge (S256)
GET /login/oauth2/code/tosiam?code=…navigateur → applicationle code d’autorisation
POST /tosiam/oauth2/ref/access_tokenapplication → TOSIAMcode, code_verifier, identifiant et secret (Authorization: Basic)
GET /tosiam/oauth2/ref/userinfoapplication → TOSIAMjeton d’accès (Authorization: Bearer)

Se déconnecter envoie le formulaire POST /logout : l’application ferme sa session, puis redirige le navigateur vers le point de déconnexion de TOSIAM avec id_token_hint (le jeton d’identité, qui désigne la session à fermer) et post_logout_redirect_uri. TOSIAM ferme sa session et renvoie sur http://localhost:8084/. Un nouveau Se connecter redemande le mot de passe.

Deux sessions : l’application et TOSIAM#

L’application et TOSIAM ont chacune leur session, avec leur propre durée de vie :

  • Session de l’application expirée, session TOSIAM ouverte : la prochaine page protégée repasse par TOSIAM, qui renvoie aussitôt un code sans redemander le mot de passe (authentification unique).
  • Session TOSIAM fermée par une autre application : l’application ne le sait pas et garde sa session jusqu’à son expiration. Pour la fermer aussitôt, TOSIAM peut prévenir l’application par un appel de serveur à serveur : voir Déconnexion par canal dérobé.
  • Jeton d’accès expiré : renouvelé avec le jeton de rafraîchissement, sans intervention de l’utilisateur (étape 3).
  • Déconnexion après l’expiration de la session de l’application : l’application ne connaît plus le jeton d’identité et ne peut pas fermer la session TOSIAM. L’accueil le signale ; pour fermer aussi la session TOSIAM, il faut se reconnecter puis se déconnecter.

En cas de problème#

Faites défiler le tableau
SymptômeCause probable
L’application ne démarre pas : Unable to resolve Configuration with the provided IssuerTOSIAM injoignable au démarrage : l’application lit le document de découverte au démarrage
L’application ne démarre pas : The Issuer … provided in the configuration metadata did not match the requested issuer …issuer-uri différent de l’émetteur annoncé par TOSIAM, par exemple avec un / final
« TOSIAM a refusé le client » sur l’accueil ([invalid_token_response] … 401 : [no body])Client absent du royaume ou secret différent de client-secret : TOSIAM répond 401 au point des jetons
« La demande de connexion a été perdue » sur l’accueil ([authorization_request_not_found])Session de l’application expirée pendant que l’utilisateur était sur la page de TOSIAM, connexion lancée dans deux onglets, ou retour arrière vers la page de TOSIAM : relancez Se connecter
« La connexion a échoué » avec [invalid_request] Missing parameter, 'code_challenge'PKCE obligatoire côté TOSIAM, mais pas envoyé par l’application (withPkce() absent)
Page d’erreur TOSIAM redirect_uri_mismatch au clic sur Se connecterURI de redirection du client différente de http://localhost:8084/login/oauth2/code/tosiam
Réponse JSON redirect_uri_mismatch de TOSIAM à la déconnexionURI de redirection après déconnexion du client différente de http://localhost:8084/ ; la session TOSIAM est fermée quand même
La connexion d’une autre application de localhost déconnecte celle-ciLes deux gardent le cookie JSESSIONID : donnez un nom propre au cookie de session (server.servlet.session.cookie.name)

En production#

  • HTTPS partout : URI de redirection et de retour en https://, cookie de session Secure (server.servlet.session.cookie.secure: true), et server.forward-headers-strategy: native derrière un proxy, pour que {baseUrl} donne l’adresse publique.
  • Secret hors du code : variable d’environnement ou coffre de secrets ; changez-le dans TOSIAM et dans l’application en même temps. La méthode private_key_jwt évite tout secret partagé.
  • Plusieurs instances de l’application : la session (et donc les jetons) doit être partagée, par exemple avec Spring Session et Redis, ou l’affinité de session du répartiteur de charge.
  • Démarrage sans TOSIAM : avec issuer-uri, l’application ne démarre pas si TOSIAM ne répond pas. Démarrez TOSIAM d’abord, ou fournissez les points d’accès explicitement (authorization-uri, token-uri, jwk-set-uri, user-info-uri).
  • Déconnexion de toutes les applications : ajoutez la déconnexion par canal dérobé.

Pour aller plus loin#

Mis à jour le