From 9e21586c906d6a19dfb27499a032b4dfb687079b Mon Sep 17 00:00:00 2001
From: Andreas Svanberg
Date: Wed, 20 May 2026 20:59:36 +0200
Subject: [PATCH 1/2] Upgrade to Spring Boot 4.0
The new ["modular design"](https://spring.io/blog/2025/10/28/modularizing-spring-boot/) required a few changes in the starter Maven dependencies. It also caused some minor Java import changes. The main problem that arose was that the spring-boot-web-server dependency got scoped to "runtime" in Maven. This broke the embedded Docker build since it was excluded from dependency lists. Therefore, it had to be manually added back in the correct "provided" scope.
spring-boot-starter-web, JTE, and Testcontainers had new artifact names. Jackson had to be added as an explicit dependency (for testing only) since it's no longer included by default in Spring Boot.
## Spring Boot OAuth 2.0 Authorization Server
There were some internal changes that broke the "custom" developer authorization. The `OAuth2AuthorizationEndpointFilter` got split into two separate ones (for MFA purposes), and the split off one is executed much much earlier in the chain. Therefore, the old method of changing the HTTP method of the request to trick the regular filter from the custom one no longer works. Luckily an unrelated change had added POST support to the `OAuth2AuthorizationEndpointFilter` which meant we could stop changing the HTTP method and change the form to submit everything as form fields instead of query parameters. A lot of mechanical changes in the tests were required for this.
MFA support also meant that OIDC ID tokens require the authenticated principal to have a `FactorGrantedAuthority` which was added to the Shibboleth authentication.
---
pom.xml | 34 +++++++++++++----
.../se/su/dsv/oauth2/AuthorizationServer.java | 4 +-
.../su/dsv/oauth2/dev/DevConfiguration.java | 6 +--
...ShibbolethAuthenticationDetailsSource.java | 3 ++
.../CustomAuthorizationEndpointFilter.java | 25 ++++--------
src/main/resources/templates/authorize.jte | 12 ++++++
.../oauth2/AbstractMetadataCodeFlowTest.java | 30 ++++++++++-----
.../su/dsv/oauth2/AbstractMetadataTest.java | 18 ++++-----
.../dsv/oauth2/AuthorizationCodeFlowTest.java | 14 +++----
.../dsv/oauth2/AuthorizationServerTest.java | 4 +-
.../ConsentFlowCustomAuthorizationTest.java | 12 +++---
.../se/su/dsv/oauth2/ConsentFlowTest.java | 5 ++-
.../su/dsv/oauth2/EmbeddedContainerTest.java | 8 ++--
.../dsv/oauth2/PublicClientCodeFlowTest.java | 6 +--
.../ResourceServerRegisteredClientTest.java | 2 +-
.../oauth2/ShibbolethRequestProcessor.java | 2 +-
.../se/su/dsv/oauth2/StagingProfileTest.java | 38 +++++++++----------
.../TestRegisteredClientConfiguration.java | 4 ++
.../su/dsv/oauth2/UserInfoEndpointTest.java | 4 +-
.../dsv/oauth2/web/AdminControllerTest.java | 8 ++--
.../web/client/ClientAdminControllerTest.java | 6 +--
21 files changed, 142 insertions(+), 103 deletions(-)
diff --git a/pom.xml b/pom.xml
index bf38b3e..adf2696 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.11
+ 4.0.6
@@ -19,7 +19,8 @@
17
- 3.1.12
+ 3.2.4
+ 4.0.6
@@ -29,12 +30,12 @@
org.springframework.boot
- spring-boot-starter-web
+ spring-boot-starter-webmvc
gg.jte
- jte-spring-boot-starter-3
+ jte-spring-boot-starter-4
${jte.version}
@@ -66,6 +67,11 @@
spring-boot-starter-tomcat
provided
+
+ org.springframework.boot
+ spring-boot-web-server
+ provided
+
@@ -73,6 +79,15 @@
spring-boot-starter-test
test
+
+ org.springframework.boot
+ spring-boot-starter-webmvc-test
+
+
+ org.springframework.boot
+ spring-boot-starter-jackson-test
+ test
+
org.springframework.boot
spring-boot-testcontainers
@@ -85,12 +100,12 @@
org.testcontainers
- junit-jupiter
+ testcontainers-junit-jupiter
test
org.testcontainers
- mariadb
+ testcontainers-mariadb
test
@@ -105,7 +120,7 @@
org.springframework.boot
spring-boot-configuration-processor
- 3.3.0
+ ${spring-boot.version}
@@ -171,6 +186,11 @@
org.springframework.boot
spring-boot-starter-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-flyway
+
org.flywaydb
flyway-core
diff --git a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
index 0568506..2c9cb56 100644
--- a/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
+++ b/src/main/java/se/su/dsv/oauth2/AuthorizationServer.java
@@ -13,6 +13,7 @@ import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -21,7 +22,6 @@ import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
-import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
@@ -95,7 +95,7 @@ public class AuthorizationServer extends SpringBootServletInitializer {
throws Exception
{
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
- OAuth2AuthorizationServerConfigurer.authorizationServer();
+ new OAuth2AuthorizationServerConfigurer();
String tokenEndpoint = authorizationServerSettings.getTokenEndpoint();
RequestMatcher corsEnabledMatcher = new OrRequestMatcher(
diff --git a/src/main/java/se/su/dsv/oauth2/dev/DevConfiguration.java b/src/main/java/se/su/dsv/oauth2/dev/DevConfiguration.java
index 914d585..d36aec5 100644
--- a/src/main/java/se/su/dsv/oauth2/dev/DevConfiguration.java
+++ b/src/main/java/se/su/dsv/oauth2/dev/DevConfiguration.java
@@ -1,7 +1,7 @@
package se.su.dsv.oauth2.dev;
import jakarta.servlet.http.HttpFilter;
-import org.springframework.boot.autoconfigure.security.SecurityProperties;
+import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -12,9 +12,9 @@ import se.su.dsv.oauth2.Config;
@Profile("dev")
public class DevConfiguration {
@Bean
- public FilterRegistrationBean fakeSSO(SecurityProperties securityProperties, Config config) {
+ public FilterRegistrationBean fakeSSO(SecurityFilterProperties securityProperties, Config config) {
var filter = new FilterRegistrationBean(new FakeSSOFilter(config.developerEntitlement()));
- filter.setOrder(securityProperties.getFilter().getOrder() - 1);
+ filter.setOrder(securityProperties.getOrder() - 1);
return filter;
}
}
diff --git a/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticationDetailsSource.java b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticationDetailsSource.java
index ecae400..a55870e 100644
--- a/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticationDetailsSource.java
+++ b/src/main/java/se/su/dsv/oauth2/shibboleth/ShibbolethAuthenticationDetailsSource.java
@@ -3,6 +3,7 @@ package se.su.dsv.oauth2.shibboleth;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.FactorGrantedAuthority;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -46,6 +47,8 @@ public class ShibbolethAuthenticationDetailsSource implements
authorities.add(entitlement);
}
}
+ // The authentication performed by Shibboleth is password based.
+ authorities.add(FactorGrantedAuthority.fromAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY));
return authorities;
}
}
diff --git a/src/main/java/se/su/dsv/oauth2/staging/CustomAuthorizationEndpointFilter.java b/src/main/java/se/su/dsv/oauth2/staging/CustomAuthorizationEndpointFilter.java
index 2089b79..e1c6d0c 100644
--- a/src/main/java/se/su/dsv/oauth2/staging/CustomAuthorizationEndpointFilter.java
+++ b/src/main/java/se/su/dsv/oauth2/staging/CustomAuthorizationEndpointFilter.java
@@ -7,7 +7,6 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
@@ -34,10 +33,7 @@ import se.su.dsv.oauth2.shibboleth.ShibbolethAuthenticationDetails;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@@ -100,7 +96,8 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
authenticationConverter.convert(request);
String authorizationUrl = getAuthorizationUrl(request);
- JteModel view = templates.authorize(authorizationUrl, loggedInUser.getName(), (ShibbolethAuthenticationDetails) loggedInUser.getDetails());
+ Map parameters = request.getParameterMap();
+ JteModel view = templates.authorize(authorizationUrl, parameters, loggedInUser.getName(), (ShibbolethAuthenticationDetails) loggedInUser.getDetails());
respondWithTemplate(response, view);
} else if (Objects.equals(request.getMethod(), "POST")) {
handleIncomingCustomAuthorizationRequest(request, response, loggedInUser);
@@ -119,14 +116,15 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
principal.setDetails(buildShibbolethDetails(request));
Authentication authenticatedPrincipal = authenticationManager.authenticate(principal);
- Authentication normalCodeRequest = authenticationConverter.convert(withGetMethod(request));
+ Authentication normalCodeRequest = authenticationConverter.convert(request);
OAuth2AuthorizationCodeRequestAuthenticationToken codeRequestAuthenticationToken = (OAuth2AuthorizationCodeRequestAuthenticationToken) normalCodeRequest;
Authentication codeRequest = overridePrincipal(authenticatedPrincipal, codeRequestAuthenticationToken);
Authentication authenticatedCodeRequest = authenticationManager.authenticate(codeRequest);
if (!authenticatedCodeRequest.isAuthenticated()) {
String authorizationUrl = getAuthorizationUrl(request);
- respondWithTemplate(response, templates.authorize(authorizationUrl, loggedInUser.getName(), (ShibbolethAuthenticationDetails) authenticatedPrincipal));
+ Map parameters = request.getParameterMap();
+ respondWithTemplate(response, templates.authorize(authorizationUrl, parameters, loggedInUser.getName(), (ShibbolethAuthenticationDetails) authenticatedPrincipal));
} else {
if (authenticatedCodeRequest instanceof OAuth2AuthorizationCodeRequestAuthenticationToken authenticatedCodeRequestAuthenticationToken) {
sendAuthorizationResponse(request, response, authenticatedCodeRequestAuthenticationToken);
@@ -139,16 +137,7 @@ public class CustomAuthorizationEndpointFilter extends HttpFilter {
}
private static String getAuthorizationUrl(final HttpServletRequest request) {
- return request.getRequestURL().append('?').append(request.getQueryString()).toString();
- }
-
- private HttpServletRequest withGetMethod(final HttpServletRequest request) {
- return new HttpServletRequestWrapper(request) {
- @Override
- public String getMethod() {
- return "GET";
- }
- };
+ return request.getRequestURL().toString();
}
private Authentication overridePrincipal(
diff --git a/src/main/resources/templates/authorize.jte b/src/main/resources/templates/authorize.jte
index 3974f92..7081991 100644
--- a/src/main/resources/templates/authorize.jte
+++ b/src/main/resources/templates/authorize.jte
@@ -1,8 +1,10 @@
@import se.su.dsv.oauth2.shibboleth.Entitlement
@import se.su.dsv.oauth2.shibboleth.ShibbolethAuthenticationDetails
+@import java.util.Map
@import java.util.stream.Collectors
@param String authorizationUrl
+@param Map parameters
@param String principalName
@param ShibbolethAuthenticationDetails shibbolethDetails
@@ -15,6 +17,11 @@