Aggiungi l'accesso a un'app Spring Boot
Questa quickstart aggiunge l'accesso KeenAuth a un'app Spring Boot 4.1 con il client OAuth 2.0 di Spring Security, il client OpenID Connect standard dell'ecosistema Spring. KeenAuth non ha ancora un SDK Java e non ne serve uno: KeenAuth è un provider OpenID Connect standard. La tua app è il client OAuth e conserva ogni token nella sessione lato server; il browser riceve solo un cookie con l'id di sessione. Configuri l'accesso, proteggi una pagina, chiami un'API protetta con l'access token dell'utente, ti disconnetti e testi l'app offline.
L'app ha queste route:
| Route | Cosa fa |
|---|---|
GET / |
Home page pubblica: un link per accedere, oppure il nome dell'utente e un pulsante per disconnettersi |
GET /dashboard |
Richiede una sessione. Mostra l'utente e i messaggi restituiti dalla tua API |
GET /oauth2/authorization/keenauth |
Servita da Spring Security: avvia l'accesso |
GET /login/oauth2/code/keenauth |
Servita da Spring Security: la callback |
POST /logout |
Servita da Spring Security: termina la sessione qui e su KeenAuth. Richiede il token CSRF |
Prima di iniziare
- Un ambiente KeenAuth. Il suo issuer ha la forma
https://your-environment.eu.keenauth.app. - Un'applicazione web nell'ambiente (Crea un'applicazione) con un client secret (
client_secret_basic). In un ambientedevelopment, registra l'URI di redirecthttp://localhost/login/oauth2/code/keenauthe l'URI di redirect dopo la disconnessionehttp://localhost/; un URI di loopback viene registrato senza porta e corrisponde a qualsiasi porta, come8080. - Un'API registrata nell'ambiente (Crea un'API) con lo scope
messages:read. Il suoidentifier, per esempiohttps://api.example.com, è l'audience dei suoi access token. La quickstart Express crea un'API che accetta questi token. - JDK 21 e Maven 3.9.
1. Aggiungi le dipendenze
Il parent di Spring Boot (spring-boot-starter-parent 4.1.2) fissa tutte le versioni. Aggiungi gli starter per web, client OAuth 2.0 e 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'esempio sostituisce due dipendenze le cui licenze sono fuori dalla lista di licenze ammesse dai nostri esempi: Logback (EPL-1.0 o LGPL-2.1) con Log4j 2, e jakarta.annotation-api (EPL-2.0 o GPL-2.0 con Classpath Exception), che l'app non usa. Per lo stesso motivo i suoi test usano TestNG invece di JUnit (EPL-2.0). La tua app non deve necessariamente fare lo stesso.
2. Configura
L'app legge la configurazione solo dalle variabili d'ambiente. Non committare mai i loro valori.
| Variabile | Obbligatoria | Significato |
|---|---|---|
KEENAUTH_ISSUER |
sì | L'issuer del tuo ambiente, https |
KEENAUTH_CLIENT_ID |
sì | Il client_id dell'applicazione web |
KEENAUTH_CLIENT_SECRET |
sì | Il suo client secret |
APP_BASE_URL |
sì | L'URL pubblico dell'app. L'URI di redirect viene costruito a partire da esso. http solo per localhost, 127.0.0.1 o [::1] |
API_URL |
sì | L'API che l'app chiama, per esempio http://localhost:3001 |
KEENAUTH_API_AUDIENCE |
sì | L'identifier di quell'API in KeenAuth |
SESSION_SECURE_COOKIE |
no | false solo per http://localhost semplice |
KeenAuthSettings.from legge e verifica questi valori all'avvio. Un valore mancante o non utilizzabile ferma l'app, e nessun messaggio di errore contiene un valore.
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. Registra il client
La registrazione del client deriva dalla discovery (<issuer>/.well-known/openid-configuration), che Spring Security recupera all'avvio. L'esempio imposta le credenziali del client, l'URI di redirect e gli scope, e attiva PKCE per questo client confidenziale:
/** 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. Configura l'accesso, l'audience dell'API e la disconnessione
Il resolver della richiesta di autorizzazione aggiunge l'identifier della tua API come resource (RFC 8707). Senza di esso, l'access token non è destinato alla tua API e l'API lo rifiuta. La disconnessione è il POST /logout di Spring Security con il token CSRF, seguito da OpenID Connect RP-Initiated Logout con id_token_hint e 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 verifica state sulla callback, scambia il codice con il verifier PKCE e controlla l'ID token (firma con le chiavi del tuo ambiente, RS256, iss, aud, exp, iat e nonce). Dopo l'accesso assegna all'utente un nuovo id di sessione. La sessione vive sul server; con più server, usa Spring Session con uno store condiviso.
La home page contiene il link per accedere e invia un POST per disconnettersi; th:action aggiunge il token 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. Chiama la tua API
La dashboard richiede una sessione. Prende l'access token dal client autorizzato che Spring Security conserva nella sessione e chiama la tua API con esso. Il token resta sul server; il browser riceve solo l'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'esempio non ha un refresh token: quando l'access token scade, l'utente accede di nuovo.
6. Avvia l'app
Avvia l'API della quickstart Express (o qualsiasi API che accetti i token) ed esegui l'app con le variabili esportate:
mvn spring-boot:run
Apri http://localhost:8080, scegli Accedi, accedi su KeenAuth e apri la dashboard.
7. Testa senza rete
I test non richiedono rete né un account KeenAuth. Un piccolo server HTTP su una porta di loopback simula il tuo ambiente (discovery, un set di chiavi creato per l'esecuzione dei test, l'endpoint del token e userinfo) e l'API. Il test sostituisce la registrazione del client con una ricavata da quel server, tramite lo stesso metodo 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
I test verificano che la configurazione venga validata all'avvio (e che nessun errore contenga un secret); che l'accesso invii PKCE S256, state, nonce e resource; che il codice venga scambiato con il verifier corrispondente e l'autenticazione del client; che l'access token venga inviato all'API nell'header Authorization e non finisca mai in un cookie; che uno state sconosciuto, un ID token per un altro client e un ID token con un altro nonce vengano rifiutati senza creare una sessione; che una pagina senza sessione avvii l'accesso; e che la disconnessione richieda il token CSRF e vada all'endpoint di logout con id_token_hint.
L'accesso completo con un browser reale richiede un ambiente KeenAuth. Non fa parte dei test offline.
Dietro un proxy o un load balancer
Imposta APP_BASE_URL sull'URL pubblico dell'app; l'URI di redirect viene costruito a partire da esso. Se un proxy termina la TLS, imposta server.forward-headers-strategy=framework solo quando il proxy è l'unico modo per raggiungere l'app.
Limiti attuali
- L'accesso parte con una
GETa/oauth2/authorization/keenauth, come fa Spring Security di default. Il controllo distatee PKCE proteggono la callback. - Spring Security non verifica il parametro della risposta di autorizzazione
iss(RFC 9207). - Spring Security accetta per default ID token RS256. Se il tuo ambiente firma con un altro algoritmo, configura un bean
OidcIdTokenDecoderFactorycon quell'algoritmo. - Nessun refresh token e nessun accesso silenzioso.
Passi successivi
- Proteggi un'API Express: l'API che questa app chiama.
- Verifica dei token: il verificatore sotto il middleware della tua API.