3

I'm using Spring Boot and Keycloak to develop a web-app. Then I wrote a scheduled task where I'm using the KeycloakRestTemplate to ask some data to another app, as you can see below:

    @Override
    @Scheduled(cron="0 50 09 * * MON-FRI")
    public void concludiCommessa() {

        try {
            FDto[] ftts = new ObjectMapper().readValue(restTemplate.getForEntity(URI.create(MY_URL), String.class).getBody(), FDto[].class);

             ..............................
            }
        } catch (RestClientException | IOException e) {
        }
    }

If I run it on the server I have the following error:

2018-04-18 09:50:00.067 ERROR 2503 --- [pool-8-thread-1] o.s.s.s.TaskUtils$LoggingErrorHandler    : Unexpected error occurred in scheduled task.

java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.getKeycloakSecurityContext(KeycloakClientRequestFactory.java:70) ~[keycloak-spring-security-adapter-3.4.2.Final.jar:3.4.2.Final]
    at org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory.postProcessHttpRequest(KeycloakClientRequestFactory.java:55) ~[keycloak-spring-security-adapter-3.4.2.Final.jar:3.4.2.Final]
    at org.springframework.http.client.HttpComponentsClientHttpRequestFactory.createRequest(HttpComponentsClientHttpRequestFactory.java:207) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.http.client.support.HttpAccessor.createRequest(HttpAccessor.java:85) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:656) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:336) ~[spring-web-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at it.edile.service.api.ApiServiceImpl.concludiCommessa(ApiServiceImpl.java:287) ~[classes/:0.0.1-SNAPSHOT]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
    at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81) [spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [na:1.8.0_161]
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_161]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_161]
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_161]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_161]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_161]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_161]

Why?

How can I pass a principal if I'm using an async task?

EDIT This is my security configuration:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(keycloakAuthenticationProvider());
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public KeycloakRestTemplate keycloakRestTemplate() {
    return new KeycloakRestTemplate(keycloakClientRequestFactory);
}

@Bean
public KeycloakConfigResolver keycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
}

EDIT This is my keycloak properties:

#######################################
#             KEYCLOAK                #
#######################################
keycloak.realm=MY_REALM
keycloak.auth-server-url=MY_URL/auth
keycloak.ssl-required=external
keycloak.resource=EdilGest
keycloak.credentials.jwt.client-key-password=PWD
keycloak.credentials.jwt.client-keystore-file=classpath:CLIENT.jks
keycloak.credentials.jwt.client-keystore-password=PWD
keycloak.use-resource-role-mappings=true
keycloak.principal-attribute=preferred_username

EDIT:

I'm trying to use the Service Account now, but it doesn't work at the moment... Reading here: https://www.keycloak.org/docs/latest/server_admin/index.html#_service_accounts

I have to send a request like:

POST /auth/realms/demo/protocol/openid-connect/token
    Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
    Content-Type: application/x-www-form-urlencoded

    grant_type=client_credentials

to keycloak, but how can I send it using Spring? and how can I set the jks instead of client and secret?

EDIT 2

My security config

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {  

    @Autowired
    public KeycloakClientRequestFactory keycloakClientRequestFactory;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);

        http
            .httpBasic()
            .disable();

        http
        .authorizeRequests()
            .antMatchers("/webjars/**").permitAll()
            .antMatchers("/resources/**").permitAll()
            .anyRequest().hasAuthority("......")
        .and()
        .logout()
            .logoutUrl("/logout")
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
            .permitAll()
            .logoutSuccessUrl(mux)
            .invalidateHttpSession(true);

    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        auth.authenticationProvider(keycloakAuthenticationProvider());
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public KeycloakRestTemplate keycloakRestTemplate() {
        return new KeycloakRestTemplate(keycloakClientRequestFactory);
    }

    @Bean
    public KeycloakConfigResolver keycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
           .ignoring()
           .antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
    }

}

EDIT 3 Here what I tryed... It doesn't work.. I have the same error: java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal

KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("EdilGest.jks"), "EdilGest".toCharArray());

JWTClientCredentialsProvider jwtClientCredentialsProvider = new JWTClientCredentialsProvider();
jwtClientCredentialsProvider.setupKeyPair(keyStoreKeyFactory.getKeyPair("MyClient"));
String token = jwtClientCredentialsProvider.createSignedRequestToken("MyClient", "http://myKeycloak/auth/");

String data = "grant_type=client_credentials" ;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.AUTHORIZATION, "Bearer " +token);

HttpEntity<String> requestEntity = new HttpEntity<String>(data, headers);
String ftt = keycloakRestTemplate.exchange(URI.create(MyUrl), HttpMethod.POST, requestEntity, String.class).getBody();

What am I doing wrong?

Teo
  • 3,143
  • 2
  • 29
  • 59
  • Does this code work if you take it out from the `@Scheduled` method and wrap it in a standard method which the user calls to? – Aritz Apr 19 '18 at 06:35
  • Yes, it does! If I executed the method as "stadarnd" method it works perfectly – Teo Apr 19 '18 at 06:36
  • Where have you configured the client credentials? – Aritz Apr 19 '18 at 06:49
  • I'm using JWT, I added the confiuration inside the properties.. Please, take a look – Teo Apr 19 '18 at 06:54
  • There's no principal because there's no user logged in. Here, you need to log in your application as a client. See my post here https://stackoverflow.com/a/46116485/1199132 I guess there's still no support for service accounts in java adapters. – Aritz Apr 19 '18 at 10:58
  • @XtremeBiker yes, I know that... But I need to shedule that method and so I don't do any login. Is it possible to pass some credential without do a login? Or otherwise I'm thinking to create a different security configuration for only the `api` request... For example for `api` I'm thinking to not use the SSO with keycloak, but a standard authentication, in this way maybe I can use Spring Rest Template.. What do you think? Is it possible to pass the credential with Spring Rest Template? – Teo Apr 19 '18 at 11:08
  • @XtremeBiker ok, maybe my comment is the same as your answer... – Teo Apr 19 '18 at 11:10
  • Better go with a service account ;-) – Aritz Apr 19 '18 at 12:05
  • @XtremeBiker maybe I don't understand how it works, but I'm trying to do this: keycloakRestTemplate.postForEntity(/realms/REALM/protocol/openid-connect/token, null, String.class).getBody(); and I receive 403... How can I pass also the JWT to keycloak? – Teo Apr 20 '18 at 08:42
  • Have you followed all the required steps declared [here](https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/client-authentication.html) in order to work with a JWT? Are you using a URL to serve the client public keys or importing them into keycloak? – Aritz Apr 20 '18 at 08:56
  • @XtremeBiker yes, I did.. As you can see, I have all those properties inside the application.properties... I have org.springframework.web.client.HttpClientErrorException: 400 Bad Request now, becasue before I wrote the wrong url – Teo Apr 20 '18 at 14:39
  • `400 Bad Request`: you'll have some error message at server side telling your what you miss. Or if you parse the response you'll see it in the `message` part. – Aritz Apr 20 '18 at 15:23
  • @XtremeBiker I don't find any error message.. I checked on Keycloak, on my server, but nothing.. Then I tried to undesrtand what you suggested [https://stackoverflow.com/questions/46073485/keycloak-spring-security-client-credential-grant/46116485#46116485](service accounts), but I don't know how implement it... – Teo Apr 21 '18 at 07:21
  • This has the extra complexity of using a JWT as client key. You should firstly set up your code to work with [JWT client authentication](https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/client-authentication.html), which I haven't never done myself. Then you'll need to manage that auth flow for each of the requests being made in the `@Async` thread. I don't know if you have the chance to go with client credentials, but it would be easier to implement. EDIT: It looks like there's some work done in the Spring Boot adapter: https://stackoverflow.com/a/46784443/1199132 – Aritz Apr 25 '18 at 09:08
  • From your question, comparing to the docs, I noticed you're missing to tell the adapter about the alias to be used in the keystore file. I would also try to do further debugging both in keycloak side (launch the server with an increased logging threshold) and client (try to set some breakpoints in [JWTClientCredentialsProvider](https://github.com/keycloak/keycloak/blob/master/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java)) and related classes to try to see whats going on. – Aritz Apr 29 '18 at 17:23

3 Answers3

1

If you want to send a request like below through spring

POST /auth/realms/demo/protocol/openid-connect/token
    Authorization: Basic cHJvZHVjdC1zYS1jbGllbnQ6cGFzc3dvcmQ=
    Content-Type: application/x-www-form-urlencoded

    grant_type=client_credentials

what you need is something like

RestTemplate template = new RestTemplate();
String uri = "https://host:port/auth/realms/demo/protocol/openid-connect/token";
String data = "grant_type=client_credentials" ;
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString("clientId:clientSecret".getBytes()));
HttpEntity<String> requestEntity = new HttpEntity<String>(data, headers);

ResponseEntity<JsonNode> result = template.exchange(uri, HttpMethod.POST, requestEntity, JsonNode.class);
JsonNode jn = result.getBody();
String access_token = jn.get("access_token").asText();

Replace clientId and clientSecret with actual values.

Update:-

Referring to the link that you have mentioned in your question, you can generate the jwt bearer token (client access token) by keycloak itself. Once you get the jwt token, your subsequent requests to resource server should contain header

Authorization: Bearer <jwt bearer token>

In the post by @Xtreme Biker on topic Keycloak spring security client credential grant , he has provided some sample code for perhaps how you can achieve this using interceptor approach.

MORE UPDATE :-

According to keycloak docs - https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/java-adapter-config.html

to set the jks you should have following properties in your application.properties file.

keycloak.client-keystore-password=PWD
keycloak.client-keystore=classpath:CLIENT.jks
keycloak.client-key-password=PWD

and according to - https://www.keycloak.org/docs/3.1/securing_apps/topics/oidc/java/spring-security-adapter.html , once you set the jks properly,KeycloakRestTemplate should be able to add proper authentication header to your request.

UPDATE 3 :-

After going through - https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/java/spring-security-adapter.adoc , I firmly believe that KeycloakRestTemplate should be able to add the required jwt token to the authorization header of the request because it uses KeycloakClientRequestFactory which gets token string from KeycloakSecurityContext.

Please try to add all the configuration suggested in this doc like all the filter bean configurations

@Bean
    public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
            KeycloakAuthenticationProcessingFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
            KeycloakPreAuthActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakAuthenticatedActionsFilterBean(
            KeycloakAuthenticatedActionsFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean keycloakSecurityContextRequestFilterBean(
        KeycloakSecurityContextRequestFilter filter) {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
        registrationBean.setEnabled(false);
        return registrationBean;
    }

and ensure you provide all the required properties in the application.properties file.

Since you are shooting the request from scheduled task which may be asyc in nature so you may have to change the strategy in the constructor of your SecurityConfig, so that instead of a ThreadLocal SecurityContext an InheritableThreadLocal is used, which passes this information when spawning childthreads.

public SecurityConfig ( KeycloakClientRequestFactory keycloakClientRequestFactory ) {
         this.keycloakClientRequestFactory = keycloakClientRequestFactory ;

         // to use principal and authentication together with @ async
         SecurityContextHolder.setStrategyName ( SecurityContextHolder.MODE_INHERITABLETHREADLOCAL ) ;

     }

Ref - https://translate.google.co.in/translate?hl=en&sl=de&u=https://blog.codecentric.de/2017/09/keycloak-und-spring-security-teil-3-kommunikation-via-keycloakresttemplate/&prev=search for more details.

Hope this will help you some way in resolving your issue.

vsoni
  • 2,828
  • 9
  • 13
  • Thank you.. I think your answer will be useful, but what I'm using is the JWT so I don't have the client and secret, but I have a JKS instead.. So do you know how can I send or use the JKS to request the access token? – Teo Apr 29 '18 at 11:43
  • @Teo - pls see few more comments on my answer, if it helps you some way in resolving your issue. – vsoni May 01 '18 at 14:19
  • sorry, but I took some days off.. I just tested what you suggested, but I have the same exception: `java.lang.IllegalStateException: Cannot set authorization header because there is no authenticated principal` – Teo May 02 '18 at 08:33
  • @Teo - ref - https://stackoverflow.com/questions/48752674/spring-security-keycloak-accept-bearer-token . He was able to set authentication header with his configuration. – vsoni May 02 '18 at 09:24
  • @Teo - perhaps you can try with adding property `keycloak.bearer-only=true` in the application.properties – vsoni May 02 '18 at 09:31
  • I'm using confidential not bearer-only.. Then the configuration is basically the same.. please take a look to my edit.. – Teo May 02 '18 at 11:00
  • just to be clear.. what I would like to obtain is exactly what you wrote inside **update** section... I can't retrieve the access token from keycloak... – Teo May 03 '18 at 08:09
  • it doesn't work yet.. I have added what you suggested, but nothing... I'm thinking that maybe the cause is that I'm not `using-bearer only=true`.. But this web-app serves also web page so I can't set that propery to true.. Does it make sense to you? – Teo May 04 '18 at 09:06
  • @Teo - I am sure we are missing some link ... some configuration point ... anyway if you do not want to disturb the other parts of your application then you can manually create the jwt token and set it to the rest template. Since you already have the private key you can easily create a jwt token. – vsoni May 04 '18 at 13:00
  • @Teo - Ref this adapter code for how you can create a jwt token yourself. https://github.com/keycloak/keycloak/blob/master/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authentication/JWTClientCredentialsProvider.java . You can probably use this class as it is (this class is already part of your adapter lib) by calling some function like `createSignedRequestToken()` that can return you the jwt token. – vsoni May 04 '18 at 13:05
  • mmmm.. and after I generate it, how can I set it to my KeycloakRestTemplate? – Teo May 04 '18 at 14:02
  • @Teo - If you are setting the authorization header manually then you can use any of the RestTemplate or KeycloakRestTemplate. I have already provided sample for adding a "Basic" authorization header. For Bearer it would be `headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + jwttoken);` – vsoni May 04 '18 at 16:59
  • I'm feeling very stupid right now, but please take a look to **EDIT 4**.. Thanks! – Teo May 07 '18 at 07:24
0

By default SecurityContextHolder use a THREAD_LOCAL strategy, and thus other threads don't inherit the context from their parent. Look at the Javadoc for MODE_INHERITABLETHREADLOCAL to see if it corresponds to your needs.

p3consulting
  • 2,721
  • 2
  • 12
  • 10
  • I don't think that is the solution or the problem.. Anyway I tried, but nothing changed – Teo Apr 27 '18 at 14:45
0

Your method being an @Scheduled it is executed in a different thread than the caller thread. SecurityContextHolder being a Thread Local will not propagate the security context to the scheduled thread. One method would be to change the default strategy of the SecurityContextHolder from THREAD_LOCAL, to MODE_INHERITABLETHREADLOCAL, by adding this line in the default constructor of a @Configuration class.

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

But depending on your use case, you should be aware that the security context might contain sensitive information that you might not want to be passed down.

  • I don't think that is the solution or the problem.. Anyway I tried, but nothing changed – Teo Apr 27 '18 at 14:45