7

I'm trying to implement authentication throughout my backend services of a microservice oriented application using Keycloak and Spring Boot with Spring Security and JWT-tokens (bearer-only setting in Keycloak).

I have a backend service that requires authentication to access the REST endpoints. This service provides data for a web UI and also takes data to store in the database so that it can be processed later. Authentication of the user in the UI and also the UI against that backend service already both work.

Then, there is another backend service that runs in the background, calculating values that should also be present in the first mentioned backend service. As that one requires authentication, the service doing the calculations first needs to retrieve an access token from Keycloak to authenticate against the other backend service for the HTTP post to work.

I'm trying to do the HTTP post with the KeycloakRestTemplate, but when I call the .postForObject method, I get an exception:

Caused by: java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.getKeycloakSecurityContext(KeycloakClientRequestFactory.java:70)
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.postProcessHttpRequest(KeycloakClientRequestFactory.java:55)
    at org.springframework.http.client.HttpComponentsClientHttpRequestFactory.createRequest(HttpComponentsClientHttpRequestFactory.java:160)

It seems that the calculation service does not retrieve the authentication token automatically before calling the other REST service. I did a lot of research on Google about all those Keycloak specific classes, but I didn't find out what I need to do.

Can anyone please give me a hint? I also don't know which parts of the Spring config are relevant here but I will provide them if you need them.

EDIT

My application.properties of the calculation service looks like this:

keycloak.auth-server-url=https://localhost/auth
keycloak.realm=myrealm
keycloak.bearer-only=true
keycloak.resource=backend-service2
keycloak.principal-attribute=preferred_username
keycloak.cors=true
keycloak.realm-key=<PUBKEY>
keycloak.credentials.secret=<SECRET_UUID_STYLE>
keycloak.use-resource-role-mappings=true

UPDATE

Thanks @Sai prateek and @Xtreme Biker. That seems to lead me to the right direction.

I applied this solution but I still get an exception, I think the keycloak configuration is wrong. I have three clients in keycloak now: webui, backend-service1, backend-service2.

The webui is configured as: Access Type: public

The backend-service1 is configured as: Access Type: bearer-only

The backend-service2 is configured as: Access Type: bearer-only

The exception is:

2019-02-18 11:15:32.914 DEBUG 22620 --- [  restartedMain] o.s.web.client.RestTemplate              : POST request for "http://localhost:<PORT>/auth/realms/<REALM_NAME>/protocol/openid-connect/token" resulted in 400 (Bad Request); invoking error handler

Exception in thread "restartedMain" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
Caused by: error="access_denied", error_description="Access token denied."
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:142)
    at org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider.obtainAccessToken(ClientCredentialsAccessTokenProvider.java:44)
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainNewAccessTokenInternal(AccessTokenProviderChain.java:148)
    at org.springframework.security.oauth2.client.token.AccessTokenProviderChain.obtainAccessToken(AccessTokenProviderChain.java:121)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.acquireAccessToken(OAuth2RestTemplate.java:221)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.getAccessToken(OAuth2RestTemplate.java:173)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.createRequest(OAuth2RestTemplate.java:105)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:683)
    at org.springframework.security.oauth2.client.OAuth2RestTemplate.doExecute(OAuth2RestTemplate.java:128)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:644)
    at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:399)
[STRIPPED]


    ... 5 more
Caused by: error="invalid_client", error_description="Bearer-only not allowed"
    at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:80)
    at org.springframework.security.oauth2.common.exceptions.OAuth2ExceptionJackson2Deserializer.deserialize(OAuth2ExceptionJackson2Deserializer.java:33)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4001)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3072)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:237)
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readInternal(AbstractJackson2HttpMessageConverter.java:217)
    at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:198)
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport$AccessTokenErrorHandler.handleError(OAuth2AccessTokenSupport.java:237)
    at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
    at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:730)
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:688)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:654)
    at org.springframework.security.oauth2.client.token.OAuth2AccessTokenSupport.retrieveToken(OAuth2AccessTokenSupport.java:137)
    ... 18 more

Also note that I changed the keycloak.auth-server-url to http://localhost:<PORT>/auth (no HTTPS) so the certificate validation does not fail due to self signed certificate in development.

dfsg76
  • 504
  • 1
  • 6
  • 22
  • 1
    With no user present you may find this impossible. Service to service calls are best authenticated using SSL client certificates but if your infrastructure isn't capable of that then simple basic auth using an API key (which is a password by any other name) will work. It's too much to discuss here though. – Andy Brown Feb 15 '19 at 15:30
  • Seems to make sense. Do you know a good tutorial / example? What exactly does the "infrastructure" be capable of? A very naive implementation I think would be giving the calculation a private key to load at startup with the other service having to public key so it can verify a signature from the calculation service? But then what if the private key gets compromised .. is that what you mean the infrastructure needs to take care of ... renewing keys etc.? – dfsg76 Feb 15 '19 at 16:33
  • You would need a PKI infra to issue/revoke/renew trusted certs and a mechanism to distribute said certs to the servers. Self-signed certs would work in dev/test but you don't want them in prod. The good news is that the cert exchange is an (optional) part of the SSL protocol and X.509 authentication is a built-in feature of spring-security (many tutorials on web to get you off the ground) – Andy Brown Feb 15 '19 at 16:55
  • backend-service2 should be confidential Access-Type – ravthiru Feb 18 '19 at 20:30
  • unfortunately, this does not work either. I get the error `Caused by: error="access_denied", error_description="Error requesting access token."` – dfsg76 Feb 19 '19 at 07:54
  • I'll try to set up a MCVE on github, stay tuned – dfsg76 Feb 21 '19 at 10:00
  • Here is the MCVE: https://github.com/CodingSpiderFox/keycloak-backend-to-backend-mcve . Could you please have a look at it? – dfsg76 Feb 21 '19 at 19:03
  • Still not solved :/ – dfsg76 Feb 28 '19 at 07:49
  • Solved by myself and updated MCVE on GitHub, too – dfsg76 Mar 04 '19 at 19:28

2 Answers2

5

OK, found the solution myself: I needed to set the switch "Service accounts enabled" button to ON in the client config for "backend-service2" inside keycloak.

dfsg76
  • 504
  • 1
  • 6
  • 22
3

Looks like you are missing some configuration for authenication server. KeycloakRestTemplate used client ID, client secret, username and password to validate against the Keycloak server. You need to set the clientid, clientsecret, realm and authentication server url for KeycloakClientCredentialsRestTemplate like -

@Service
public class MyKeycloakClientCredentialsConfig {

    @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;
    }

}

My resttemplate is like this-

public class SampleRestTemplate extends OAuth2RestTemplate {

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

}

its working perfectly for me.

Sai prateek
  • 11,842
  • 9
  • 51
  • 66
  • I updated my question with the application.properties. I do not fully understand how I would use your example for a request. Do you have a minimal working example? – dfsg76 Feb 15 '19 at 16:14
  • 1
    This answer is the way to go.. @pudelwudel you need to perform a login as a client (here you are acting as a client, not on behalf of any user). You might find this question useful: https://stackoverflow.com/questions/46073485/keycloak-spring-security-client-credential-grant/46400975 – Aritz Feb 16 '19 at 14:04
  • Now, I'm one step further, please see my updated answer ;) – dfsg76 Feb 18 '19 at 10:34