Ajouter la connexion à une application Spring Boot
Ce démarrage rapide ajoute la connexion KeenAuth à une application Spring Boot 4.1 avec le client OAuth 2.0 de Spring Security, le client OpenID Connect standard de l'écosystème Spring. KeenAuth ne dispose pas encore de SDK Java, et il n'en a pas besoin : KeenAuth est un fournisseur OpenID Connect standard. Votre application est le client OAuth et conserve tous les jetons dans la session côté serveur ; le navigateur ne reçoit qu'un cookie contenant l'identifiant de session. Vous configurez la connexion, protégez une page, appelez une API protégée avec le jeton d'accès de l'utilisateur, déconnectez l'utilisateur et testez l'application hors ligne.
L'application expose les routes suivantes :
| Route | Rôle |
|---|---|
GET / |
Page d'accueil publique : un lien de connexion, ou le nom de l'utilisateur et un bouton de déconnexion |
GET /dashboard |
Nécessite une session. Affiche l'utilisateur et les messages renvoyés par votre API |
GET /oauth2/authorization/keenauth |
Servie par Spring Security : démarre la connexion |
GET /login/oauth2/code/keenauth |
Servie par Spring Security : le callback |
POST /logout |
Servie par Spring Security : termine la session ici et chez KeenAuth. Nécessite le jeton CSRF |
Avant de commencer
- Un environnement KeenAuth. Son issuer ressemble à
https://your-environment.eu.keenauth.app. - Une application web dans l'environnement (Créer une application) avec un secret client (
client_secret_basic). Dans un environnementdevelopment, enregistrez l'URI de redirectionhttp://localhost/login/oauth2/code/keenauthet l'URI de redirection après déconnexionhttp://localhost/; une URI de bouclage est enregistrée sans port et correspond à n'importe quel port, comme8080. - Une API enregistrée dans l'environnement (Créer une API) avec l'autorisation
messages:read. Sonidentifier, par exemplehttps://api.example.com, est l'audience de ses jetons d'accès. Le démarrage rapide Express construit une API qui accepte ces jetons. - JDK 21 et Maven 3.9.
1. Ajouter les dépendances
Le parent Spring Boot (spring-boot-starter-parent 4.1.2) fixe toutes les versions. Ajoutez les starters web, client OAuth 2.0 et Thymeleaf :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
<exclusions>
<!-- Logback is EPL-1.0 OR LGPL-2.1, outside the licence allow list: Log4j 2 (Apache-2.0) logs instead. -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
<!-- EPL-2.0 OR GPL-2.0 with Classpath Exception, outside the allow list; this app uses none of its annotations. -->
<exclusion>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Spring Security's OAuth 2.0 client: OpenID Connect sign-in with discovery, PKCE and ID token checks. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
L'exemple remplace deux dépendances dont les licences sortent de la liste des licences autorisées pour nos exemples : Logback (EPL-1.0 ou LGPL-2.1) par Log4j 2, et jakarta.annotation-api (EPL-2.0 ou GPL-2.0 avec Classpath Exception), que l'application n'utilise pas. Pour la même raison, ses tests utilisent TestNG au lieu de JUnit (EPL-2.0). Votre propre application n'a pas besoin de faire cela.
2. Configurer
L'application lit sa configuration uniquement depuis des variables d'environnement. Ne committez jamais leurs valeurs.
| Variable | Obligatoire | Signification |
|---|---|---|
KEENAUTH_ISSUER |
oui | L'issuer de votre environnement, en https |
KEENAUTH_CLIENT_ID |
oui | Le client_id de l'application web |
KEENAUTH_CLIENT_SECRET |
oui | Son secret client |
APP_BASE_URL |
oui | L'URL publique de l'application. L'URI de redirection est construite à partir d'elle. http uniquement pour localhost, 127.0.0.1 ou [::1] |
API_URL |
oui | L'API appelée par l'application, par exemple http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
oui | L'identifiant de cette API dans KeenAuth |
SESSION_SECURE_COOKIE |
non | false uniquement pour un http://localhost simple |
KeenAuthSettings.from lit et vérifie ces valeurs au démarrage. Une valeur manquante ou inutilisable arrête l'application, et aucun message d'erreur ne contient de valeur.
public static KeenAuthSettings from(Function<String, String> env) {
return new KeenAuthSettings(url(need(env, "KEENAUTH_ISSUER"), "KEENAUTH_ISSUER", true, false),
need(env, "KEENAUTH_CLIENT_ID"), need(env, "KEENAUTH_CLIENT_SECRET"),
url(need(env, "APP_BASE_URL"), "APP_BASE_URL", false, false),
url(need(env, "API_URL"), "API_URL", false, false),
// RFC 8707: the access token is requested for your API, which then accepts it as its audience.
url(need(env, "KEENAUTH_API_AUDIENCE"), "KEENAUTH_API_AUDIENCE", false, true));
}
3. Enregistrer le client
L'enregistrement du client provient de la découverte (<issuer>/.well-known/openid-configuration), que Spring Security récupère au démarrage. L'exemple définit les identifiants du client, l'URI de redirection et les scopes, et active PKCE pour ce client confidentiel :
/** The endpoints and the signing keys come from discovery: <issuer>/.well-known/openid-configuration. */
@Bean
ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
return new InMemoryClientRegistrationRepository(
registration(ClientRegistrations.fromIssuerLocation(settings.issuer()), settings));
}
static ClientRegistration registration(ClientRegistration.Builder discovered, KeenAuthSettings settings) {
return discovered.registrationId("keenauth")
.clientId(settings.clientId())
.clientSecret(settings.clientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(settings.redirectUri())
// messages:read is a scope of your API; the example API requires it on /api/messages.
.scope("openid", "profile", "email", "messages:read")
// PKCE (RFC 7636) with S256 for this confidential client too; state and nonce are always on.
.clientSettings(ClientRegistration.ClientSettings.builder().requireProofKey(true).build())
.build();
}
4. Configurer la connexion, l'audience de l'API et la déconnexion
Le resolver de requête d'autorisation ajoute l'identifiant de votre API comme resource (RFC 8707). Sans lui, le jeton d'accès n'est pas destiné à votre API, et celle-ci le rejette. La déconnexion correspond au POST /logout de Spring Security avec le jeton CSRF, suivi de la déconnexion initiée par la partie utilisatrice d'OpenID Connect (RP-Initiated Logout) avec id_token_hint et post_logout_redirect_uri :
DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(clients,
"/oauth2/authorization");
// RFC 8707: an access token for your API.
resolver.setAuthorizationRequestCustomizer(
(request) -> request.additionalParameters((params) -> params.put("resource", settings.apiAudience())));
OidcClientInitiatedLogoutSuccessHandler logout = new OidcClientInitiatedLogoutSuccessHandler(clients);
logout.setPostLogoutRedirectUri(settings.postLogoutRedirectUri());
http.authorizeHttpRequests((authorize) -> authorize.requestMatchers("/", "/error").permitAll()
.anyRequest()
.authenticated())
.oauth2Login((login) -> login.authorizationEndpoint((endpoint) -> endpoint.authorizationRequestResolver(resolver)))
// POST /logout with the CSRF token; then OpenID Connect RP-Initiated Logout with id_token_hint.
.logout((out) -> out.logoutSuccessHandler(logout));
return http.build();
Spring Security vérifie state sur le callback, échange le code avec le verifier PKCE et contrôle le jeton ID (signature avec les clés de votre environnement, RS256, iss, aud, exp, iat et nonce). Après la connexion, il attribue à l'utilisateur un nouvel identifiant de session. La session réside sur le serveur ; avec plusieurs serveurs, utilisez Spring Session avec un stockage partagé.
La page d'accueil renvoie vers la connexion et envoie le formulaire de déconnexion en POST ; th:action ajoute le jeton CSRF :
<!-- th:action adds the CSRF token. -->
<form th:action="@{/logout}" method="post"><button type="submit">Sign out</button></form>
</div>
<p th:unless="${user}"><a href="/oauth2/authorization/keenauth">Sign in</a></p>
5. Appeler votre API
Le tableau de bord nécessite une session. Il prend le jeton d'accès dans le client autorisé que Spring Security conserve en session, et appelle votre API avec celui-ci. Le jeton reste sur le serveur ; le navigateur ne reçoit que le HTML.
/** Needs a session (SecurityConfig). The access token stays on the server; the browser receives only the HTML. */
@GetMapping("/dashboard")
public String dashboard(@AuthenticationPrincipal OidcUser user,
@RegisteredOAuth2AuthorizedClient("keenauth") OAuth2AuthorizedClient client, Model model) {
ResponseEntity<String> response = this.messages.fetch(client.getAccessToken().getTokenValue());
model.addAttribute("user", user);
model.addAttribute("status", response.getStatusCode().value());
model.addAttribute("messages", response.getStatusCode().is2xxSuccessful() ? response.getBody() : null);
return "dashboard";
}
public ResponseEntity<String> fetch(String accessToken) {
return this.rest.get()
.uri("/api/messages")
.headers((headers) -> headers.setBearerAuth(accessToken))
.retrieve()
.onStatus((status) -> true, (request, response) -> {
})
.toEntity(String.class);
}
L'exemple n'a pas de jeton de rafraîchissement : lorsque le jeton d'accès expire, l'utilisateur se reconnecte.
6. Lancer l'application
Démarrez l'API du démarrage rapide Express (ou toute API qui accepte les jetons) puis lancez l'application avec les variables exportées :
mvn spring-boot:run
Ouvrez http://localhost:8080, choisissez Se connecter, connectez-vous chez KeenAuth et ouvrez le tableau de bord.
7. Tester sans réseau
Les tests n'ont besoin ni de réseau ni de compte KeenAuth. Un petit serveur HTTP sur un port de bouclage joue le rôle de votre environnement (découverte, jeu de clés créé pour l'exécution des tests, point de terminaison de jeton et userinfo) et celui de l'API. Le test remplace l'enregistrement du client par un enregistrement découvert depuis ce serveur, via la même méthode registration :
/** The registration of SecurityConfig, discovered from the fake environment instead of the issuer above. */
@TestConfiguration
static class FakeDiscovery {
@Bean
ClientRegistrationRepository clientRegistrationRepository(KeenAuthSettings settings) {
return new InMemoryClientRegistrationRepository(
SecurityConfig.registration(ClientRegistrations.fromIssuerLocation(KEENAUTH.issuer()), settings));
}
}
@Test
public void callbackWithAnUnknownStateIsRefusedBeforeAnyTokenRequest() throws Exception {
MockHttpSession session = new MockHttpSession();
startSignIn(session);
MvcResult result = returnFromKeenAuth(session, "not-the-state");
assertEquals(result.getResponse().getRedirectedUrl(), "/login?error");
assertTrue(KEENAUTH.requestsTo("/oauth2/token").isEmpty());
mvn dependency:list -DoutputFile=target/dependencies.txt
python3 ../scripts/maven_deps.py target/dependencies.txt # SHA-256 lock and licences
mvn test
Les tests vérifient que la configuration est validée au démarrage (et qu'aucune erreur ne contient un secret) ; que la connexion envoie PKCE S256, state, nonce et resource ; que le code est échangé avec le verifier correspondant et l'authentification du client ; que le jeton d'accès est envoyé à l'API dans l'en-tête Authorization et n'est jamais placé dans un cookie ; qu'un state inconnu, un jeton ID destiné à un autre client et un jeton ID avec un autre nonce sont refusés sans session ; qu'une page sans session lance la connexion ; et que la déconnexion nécessite le jeton CSRF et se rend au point de terminaison de déconnexion avec id_token_hint.
La connexion complète dans un vrai navigateur nécessite un environnement KeenAuth. Elle ne fait pas partie des tests hors ligne.
Derrière un proxy ou un équilibreur de charge
Définissez APP_BASE_URL sur l'URL publique de l'application ; l'URI de redirection est construite à partir d'elle. Si un proxy termine le TLS, définissez server.forward-headers-strategy=framework uniquement lorsque le proxy est le seul moyen d'atteindre l'application.
Limites actuelles
- La connexion démarre par une requête
GETvers/oauth2/authorization/keenauth, comme Spring Security le fait par défaut. La vérification destateet PKCE protègent le callback. - Spring Security ne vérifie pas le paramètre
issde la réponse d'autorisation (RFC 9207). - Spring Security accepte par défaut les jetons ID RS256. Si votre environnement signe avec un autre algorithme, configurez un bean
OidcIdTokenDecoderFactoryavec cet algorithme. - Pas de jeton de rafraîchissement et pas de connexion silencieuse.
Étapes suivantes
- Protéger une API Express : l'API appelée par cette application.
- Vérification des jetons : le vérificateur utilisé par le middleware de votre API.