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

JWT Token problem with client and user authorities. #509

Open ivanmcshane opened 9 years ago

ivanmcshane commented 9 years ago

Hi, I recently migrated a working Authorisation and Resource server setup from JDBC to JWT token store. The token creation and verification is all working ok but I'm having a problem with access rules.

Scenario:

e.g. Authorisation server side object (some details changed for security) ...

OAuth2Authentication@7e500501[
storedRequest=org.springframework.security.oauth2.provider.OAuth2Request@dc0770e1  
userAuthentication=org.springframework.security.authentication.UsernamePasswordAuthenticationToken@bbe2aa81: 
Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffed504: RemoteIpAddress: 127.0.0.1; SessionId: 84EB7631345E8601CB9074E851BCF3A8; Granted Authorities: ROLE_USER
details=<null>
authorities=[ROLE_USER]
authenticated=false
]
OAuth2Request@713f0117[
  resourceIds=[]
  authorities=[ROLE_CLIENT]
  approved=true
  refresh=<null>
  redirectUri=xxxx
  responseTypes=[code]
  extensions={}
  clientId=client
  scope=[]
  requestParameters={client_secret=xxxxx, response_type=code, grant_type=authorization_code, redirect_uri=xxxx, code=mgiF4w, client_id=client}

]

Is re-hydrated as follows on the resource server

OAuth2Authentication@22712a82[
  storedRequest=org.springframework.security.oauth2.provider.OAuth2Request@c715df8d
userAuthentication=org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d5545: 
Principal: bob; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
  details=remoteAddress=127.0.0.1, tokenType=BearertokenValue=<TOKEN>
  authorities=[ROLE_USER]
  authenticated=true
]
OAuth2Request@6450fe3[
  resourceIds=[]
  authorities=[]
  approved=true
  refresh=<null>
  redirectUri=<null>
  responseTypes=[]
  extensions={}
  clientId=client
  scope=[]
  requestParameters={client_id=client}
]

As per above, the request only contains the client_id and not the granted authorities.

Decoding the token shows the following info.

{
  "exp": 1434147977,
  "user_name": "bob",
  "scope": [],
  "authorities": [
    "ROLE_USER"
  ],
  "jti": "7f9f44a0-9b3b-44bd-bec9-af4d0d7ff656",
  "client_id": "client"
}

The upshot of this is my client and user role check which worked with JDBC now fails ...

#oauth2.clientHasRole('ROLE_CLIENT') and hasAuthority('ROLE_USER')

Tested with versions:

spring-security, version: '3.2.7.RELEASE'
spring-security-jwt, version: '1.0.3.RELEASE'
spring-security-oauth2, version: '2.0.7.RELEASE'

I'm guessing this is a bug as no auth code was changed other than the migration from JDBC to JWT token stores but I'm not sure. Could it be because the 'authorities' in both authentication parts has the same name and is therefore being over-written in the JSON mapping?

ivanmcshane commented 9 years ago

Further to the above issue I was able to work around it by creating a custom AccessTokenConverter and passing it to my JwtAccessTokenConverter. The custom implementation extends the default and inserts/extracts the client authorities.

I'm not sure if this is the right approach but using it has meant my original #oauth2.clientHasRole('ROLE_CLIENT') check now works again.

Sample code ...

public class WithClientAuthoritesAccessTokenConverter extends DefaultAccessTokenConverter implements
                                                                                         AccessTokenConverter {

    private static final String CLIENT_AUTHORITIES_MAP_ATTR = "clientAuthorities";

    @Override
    public Map<String, ?> convertAccessToken(final OAuth2AccessToken token,
                                             final OAuth2Authentication authentication) {

        @SuppressWarnings("unchecked")
        final Map<String, Object> response = (Map<String, Object>) super.convertAccessToken(token,
                                                                                            authentication);

        response.put(CLIENT_AUTHORITIES_MAP_ATTR,
                     AuthorityUtils.authorityListToSet(authentication.getOAuth2Request()
                                                                     .getAuthorities()));

        return response;
    }

    @Override
    public OAuth2Authentication extractAuthentication(final Map<String, ?> map) {
        final OAuth2Authentication result = super.extractAuthentication(map);

        if (result.getOAuth2Request() != null && map.containsKey(CLIENT_AUTHORITIES_MAP_ATTR)) {
            // Map authorities back in.

            @SuppressWarnings("unchecked")
            final String[] roles = ((Collection<String>) map.get(CLIENT_AUTHORITIES_MAP_ATTR)).toArray(new String[0]);
            final Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles);

            // No setters on OAuth2Request so need to clone
            final OAuth2Request originalOAuth2Request = result.getOAuth2Request();
            final OAuth2Request request = new OAuth2Request(originalOAuth2Request.getRequestParameters(),
                                                            originalOAuth2Request.getClientId(),
                                                            authorities,
                                                            originalOAuth2Request.isApproved(),
                                                            originalOAuth2Request.getScope(),
                                                            originalOAuth2Request.getResourceIds(),
                                                            originalOAuth2Request.getRedirectUri(),
                                                            originalOAuth2Request.getResponseTypes(),
                                                            originalOAuth2Request.getExtensions());
            return new OAuth2Authentication(request, result.getUserAuthentication());
        }

        return result;
    }
}
dsyer commented 9 years ago

Are you sure you are using the code from master (2.0.8.BUILD-SNAPSHOT)?

ivanmcshane commented 9 years ago

We were using 2.0.7.RELEASE, but after testing I can confirm the same problem occurs with 2.0.8.BUILD-SNAPSHOT.

dsyer commented 9 years ago

Can you please extract a simple, minimal project that reproduces the problem? I know we fixed something similar so I want to be sure I have exactly the same problem as you.

ivanmcshane commented 9 years ago

Hi Dave, no problem I've created a sample test case and uploaded it here - https://github.com/ivanmcshane/jwt_client_roles.git

The OAuthTestServerConfig could possibly be stripped down a bit more but it does reproduce the problem which can be verified by running the Junit test.

dsyer commented 9 years ago

OK I get it. This is for a token that is a user token (not client credentials, which is where we made changes recently). There isn't really an existing claim for client authorities. Can you use scopes instead (would be more conventional anyway).

ivanmcshane commented 9 years ago

Yip that'll be no problem, we can use scopes and will be doing so as part of the solution we're working on. So this won't be a blocker to us.

It may affect others doing the same migration since it's something that works for JDBC but doesn't for JWT token stores.

Client based expressions are also used in the sparklr sample which I'm guessing many people will use as an example reference ... https://github.com/spring-projects/spring-security-oauth/blob/master/samples/oauth2/sparklr/src/main/java/org/springframework/security/oauth/examples/sparklr/config/OAuth2ServerConfig.java