To ensure backward compatibility, Security components that implement Serializable should have a serialVersionUID.

Based on internal testing across a few dozen JVMs, it appears that the serialVersionUID is consistent for Security's components. As such, we can safely add the calculated serialVersionUID value to each class that is missing it during the 6.4.x maintenance cycle.

  • [x] #16274
  • [x] Produce list of Serializable classes missing serialVersionUID
  • [ ] Fail build when a Serializable class is missing a serialVersionUID
  • [ ] Consider adding an exclusion list for classes we don't support being serialized

When addressing a class that is missing its serialVersionUID, please do the following:

  1. Add the calculated serialVersionUID (IDEs can usually do this for you, or you can use serialver which ships with the JVM)
  2. In SpringSecurityCoreVersionSerializableTests, add the class and an example construction to the generatorByClassName map
  3. Run SpringSecurityCoreVersionSerializableTests#serializeCurrentVersionClasses.
  4. If successful, it will create a {className}.serialized file in config/src/main/resources/serialized:

    Run the other tests in SpringSecurityCoreVersionSerializableTests; because it's new, the class will not be added to the list in shouldBeAbleToDeserializeClassFromPreviousVersion; however, the class should no longer be in the output for listClassesMissingSerialVersion

    Commit the Serialiizable class(es) and SpringSecurityCoreVersionSerializableTests 5. If unsuccessful, it is usually because one of its members is not serializable. Find the unserializable member; file a ticket to ensure that it is made Serializable

Here are the classes:

  • [x] org.springframework.security.cas.jackson2.CasJackson2Module
  • [x] org.springframework.security.saml2.Saml2Exception
  • [x] org.springframework.security.saml2.jackson2.Saml2Jackson2Module
  • [x] org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException
  • [x] ~~org.springframework.security.web.access.expression.WebExpressionConfigAttribute~~
  • [x] org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException
  • [x] org.springframework.security.web.authentication.rememberme.CookieTheftException
  • [x] org.springframework.security.web.authentication.rememberme.InvalidCookieException
  • [x] org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException
  • [x] org.springframework.security.web.authentication.session.SessionAuthenticationException
  • [x] org.springframework.security.web.authentication.session.SessionFixationProtectionEvent
  • [x] org.springframework.security.web.authentication.switchuser.AuthenticationSwitchUserEvent
  • [x] org.springframework.security.web.authentication.www.NonceExpiredException
  • [x] org.springframework.security.web.csrf.CsrfException
  • [x] org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler$SupplierCsrfToken
  • [x] org.springframework.security.web.csrf.DefaultCsrfToken
  • [x] org.springframework.security.web.csrf.InvalidCsrfTokenException
  • [x] org.springframework.security.web.csrf.LazyCsrfTokenRepository$SaveOnAccessCsrfToken
  • [x] org.springframework.security.web.csrf.MissingCsrfTokenException
  • [x] org.springframework.security.web.firewall.RequestRejectedException
  • [x] org.springframework.security.web.jackson2.WebJackson2Module
  • [x] org.springframework.security.web.jackson2.WebServletJackson2Module
  • [x] org.springframework.security.web.savedrequest.SimpleSavedRequest
  • [x] org.springframework.security.web.server.authentication.SwitchUserWebFilter$SwitchUserAuthenticationException
  • [x] org.springframework.security.web.server.csrf.CsrfException
  • [x] org.springframework.security.web.server.csrf.DefaultCsrfToken
  • [x] org.springframework.security.web.server.firewall.ServerExchangeRejectedException
  • [x] org.springframework.security.web.server.jackson2.WebServerJackson2Module
  • [x] org.springframework.security.web.session.HttpSessionCreatedEvent
  • [x] org.springframework.security.web.session.HttpSessionDestroyedEvent
  • [x] org.springframework.security.web.session.HttpSessionIdChangedEvent
  • [x] org.springframework.security.web.session.SessionInformationExpiredEvent
  • [x] org.springframework.security.web.webauthn.authentication.WebAuthnAuthentication
  • [ ] org.springframework.security.web.webauthn.authentication.WebAuthnAuthenticationRequestToken
  • [x] org.springframework.security.web.webauthn.jackson.AttestationConveyancePreferenceSerializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticationExtensionsClientInputSerializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticationExtensionsClientInputsSerializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticationExtensionsClientOutputsDeserializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticatorAttachmentDeserializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticatorAttachmentSerializer
  • [x] org.springframework.security.web.webauthn.jackson.AuthenticatorTransportDeserializer
  • [x] org.springframework.security.web.webauthn.jackson.BytesSerializer
  • [x] org.springframework.security.web.webauthn.jackson.COSEAlgorithmIdentifierDeserializer
  • [x] org.springframework.security.web.webauthn.jackson.COSEAlgorithmIdentifierSerializer
  • [x] org.springframework.security.web.webauthn.jackson.CredProtectAuthenticationExtensionsClientInputSerializer
  • [x] org.springframework.security.web.webauthn.jackson.DurationSerializer
  • [x] org.springframework.security.web.webauthn.jackson.PublicKeyCredentialTypeDeserializer
  • [x] org.springframework.security.web.webauthn.jackson.PublicKeyCredentialTypeSerializer
  • [x] org.springframework.security.web.webauthn.jackson.ResidentKeyRequirementSerializer
  • [x] org.springframework.security.web.webauthn.jackson.UserVerificationRequirementSerializer
  • [x] org.springframework.security.web.webauthn.jackson.WebauthnJackson2Module
  • [x] org.springframework.security.oauth2.core.OAuth2AuthenticationException
  • [x] org.springframework.security.oauth2.core.OAuth2AuthorizationException
  • [x] org.springframework.security.access.AccessDeniedException
  • [x] org.springframework.security.access.AuthorizationServiceException
  • [x] org.springframework.security.access.SecurityConfig
  • [x] org.springframework.security.access.annotation.Jsr250SecurityConfig
  • [x] org.springframework.security.access.event.AuthenticationCredentialsNotFoundEvent
  • [x] org.springframework.security.access.event.AuthorizationFailureEvent
  • [x] org.springframework.security.access.event.AuthorizedEvent
  • [x] org.springframework.security.access.event.PublicInvocationEvent
  • [x] org.springframework.security.access.expression.method.PostInvocationExpressionAttribute
  • [x] org.springframework.security.access.expression.method.PreInvocationExpressionAttribute
  • [x] org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor
  • [x] org.springframework.security.authentication.AccountExpiredException
  • [x] org.springframework.security.authentication.AuthenticationCredentialsNotFoundException
  • [x] org.springframework.security.authentication.AuthenticationServiceException
  • [x] org.springframework.security.authentication.BadCredentialsException
  • [x] org.springframework.security.authentication.CredentialsExpiredException
  • [x] org.springframework.security.authentication.DisabledException
  • [x] org.springframework.security.authentication.InsufficientAuthenticationException
  • [x] org.springframework.security.authentication.InternalAuthenticationServiceException
  • [x] org.springframework.security.authentication.LockedException
  • [x] org.springframework.security.authentication.ProviderNotFoundException
  • [x] org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureCredentialsExpiredEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureDisabledEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureExpiredEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureLockedEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureProviderNotFoundEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureProxyUntrustedEvent
  • [x] org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent
  • [x] org.springframework.security.authentication.event.AuthenticationSuccessEvent
  • [x] org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent
  • [x] org.springframework.security.authentication.event.LogoutSuccessEvent
  • [x] org.springframework.security.authentication.jaas.event.JaasAuthenticationFailedEvent
  • [x] org.springframework.security.authentication.jaas.event.JaasAuthenticationSuccessEvent
  • [x] org.springframework.security.authentication.ott.InvalidOneTimeTokenException
  • [x] org.springframework.security.authentication.password.CompromisedPasswordException
  • [x] org.springframework.security.authorization.AuthorizationDeniedException
  • [x] org.springframework.security.authorization.event.AuthorizationDeniedEvent
  • [x] org.springframework.security.authorization.event.AuthorizationEvent
  • [x] org.springframework.security.authorization.event.AuthorizationGrantedEvent
  • [x] org.springframework.security.core.ComparableVersion$ListItem
  • [x] org.springframework.security.core.context.SecurityContextChangedEvent
  • [x] org.springframework.security.core.context.TransientSecurityContext
  • [x] org.springframework.security.core.session.AbstractSessionEvent
  • [x] org.springframework.security.core.userdetails.UsernameNotFoundException
  • [x] org.springframework.security.jackson2.CoreJackson2Module
  • [x] org.springframework.security.jackson2.SecurityJackson2Modules$AllowlistTypeResolverBuilder
  • [x] org.springframework.security.access.annotation.BusinessServiceImpl
  • [x] org.springframework.security.access.annotation.ExpressionProtectedBusinessServiceImpl
  • [x] org.springframework.security.access.annotation.Jsr250BusinessServiceImpl
  • [x] org.springframework.security.crypto.codec.Base64$InvalidBase64CharacterException
  • [x] org.springframework.security.ldap.authentication.ad.ActiveDirectoryAuthenticationException
  • [x] org.springframework.security.ldap.jackson2.LdapJackson2Module
  • [x] org.springframework.security.ldap.ppolicy.PasswordPolicyControl
  • [x] org.springframework.security.ldap.ppolicy.PasswordPolicyException
  • [x] org.springframework.security.ldap.ppolicy.PasswordPolicyResponseControl
  • [x] org.springframework.security.messaging.access.expression.MessageExpressionConfigAttribute
  • [x] org.springframework.security.oauth2.client.ClientAuthorizationException
  • [x] org.springframework.security.oauth2.client.ClientAuthorizationRequiredException
  • [x] org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module
  • [x] org.springframework.security.oauth2.client.web.InvalidClientRegistrationIdException
  • [x] org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter$OAuth2AuthorizationRequestException
  • [x] org.springframework.security.oauth2.jwt.BadJwtException
  • [x] org.springframework.security.oauth2.jwt.JwtDecoderInitializationException
  • [x] org.springframework.security.oauth2.jwt.JwtEncodingException
  • [x] org.springframework.security.oauth2.jwt.JwtException
  • [x] org.springframework.security.oauth2.jwt.JwtValidationException
  • [x] org.springframework.security.oauth2.server.resource.InvalidBearerTokenException
  • [x] org.springframework.security.oauth2.server.resource.introspection.BadOpaqueTokenException
  • [x] org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionException

You can also see the list of Serializable files by running:

./gradlew :spring-security-config:test --tests "*MissingSerialVersion*" -Pserialization

Comment From: jzheaux

Here is an example: https://github.com/spring-projects/spring-security/commit/e3cd4339b2c5cb43f5cde41b7ecbd659a1d8bf59

Comment From: dalbani

Based on internal testing across a few dozen JVMs, it appears that the serialVersionUID is consistent for Security's components. As such, we can safely add the calculated serialVersionUID value to each class that is missing it during the 6.4.x maintenance cycle.

Upgrading my application storing sessions in database from Spring Boot 3.3.6 to Spring Boot 3.4.1 (i.e. Spring Security 6.4.2), I ran into this issue:

java.io.InvalidClassException: org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; local class incompatible: stream classdesc serialVersionUID = -4675866280835753141, local class serialVersionUID = -196018737016047617

So it looks like that the serialVersionUID that my JVM (RedHat's OpenJDK 21.0.5+10-LTS) calculated is different that yours, right?

I'm surprised that no one has been hit by the same issue 🤔

Comment From: jzheaux

Hi, @dalbani, thanks for the report. I'm a little surprised to see a difference as we internally tested several dozen JVMs to check the calculated value.

That may be why no one else has reported just yet.

We need to set a value at some point; otherwise folks will break every time one of these classes is edited. I'll take a look at the JVM you listed and get back to you.

Comment From: jzheaux

Here is my output from the latest from RedHat:

~/Downloads/java-21-openjdk-21.0.5.0.11-1.portable.jdk.x86_64/bin/serialver \
-classpath oauth2/oauth2-core/build/libs/spring-security-oauth2-core-6.4.2-SNAPSHOT.jar:core/build/libs/spring-security-core-6.4.2-SNAPSHOT.jar \
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority:    private static final long serialVersionUID = -4675866280835753141L;

This appears to agree with the value in OidcUserAuthority currently.

However, if I look at older versions:

Spring Security 6.2.7

~/Downloads/java-21-openjdk-21.0.5.0.11-1.portable.jdk.x86_64/bin/serialver \
-classpath oauth2/oauth2-core/build/libs/spring-security-oauth2-core-6.2.7-SNAPSHOT.jar:core/build/libs/spring-security-core-6.2.7-SNAPSHOT.jar \
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority:    private static final long serialVersionUID = -196018737016047617L;

Spring Security 6.3.0

~/Downloads/java-21-openjdk-21.0.5.0.11-1.portable.jdk.x86_64/bin/serialver \
-classpath oauth2/oauth2-core/build/libs/spring-security-oauth2-core-6.3.0-SNAPSHOT.jar:core/build/libs/spring-security-core-6.3.0-SNAPSHOT.jar \
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority
org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority:    private static final long serialVersionUID = -196018737016047617L;

Then I think it's a little clearer what's going on.

Because OidcUserAuthority changed its public API in 6.4, this resulted in a different calculated serialVersionUID. The local id you are seeing is likely from a serialized version of OidcUserAuthority from a previous minor version of Spring Security.

In other words, this is something you would have experienced when upgrading, independent of the commit that added serialVersionUID to this class. Once this ticket is complete, then deserializations across future minor versions of Spring Security will be much more stable.

As a whole, this is something that we are addressing in order to eliminate deserialization issues across minor versions in Spring Security. You can read https://github.com/spring-projects/spring-security/issues/16163 to learn about the issue that triggered the need for this specific ticket.

Comment From: dalbani

Thanks @jzheaux for your feedback. The issue comes indeed from a different serialVersionUID value, when session objects originally serialized with Spring Security pre-6.4 are then read with Spring Security 6.4.

I was actually aware of the effort mentioned in https://docs.spring.io/spring-security/reference/6.3/whats-new.html#_passive_jdk_serialization_support — and I very much appreciate the improvement.

Though I'm not sure this "concern [has become] a thing of the past" as mentioned on https://spring.io/blog/2024/01/19/spring-security-6-3-adds-passive-jdk-serialization-deserialization-for. I mean, are all Java classes now covered? Won't there be new changes in say Spring Security 6.5?

I suppose I need to ask the people in charge of Spring Session, but what do think of introducing a behaviour in Spring Session that failing to deserialize a session automatically invalidates it? So that manually messing with sessions stored in database is not necessary anymore when doing "breaking" upgrades of Spring Security.

Comment From: jzheaux

You are correct that @dalbani was perhaps too confldent in hindsight. At the time, there was not an analysis performed to see which classes were missing an id.

I think this point in the description:

Fail build when a Serializable class is missing a serialVersionUID

Will go a long way to catching these before future releases.

Comment From: sn3jkeee

Experienced same issues as @dalbani mentioned, has to clear out Redis as it used by Spring Session. Quite unfortunate all users forced to logged out