6

We're using resource-owner credentials grant type (with oauth2:password in security-config.xml. Let's play out this scenario to explain my predicament:

  1. Bob was created with authorities ROLE_USER
  2. Bob tries to access an oauth2 protected resource
  3. Bob uses the official mobile app to access it, so the client credentials are correct
  4. Bob's access token is created and stored in the TokenStore, keyed on his username, client_id, and scope. (see DefaultAuthenticationKeyGenerator.java)
  5. Bob's phone tries to call the protected services with this access token, but those services require users to have an authority of ROLE_MOBILE_USER.
  6. Bob contacts the db owner and has ROLE_MOBLE_USER added to his user in the database.
  7. Bob tries to get another access token, but the DefaultTokenServices returns him the same, non-working access token.
  8. The only way to take advantage of his new authority is to wait until his old access token expires so he can get a new access token with the correct authority.

There are a number of ways to address this.

For one, the administration app that adds ROLE_MOBILE_USER to Bob's authorities could then clear all access tokens and authorizations in the database. This way the DefaultTokenServices will just create a new one with the correct authorities serialized as his new OAuth2Authentication. However we may not want the Administration webapp to be concerned with OAuth at this point (at least not yet). If possible we'd like to keep the administration app concerns as concise as possible, and right now there are no dependencies on oauth.

We could expose the DELETE method to the /oauth/access_token endpoint and tell the mobile app to try deleting that access token and re-requesting one, just in case the stored authorities are stale. This feels more like a work-around though.

Finally I can serialize the authorities in my own defined AuthenticationKeyGenerator. It would basically use the username, client_id, scope, and authorities of the authorization and perform the same digest algorithm on them. This way when Bob tries to log in he'll get the same access token, but the underlying token store will recognize that he has a different authentication (from the authentication manager in the token granter bean) and refresh its database. The problem I have with this solution is that it simply relies on the implementation behavior of the underlying token store (though both InMemoryTokenStore and JdbcTokenStore behave this way).

Can you think of any better/cleaner solutions? Am I over-thinking this?

Thanks in advance.

Joe
  • 4,585
  • 3
  • 37
  • 51
  • I will add that at this point I used the custom `AuthenticationKeyGenerator` and things worked correctly. I still would like a better/cleaner solution, since the key is only checked when the authorization server is asked for a new token, but the resource server could still be using old authorities. – Joe Aug 20 '13 at 16:19
  • Are you still sticking with this solution or did you find a better way to deal with the issue? I'm with the exact same issue right now and I'm not sure how to tackle this. – rpvilao Mar 09 '15 at 17:22
  • We've stuck with this solution and haven't run into any issues thus far. I would recommend the first solution however, and accept that changing the authorization of a user necessitates touching the oauth token database. – Joe Mar 12 '15 at 18:14
  • I created a filter in the resource server that tests if the authorities must be reloaded. If so, all the tokens for the user are invalidated and unauthorized is returned. Not perfect, but probably the best way without mixing authentication/authorisation and resource handling I think. Also, check my post and respective Dave Syer's response in http://stackoverflow.com/questions/28947285/oauth2-reload-user-authorities/28959452#28959452. It seems that in the future this might be easier to do ;) – rpvilao Mar 13 '15 at 12:32
  • How do you make sure that the new authority ROLE_MOBILE_USER is available in the users security context in the current session? The context with the current authorities is stored in the session and not updated until the next login. – James Mar 21 '15 at 21:06
  • @James for our purposes OAuth2 was used for mobile only, so there was no session in play. This answer is for serializing the authorities. As soon as the user tried to use their old token (the one serialized without the new authority), they would get a 4xx response and the client would try to renew their token with a refresh token. Their new token would then have the new authority. – Joe Mar 26 '15 at 14:20

3 Answers3

2

I resolved this issue in my app by deleting all tokens for a given user when the authentication information is sent.

Use a custom AuthenticationProvider bean.

@Component("authenticationProvider")
public class AuthenticationProviderImpl implements AuthenticationProvider

Autowire in the token store bean.

@Autowired
@Qualifier("tokenStore")
private TokenStore tokenStore;

Then in the authenticate method, remove all tokens for a given user if the credentials are passed a second time.

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

    try {
         //Do authentication

        //Delete previous tokens
        Collection<OAuth2AccessToken> tokenCollection = tokenStore.findTokensByUserName(token.getName());
        for (OAuth2AccessToken oToken : tokenCollection){
            tokenStore.removeAccessToken(oToken);
        }

        //return Authentication;
    }
}

Most of the requests will be using a token and will bypass this entirely, but when the credentials are passed, a new token will be generated. This token will be associated with the new authentication object which will include all new roles, and changes made to the user.

Scott Leonard
  • 746
  • 7
  • 19
  • I understand this way of doing things, but isn't there a better way to do this? It's very hard for me to believe that the spring oauth2 developers didn't think of this issue... I doesn't seem right to have to make the credentials travel again just to invalidate a previous state because first the app should not store the credentials and that is the whole point of oauth and second it doesn't seem right to ask the user again the credentials just because we cannot do a basic server side operation :/ Are you still sticking with this one or did you find any other solution to this problem? – rpvilao Mar 09 '15 at 17:17
  • I solved it in the same way. I think we miss the support to update the authentication from oauth2. – Gazeciarz Nov 04 '16 at 08:08
1

I had the same problem and I solved it with this function:

protected void reloadUserFromSecurityContext(SecurityContext securityContext, Person user){
    OAuth2Authentication requestingUser = (OAuth2Authentication) securityContext.getUserPrincipal();
    Object principal = (PersonUserDetails) requestingUser.getUserAuthentication().getPrincipal();
    if(principal instanceof PersonUserDetails) {
        ((PersonUserDetails) principal).setPerson(user);
    }
    OAuth2AuthenticationDetails authDetails = (OAuth2AuthenticationDetails) requestingUser.getDetails();
    OAuth2AccessToken tokenStored = jdbcTokenStore.readAccessToken(authDetails.getTokenValue());
    jdbcTokenStore.storeAccessToken(tokenStored, requestingUser);
}

That is a example for update an attribute for PersonUserDetails object that is in OAuth2Authentication

0

I Had the same problem and this was the solution

@RequestMapping(value = "/updateToken", method = RequestMethod.POST)
 void updateToken(@RequestBody tokenReq req) { 
            Collection<OAuth2AccessToken> tokenCollection = tokenStore.findTokensByClientIdAndUserName(req.idclient, req.username);
            for (OAuth2AccessToken AToken : tokenCollection){
                OAuth2Authentication Auth = tokenStore.readAuthentication(AToken);
                OAuth2AccessToken newToken = tokenServices.createAccessToken(Auth);
                tokenStore.removeAccessToken(AToken);
                tokenStore.storeAccessToken(newToken, Auth);
            }

 }