Skip to content

Migrate to spring boot 4#3805

Draft
gdgenchev wants to merge 41 commits intocloudfoundry:developfrom
gdgenchev:migrate-to-spring-boot-4
Draft

Migrate to spring boot 4#3805
gdgenchev wants to merge 41 commits intocloudfoundry:developfrom
gdgenchev:migrate-to-spring-boot-4

Conversation

@gdgenchev
Copy link
Copy Markdown
Contributor

@gdgenchev gdgenchev commented Apr 1, 2026

I have worked on researching the spring boot 4 migration of uaa.

  1. Run open-rewrite recipe for Spring Boot 3 -> Spring Boot 4 without Jackson 2 -> 3
  2. Fix some issues with AI in incremental commits
    • Needs thorough reviewing commit by commit
    • I have removed the fips and used non-fips just so that I can reach other issues that can be fixed. (commit 30. dec5452 - migrate bc to non-fips to just check..) - this is hard blocker

Current State:

  1. Build is successful
  2. Only 12 unit tests related to SAML fail
  3. UAA starts and on login I see authentication success log
  4. For some reason I cannot run the ITs on my MAC. Could be related to the migration or not

Conclusions:

Jackson:

  • Jackcson 2 -> 3 cannot be done in isolation without Spring Boot 4 migration, because they changed the packages and there are spring libs that statically depend on those packages, so upgrade is needed
  • Open-rewrite recipe is buggy and does not resolve all migration issues in jackson 2 -> 3.
    • Moderne ran the full boot migration recipe over uaa here: https://github.com/timtebeek/uaa/tree/feature/migrate-to-spring-boot-4-0
    • I have discussed with colleagues familiar with Migrations topic and it turned out that our guess that Jackson 3 is not needed for Spring Boot 4 is correct. There is this compatibility dependency that can be used: spring-boot-jackson2. I was given a recipe that does the spring migration without Jackson and this is the one that I have used as a base. I was suggested to do Spring Boot 4 without Jackson, as it causes too many issues (though, as we can see the spring migration itself also does..)

OpenSAML 5:

  • OpenSAML 5 support is added from Spring Security 6.4.x: Update to OpenSAML 5 spring-projects/spring-security#11658 (though, OpenSAML 4 is used by default and you need some extra config to enable OpenSAML 5), cfuaa currently is on 6.5.9, so technically it could be possible to upgrade it in isolation, but when I tried, it still had incompatibility issues (though I am not sure if they were spring related or open saml bc-fips related)
  • OpenSAML 4 is not FIPS compliant. UAA has excluded bc non fips and added bc-fips instead and it somehow works, but nowhere OpenSAML states that it is FIPS compliant.
  • If we go for Spring Boot 4, we need to upgrade to OpenSAML 5, as OpenSAML 4 support is completely removed

Migration to boot 4 currently seems to be a dead end due to Open SAML 5 FIPS compliance.


#Bonus research:

Java 25:
bcgit/bc-java#1287 (comment) - from this it seems that FIPS certification can take up to 14 months, at least this is how long it has taken before. This is actually very good information, as previously we just didn't know and could not set any expectations.

gdgenchev added 30 commits April 1, 2026 15:26
# Conflicts:
#	dependencies.gradle
#	k8s/templates/deployment.yml
#	k8s/templates/values/_values.yml
Use spring boot 4 and spring-boot-jackson2

# Conflicts:
#	dependencies.gradle
…ng Boot 4

In Spring Security 7 (used by Spring Boot 4), AntPathRequestMatcher was
deprecated in favor of PathPatternRequestMatcher for servlet-based matching.

Changes:
- UaaMetricsFilter: Replace AntPathRequestMatcher with PathPatternRequestMatcher
- BackwardsCompatibleTokenEndpointAuthenticationFilter: Update field type and import
- LimitedModeUaaFilter: Update field type and constructor usage
- PasswordChangeUiRequiredFilter: Update field types

All files were already using PathPatternRequestMatcher.withDefaults().matcher()
for instantiation, this commit updates the type declarations and imports to match.
Several Spring classes were removed or moved in Spring Framework 7:

1. Base64 and Utf8 from spring-security-crypto.codec
   - Replaced with java.util.Base64 and StandardCharsets.UTF_8
   - Updated UserNotFoundEvent to use Java standard APIs

2. NestedServletException from spring-web
   - Replaced with plain ServletException
   - Updated OAuth2ClientContextFilter

3. PortResolver and PortResolverImpl from spring-security-web
   - Removed from Spring Security 7 API
   - Updated UaaSavedRequestCache.doesRequestMatch() signature
   - Updated InvitationsController to use single-arg DefaultSavedRequest constructor

4. WebMvcAutoConfiguration package moved
   - Removed unused import from WebConfig (only used in JavaDoc)
Spring Security 7 upgraded from OpenSAML 4 to OpenSAML 5 and renamed
all OpenSaml4* classes to OpenSaml5*:

- OpenSaml4AuthenticationRequestResolver → OpenSaml5AuthenticationRequestResolver
- OpenSaml4LogoutRequestResolver → OpenSaml5LogoutRequestResolver
- OpenSaml4LogoutResponseResolver → OpenSaml5LogoutResponseResolver
- OpenSamlLogoutRequestValidator → OpenSaml5LogoutRequestValidator
- OpenSamlLogoutResponseValidator → OpenSaml5LogoutResponseValidator
- OpenSamlMetadataResolver → OpenSaml5MetadataResolver

Files updated:
- SamlAuthenticationFilterConfig.java
- SamlLogoutRequestValidator.java
- SamlLogoutResponseValidator.java
- SamlMetadataEndpoint.java
- SamlMetadataEntityDescriptorCustomizer.java
Spring Security 7 changed AuthorizationManager.authorize() to return
AuthorizationResult (nullable) instead of AuthorizationDecision.
AuthorizationDecision is a subtype of AuthorizationResult.

Changes:
- AuthorizationManagersUtils: Update authorize() return type and handle AuthorizationResult
- SelfCheckAuthorizationManager: Update authorize() return type

Both classes now properly implement the new interface signature while
continuing to return AuthorizationDecision instances.
1. SAML API changes:
   - getAssertingPartyDetails() → getAssertingPartyMetadata()
   - AssertingPartyDetails → AssertingPartyMetadata
   - withRelyingPartyRegistration() → from()
   - assertingPartyDetails() → assertingPartyMetadata()

2. MediaType sorting:
   - MediaType.sortByQualityValue() replaced with MediaType.QUALITY_VALUE_COMPARATOR
   - Updated DefaultOAuth2ExceptionRenderer and ConvertingExceptionView

3. HTTP client timeout API:
   - Changed to Duration-based API (setConnectTimeout(Duration))
   - Updated UaaHttpRequestUtils

4. DaoAuthenticationProvider constructor:
   - Now requires UserDetailsService and PasswordEncoder in constructor
   - Removed setUserDetailsService() and setPasswordEncoder() calls

5. RequestMappingHandlerAdapter:
   - Removed setIgnoreDefaultModelOnRedirect() (always true in Spring 6+)
   - Cleaned up WebConfig

6. AuthorizationManager with @nullable:
   - Added @nullable annotation to Authentication in Supplier parameter
   - Updated AuthorizationManagersUtils and SelfCheckAuthorizationManager
1. AuthorizationManager.authorize() signature:
   - Use wildcard `? extends @nullable Authentication` in Supplier parameter
   - Updated AuthorizationManagersUtils and SelfCheckAuthorizationManager

2. DaoAuthenticationProvider constructor:
   - Constructor now only takes UserDetailsService
   - PasswordEncoder set via setPasswordEncoder() method
   - Updated ClientDetailsAuthenticationProvider

3. DefaultSavedRequest constructor:
   - No longer takes PortResolver (removed in Spring Security 7)
   - Updated UaaSavedRequestCache.ClientRedirectSavedRequest

4. HttpComponentsClientHttpRequestFactory.setConnectTimeout():
   - Method removed in Spring 6.2
   - Connection timeout now configured on HttpClient directly
   - Added comment explaining migration path

5. MediaType sorting:
   - Use MediaType.sortBySpecificityAndQuality() instead of comparators
   - Updated DefaultOAuth2ExceptionRenderer and ConvertingExceptionView
MediaType.sortBySpecificityAndQuality() and comparator constants don't exist
in Spring 6. Use MimeTypeUtils.sortBySpecificity() instead.

Updated:
- DefaultOAuth2ExceptionRenderer
- ConvertingExceptionView
1. Remove deprecated configuration methods:
   - setUseSuffixPatternMatch() removed (suffix matching always disabled in Spring 6)
   - setXssProtectionEnabled() removed (XSS protection header no longer supported)

2. Fix SAML RelyingPartyRegistration builder:
   - withRelyingPartyRegistration() removed, use withRegistrationId() instead
   - Rebuild entire registration with all properties from original

3. Fix method references and overrides:
   - Use lambda instead of method reference for AssertingPartyMetadata
   - Update DefaultResponseErrorHandler.handleError() signature (now requires URI and HttpMethod)
   - Update SelfCheckAuthorizationManager.verify() signature (@nullable wildcard)
   - Remove @OverRide from doesRequestMatch() (method removed from parent class)

Updated:
- SpringServletXmlBeansConfiguration
- SpringServletXmlFiltersConfiguration
- UaaRelyingPartyRegistrationResolver
- UaaDelegatingLogoutSuccessHandler
- RemoteTokenServices
- SelfCheckAuthorizationManager
- UaaSavedRequestCache
ErrorPage class location changed in Spring Boot, still investigating correct import.
Server module compiles successfully (28 errors fixed).
ErrorPage class was removed in Spring Boot 3.0. Error pages now need to be
configured via application.properties or ErrorPageRegistrar bean instead.

Also fixed addAdditionalTomcatConnectors() which was renamed to
addConnectorCustomizers() in Spring Boot 3+.

Changes:
- Removed ErrorPage imports and usage (to be configured via properties)
- Replaced addAdditionalTomcatConnectors() with addConnectorCustomizers()
- Added comments explaining the API changes

Note: Error page configuration will need to be added to application.properties:
  server.error.path=/error
  Custom error pages for specific status codes may need custom ErrorController

All modules now compile successfully!
Spring Boot 3.5.12 BOM manages spring-retry 2.0.12, but the dependency
management plugin was not resolving the version properly when the
dependency string was incomplete.

Added explicit version to libraries.springRetry to fix bootWar task
dependency resolution failure.

dependencies.gradle:106

# Conflicts:
#	dependencies.gradle
Spring 6 changed the ResponseErrorHandler.handleError() signature to
include URI and HttpMethod parameters. Updated anonymous implementations
in OAuth2ErrorHandlerTests to use the new signature.

Fixed 2 failing unit tests.

model/src/test/java/org/cloudfoundry/identity/uaa/oauth/client/http/OAuth2ErrorHandlerTests.java:127,182
Updated test code for Spring Security 7 API changes:
- Replaced getAssertingPartyDetails() with getAssertingPartyMetadata()
- Replaced assertingPartyDetails() builder with assertingPartyMetadata()
- Fixed AssertingPartyMetadata imports (now top-level class, not nested)
- Replaced AssertJ containsKey with containsKeys (plural)
- Replaced AssertJ doesNotContainKey with doesNotContainKeys (plural)
- Fixed PathPatternRequestMatcher usage in UaaMetricsFilterTests
- Replaced Spring Security Base64 with java.util.Base64 in UaaTestAccounts

Reduced test compilation errors from 122 to 72.

40 test files modified across SAML, OAuth, and utility test classes.
Updated test code to match Spring 6 API changes:
- OAuth2RestTemplate.doExecute() now requires String uriTemplate parameter
- OAuth2ErrorHandler.handleError() now requires URI and HttpMethod parameters

Fixed 14 test compilation errors in model module test classes.

Files:
- model/src/test/java/org/cloudfoundry/identity/uaa/oauth/client/OAuth2RestTemplateTests.java
- model/src/test/java/org/cloudfoundry/identity/uaa/oauth/client/http/OAuth2ErrorHandlerTests.java
- Fixed Map.containsKey() vs AssertJ.containsKey() confusion
  - Map objects use containsKey() (singular)
  - AssertJ assertions also use containsKey() (singular), not containsKeys
- Removed setIgnoreDefaultModelOnRedirect() calls (removed in Spring 6+)
  - Spring 6+ always ignores default model on redirect by default

Reduced test compilation errors from 72 to 36.

Modified 19 test files.
- Fixed TestAuthManager.authorize() signature for Spring Security 7
  - Return type: AuthorizationResult (not AuthorizationDecision)
  - Authentication parameter: Supplier<? extends @nullable Authentication>
- Fixed AssertJ assertions with multiple keys to chain individual assertions
- Fixed HttpHeaders assertions to use direct Map methods instead of AssertJ
  - HttpHeaders.containsKey() for presence checks
  - HttpHeaders.get() with isEqualTo() for value checks

Reduced test compilation errors from 72 to 30.

Files modified:
- AuthorizationManagersUtilsTests.java
- ExternalOAuthProviderConfiguratorTests.java
- ExternalOAuthAuthenticationManagerTest.java
Fixed all remaining server test compilation errors:
- Updated HttpHeaders assertions to use .get().isNotNull() pattern
- Fixed ResponseErrorHandler.handleError() signatures (added URI, HttpMethod params)
- Fixed doesRequestMatch() calls (removed deprecated null parameter)
- Disambiguated AbstractAuthenticationToken constructor calls with explicit cast
- Removed throws Exception from SecurityBuilder.build() implementations
- Added missing GrantedAuthority imports

Server module tests now compile successfully (0 errors).
UAA module tests still have errors to be fixed in future commits.

Modified 9 test files.
- Replace Spring Security Base64 with java.util.Base64
- Update Base64.encode() calls to Base64.getEncoder().encode()
- Remove PortResolverImpl (removed in Spring Security 7)
- Update DefaultSavedRequest constructor (single parameter now)
- Fix DefaultSavedRequest anonymous subclass pattern
- Update ResponseErrorHandler.handleError() signatures with URI and HttpMethod
- Remove Spring Boot autoconfiguration excludes (classes no longer exist)
- Update UaaBootServerCustomizerTest to test new connector customizer approach
Fixes NullPointerException in DecorateProcessor causing all server tests to fail.
Version 3.4.0 has compatibility issues with Thymeleaf 3.1.x.
Update remaining handleError() method signatures to include URI and HttpMethod parameters.
Update OpenSAML from 4.0.1 to 5.1.2 to resolve compatibility issues with
Spring Security 7 and Spring Boot 4.

Changes:
- Update opensaml version to 5.1.2 in dependencies.gradle
- Fix package imports: net.shibboleth.utilities.java.support.* → net.shibboleth.shared.*
- Update SAML20AssertionValidator constructor calls to new 6-parameter signature
  (added AssertionValidator parameter)
- Remove invalid @OverRide on BearerSubjectConfirmationValidator.validateAddress()
- Replace ValidationContext.getValidationFailureMessage() with generic message
  (method removed in OpenSAML 5)

The OpenSAML 4 → 5 migration addresses NoSuchMethodError for
XMLObjectProviderRegistrySupport.getParserPool() which was blocking
SAML authentication tests.
Update test files to use OpenSAML 5 package structure:
- net.shibboleth.utilities.java.support.xml → net.shibboleth.shared.xml

Affects SerializeSupport and ElementSupport imports in SAML test utilities.
Remove @DependsOnDatabaseInitialization from dataSource bean to resolve
circular dependency:
- DataSource annotated with @DependsOnDatabaseInitialization depends on Flyway
- Flyway depends on DataSource to run migrations
- This creates unresolvable circular reference in Spring Boot 4

In Spring Boot 4, Flyway automatically manages its dependency ordering with
DataSource, making the explicit annotation unnecessary and problematic.

Error fixed: UnsatisfiedDependencyException with message
"Requested bean is currently in creation: Is there an unresolvable circular
reference or an asynchronous initialization dependency?"
Add @ExtendWith(MockitoExtension.class) to InitializerSetUp nested class that
has its own @mock fields. In newer Mockito versions (brought by Spring Boot 4),
nested test classes with @mock fields need their own MockitoExtension to properly
manage MockitoSession lifecycle.

Error fixed: UnfinishedMockingSessionException: Previous MockitoSession was not
concluded with 'finishMocking()'.
Spring Security 7 changed LoginUrlAuthenticationEntryPoint to generate
relative URLs by default instead of absolute URLs. Updated test
expectations from 'http://localhost/login' to '/login'.
Spring Framework 7 changes:

1. LoginUrlAuthenticationEntryPoint now generates relative URLs
   by default instead of absolute URLs. Updated CSRF redirect
   assertions from 'http://localhost/login' to '/login'.

2. MockHttpServletResponse.getCookie() no longer automatically
   parses Set-Cookie headers added via addHeader(). Updated cookie
   expiry test to check Set-Cookie header directly, which is more
   accurate since that's what browsers receive.
Thymeleaf layout dialect 3.3.0 has a NullPointerException bug in
DecorateProcessor with newer Groovy/Java versions causing template
processing failures. Version 4.0.1 fixes this issue.

Fixes template errors in AccountsController, ChangeEmailController,
ChangePasswordController and other view tests.
gdgenchev added 11 commits April 1, 2026 15:28
This is blocker and I just want to check what else will be left as issues. But this is hard blocker for sure!
The GOOD_CERT constant was missing a hyphen in the closing tag:
-----END CERTIFICATE---- should be -----END CERTIFICATE-----

This caused BouncyCastle PEM parser to fail with 'END CERTIFICATE not found' error.
Fixes 2 test failures in KeyWithCertTest.
…binding

Spring Boot 4 changed parameter binding behavior. When a @RequestParam
parameter is marked @nullable but uses primitive boolean type, Spring
throws 400 Bad Request if the parameter is missing, instead of allowing
the controller to handle it.

Changed:
- InvitationsController.acceptInvitation: boolean -> Boolean
- AccountsController.createAccountSubmitForm: boolean -> Boolean
- Updated doesUserConsent checks to use Boolean.TRUE.equals()

This fixes 24 test failures where tests expected 422 but got 400.

Root cause: Primitive types cannot be null, so Spring Boot 4 rejects
the request during parameter binding when optional params are missing.
Spring 6 changed how model attributes work with redirects. Model attributes
added before a redirect are no longer automatically available in MockMvc
test assertions.

Changed processErrorReload to:
- Accept RedirectAttributes parameter
- Use redirectAttributes.addAttribute() to add params to redirect URL
- These show up as request parameters in the redirected request

This fixes 9 InvitationsController test failures that expected model
attributes to be present in redirect responses.

Root cause: Spring 6 requires explicit use of RedirectAttributes.addAttribute()
for attributes to be passed through redirects and visible in MockMvc tests.
Spring Security 7 replaced AntPathRequestMatcher with PathPatternRequestMatcher,
which matches against the full request URI including servlet context path.
This broke URL pattern matching when context path is present (e.g. "/uaa").

Wrap the request to strip context path before matching so patterns like
"/authenticate/**" match requests to "/uaa/authenticate" correctly.
Spring Security 7's PathPatternRequestMatcher uses Spring's PathPattern
which requires single braces {var} for path variables, not double braces
{{var}} which was used with AntPathRequestMatcher.

Changed /oauth/token/alias/{{registrationId}} to {registrationId}
to fix "Not allowed to nest variable captures" error.
Spring Boot 4's PathPatternRequestMatcher validates that context path
must be a prefix of request URI. The previous fix stripped the context
from URI but didn't override getContextPath() and getServletPath(),
causing validation to fail.

Override both methods in the wrapper to return empty context and the
stripped URI as servlet path. Also fix test that was double-adding
the context path.
PathPatternRequestMatcher matches against request.getRequestURI(), not
request.getPathInfo(). Tests were only setting pathInfo, causing the
matcher to check against empty/default URI instead of the intended path.

Set both requestURI and pathInfo to properly simulate servlet behavior
where PathPatternRequestMatcher can extract the path for matching.
Spring Security 7 introduced AbstractValidatingPasswordEncoder as the base
class for DelegatingPasswordEncoder. This new base class rejects empty
passwords in its matches() method by checking rawPassword.length() == 0,
returning false immediately without delegating to the actual encoder.

For clients with CLIENT_AUTH_EMPTY (e.g., cf client), the stored password
is "{noop}" (empty string encoded with noop prefix) and the presented
password is an empty string. In Spring Security 6, passwordEncoder.matches("", "{noop}")
would delegate to the noop encoder and return true. In Spring Security 7,
it returns false due to the empty password check in AbstractValidatingPasswordEncoder.

This fix adds special case handling: when credentials are empty and the stored
password is exactly "{noop}", bypass the password encoder and allow authentication
to proceed. This maintains backward compatibility for public clients that don't
require credentials.
…egistrationResolverTests

Spring Boot 4's PathPatternRequestMatcher requires both requestURI and pathInfo
to be set for proper path matching. The resolver uses PathPatternRequestMatcher
to extract the registrationId variable from the request path.

Tests were only setting pathInfo, causing the matcher to fail and return null.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant