spring-attic / spring-security-oauth

Support for adding OAuth1(a) and OAuth2 features (consumer and provider) for Spring web applications.
http://github.com/spring-projects/spring-security-oauth
Apache License 2.0
4.69k stars 4.04k forks source link

Fail to deserialize OAuth2Authentication from different server #535

Open sckoh opened 9 years ago

sckoh commented 9 years ago

Server A is unable to deserialize OAuth2Authentication that is serialized by Server B

WARN (JdbcTokenStore.java:204) - Failed to deserialize authentication for 3abc332b-a405-4d98-8b80-4bb06f2e5353
java.lang.IllegalArgumentException: java.io.InvalidClassException: org.hibernate.collection.internal.AbstractPersistentCollection; local class incompatible: stream classdesc serialVersionUID = -7238232378593030571, local class serialVersionUID = -8914173462748164853
at org.springframework.security.oauth2.common.util.SerializationUtils.deserialize(SerializationUtils.java:40)
at org.springframework.security.oauth2.provider.token.store.JdbcTokenStore.deserializeAuthentication(JdbcTokenStore.java:385)

I'm using version 2.0.3

jgladch commented 9 years ago

I'm seeing this issue as well... any resolution?

benkiefer commented 8 years ago

We had a similar issue yesterday and figured out that we had serialized the authentication object with an old version of spring security (3.2.8) when the tokens were stored (access and refresh were effected), and then attempted to deserialize it with a new version (4.0.2). Authorization codes were also effected.

Spring Security has a SpringSecurityCoreVersion.SERIAL_VERSION_UID that is bumped periodically, and a major version got us a conflict that we weren't expecting.

We ended up having to read the serialized value out of the database, and then "bump" the serializationUID from the old number (320) to the new number (400). And by bump, we're talking re-writing bytes.

We added a test to our project to make sure that we are fully aware when these values change, as well as a couple other serial version uids that are in spring security oauth.

@dsyer any thoughts? Expected behavior? Alternative options? Any chance that a migration can be provided?

dsyer commented 8 years ago

I guess if someone contributes some code we can consider it. For my part it's not a high priority because all tokens are essentially ephemeral so it shouldn't cost users much to re-acquire them if the token store is simply wiped.

benkiefer commented 8 years ago

If a token was obtain with a resource owner or client credentials grant, I agree with you; for three-legged oauth with tokens that were obtained using an authorization code, then you'd need to put all of your users back through the OAuth login flow. Until this was completed, you'd have a disruption of service.

dsyer commented 8 years ago

Not sure I agree with that assessment. Social login providers put me through that process all the time, and I'm quite used to it, notwithstanding that they never tell me why they do it. However, I'm not sure that's relevant. You can always engineer a solution if you want to grant tokens based on previous user input. Maybe the ApprovalStore would be a useful avenue of research, or indeed a custom TokenStore if you care deeply about it?

benkiefer commented 8 years ago

Thanks @dsyer. We discussed this today and are going to get by with just overriding the serialization strategy for tokens and auth codes. Our tokens are too important to run the risk of an outage that requires the user to log back in.

I opened #617 so that we can override the serialization mechanism for authorization codes just like the JdbcTokenStore works. That would save us some code duplication.

zergxyz commented 8 years ago

I think the workaround solution is to use same spring-security-core version in your maven pom file to make sure different server can share the same SERIAL_VERSION_UID.

backtrack-5 commented 8 years ago

One workaround what I have done is,
1) Truncate table oauth_access_token 2) Make the new Oauth token request

It solved the problem for me. But this is an temporary fix.

smehgal commented 8 years ago

Thanks @sornalingam. It works for me.

dhairyashil120 commented 8 years ago

@benkiefer :+1: We are also getting the exactly same issue. @dsyer : We have around 10 million users spread across 1000 merchants. And making them login again will be a huge disruption of service.

Is there any solution or workaround to solve the issue elegantly?

benkiefer commented 8 years ago

@dhairyashil120 We had the exact same needs. Forcing the user to log in again was not an option.

It turns out that Spring "bumps" their serialization id and keeps it in sync with major/minor releases, and in our case, the underlying object had not actually changed (certainly not something we can count on in the future).

One of our devs wrote a groovy script that reads field out of the database and re-writes the authentication object's serialized id to the new value, and that got us through the crisis. We ended up having to rewrite the authentication object on both the refresh_token and access_token tables if I remember correctly.

We also explored storing the field as JSON using the hooks in the jdbc token store, but ultimately didn't have enough time to finish it. Note: Because of the way the class is structured, it isn't as simple as just using ObjectMapper to read and write the value. This also has drawbacks, because at least the serialization id is a clear warning that something changed and you need to pay attention.

We have since "pinned" our Spring security and oauth versions by writing tests that ensure that the serialization version doesn't change. If they fail, we know trouble is near.

This problem will become something we have to address before we can do a major update Spring again. Hope that info helps.

dhairyashil120 commented 8 years ago

@benkiefer Were your issue sorted out after running groovy script for changing object's serialized id to new value? Or have done anything else for it?

benkiefer commented 8 years ago

Things worked fine after upgrading the serialized version uid for us.

The important thing is that you need to make sure all your code is is using the same seralization version, and that all your tokens are up to date with that version id. You can't live in a world of half done or some of your customers won't be able to use your product.

Keep in mind, this is a time bomb. We update spring security with much more care now.

dhairyashil120 commented 8 years ago

@benkiefer : Thanks man..! We are going to use serialization uid "bump" approach. Also, We are not getting OAuth2Request object while deserialization. Had u also encountered such scenario ever?

benkiefer commented 8 years ago

It's been a long time since we worked through that issue, and I honestly can't remember.

As far as the deserialization fix goes, I can't stress how important it is to try this in a lower environment than production. It's actually fairly easy to produce the problem locally. Start up an oauth server on your current version of spring and get an oauth access token and refresh token for a user. Without cleaning your database, upgrade to the "next" version of spring. Then try and use that access token or refresh token. You should see the exception right away.

I'm glad to hear that not everyone is considering refresh tokens ephemeral. Forcing a user to log in again can't be the fix for everything.

youribonnaffe commented 8 years ago

Unless I'm misunderstanding the oauth token refresh/renewal mechanism, it seems to me too that the user is forced to login again after upgrading to a more recent release where the serialVersionUUID has been changed. For us it felt like a major issue.

In my case it seems only SimpleGrantedAuthority was affected by the changed UUID so I extended the JdbcTokenStore to handle the case where serialization fails:

        private class FixedSerialVersionUUIDJdbcTokenStore extends JdbcTokenStore {
            public FixedSerialVersionUUIDJdbcTokenStore(DataSource dataSource) {
                super(dataSource);
            }

            @Override
            protected OAuth2Authentication deserializeAuthentication(byte[] authentication) {
                return deserialize(authentication);
            }

            @Override
            protected OAuth2AccessToken deserializeAccessToken(byte[] token) {
                return deserialize(token);
            }

            @Override
            protected OAuth2RefreshToken deserializeRefreshToken(byte[] token) {
                return deserialize(token);
            }

            @SuppressWarnings("unchecked")
            private <T> T deserialize(byte[] authentication) {
                try {
                    return (T) super.deserializeAuthentication(authentication);
                } catch (Exception e) {
                    try (ObjectInputStream input = new FixSerialVersionUUID(authentication)) {
                        return (T) input.readObject();
                    } catch (IOException | ClassNotFoundException e1) {
                        throw new IllegalArgumentException(e1);
                    }
                }
            }
        }

        private class FixSerialVersionUUID extends ObjectInputStream {

            public FixSerialVersionUUID(byte[] bytes) throws IOException {
                super(new ByteArrayInputStream(bytes));
            }

            @Override
            protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
                ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();

                if (resultClassDescriptor.getName().equals(SimpleGrantedAuthority.class.getName())) {
                    ObjectStreamClass mostRecentSerialVersionUUID = ObjectStreamClass.lookup(SimpleGrantedAuthority.class);
                    return mostRecentSerialVersionUUID;
                }

                return resultClassDescriptor;
            }
        }        

Be aware that if SimpleGrantedAuthority changes, bad things might happen, hopefully it won't (https://github.com/spring-projects/spring-security/commits/master/core/src/main/java/org/springframework/security/core/authority/SimpleGrantedAuthority.java)

With this approach, serialized tokens with old serialVersionUUID are migrated to the new serialVersionUUID when they are used.

benkiefer commented 7 years ago

FYI - more info on how we got rid of using java serialization.

https://github.com/spring-projects/spring-security-oauth/issues/815

michaelmaitland commented 3 years ago

Still an issue. Any solution?