Skip to content
This repository has been archived by the owner on May 31, 2022. It is now read-only.

Add post-authentication callback in ResourceOwnerPasswordTokenGranter #791

Closed
wimdeblauwe opened this issue Jun 22, 2016 · 9 comments
Closed
Assignees

Comments

@wimdeblauwe
Copy link

I need to allow only users with a certain role access via OAuth2 when using a certain clientId. Currently to do this, I have created a subclass of ResourceOwnerPasswordTokenGranter where I copied the code from protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) and added my own logic:

      ...
      if (userAuth == null || !userAuth.isAuthenticated()) {
        throw new InvalidGrantException("Could not authenticate user: " + username);
      }

 // Start of custom code
    if( client.getClientId().equals("gamePlayClient")) {
      if( !((CustomUserDetails)userAuth.getPrincipal()).getRole().equals(Roles.PLAYER)) {
        throw new InsufficientAuthenticationException("Only users with the role 'PLAYER' can log on this client.");
      }
    }
  // End of custom code

  OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
  return new OAuth2Authentication(storedOAuth2Request, userAuth);

It would be better if there was an extension point where I can add my code without having to copy over parts of the framework.

@dsyer
Copy link
Contributor

dsyer commented Sep 23, 2016

Can't you do that in the AuthenticationManager?

@wimdeblauwe
Copy link
Author

Don't know, I don't have an own implementation of AuthenticationManager, I just ask Spring Boot to create one for me. I assume this gives me an instance of OAuth2AuthenticationManager, but I don't see how I could now customize this?

@dsyer
Copy link
Contributor

dsyer commented Sep 23, 2016

No, the AuthenticationManager in the ResourceOwnerPasswordTokenGranter is not an OAuth2AuthenticationManager - it is the regular default (parent if you are using Spring Boot autoconfig) user authentication manager. You can override that in a Boot app in more than one way (per the user guide), e.g. create a bean of type AuthenticationManager.

@wimdeblauwe
Copy link
Author

I am writing a small test program to fully understand what you mean in order to properly implement this. If I create my own AuthenticationManager bean, I get a UsernamePasswordAuthenticationToken instance as parameter to the authenticate method. But I would still need to copy the things OAuth2AuthenticationManager currently does to get the same behaviour.

I then tried to make my own AuthenticationManager extend from OAuth2AuthenticationManager, but I did not get this working.

I tried this code:

@Component
public class MyAuthenticationManager extends OAuth2AuthenticationManager {

    private final ResourceServerTokenServices tokenServices;

    public MyAuthenticationManager(ResourceServerTokenServices tokenServices) {
        this.tokenServices = tokenServices;
    }

    @Override
    public void afterPropertiesSet() {
        setTokenServices(tokenServices);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Authentication result = super.authenticate(authentication);

        Map<String,String> details = (Map<String, String>) authentication.getDetails();
        String clientId = details.get("client_id");
        ApplicationUserDetails userDetails = (ApplicationUserDetails) authentication.getPrincipal();
        if( userDetails.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"))
                && !clientId.equals("angular_app")) {
            authentication.setAuthenticated(false);
        }

        return result;
    }
}

with:

    @Bean
    public ResourceServerTokenServices resourceServerTokenServices(TokenStore tokenStore) {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        return tokenServices;
    }

But it does not work, I get this when trying to get a token:

{
  "error": "invalid_token",
  "error_description": "Invalid access token: admin@example.com"
}

If it would help to understand, I created a public GitHub repo with the example: https://github.com/wimdeblauwe/multiple-oauth-clients

The file https://github.com/wimdeblauwe/multiple-oauth-clients/blob/master/auth-tests.http can be used in IntelliJ to test.

The checked in version just allows any user on any Oauth client. I would like to now modify the code to only allow a user with role PLAYER on the mobile_client_id and the user with role ADMINISTRATOR on angular_app_id OAuth client.

@jgrandja jgrandja self-assigned this Jan 25, 2019
@jgrandja
Copy link
Contributor

@wimdeblauwe I modified an existing sample I have that demonstrates how to perform post authentication logic for a password grant. Take a look at this branch and specifically AuthorizationServerConfig.

The delegation-based AuthorizationServerTokenServices allows you to perform a post authentication check before an access token is issued. This works for any grant type. I feel this was the easiest extension point for what you need to do.

Let me know if you have any questions.

wimdeblauwe pushed a commit to wimdeblauwe/multiple-oauth-clients that referenced this issue Feb 3, 2019
…But it does not seem to be working. The Admin still gets an access token, no matter what client he is using.
@wimdeblauwe
Copy link
Author

@jgrandja Thank you for this. I tried to apply this on my example, but I don't get it working. I created a branch in my repo: https://github.com/wimdeblauwe/multiple-oauth-clients/tree/test-jgrandja-example

The admin user still gets an access token, no matter what client he uses. Am I doing something wrong? Since I am not using JWT, I also did not configure a TokenEnhancer. I don't know if that would be a problem?

@jgrandja
Copy link
Contributor

@wimdeblauwe

The admin user still gets an access token, no matter what client he uses.

I'm not sure I understand the problem you are having. FYI, a user can use any client depending on your application setup. If the user authorizes the client than the client gets an access token.

I looked at your sample and it's not clear which flow you are going through that produces the issue you are having.

It would be helpful if the sample contained detailed steps on how to reproduce. Please see the following references for what a complete and minimal sample should consist of.

@wimdeblauwe
Copy link
Author

@jgrandja

I have added a unit test (https://github.com/wimdeblauwe/multiple-oauth-clients/blob/c186c4a2c5c191fc07212460dcb68afa9c968ef1/src/test/java/com/example/multipleoauthclients/infrastructure/security/SecurityTest.java) on the https://github.com/wimdeblauwe/multiple-oauth-clients/tree/test-jgrandja-example branch to show the problem.

You'll see 2 tests. One will succeed (testLoginAsAdministratorWithCorrectClient()) and the other one will fail (testLoginAsAdministratorWithWrongClient()). I want that second one to also succeed obviously :-)

Let me know if you still have questions and thank you for looking into this!

@jgrandja
Copy link
Contributor

jgrandja commented Apr 2, 2019

@wimdeblauwe To get both tests to pass in SecurityTest, perform the following steps.

In OAuth2ServerConfiguration.AuthorizationServerConfiguration, update configure() and assign authorities to each client registration.

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
           .withClient("mobile_client_id")
           .authorities("MOBILE_APP")
           ....
           .and()
           .withClient("angular_app_id")
           .authorities("WEB_APP")
           ....
}

In OAuth2ServerConfiguration.AuthorizationServerConfiguration, update the createAccessToken() method of the AuthorizationServerTokenServices @Bean as follows:

public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
    UsernamePasswordAuthenticationToken userAuthentication =
            (UsernamePasswordAuthenticationToken) authentication.getUserAuthentication();
    ApplicationUserDetails userDetails = (ApplicationUserDetails) userAuthentication.getPrincipal();

    Map clientDetails = (Map) userAuthentication.getDetails();
    String clientId = (String) clientDetails.get("client_id");

    // Post-authentication callback
    // TODO Validate/authenticate user and/or client

    LOGGER.info("username: {}", userDetails.getUsername());
    LOGGER.info("clientId: {}", clientId);

    // *** Add this bit of code
    ClientDetails authenticatedClient = clientDetailsService.loadClientByClientId(clientId);

    boolean isUserAdmin = authentication.getUserAuthentication().getAuthorities().stream()
            .map(GrantedAuthority::getAuthority).anyMatch(a -> a.equals("ROLE_ADMINISTRATOR"));
    boolean isClientWebApp = authenticatedClient.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority).anyMatch(a -> a.equals("WEB_APP"));

    if (isUserAdmin && !isClientWebApp) {
        throw new UnauthorizedClientException("TODO: Add message");
    }
    // *** Up to here

    if (userDetails.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"))
            && !clientId.equals("angular_app_id")) {
        LOGGER.info("ADMIN trying to log on via wrong client!");
        authentication.setAuthenticated(false);
    }

    return tokenServices.createAccessToken(authentication);
}

The thrown UnauthorizedClientException will return a 401 so update SecurityTest.testLoginAsAdministratorWithWrongClient() to assert on HttpStatus.UNAUTHORIZED and it will pass.

I'm going to close this issue as answered. However, if you're still having an issue we can re-open.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

No branches or pull requests

3 participants