Actually, I managed to make Keycloak client do this job for me after spending some time with the issue. In my case I had to connect to a Keycloak server with password grant type, and use access token to fetch data from a third party protected endpoint in a Spring Boot server side application.
At the end I came up with a service, which provides an access token after initial authentication, and automatic refresh/re-authentication on demand.
I added a @Configuration
bean, which contained the connection parameters to the third party Keycloak instance:
package no.currentclient.application.api.config; // real package name masked
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OauthClientConfig {
public record OauthConfig(String realm, String authServerUrl, String clientId, String username, String password) {
}
@Bean
OauthConfig oauthConfig(
@Value("${client.oauth.realm}") String realm,
@Value("${client.oauth.auth-server-url}") String authServerUrl,
@Value("${client.oauth.resource}") String clientId,
@Value("${client.oauth.username}") String username,
@Value("${client.oauth.password}") String password
) {
return new OauthConfig(realm,
authServerUrl,
clientId,
username,
password);
}
}
After I created a Spring Service
which is capable of authenticating, getting and refreshing an access token:
package no.currentclient.application.auth.oauthclient; // real package name masked
import com.fasterxml.jackson.databind.ObjectMapper;
import no.currentclient.application.api.config.OauthClientConfig;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.TokenCallable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Collections;
@Service
public class OauthTokenService {
private final TokenCallable tokenCallable;
public OauthTokenService(
OauthClientConfig.OauthConfig oauthConfig,
OkHttpClient okHttpClient
) throws IOException {
var serverConfiguration = getServerConfiguration(oauthConfig.authServerUrl()+"/auth/realms/"+oauthConfig.realm()+"/.well-known/openid-configuration", okHttpClient);
var config = new org.keycloak.authorization.client.Configuration(
// These might all be set to null -> only tokenMinimumTimeToLive is used in TokenCallable...
null,null,null, null,null);
var http = new Http(config, (requestParams, requestHeaders) -> requestParams.put("client_id", Collections.singletonList("deichman")));
tokenCallable = new TokenCallable(oauthConfig.username(), oauthConfig.password(), http, config, serverConfiguration);
}
/*
* Call this method to get hold of an on-demand refreshed auth token. TokenCallable handles the burden of token
* refresh and re-authentication in case of session timeout.
*/
public String getAccessToken() {
return tokenCallable.call();
}
private ServerConfiguration getServerConfiguration(String configUrl, OkHttpClient okHttpClient) throws IOException {
var configRequest = new Request.Builder().url(configUrl).get().build();
try (var response = okHttpClient.newCall(configRequest).execute()) {
return new ObjectMapper().readValue(response.body().string(), ServerConfiguration.class);
}
}
}
TokenCallable
hides all the complexity of refresh/re-authentication on demand.
Hope it helps a few struggling with this problem.