Softwareentwickler / Software Developer
User Login Screen

Java EE & Keycloak | Secure your application with Keycloak

When developing a new application, user management is usually required as well. To relieve the developers of this task in Java EE application, Keycloak is offered as an identity and access management solution. Assuming that the Keycloak server is already prepared accordingly, I will show below how the interaction with an application server can succeed.

Install Keycloak Adapters into JBoss/WildFly server

In order to use a Keycloak binding in the application server, the first step is to install an adapter in JBoss or WildFly. This is relatively easy to do and you just download the adapter in the current version, unpack it and then use the JBoss CLI to execute the adapter-elytron-install-offline.cli script that performs the installation. For example, a Dockerfile could look like this:

FROM jboss/wildfly

ADD --chown=jboss:jboss "https://github.com/keycloak/keycloak/releases/download/15.0.1/keycloak-oidc-wildfly-adapter-15.0.1.tar.gz" ${JBOSS_HOME}

RUN tar xvf ${JBOSS_HOME}/keycloak-wildfly-adapter-dist-11.0.3.tar.gz -C ${JBOSS_HOME}
RUN rm -rf ${JBOSS_HOME}/keycloak-wildfly-adapter-dist-11.0.3.tar.gz
RUN chown jboss ${JBOSS_HOME} -R
RUN ${JBOSS_HOME}/bin/jboss-cli.sh --file=${JBOSS_HOME}/bin/adapter-elytron-install-offline.cli

RUN rm -rf ${JBOSS_HOME}/standalone/configuration/standalone_xml_history

Connect application to Keycloak instance

Once a connection of a Keycloak server has been enabled in principle, the exact server with which a connection is to be established must be specified. Two options are provided for this purpose. First, for a server-wide configuration, the standalone.xml file can be manipulated manually or using the JBoss CLI and the Keycloak connection can be added as a subsystem. For example, in the JBoss CLI the following command can be used, where the name of the secure-deployment needs to be equal to the value for the resource parameter and which can include further parameters:

/subsystem=keycloak/secure-deployment=<CLIENT_NAME>.war:add(realm=<REALM_NAME>, resource=<CLIENT_NAME>, public-client=true, auth-server-url=<KEYCLOAK_URL>, ssl-required=EXTERNAL)

However, since the JBoss CLI does not support all parameters that the Keycloak instance may request, the second option is recommended, where configuration is done only application-wide. For this purpose, the configuration can be downloaded in JSON format from the Keycloak server for the realm created there and the corresponding client. To do this, open the client configuration in the Keycloak interface, navigate to the installation tab and select Keycloak OICD JSON as the format option. The file that can be downloaded afterwards may look something like the one shown below.

{
  "realm": "<REALM_NAME>",
  "auth-server-url": "<KEYCLOAK_URL>",
  "ssl-required": "external",
  "resource": "<CLIENT_NAME>",
  "public-client": true,
  "confidential-port": 0
}

Finally, it is enough to place this file under the name keycloak.json in the src/main/webapp/WEB-INF/ directory. By following this convention, the file will be automatically respected and read by the application server. In any case, a correct configuration of the client in the Keycloak server must also be checked and, among other things, the URL of the JBoss must be set as a valid redirect URI.

How to handle multi-tenancy

If this convention cannot be followed or different Keycloak instances are to be addressed depending on the environment (development or production), a selection of the Keycloak configuration can also be made programmatically. For the corresponding dependencies, the following libraries must be included, for which it is shown here with Gradle.

compileOnly "org.keycloak:keycloak-adapter-spi:15.0.1", "org.keycloak:keycloak-adapter-core:15.0.1"

The packages should only be included when compiling, but not when building the application, because they are already present in the classpath of the application server by installing the keycloak adapter and conflicts may arise. To access these libraries at runtime, a file called jboss-deployment-structure.xml must be placed in the src/main/webapp/WEB-INF/ directory.

<jboss-deployment-structure>
    <deployment>
        <dependencies>
            <module name="org.keycloak.keycloak-adapter-spi"/>
            <module name="org.keycloak.keycloak-adapter-core"/>
            <module name="org.keycloak.keycloak-common"/>
            <module name="org.keycloak.keycloak-core"/>
        </dependencies>
    </deployment>
</jboss-deployment-structure>

Then, a class can be created that inherits from org.keycloak.adapters.KeycloakConfigResolver and implements the KeycloakDeployment resolve(OIDCHttpFacade.Request request) method. For example, the following code can be used when a decision is to be made based on a string in the URL. In this case, the configurations were stored under src/main/resources/keycloak.

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.OIDCHttpFacade;

public class KeycloakKonfigurationAufloeser implements KeycloakConfigResolver
{

	@Override
	public KeycloakDeployment resolve(OIDCHttpFacade.Request request)
	{
		var url = request.getRelativePath();
		if (url.contains("development"))
		{
			return KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak/development.json"));
		}
		return KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak/production.json"));
	}
}

The use of this resolver now only needs to be configured in the web.xml located in the src/main/webapp/WEB-INF/ directory. The following entry is added for this purpose:

<context-param>
    <param-name>keycloak.config.resolver</param-name>
    <param-value>net.aokv.smartinfo.application.KeycloakKonfigurationAufloeser</param-value>
</context-param>

Secure the application

Now it must also be defined in the web.xml for which paths in the application authentication is required and which user groups are authorized for this access. For this purpose, the following entries can be added, for example:

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

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Registrierung</web-resource-name>
        <url-pattern>/test1/*</url-pattern>
        <url-pattern>/test2.xhtml</url-pattern>
    </web-resource-collection>
    <auth-constraint>
        <role-name>*</role-name>
    </auth-constraint>
    <user-data-constraint>
        <transport-guarantee>NONE</transport-guarantee>
    </user-data-constraint>
</security-constraint>

<security-role>
    <role-name>*</role-name>
</security-role>

Read username after Keycloak login

If a JAX-RS resource is protected with keycloak authentication and, for example, a GET call to a URL redirects to the login interface, it is possible to read the username when returning to your own application after a successful login. This is possible with the following code, which simply prints the username to the console:

import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import java.io.Serializable;

@Path("test-resource")
public class FallunterscheidungRessource implements Serializable
{
	private static final long serialVersionUID = -6738627159032434715L;

	@GET
	public Response entscheideUseCaseAnhandDesBenutzernamens(
		@Context SecurityContext securityContext,
		@Context UriInfo uri)
	{
		var userPricipal = securityContext.getUserPrincipal();
		if (!(userPricipal instanceof KeycloakPrincipal))
		{
			return Response.status(Response.Status.UNAUTHORIZED).build();
		}
		KeycloakPrincipal<KeycloakSecurityContext> kp = (KeycloakPrincipal<KeycloakSecurityContext>) userPricipal;
		var username = kp.getKeycloakSecurityContext().getIdToken().getPreferredUsername();
		System.out.println(username);
		return Response.temporaryRedirect(uri.getBaseUri()).build();
	}
}

How to combine Keycloak and Basic Authentication in one application

Only one login-config can be defined in the web.xml. So if – as was the case with me – authentication with Basic Authentication is to be used for all REST resources developed with JAX-RS, this is not easily possible. However, JAX-RS offers to implement a ContainerRequestFilter, with which such a functionality can be simulated as follows:

import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.Provider;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Provider
public class BasicAuthSecurityFilter implements ContainerRequestFilter
{
	@Override
	public void filter(ContainerRequestContext requestContext) throws IOException
	{
		var authHeader = requestContext.getHeaderString("Authorization");
		if (authHeader == null || !authHeader.startsWith("Basic"))
		{
			requestContext.abortWith(Response.status(401).header("WWW-Authenticate", "Basic").build());
			return;
		}
		var tokens =
			(new String(Base64.getDecoder().decode(authHeader.split(" ")[1]), StandardCharsets.UTF_8)).split(":");
		if (!tokens[0].equals("user") || !tokens[1].equals("password"))
		{
			requestContext.abortWith(Response.status(401).build());
		}
	}
}

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert