Tecnologia
Keycloak: Acess Token para envio em requisições internas via Feign
4 minutos de leitura
Neste artigo serão vistas duas formas de realizar a recuperação do token JWT fornecido via Keycloak para envio de requisições internas entre duas aplicações em Java utilizando Spring.
Vale ressaltar que o foco não é destrinchar sobre conceitos e funcionamento do Keycloak, mas mostrar de forma clara e direta a implementação para recuperação e envio do token entre serviços.
Keycloak: o que é?
O Keycloak é um produto de software de código aberto escrito em Java e licenciado pela RedHat que permite o login único com Gerenciamento de Identidade e Acesso voltado para aplicativos e serviços modernos[1].
Para saber mais sobre a ferramenta, acesse o link [1] nas referências.
Como funciona o Feign?
Uma das formas mais elegantes e simples de se comunicar com outros serviços Rest é criando uma interface com o FeignClient.
O Feign funciona processando anotações em uma solicitação padronizada, cujos argumentos são aplicados a esses modelos de maneira direta antes da saída. Ele simplifica drasticamente os aspectos do sistema, como a repetição de solicitações. Além disso, o Feign facilita o teste unitário de suas conversões[2].
Uma das facilidades ao fazer uso do Feign é permitir configurá-lo através de uma classe de configuração. Para o proposto aqui, será feito o uso desta classe.
Direto ao ponto
Existem algumas maneiras de obter o token JWT gerado pelo Keycloak, entre elas temos uma através da biblioteca Java keycloak-admin-client e outra por meio do HttpClient do Java, fazendo assim uma chamada REST. Vale ressaltar que nos exemplos abaixo utilizaremos Java 11.
É possível aproveitar o token da chamada REST original para ser enviado nas chamadas REST internas para outros serviços desde que estejam no mesmo REALM e regras configuradas no Keycloak. Como mencionado acima, os conceitos e funcionamento da ferramenta não serão o centro deste artigo.
Agora, imagine que não existe uma chamada REST para um determinado serviço que chame outro, logo não existe um token para reaproveitá-lo. Um exemplo clássico se trata de Jobs, que internamente chama um serviço REST externo à aplicação. Como então recuperar o token para enviar nesta chamada, realizando assim a autenticação e autorização, e obter um determinado resultado?
Uma das formas é através da biblioteca java keycloak-admin-client.
Obtendo Token através da Biblioteca
1- No seu pom.xml adicione a dependências do Keycloak.
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>${keycloak-admin.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak-boot.version}</version>
</dependency>
2- Adicione em seu application todas as informações necessárias para comunicação com o Keycloak.
# Security
spring.security.oauth2.resourceserver.jwt.issuer-uri=${BACKEND_AUTH_SERVER_URL}
security.oauth2.client.access-token-uri=${BACKEND_ACCESS_TOKEN_URL}
security.oauth2.client.client-id=${BACKEND_CLIENT_ID}
security.oauth2.client.realm=${BACKEND_REALM}
security.oauth2.client.client-secret=${BACKEND_CLIENT_SECRET}
security.oauth2.client.grant-type=client_credentials
3- Crie uma classe de serviço que retornará o token gerado no keycloak.
@Service
@RequiredArgsConstructor
@Slf4j
public class KeycloakService {
private static final String BEARER = "Bearer ";
@Value("${security.oauth2.client.access-token-uri}")
private String uri;
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.realm}")
private String realm;
@Value("${security.oauth2.client.client-secret}")
private String secret;
public String fetchToken() {
log.debug("Executando autenticação");
String token;
try {
Keycloak keycloak = KeycloakBuilder.builder()
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.serverUrl(uri)
.realm(realm)
.clientId(clientId)
.clientSecret(secret)
.build();
token = keycloak.tokenManager().getAccessTokenString();
} catch (Exception e) {
log.warn("Erro ao retornar token de acesso via keycloak.");
throw new ServiceUnavailableException();
}
log.debug("Token de acesso recebido: {}", token);
return format("%s%s", BEARER, token);
}
}
4- Em seguida, crie uma classe de configuração, criando um @bean que retorne um RequestInterceptor para envio do token na requisição[3].
@Configuration
@RequiredArgsConstructor
@Slf4j
public class FeignConfig {
private static final String AUTHORIZATION = "Authorization";
private final KeycloakService keycloakService;
@Bean
public RequestInterceptor requestKeycloakInterceptor() {
log.debug("Realizando autenticação keycloak com serviços internos para chamada via Feign Client");
return requestTemplate -> {
final var token = keycloakService.fetchToken();
requestTemplate.header(AUTHORIZATION, token);
};
}
}
5- Por fim, adicione a classe de configuração no parâmetro ‘configuration’ da interface.
@FeignClient(name = "feign-manager", url = "${feign.manager.url}", configuration = FeignConfig.class)
public interface ManagerClient {
@PostMapping("/v1/exemplo")
Object create(@RequestBody Object request);
}
Obtendo Token através de HttpClient
Basicamente, para obter o token via HttpClient implementado no Java 11, será necessário alterar apenas a classe de serviço, e não será adicionada as dependências do Keycloak como visto no exemplo anterior. Também criamos um objeto chamado KeycloakToken para receber o token do response.
Para saber mais informações sobre HttpClient basta acessar o link da referência[4].
@Data
@Builder
@AllArgsConstructor
public class KeycloakToken {
@JsonProperty("access_token")
private final String accessToken;
}
@Service
@RequiredArgsConstructor
@Slf4j
public class KeycloakService {
private static final String GRANT_TYPE = "grant_type";
private static final String CLIENT_ID = "client_id";
private static final String CLIENT_SECRET = "client_secret";
private static final String BEARER = "Bearer ";
@Value("${security.oauth2.client.access-token-uri}")
private String uri;
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.client-secret}")
private String secret;
@Value("${security.oauth2.client.grant-type}")
private String grantType;
private final Gson gson;
private final HttpClient client;
public String fetchToken() {
log.debug("Executando autenticação");
var urlParameters = format("%s=%s&%s=%s&%s=%s", GRANT_TYPE, grantType, CLIENT_ID, clientId, CLIENT_SECRET, secret);
HttpResponse<String> response;
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(uri))
.POST(HttpRequest.BodyPublishers.ofString(urlParameters))
.headers("Content-Type", "application/x-www-form-urlencoded")
.build();
response = client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (InterruptedException e) {
log.warn("Erro ao retornar token de acesso via keycloak. Thread interrompida.");
Thread.currentThread().interrupt();
throw new ServiceUnavailableException();
} catch (Exception e) {
log.warn("Erro ao retornar token de acesso via keycloak.");
throw new ServiceUnavailableException();
}
var token = gson.fromJson(response.body(), KeycloakToken.class);
log.debug("Token de acesso recebido: {}", token.getAccessToken());
return format("%s%s", BEARER, token.getAccessToken());
}
}
Como visto acima, primeiro foi criada a classe que receberá o token de acesso e em seguida, no service, um request foi construído para ser enviado através do HttpClient.
Conclusão
O objetivo deste artigo foi mostrar de forma direta como obter o Token via Keycloak e enviá-lo para requisições entre serviços que se comunicam via Feign. Para tal, foi utilizado duas formas: HttpClient, fornecido pelo Java 11, e a biblioteca keycloak-admin-client. É perceptível que fazer uso da biblioteca deixa o código mais limpo e fácil de ser lido, mas é importante entender o que melhor se adequa para a sua aplicação.
Referências
[2] https://www.baeldung.com/java-9-http-client
[3] https://www.baeldung.com/spring-cloud-feign-oauth-token
[4] https://openjdk.org/groups/net/httpclient/intro.html
[5] https://github.com/OpenFeign/feign
[6] https://www.baeldung.com/spring-cloud-openfeign
[7] https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/