Skip to content

Migrate to spring boot 4#3805

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

Migrate to spring boot 4#3805
gdgenchev wants to merge 64 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 before 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..). So, it turns out Jackson 2 -> 3 could be done in isolation after spring boot 4 migration

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.

@strehle
Copy link
Copy Markdown
Member

strehle commented Apr 6, 2026

thanks for this PR, we need to solve OpenSAML first and therefore started with: #3811

@strehle strehle linked an issue Apr 6, 2026 that may be closed by this pull request
@strehle
Copy link
Copy Markdown
Member

strehle commented Apr 18, 2026

@gdgenchev ok, with spring update you need opensaml5 and therefore rebase this and also pull changes from #3840 for testing. with this your SAML erros should disappear

strehle and others added 4 commits April 18, 2026 12:16
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@gdgenchev gdgenchev force-pushed the migrate-to-spring-boot-4 branch from 5e94f36 to f142b62 Compare April 18, 2026 22:20
@gdgenchev gdgenchev force-pushed the migrate-to-spring-boot-4 branch from 43ae15f to 2e4a063 Compare April 28, 2026 10:55
…rate-to-spring-boot-4

# Conflicts:
#	server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/Saml2BearerGrantAuthenticationConverter.java
#	server/src/main/java/org/cloudfoundry/identity/uaa/provider/saml/SamlAuthenticationFilterConfig.java
In Spring 6.2.x handleError(ClientHttpResponse response) was deprecated and later removed in Spring 7.x in favor of handleError(URI url, HttpMethod method, ClientHttpResponse response)
Spring Boot 4 moved DependsOnDatabaseInitialization to a separate
spring-boot-sql module. Added this dependency to the server module
where it's needed.

Related to Spring Boot 4 migration.
Spring Security 7 moved vote classes (AuthenticatedVoter, RoleVoter,
UnanimousBased) to a separate spring-security-access module that is
not automatically included. Added explicit dependency.

Related to Spring Boot 4 migration.
Spring Security 7 removed the Base64 class from the
spring-security-crypto.codec package. Replaced with java.util.Base64
in all files that used org.springframework.security.crypto.codec.Base64.

Related to Spring Boot 4 migration.
Spring Boot 4 moved autoconfigure classes to technology-specific modules:
- org.springframework.boot.autoconfigure.jdbc → org.springframework.boot.jdbc.autoconfigure
- org.springframework.boot.autoconfigure.transaction → org.springframework.boot.transaction.autoconfigure

Updated DatabaseConfiguration imports.

Related to Spring Boot 4 migration.
Spring Boot 4 reorganized autoconfigure packages.
- Removed unused WebMvcAutoConfiguration import from WebConfig
- Updated test annotation to use new autoconfigure package locations

Related to Spring Boot 4 migration.
Replace deprecated HttpComponentsClientHttpRequestFactory.setConnectTimeout()
with ConnectionConfig.setConnectTimeout() on PoolingHttpClientConnectionManager.
The connection timeout is now configured at the connection manager level using
the recommended ConnectionConfig.Builder API.
Spring Framework 7 replaced HttpHeaders.containsKey() with
containsHeader(). This updates all occurrences in OAuth2 token
handling code and test utilities.

Related to Spring Boot 4 migration.
…ng 7

Spring 7 removed MediaType.sortByQualityValue() and QUALITY_VALUE_COMPARATOR
that were deprecated in Spring 6. Copy the sorting logic into a new
MediaTypeComparators utility class to preserve content negotiation behavior
that respects client quality value preferences from Accept headers.

https://github.com/spring-projects/spring-framework/blob/9f431e2eac1b6d8d5ca385d0cc367bac94dd37e7/spring-web/src/main/java/org/springframework/http/MediaType.java#L927-L965
- Rename MediaTypeComparators to MediaTypeUtils for better semantics
- Add sortByQualityValue() utility method to handle immutable lists
- Fix usages to create mutable copies before sorting
- Remove unnecessary if-else in ConvertingExceptionView
- Add comprehensive unit tests including parameterized tests
In Spring Boot 4, LDAP and Session support became optional starters:
- spring-boot-starter-ldap
- spring-boot-starter-session-*

Since these starters are not included as dependencies, their
AutoConfiguration classes don't exist on the classpath and don't
need to be excluded.

Removed exclusions:
- LdapAutoConfiguration
- SessionAutoConfiguration

**Cannot be done in isolation** - Boot 4 only change.
Spring Security 7's DaoAuthenticationProvider rejects empty passwords.
Handle empty credentials by manually checking password encoding instead
of calling super.additionalAuthenticationChecks().
Spring Security 7 changed LoginUrlAuthenticationEntryPoint to generate
relative URLs by default instead of absolute URLs.
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.
Spring Security 7's RelyingPartyRegistration.mutate() is called by
UaaRelyingPartyRegistrationResolver. Mock the builder chain to prevent NPE.
Replace providedRuntime(spring-boot-starter-tomcat) with
providedRuntime(spring-boot-starter-tomcat-runtime) per the Spring Boot 4
migration guide for external Tomcat WAR deployment.

In Spring Boot 4, spring-boot-starter-tomcat transitively pulls in
spring-boot-starter which includes spring-boot, spring-context etc., causing
those to land in WEB-INF/lib-provided and be missing at runtime on external
Tomcat. The new spring-boot-starter-tomcat-runtime artifact contains only the
Tomcat embed jars that the container actually provides.

Also remove the now-redundant providedCompile(tomcatEmbed) — originally added
to provide javax.servlet.* at compile time, but tomcat-embed-core is already
available transitively via spring-boot-starter-web.
Spring Boot BOM version resolution requires explicit version.
# Conflicts:
#	dependencies.gradle
Spring Boot 4 defaults to Jackson 3 (spring-boot-jackson) via
spring-boot-starter-web. This breaks custom Jackson 2 serializers
(e.g. OAuth2ExceptionJackson2Serializer) and spring.jackson.*
property binding which uses tools.jackson enums instead of
com.fasterxml.jackson enums.

Globally exclude spring-boot-jackson and add spring-boot-jackson2
(the Jackson 2 compatibility layer) per the Spring Boot 4 migration
guidance for projects staying on Jackson 2.
In Spring Boot 4, Flyway auto-configuration was extracted from
spring-boot-autoconfigure into the separate spring-boot-flyway module.
Without it, FlywayDatabaseInitializerDetector is not registered, making
@DependsOnDatabaseInitialization a no-op and causing beans to query
the database before Flyway migrations have run.
…-config

SecurityAutoConfiguration previously provided a DefaultAuthenticationEventPublisher
bean via spring-boot-autoconfigure. In Spring Boot 4 it moved to the separate
spring-boot-security module which is not on the classpath.
Spring Security 7 enforces that patterns passed to requestMatchers()
must start with a /. The pattern "oauth/clients/meta" was missing
the leading slash.
…tdocs

spring-restdocs-core:4.0.0 depends on tools.jackson.core:jackson-databind:3.0.2.
With Jackson 3 on the classpath, Spring Framework 7's DefaultHttpMessageConverters
selects JacksonJsonHttpMessageConverter (Jackson 3) over MappingJackson2HttpMessageConverter
(Jackson 2). Jackson 3 ignores Jackson 2's @JsonSerialize annotations, causing
OAuth2AccessToken responses to use bean-property serialization ("value", "tokenType")
instead of the custom serializer output ("access_token", "token_type"). This fixes
~1727 of the ~1783 test failures in the uaa module.
Spring Security 7's LoginUrlAuthenticationEntryPoint.favorRelativeUris
defaults to true, returning relative URLs (/login) instead of absolute
URLs (http://localhost/login). Update all affected test assertions in
login, invitation, password reset, and SCIM group endpoint tests.
Spring Security 7 treats anonymous requests as authenticated (with
AnonymousAuthenticationToken) but unauthorized, returning 403 Forbidden
instead of 401 Unauthorized when accessing protected API endpoints
without a Bearer token.
…ng Security 7

Spring Security 7's HttpSessionRequestCache.matchesSavedRequest() now
calls savedRequest.getRedirectUrl() without null check, and
FrameworkServlet.service() calls Set.contains(request.getMethod())
which throws on null. Both NPE when the mock doesn't stub these methods.
Spring Security 7's DelegatingPasswordEncoder now extends
AbstractValidatingPasswordEncoder which rejects empty rawPassword
in matches(). Non-UAA users store "" encoded as "{noop}" — assert
the stored value directly instead of going through matches().
Spring Security 7's AbstractLdapAuthenticationProvider now adds a
FactorGrantedAuthority("FACTOR_PASSWORD") to every password-based
authentication. This is framework-internal MFA tracking, not a UAA
scope. Filter it from test assertions that verify UAA scope mapping.
@gdgenchev gdgenchev force-pushed the migrate-to-spring-boot-4 branch from 2e4a063 to 51d132e Compare April 28, 2026 11:41
Run org.openrewrite.java.jackson.UpgradeJackson_2_3 via Moderne CLI.
Covers 127 files with mechanical renames:
- com.fasterxml.jackson.core → tools.jackson.core
- com.fasterxml.jackson.databind → tools.jackson.databind
- JsonSerializer → ValueSerializer
- JsonDeserializer → ValueDeserializer
- SerializerProvider → SerializationContext
- IOException → JacksonException in catch blocks
- writeObjectField → writeObjectProperty
- textValue() → asString()

Note: com.fasterxml.jackson.annotation imports are unchanged
(jackson-annotations remains at com.fasterxml namespace in Jackson 3).
- Remove global exclusions for tools.jackson.core and tools.jackson
  (we now use Jackson 3 directly)
- Remove spring-boot-jackson exclusion (Jackson 3 auto-config is wanted)
- Switch jacksonDatabind to tools.jackson.core:jackson-databind
- Switch jacksonDataformatYaml to tools.jackson.dataformat:jackson-dataformat-yaml
- Remove spring-boot-jackson2 dependency from server and uaa modules
Jackson 3 changed several defaults from Jackson 2. Configure both
JsonUtils and JacksonMapperCustomizer to restore Jackson 2 behavior:
- DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS: was true (Jackson 2), now false
- SerializationFeature.FAIL_ON_EMPTY_BEANS: was true (Jackson 2), now false
- DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES: was false (Jackson 2), now true
- MapperFeature.SORT_PROPERTIES_ALPHABETICALLY: was false (Jackson 2), now true
- ConstructorDetector: prevent Jackson 3 from preferring multi-param
  constructors when a no-arg constructor exists

Also catch JacksonException in convertValue (Jackson 3 throws
MismatchedInputException directly instead of wrapping in
IllegalArgumentException).
The spring.jackson.serialization.write-dates-as-timestamps property is
no longer valid in Jackson 3/Spring Boot 4. The date timestamp behavior
is now configured directly in the ObjectMapper via
DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS in JacksonMapperCustomizer.

Also remove the javadoc and @propertysource annotation since the
property file no longer exists.
Manual corrections for Jackson 3 API changes that the OpenRewrite
recipe either missed or got wrong:

- getCurrentToken() → currentToken(), getCurrentName() → currentName()
- writeObjectProperty() → writePOJOProperty() for writing POJO fields
- SimpleType.construct() removed; use ctxt.constructType() instead
- JsonUtils.readTree(parser) → parser.readValueAsTree() in deserializers
- StreamReadException constructor changed: use (parser, message, cause)
- UnrecognizedPropertyException: use .from(parser, ...) factory method
- DatabindException: use .from(parser, message) factory method
- Remove leftover IOException imports
- Remove empty @JsonSerialize/@JsonDeserialize on ExpiringCode
- Replace deprecated @JsonSerialize(include=) with @JsonInclude
- Remove deprecated include= parameter from @JsonSerialize on ScimMeta
IdToken: Add @JsonCreator with explicit @JsonProperty on all constructor
parameters. Jackson 3 reports conflicting property names when a field
has @JsonProperty("cid") but the getter has @JsonProperty("client_id").
The explicit @JsonCreator annotation resolves the ambiguity.

HeaderParameters: Remove @JsonCreator from the 3-param constructor.
Jackson 3's ConstructorDetector was selecting it during convertValue()
operations even when 'alg' was absent, causing "alg is required" errors.
The no-arg constructor with field-level @JsonProperty handles
deserialization correctly.
- Update exception class name assertions:
  com.fasterxml.jackson.core.JsonParseException → tools.jackson.core.exc.StreamReadException
  com.fasterxml.jackson.databind.exc.InvalidDefinitionException → tools.jackson.databind.exc.InvalidDefinitionException
  IllegalArgumentException → MismatchedInputException (convertValue)
- JsonDateDeserializerTest: pass JsonParser instead of TokenStreamLocation
- JsonDateSerializerTest: use JsonMapper.shared().createGenerator() instead
  of TokenStreamFactory (removed in Jackson 3)
- JsonTranslation: enable WRITE_DATES_AS_TIMESTAMPS (no longer default)
- OAuth2AccessTokenJackson2SerializerTests: DatabindException → IllegalArgumentException
  (thrown directly by serializer, not wrapped by Jackson 3)
- SAML tests: use Jackson 2 ObjectMapper for Spring Security's Jackson2
  modules (SecurityJackson2Modules requires Jackson 2 ObjectMapper)
@gdgenchev
Copy link
Copy Markdown
Contributor Author

gdgenchev commented Apr 29, 2026

Current Progress:

I picked saml 5 update.

I did spring boot 4 migration without open rewrite from scratch, incrementally, so that I can isolate changes that are compatible with current Spring Boot 3. All such have been proposed as PRs. Maybe we can also try with open rewrite after those are merged.

I reached a point where all unit tests pass and 35 ITs failed. But apidoc pipeline failed with Jackson issues, as I tried keeping Jackson 2... As effort was too high to make it work with Jackson 2, I just decided to try to migrate the whole project to Jackson 3 and it seems it worked nicely. Unit tests still pass, apidoc passes, but we have 90 failing ITs and I see some stacktraces related to jackson. Will check.

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.

Migrate to spring boot 4

2 participants