Blog de Nicolas DUMINIL

Lasciate ogni speranza vuoi che entrate qui !

Basics of Microservices - Part 6: Oauth 2.0 Security with Keycloak

Publié le 05/10/2018

Welcome to the 6th part of the Microservices article series. This 6th part shows how to use a Netflix Zuul filters in order to secure micro-services.

The micro-services used until now were publicly accessible resources. In this example we will secure them by using the OAuth 2.0 protocol. Please notice that this is not an OAuth 2.0 tutorial and it assumes that the reader is familiar with it. For more information please see https://oauth.net/2/.

There are several possible approaches to use the OAuth 2.0 protocol. Here we choose to use the Keycloak implementation. Please notice also that this is neither a Keycloak tutorial and that we assume the reader is familiar with it. For more information please see https://www.keycloak.org/.

In this new part, we add two new docker containers to our infrastructure, as follows:

  • a container named "keycloak" running Keycloak 3.4.2
  • a container named "ms-keycloak" running a Spring Boot service which exposes Keycloak as a micro-service.

The following listing shows our new docker-compose.yml specification:

version: "2"
services:
active-mq:
image: webcenter/activemq:latest
container_name: active-mq
ports:
- "8161:8161"
- "61616:61616"
- "5672:5672"
- "61613:61613"
- "1883:1883"
- "61614:61614"
hostname: active-mq
networks:
net1:
ipv4_address: 192.19.0.4
ms-config:
image: openjdk:8-jdk-alpine
container_name: ms-config
volumes:
- ../../ms-config/src/main/docker:/usr/local/share/hml
ports:
- "8888:8888"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-config
environment:
BROKER_PORT: "61616"
KEYCLOAK_PORT: "8080"
networks:
net1:
ipv4_address: 192.19.0.2
ms-discovery:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
container_name: ms-discovery
links:
- ms-config:ms-config
volumes:
- ../../ms-discovery/src/main/docker:/usr/local/share/hml
ports:
- "8761:8761"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-discovery
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
CONFIGSERVER_LABEL: "oauth"
SERVER_PORT: "8761"
networks:
net1:
ipv4_address: 192.19.0.5
ms-routing:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
- ms-discovery
container_name: ms-routing
links:
- active-mq:active-mq
- ms-config:ms-config
- ms-discovery:ms-discovery
volumes:
- ../../ms-routing/src/main/docker:/usr/local/share/hml
ports:
- "9090:9090"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-routing
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
DISCOVERY_PORT: "8761"
CONFIGSERVER_LABEL: "oauth"
SERVER_PORT: "9090"
DISCOVERYSERVER_URI: "http://ms-discovery:8761/eureka/"
networks:
net1:
ipv4_address: 192.19.0.6
keycloak:
image: jboss/keycloak:3.4.2.Final
volumes:
- ../scripts:/opt/jboss/keycloak/customization/
container_name: keycloak
entrypoint: /opt/jboss/docker-entrypoint.sh -b 0.0.0.0 -bmanagement 0.0.0.0
hostname: keycloak
ports:
- "18080:8080"
- "19990:9990"
environment:
KEYCLOAK_USER: "admin"
KEYCLOAK_PASSWORD: "admin"
networks:
net1:
ipv4_address: 192.19.0.3
ms-keycloak:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
- ms-discovery
- ms-routing
- keycloak
container_name: ms-keycloak
links:
- ms-config:ms-config
- active-mq:active-mq
- ms-discovery:ms-discovery
- ms-routing:ms-routing
- keycloak:keycloak
volumes:
- ../../ms-keycloak/src/main/docker:/usr/local/share/hml
hostname: ms-keycloak
ports:
- "8085:8085"
entrypoint: /usr/local/share/hml/run.sh
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
SERVER_PORT: "8085"
DISCOVERYSERVER_PORT: "8761"
CONFIGSERVER_LABEL: "oauth"
DISCOVERYSERVER_URI: "http://ms-discovery:8761/eureka/"
KEYCLOAKSERVER_PORT: "8080"
ROUTINGSERVER_PORT: "9090"
networks:
net1:
ipv4_address: 192.19.0.9
ms-core:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
- ms-discovery
- ms-routing
- keycloak
- ms-keycloak
container_name: ms-core
links:
- ms-config:ms-config
- active-mq:active-mq
- ms-routing:ms-routing
- ms-discovery:ms-discovery
- ms-keycloak:ms-keycloak
- keycloak:keycloak
volumes:
- ../../ms-core/src/main/docker:/usr/local/share/hml
ports:
- "8080:8080"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-core
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
BROKER_PORT: "61616"
SERVER_PORT: "8080"
DISCOVERYSERVER_PORT: "8761"
CONFIGSERVER_LABEL: "oauth"
ROUTINGSERVER_PORT: "9090"
DISCOVERYSERVER_URI: "http://ms-discovery:8761/eureka/"
KEYCLOAKSERVER_PORT: "8080"
MS_KEYCLOAKSERVER_PORT: "8085"
networks:
net1:
ipv4_address: 192.19.0.8
ms-core2:
image: openjdk:8-jdk-alpine
depends_on:
- ms-config
- ms-discovery
- ms-routing
- keycloak
- ms-keycloak
container_name: ms-core2
links:
- ms-config:ms-config
- active-mq:active-mq
- ms-routing:ms-routing
- ms-keycloak:ms-keycloak
- keycloak:keycloak
volumes:
- ../../ms-core/src/main/docker:/usr/local/share/hml
ports:
- "8081:8081"
entrypoint: /usr/local/share/hml/run.sh
hostname: ms-core2
environment:
PROFILE: "default"
CONFIGSERVER_URI: "http://ms-config:8888"
CONFIGSERVER_PORT: "8888"
BROKER_PORT: "61616"
SERVER_PORT: "8081"
DISCOVERYSERVER_PORT: "8761"
CONFIGSERVER_LABEL: "oauth"
ROUTINGSERVER_PORT: "9090"
DISCOVERYSERVER_URI: "http://ms-discovery:8761/eureka/"
KEYCLOAKSERVER_PORT: "8080"
MS_KEYCLOAKSERVER_PORT: "8085"
networks:
net1:
ipv4_address: 192.19.0.7
networks:
net1:
driver: bridge
driver_opts:
com.docker.network.enable_ipv6: "false"
ipam:
driver: default
config:
- subnet: 192.19.0.0/24
gateway: 192.19.0.1

 The listing above should be quite familiar by now. The new containers are keycloak and, respectively, ms-keycloak. The first one is based on the jboss/keycloak:3.4.2.Final docker image and runs a wildfly application server with the keycloak server. It defines the IP address 192.19.0 3 and exposes the TCP ports 18080 and 19090 which are mapped to the hosts's 8080 and 9090 ports. It executes the script docker-entrypoint.sh as its entrypoint. This script runs the Wildfly server in the docker container. It also mounts a volume containing different scripts which one of the mosts important is customize.sh. The listing belows shows this script:

#!/bin/bash
WILDFLY_HOME=/opt/jboss/keycloak
KCADM=$WILDFLY_HOME/bin/kcadm.sh
while ! $($KCADM config credentials --server http://localhost:8080/auth --realm master --user admin --password admin > /dev/null 2>&1)
do
echo "### The connection to the keycloak server failed. Perhaps the server didn't finish its start process. Trying again ..."
sleep 3
done
echo "### The connection to the keycloak succeeded. The server is fully operational. Executing customization."
$KCADM update realms/master -s sslRequired=NONE
$KCADM create users -r master -s username=customer-admin -s enabled=true
$KCADM set-password -r master --username customer-admin --new-password admin
$KCADM create clients -r master -s clientId=customer-manager-client -s bearerOnly="true" -s "redirectUris=["http://localhost:8080/api/services/*"]" -s enabled=true
$KCADM create clients -r master -s clientId=curl -s publicClient="true" -s directAccessGrantsEnabled="true" -s "redirectUris=["http://localhost"]" -s enabled=true
$KCADM create roles -r master -s name=customer-manager
$KCADM add-roles --uusername customer-admin --rolename customer-manager -r master


The listing above shows the customize.sh script which is used in order to customize the Keycloak server. Here are the details of this code:

  • Realms. Keycloak is based on the concept of realm as a set of security artifact. The server comes with a defauly, out-of-the-box realm called "master". This realm is dedicated to the security artifacts used by the server itself. In our example, for simplicity sake we are using the master realm but in a real case this should be avoided and a dedicated customozed realm should be used. The script connects to the master realm by using the administrator credentials. This operation might take some times, depending on your configuration, hence it is done in a while loop.
  • SSL. In order to secure the communication with the Keycloak server itself, SSL shall be used in production. Here we aren't using it hence we update the master realm consequently.
  • Users. In the master realm, we create a new user named "customer-admin" and we associate a password to this user. The user is enabled.
  • Clients. Keycloak uses the notion of clients as entities able to request users authentication. Here we are creating two clients,a first one named "customer-manager-client" with the grant type "Bearer only" and a second one named "curl" with the grant type "direct access". The Oauth 2.0 documentation explains in detail what the grant types are.
  • Roles. Keycloak is based on the notion of role as types or category of users. Here we define a role named "customer-manager" and we associate this role to the customer-admin user.

Executing this script, we customize our Keycloak installation. This is done in the build.sh script, which code is showed below:

docker stop ms-routing ms-discovery ms-config ms-core ms-core2 active-mq keycloak ms-keycloak
docker rm ms-routing ms-discovery ms-config ms-core ms-core2 active-mq keycloak ms-keycloak
mvn -DskipTests clean install
docker-compose -f docker/common/docker-compose.yml up -d
for cname in active-mq keycloak ms-config ms-discovery ms-routing ms-keycloak ms-core ms-core2
do
while ! docker ps -q -f name=$cname > /dev/null 2>&1
do
sleep 3
done
done
sleep 3
docker exec -it keycloak keycloak/customization/customize.sh



This script stops all the containers, builds the projects and, by using docker-composer, starts the containers again. It waits until all the containers finished their starting process and then it executes the customize.sh script such that to customize our Keycloak installation.

Now that we have our Keycloak service running and configured with the required realms, users, roles and clients, we can start adapting our code such that to take advantage of the Oauth 2.0 authorization. We start by protercting our resources, i.e. our REST service endpoints. This REST service has several endpoints but we want to security protect only two of them: the subscribe and the publish endpoints. This is done on the behalf of the following properties:

keycloak:
  auth-server-url: "http://192.19.0.3:8080/auth"
  realm: "master"
  ssl-required: external
  resource: customer-manager-client
  public-client: true
  bearer-only: true
keycloak.securityConstraints[0].securityCollections[0].name: "protected resource"
keycloak.securityConstraints[0].authRoles[0]: "customer-manager"
keycloak.securityConstraints[0].securityCollections[0].patterns[0]: "/api/subscribe"
keycloak.securityConstraints[0].securityCollections[0].patterns[1]: "/api/publish"

We created a new branch, named oauth, in our spring cloud configuration project at https://github.com/nicolasduminil/spring-config. It contains thes new properties. Our project need to be configured such that to use this new branch of the configuration, as shown by the following fragment of the bootstrap.yml file:

cloud:
config:
uri: http://192.19.0.2:8888
name: hml-core
label: oauth
profile: default



Here, the label property means the GIT repository branch, as we already have seen previously. These properties define access rules based on which the /pai/subscribe and /api/publish endpoints are security protected by the OAuth 2.0 protocol. These rules clearly state that only consumers having the role "customer-manager" are able to access these endpoints. And in order that a micro-service consumer has these role, it needs to authenticate first against the Keycloak server using an authentication mechanism which will grant to it the "customer-manager" role.

In order to configure our project with the Keycloak server, we need the following maven artifacts in our master POM:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>3.4.2.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-spi-bom</artifactId>
<version>3.4.2.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-bom</artifactId>
<version>3.6.1.Final</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

 

Then in the ms-core project we need as follows:

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>


At this point our micro-services are configured such that to take advantage of the Keycloak OAuth 2.0 authorization and authentication. We need an additional micro-service such that to expose the Keycloak server. This microservice is a standard Spring Boot application, having the following REST controller:

package fr.simplex_software.micro_services.keycloak.controllers;

import fr.simplex_software.micro_services.keycloak.services.*;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class KeycloakRestController
{
private static Logger slf4jLogger = LoggerFactory.getLogger(KeycloakRestController.class);
@Value("${keycloak.auth-server-url}")
private String serverUrl;
@Value("${keycloak.realm}")
private String realm;
@Value("${ms-keycloak.user-name}")
private String userName;
@Value("${ms-keycloak.user-password}")
private String userPassword;
@Value("${ms-keycloak.client-id}")
private String clientId;
@Autowired
private KeycloakService ks;

@RequestMapping(value = "/accessToken", method = RequestMethod.GET)
public String getAccessToken()
{
slf4jLogger.debug ("*** KeycloakRestController.getAccessToken: Entry {}, {}, {}, {}, {}", serverUrl, realm, userName, userPassword, clientId);
String accessToken = ks.getAccessToken(serverUrl, realm, userName, userPassword, clientId);
slf4jLogger.debug ("*** KeycloakRestController.getAccessToken: access token: {}", accessToken);
return accessToken;
}
}





 

The listing above shows the REST controller serving the endpoint "/auth". It exposes a GET resource named "accessToken" which, as its name implies, will return an OAuth 2.0 access token. To do so, this endpoint is using the KeycloakService which code is presented in the listing below:

 

package fr.simplex_software.micro_services.keycloak.services;

import org.keycloak.admin.client.*;
import org.springframework.stereotype.*;

@Service
public class KeycloakService
{
public String getAccessToken(String serverUrl, String realm, String userName, String password, String clientId)
{
return Keycloak.getInstance(serverUrl, realm, userName, password, clientId).tokenManager().getAccessToken().getToken();
}
}




As we can see, this service is very simple. It uses the Keycloak admin client to obtain an access token. This is done on the behalf of the class org.keycloak.admin.client.Keycloak which method getInstance() will instatiate a Keycloak service instance, passing to it the server URL, the working security realm, an existent user and client ID. We already have created a special user dedicated to this operation. Its name is "customer-admin" and the associated client ID is "curl". Then, by using the TokenManger by calling the tokenManager() method of this Keycloak instance, we are able to obtain an access token, on the behalf of the method getAccessToken(). This method returns a lot of stuff in addition to just our access token, so in order to isolate it, we call the getToken() method.

The token we obtained this way is returned to the REST endpoint which , in turn, returns is to the micro-service consumer.

The authentication/authorization process is implemented in a Zuul filter and here we can see the real added value of Zuul. Here is the listing:

package fr.simplex_software.micro_services.routing.filters;

import com.netflix.zuul.*;
import com.netflix.zuul.context.*;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.web.client.*;

import javax.servlet.http.*;

@Component
public class RouteFilter extends ZuulFilter
{
private static final Logger logger = LoggerFactory.getLogger(RouteFilter.class);
private static final String AUTHORIZATION_HEADER = "authorization";
private static String accessToken;
@Autowired
private RestTemplate restTemplate;

@Override
public String filterType()
{
return "pre";
}

@Override
public int filterOrder()
{
return 2;
}

@Override
public boolean shouldFilter()
{
return true;
}

public Object run()
{
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest req = context.getRequest();
logger.debug("*** RouteFilter(): Processing incoming request for {}, {}, {}.", req.getRequestURI(), req.getRequestURL().toString(), req.getRemoteAddr());
accessToken = restTemplate.getForObject("http://hml-keycloak/auth/accessToken", String.class);
logger.debug("*** RouteFilter(): Processing outgoing response for {}.", accessToken);
context.addZuulRequestHeader(AUTHORIZATION_HEADER, "Bearer " + accessToken);
return null;
}
}

This is a "pre" Zuul filter which, as explained in the Part 5th of our tutorial, intercepts the requests before their destination endpoint. Its run() method calls the Keycloak micro-service, obtains this way an authentication token and adds it as a "Bearer only" token to the request's header. Then the HTTP request is forwarded to the destination endpoint. This allows any micro-service consumer having an authentication token obtained on the behalf of an user with the role "customer-manager" to call the subscribe and publish endpoints. Commenting out the grayed line in the listing above will result is an HTTP 401 exception.

This almost everything that we need to do in order to secure our micro-services. To build and test perform the following steps:

  • open a command-line window
  • create a directory
  • move to that directory
  • clone the repository by doing the following command: 
    git clone https://github.com/nicolasduminil/micro-services.git
  • switch to the 6th part branch by doing the following command: 
    git checkout oauth
  • change to the right directory by doing the following command: 
    cd ms-core-config-discovery-resilience-routing-oauth
  • build the project by doing the following command: 
    mvn -DskipTests clean install
  • start the docker containers by doing the following command: 
    docker-compose -f docker/common/docker-compose.yml up

After performing the operations above you'll get several docker containers running, as follows:

  • a container named active-mq running the messaging broker
  • a container named ms-config running the configuration microservice
  • a container named ms-discovery running the Eureka service
  • a container named ms-core running a first instance of the core microservice
  • a container named ms-core2 running a second instance of the core microservice
  • a container named ms-routing runningthe Zuul service
  • a container named keycloak running the Keycloak 3.4.1 server
  • a container named ms-keycloak which exposes the Keycloak service as a microservice.

To test, do the following:

  • open a new command-line window
  • move to the project directory, for example 
    cd ms-core-config-discovery-resilience-routing-oauth
  • run the following: 
    mvn test

The new elements of this 6th part are the two docker containers running the Keycloak server and the Keycloak microservice. This second one is a Spring Boot application exposing the Keycloak administration client API.

The Zuul microservice has been modified also such that to invoke the Keycloak microservice in order to
obtain an OAuth 2.0 Bearer token. Once obtained on the behalf of the Keycloak microservice, via the Keycloak
administration client API, this token is inserted by the Zuul filter into the HTTP requests. This way the micro-services are protected from the public access.

Envoyer à un ami

* champs obligatoires

* champs obligatoires

« Les informations recueillies font l’objet d’un traitement informatique destiné au traitement de votre demande. Le destinataire des données est Simplex Software. Conformément à la loi « informatique et libertés » du 6 janvier 1978 modifiée en 2004, vous bénéficiez d’un droit d’accès et de rectification aux informations qui vous concernent, que vous pouvez exercer en vous adressant à Simplex Software, 26 Allée des Sapins - 95230 Soisy sous Montmorency. Vous pouvez également, pour des motifs légitimes, vous opposer au traitement des données vous concernant. »