Saturday, April 13, 2019

Securing a Microservice With Keycloak

Hi there!
In this post, I'll be demonstrating the way I secured a microservice with Keycloak. The sample microservice is created using JAX-RS and deployed to wildfly11.




Prerequisites

  1. Keycloak docker image - https://hub.docker.com/r/jboss/keycloak/
  2. Java 7/8 and Maven 3
  3. Wildfly11

The secured microservice project is hosted on GitHub. Please click here to clone the project.

A look at the Microservice

The microservice contains only a single REST endpoint called PERSON API which will return a json message with the application name, name of the person extracted from query parameter and other sample parameters.

PersonAPI.class 


package com.demo.keycloak.service;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.HashMap;
import java.util.Map;

@Path("/keycloak-demo")
public class PersonAPI {

    private static Gson gson = new GsonBuilder().create();

    @POST
    @Path("person/{name}")
    public Response start(@PathParam("name") String personName, String requestBody,
                          @HeaderParam("Authorization") String authorization, @Context UriInfo uriInfo) {

        Map<String, Object> reponseMap = new HashMap<>();

        final String path = uriInfo.getAbsolutePath().getPath();
        final String appName = path.substring(1, path.indexOf("-"));
        logger.info("\n\n Application Name : " + appName);

        reponseMap.put("CLIENT_ID", appName);
        reponseMap.put("SUCESS", true);
        reponseMap.put("PERSON_NAME", personName);

        return Response.status(200).entity(convertMapToJSONString(reponseMap)).header("Content-Type", "application/json")
                .build();
    }

    public String convertMapToJSONString(Map<String, Object> responseMap) {
        logger.debug("\n\n convertMapToJSONString :: responseMap :" + responseMap.toString());
        return gson.toJson(responseMap);
    }


    private static final Logger logger = LoggerFactory.getLogger(PersonAPI.class);

}

Securing the REST Endpoint 


Below are the keycloak maven dependencies needed. 


    <!-- keycloak jars -->

        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-core</artifactId>
            <version>${keycloak-version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-common</artifactId>
            <version>${keycloak-version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-authz-client</artifactId>
            <version>${keycloak-version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-adapter-core</artifactId>
            <version>${keycloak-version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-adapter-spi</artifactId>
            <version>${keycloak-version}</version>
            <scope>provided</scope>
        </dependency>

Now, we need to secure {{hostname}}/keycloak-demo-service/keycloakDemoService/keycloak-demo/person/ashen by adding constraints and roles to web.xml file.

web.xml


<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" metadata-complete="false" version="3.0">
  <display-name>Restful Web Application</display-name>
  <context-param>
    <param-name>resteasy.scan</param-name>
    <param-value>true</param-value>
  </context-param>
  <context-param>
    <param-name>resteasy.servlet.mapping.prefix</param-name>
    <param-value>/keycloakDemoService</param-value>
  </context-param>

  <!-- keycloak -->

  <context-param>
    <param-name>keycloak.config.resolver</param-name>
    <param-value>com.demo.keycloak.auth.KeycloakAuthResolver</param-value>
  </context-param>

  <security-constraint>
    <web-resource-collection>
      <web-resource-name>REST endpoints</web-resource-name>
      <url-pattern>/keycloakDemoService/keycloak-demo/person/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>personAPI</role-name>
    </auth-constraint>
  </security-constraint>


  <login-config>
    <auth-method>KEYCLOAK</auth-method>
    <realm-name>JavaSecurity</realm-name>
  </login-config>

  <security-role>
    <role-name>personAPI</role-name>
  </security-role>

  <!--RestEasy-servlet-->

  <listener>
    <listener-class>
      org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap</listener-class>
  </listener>

  <servlet>
    <servlet-name>resteasy-servlet</servlet-name>
    <servlet-class>
      org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>resteasy-servlet</servlet-name>
    <url-pattern>/keycloakDemoService/*</url-pattern>
  </servlet-mapping>

</web-app>

Above snippet configures  the Undertow (Undertow is the Web Server on Wildfly ) to require the users to have certain roles to be allowed to invoke the person endpoint. In this case the person endpoint requires the role personAPI.

Creating a Realm in Keycloak


What is a realm in Keycloak?

A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.

Create a realm called 'JavaSecurity' 

Creating a Client in Keycloak


Now we need to create a client for the microservice within Keycloak. The client name will be 'keycloak-demo'. Open Keycloak Admin Console in your browser. http://localhost:8080/auth/
Sign in and click on Clients in the menu on the left hand side. Once that's open, click on Create on top of the table and you will navigated to a page as shown below.











Click on Save and you will redirected to a page as shown in below screenshot.  Change the access type to public.
Access Type : Public
Public access is for client side clients that need to perform a browser login.





















Now click on the installation tab on top of the form. Select Keycloak OIDC JSON. This should provide a json object as below with required configuration for the keycloak client adapter.


{
  "realm": "JavaSecurity",
  "realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Oj1K8EHWyAkaggvteHnETu3acwn5U8PaU0BixBM1nke5cgNXWE3I5M4zDfmNG2Egub2jmMHyRbF9U10ovQzhE1HhDFN+3ob/+Wz8Dy0ZF6GXAzyWYe64vx1MiDq5764HNQGAv+zSlUD0S7TIbCrw3h9XBJQNqeJsOurpIVu2+M0wxXXOylmIaCVFqf509rMPcdmS+UocZ7+2mAC7eJ5z4u6j5rsBvSK74tKQb7tcJHXcY3TO6owKayUVrdDHnruYhQxQu1x0FeemDrtvv6D7O2FhhOWIBJcvgLyuuyFYH687o/xxaD9Ye+P9SP1NYCbRmlkVQJ3DPs6ibLQkaDr4QIDAQAB",
  "auth-server-url": "http://localhost:8080/auth",
  "ssl-required": "external",
  "resource": "keycloak-demo",
  "public-client": true,
  "use-resource-role-mappings": true
}

This json object is copied to the keycloak.json file inside resources folder. The linking between the keycloak adapter and the keycloak.json happens from KeycloakAuthResolver class. This class overrides the Keycloak config resolver which enables to reside the keycloak.json file externally and provide the path in the standalone.xml file. If the external keycloak.json file is not available, by default it will look into the keycloak,json file inside resources folder.

Creating User and Roles


Create a role called "PersonAPI" under keycloak-demo client as shown below. 








Create a user as shown below.




















Assign the "PersonAPI" role to the created user.















Get the access token




Paste the access token to the Authorization header of the Person request as shown below. If the access token is invalid, unauthorized error message will be received.



if valid, below response will be received.







!...Happy Coding...!

Ashen Jayasinghe
DMS
Full Stack Developer

No comments:

Post a Comment