spring-authorization-server: OAuth2ClientAuthenticationToken should not be persisted across requests

Describe the bug Sometimes the server returns a 500 error with the message:

java.lang.IllegalArgumentException: The class with org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken and name of org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken is not in the allowlist. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See spring-projects/spring-security#4370 for details

To Reproduce

I encountered the bug while testing a resource server in conjunction with a new Spring Authorization Server’s authorization code flow. The API tool I use automatically does the OAuth2 authentication, using the same session cookie from last time when it gets a new token, and that then stops working due to the 500 error. This is a contrived example, but it does trigger the bug reliably:

  1. Modify the sample authorization server, adding .redirectUri("https://oidcdebugger.com/debug") to the RegisteredClient and then run the server.

  2. Open a browser, go to https://oidcdebugger.com/debug, open the network tab of the browser inspector.

  3. Fill the form in, log-in, and get an authorization code.

  4. Look in the browser inspector to get the JSESSIONID cookie from /oauth2/authorize.

  5. I’m not sure how quickly the next steps need to be done, but now POST directly to the /oauth2/token endpoint. Run curl or your preferred API client:

export AUTHCODE=“YOUR CODE HERE” && curl -X “POST” “http://127.0.0.1:9000/oauth2/token” \ -H ‘Cookie: JSESSIONID=COOKIE FROM BROWSER’ \ -H ‘Content-Type: application/x-www-form-urlencoded; charset=utf-8’ \ -u ‘messaging-client:secret’ \ –data-urlencode “grant_type=authorization_code” \ –data-urlencode “code=$AUTHCODE” \ –data-urlencode “redirect_uri=https://oidcdebugger.com/debug

  1. A token should be correctly returned.

  2. Click “Start Over” in the browser, which will hopefully cause the browser to send the same JSESSIONID with the new call to /oauth2/authorize, and get the new authorization code. Try the POST again with the new code and the same JSESSIONID. It should return a 500 error, and in the logs will be the exception.

Expected behavior

I expected to get a new token, and if not, an error message rather than an exception.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 2
  • Comments: 24 (6 by maintainers)

Commits related to this issue

Most upvoted comments

@jgrandja @colin-riddell

I believe what @colin-riddell is referring to (since we discussed this together offline) is a custom scenario using a different authentication mechanism other than form login, for example using JWT authentication. In this case, it seems possible that JwtAuthenticationToken would be placed in the java.security.Principal attribute as it is actually the principal representing the user’s authentication.

Our mixin inherits from Spring Security the ability to persist a UsernamePasswordAuthenticationToken into attributes. But in case of a different authentication, there would still be the need for an application to provide their own mixins to ensure the OAuth2Authorization can be persisted via JSON in the database. I feel this would be another example where a How-to guide could capture how this can be customized, and potentially such a mixin (that Colin has already developed a draft of) could be part of that guide.

Hope that helps.

Great, thank you @jgrandja and @sjohnr! The temporary fix works perfectly.

add Annotation com.fasterxml.jackson.databind.annotation.JsonSerialize

Is this something you want more info on in a separate issue @sjohnr - if it’s something the core team believes would be useful to be able to serialize, am happy to contribute that mixin.

Hi @colin-riddell. Since that is not something that’s happening out of the box (e.g. in the existing sample), probably not at this time. However, it may be required in the future. As a current example, when using the UserInfo endpoint you need to enable oauth2ResourceServer().jwt(). So it’s very close to a core requirement, but not enabled out-of-the-box as of yet.

Thanks for the feedback. I opened #507 with code working in my project, but it may contain unnecessary mixins which the core team don’t want. Once I have your guidance I’ll improve the code and get this done. Thanks!

Awesome. Thanks for the heads up! I will review it as soon as I can and get back to you with that feedback.

Our mixin inherits from Spring Security the ability to persist a UsernamePasswordAuthenticationToken into attributes. But in case of a different authentication, there would still be the need for an application to provide their own mixins to ensure the OAuth2Authorization can be persisted via JSON in the database. I feel this would be another example where a How-to guide could capture how this can be customized, and potentially such a mixin (that Colin has already developed a draft of) could be part of that guide.

@sjohnr I just encoutered the issue when deserializing OAuth2ClientAuthenticationToken. I am implemententing a custom grant type (the password grant type) but when I try to refresh the token I get this error:

The class with org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken and name of org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken is not in the allowlist.

The issue comes from OAuth2RefreshTokenAuthenticationProvider where we have to provide authorization’s attribute. To provide one I registered it like this:

@Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
          ...
        OAuth2ClientAuthenticationToken clientPrincipal = OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient((Authentication) passwordGrantAuthenticationToken.getPrincipal());
        ...
        OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(userDetails.getUsername())
                .authorizedScopes(authorizedScopes)
                .authorizationGrantType(PASSWORD_GRANT_TYPE)
                .attribute(Principal.class.getName(), clientPrincipal) //===== here
                .token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, tokenMetadata))
                .token(refreshToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, refreshTokenMetadata))
                .build();
}

The serialization seems to work but not the deserialization. You mentionned a How-to guide in your comment, where can I find a reference guide or should I define a custom mixin? Thank you

@mbarringer @edwardzjl @colin-riddell This issue is related to a bug in Spring Security. See spring-security#9993. There is a fix in place and will be released in 5.7.0 GA, which is a way out.

FYI, the OAuth2ClientAuthenticationToken and JwtAuthenticationToken are marked as @Transient and therefore should NOT be persisted across requests. However, it is being persisted across requests, resulting in this bug.

Since Spring Security 5.7.0 GA is a way out, I’ve applied a temporary fix via d0e1107

It seems that the attributes column is not properly (serialized and) deserialized. I’m interested in making a PR of this issue.