Keycloak spring security client credential grant

2020-02-07 04:17发布

问题:

I can use KecloakRestTemplate where one keycloak client is communicating with another keycloak client. However it only works if I have logged into the first keycloak client, i.e. it sends client ID, client secret, username, password, to keycloak server. If I haven't authenticated with a user and password on the first client I get "Cannot set authorization header because there is no authenticated principle". But I have configured keycloak to use a service account for the first client (Client Credential Grant) therefore I should not be using a user/password and should be relying on client id/secret only. Is this is a bug/deviation from OAuth 2 spec?

回答1:

KeycloakRestTemplate sends client ID, client secret, username and password to the Keycloak server. I wanted to only send client ID and secret. I created a KeycloakClientCredentialsRestTemplate subclass of OAuth2RestTemplate to do this. It uses OAuth2 support in Spring Boot to do a client credentials grant. It also takes Keycloak properties from application.properties.

import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

public class KeycloakClientCredentialsRestTemplate extends OAuth2RestTemplate {

    public KeycloakClientCredentialsRestTemplate(OAuth2ProtectedResourceDetails resource,
            OAuth2ClientContext context) {
        super(resource, context);
    }

}

Also:

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.stereotype.Service;

@Service
public class KeycloakClientCredentialsConfig {

    @Value("${keycloak.realm}")
    private String realm;

    @Value("${keycloak.auth-server-url}")
    private String authServerUrl;

    @Value("${keycloak.resource}")
    private String clientId;

    @Value("${keycloak.credentials.secret}")
    private String clientSecret;

    @Bean
    public KeycloakClientCredentialsRestTemplate createRestTemplate() {
        return new KeycloakClientCredentialsRestTemplate(getClientCredentialsResourceDetails(),
                new DefaultOAuth2ClientContext());
    }

    private ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
        String accessTokenUri = String.format("%s/realms/%s/protocol/openid-connect/token",
            authServerUrl, realm);
        List<String> scopes = new ArrayList<String>(0); // TODO introduce scopes

        ClientCredentialsResourceDetails clientCredentialsResourceDetails = 
                new ClientCredentialsResourceDetails();

        clientCredentialsResourceDetails.setAccessTokenUri(accessTokenUri);
        clientCredentialsResourceDetails.setAuthenticationScheme(AuthenticationScheme.header);
        clientCredentialsResourceDetails.setClientId(clientId);
        clientCredentialsResourceDetails.setClientSecret(clientSecret);
        clientCredentialsResourceDetails.setScope(scopes);

        return clientCredentialsResourceDetails;
    }

}


回答2:

For my microservice architecture based application, I'm using both user and service accounts. I guess the spring security adapter only takes care of the user related stuff (the version I'm using, at least, which is 2.2.1). What I do is to have another RestTemplate, one which I handle myself in order to access resources as a client.

As an example:

@Service
public class RemoteAccessService{

    //Manages user access
    private KeycloakRestTemplate userAccessRestTemplate;

    //Manages client access
    private RestTemplate clientAccessRestTemplate;

    public RemoteAccessService(KeycloakRestTemplate userAccessRestTemplate, 
        @Qualifier("clientAccessRestTemplate") RestTemplate clientAccessRestTemplate;){

    }

}

Then, you build a RestTemplate bean in a @Configuration class in order to manage client authorization:

@Bean
public RestTemplate clientAccessRestTemplate() {
    RestTemplate template = new RestTemplate();
    template.getMessageConverters().add(new FormHttpMessageConverter());
    template.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
    template.getInterceptors().add(new ClientHttpRequestInterceptor() {

        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                ClientHttpRequestExecution execution) throws IOException {
            //Intercept each of the requests performed by this template 
            //and add the client access token in the Authorization header
            HttpRequest wrapper = new HttpRequestWrapper(request);
            if (clientAccessToken != null) {
                wrapper.getHeaders().set("Authorization",
                        "Bearer " + clientAccessToken.getToken());
            }
            return execution.execute(wrapper, body);
        }
    });
    return template;
}

Of course, you need to be sure you've got a proper clientAccessToken in the interceptor, you'll get a 401 or 403 code otherwise. Here you've got a post on how to perform this in OAuth (you don't need user/password, just client credentials).

As a sidenote, the keycloak adapters are handy to manage some situations, but they don't provide access to all the features of keycloak, which is a way more powerful.