Secure your Java Servlet Application with Keycloak

m4nu56

Emmanuel Balpe

Posted on May 27, 2020

Secure your Java Servlet Application with Keycloak

We'll see how to configure a Java Servlet based application so it can be secure with Keycloak.

Keycloak is an Open Source Identity and Access Management that can be used to delegate entirely the security of an application.

1. Keycloak configuration

The Keycloak documentation is really easy to follow. You can see for yourself here the section about the configuration of your Keycloak instance: https://www.keycloak.org/docs/latest/authorization_services/#_getting_started_hello_world_create_realm

You need to configure:

  • A realm
  • A user with role user, we'll see later how it's used
  • A Client. It's a representation of your Java application
    • Client protocol: openid-connect
    • Access Type: public
    • Valid Redirect URIs: the url of your development environment or * for the time being

2. Tomcat security-constraint

We're using the Tomcat security-constraint that enable a security verification at the application level on Tomcat.
The Keycloak team developed a convenient Valve for the Tomcat Security system that handle the redirect to and from the Keycloak login page.

2.1. You need to add the following to the context.xml of your application:



<Context>
    <Valve className="org.keycloak.adapters.tomcat.KeycloakAuthenticatorValve"/>
</Context>


Enter fullscreen mode Exit fullscreen mode

2.2. Install the Keycloak Valve libraries into the ${tomcat}/lib directory on your Tomcat server

2.3. You need to copy the keycloak.json config file into /WEB-INF/keycloak.json

You can download the file in your Client installation tab:

adapter-config

2.4. Add security-constraint in your web.xml



<security-constraint>
    <web-resource-collection>
        <web-resource-name>Private area</web-resource-name>
        <url-pattern>/esp_privat/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>user</role-name>
    </auth-constraint>
</security-constraint>

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Public area</web-resource-name>
        <url-pattern>/api/*</url-pattern>
    </web-resource-collection>
</security-constraint>

<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>this is ignored currently</realm-name>
</login-config>

<security-role>
    <role-name>user</role-name>
</security-role>


Enter fullscreen mode Exit fullscreen mode

Here we defined 2 URL patterns:

  • /esp_privat/* that require a user to be connected with a role user
  • /api/* that require no authentification

2.5. Results

So when you try accessing any route under /esp_privat/ in your application Keycloak valve now automatically redirect you to the login page in your Keycloak instance.
When successfuly logged in Keycloak redirects you to the asked page.

What we need to do now is to identify the user logged in thank's to the token Keycloak is adding to the cookies of the web navigator.

3. Intercept Keycloak access token to log the user into your app

3.1. Keycloak dependencies

Add the following to the pom.xml of your webapp application:



<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-core</artifactId>
    <version>9.0.2</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-adapter-core</artifactId>
    <version>9.0.2</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.keycloak</groupId>
    <artifactId>keycloak-adapter-spi</artifactId>
    <version>9.0.2</version>
    <scope>provided</scope>
</dependency>


Enter fullscreen mode Exit fullscreen mode

Notice the scope = provided since we will be using the libraries added previously into the tomcat library folder. We don't want to override it with another version of the libraries.

3.2. Read the token

The following snippet will extract the token from the request and verify if it's lifetime is expired. It returns true in case the token is valid.




import org.keycloak.KeycloakSecurityContext;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.AccessToken;

... 

/**
* Verify if user is logged in keycloak by validating token in request
*/
public boolean isLoggedInKeycloak(HttpServletRequest request) throws VerificationException {
    KeycloakSecurityContext keycloakSecurityContextToken = getKeycloakSecurityContextToken(request);
    if (keycloakSecurityContextToken == null) {
        return false;
    }
    return !isTokenExpired(keycloakSecurityContextToken);
}

private boolean isTokenExpired(KeycloakSecurityContext keycloakSecurityContextToken) throws VerificationException {
    AccessToken token = TokenVerifier.create(keycloakSecurityContextToken.getTokenString(), AccessToken.class).getToken();
    if (token.isExpired()) {
        logger.warn("User token is expired..." + token);
        return true;
    }
    return false;
}


Enter fullscreen mode Exit fullscreen mode

In our case we also needed to verify if the user is a member of the correct group so we added the following method check:



private void handleGroupMembership(@Nonnull KeycloakSecurityContext keycloakSecurityContext, String keycloakPreferredUsername) {
    Object groups = keycloakSecurityContext.getToken().getOtherClaims().getOrDefault("groups", new ArrayList<>());
    if (groups == null) {
        throw new GenericRuntimeException("Fail to read groups from the token of the user " + keycloakPreferredUsername);
    }
    ((List<String>) groups)
        .stream()
        .filter(s -> s.equalsIgnoreCase("/my-group"))
        .findFirst()
        .orElseThrow(() -> new GenericRuntimeException("User \"" + keycloakPreferredUsername + "\" is not a member of /my-group"));
}


Enter fullscreen mode Exit fullscreen mode

We then called the previous method in a pre-action hook into all the call received by our servlets so that it can be catched by any servlet like so:



boolean isUserLoggedIn = request.getSession().getAttribute(USER_SESSION) != null;
if (isLoggedInKeycloak(request) && !isUserLoggedIn) {
    logger.info("User logged in Keycloak but not logged in the app. Logging in the user...");
    new KeycloakLoginService().login(request, getKeycloakSecurityContextToken(request));
}
else if (!isLoggedInKeycloak(request) && isUserLoggedIn) {
    logger.info("User not logged in Keycloak but logged in the app. Logging out the user...");
    sessionLogout.logout(request, response);
    return;
}


Enter fullscreen mode Exit fullscreen mode

3.3. Logout

To logout a user from Keycloak you can use the request.logout() method. We use the following method:



public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    HttpSession session = request.getSession(false);
    if (session != null) {
        session.invalidate();
    }
    request.logout();
    request.getSession(true); // create a new session
    response.sendRedirect(request.getContextPath());
}


Enter fullscreen mode Exit fullscreen mode

4. Maven profiles to compile versions with and without keycloak login

In one of our project we needed to be able to deploy a version of the app that doesn't use the Keycloak login feature but our previous login mechanism.
Of course we wanted to keep a unique codebase with the less difference as possible.
We identify that the only thing preventing us from working as before was the security-constraint section in the web.xml config file.

We will be using the Maven filtering solution with a little hack we found on SO: https://stackoverflow.com/questions/3298763/maven-customize-web-xml-of-web-app-project/8593041#8593041
It consists in adding 2 variables in your web.xml like so:



${enable.security.start}
<security-constraint>
  ...
  // all of the XML that you need, in a completely readable format
  ...
</login-config>  
${enable.security.end}


Enter fullscreen mode Exit fullscreen mode

And have it replaced by comment block start &lt;!-- and end -&gt; in the profile where you don't want to use Keycloak.

So in our default ci profile we defined the following properties:



<enable.security.start></enable.security.start>
<enable.security.end></enable.security.end>


Enter fullscreen mode Exit fullscreen mode

and in the without-keycloak profile:



<enable.security.start>&lt;!--</enable.security.start>
<enable.security.end>--&gt;</enable.security.end>


Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
m4nu56
Emmanuel Balpe

Posted on May 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related