4

I have a JAX-RS application deployed in WildFly. The application's endpoints shall be protected by Keycloak with Access Type: bearer-only. This works perfectly fine for WildFly versions up to 24.

Starting from WildFly 25 the Keycloak adapter is deprecated and one should migrate to the new Elytron subsystem. According to this WildFly issue https://issues.redhat.com/browse/WFLY-15485 however the OIDC adapter is not ready yet to work with bearer-only. But it is mentioned that it should still be possible using the Keycloak Wildfly adapter.

Also the latest Keycloak documentation and this thread in Google Groups states this.

So I installed the adapter from this location and ran the installation script:

https://github.com/keycloak/keycloak/releases/download/16.1.1/keycloak-oidc-wildfly-adapter-16.1.1.zip

./bin/jboss-cli.sh --file=bin/adapter-elytron-install-offline.cli -Dserver.config=standalone-full.xml

When deploying the application I get thte following error message:

java.lang.IllegalStateException: The required mechanism 'KEYCLOAK' is not available in mechanisms [BASIC, CLIENT_CERT, DIGEST, FORM] from the HttpAuthenticationFactory

Setup

  • WildFly 26 (Jakarta EE 8)
  • Keycloak 16.1.1

web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <!-- Security configuration -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>admin-api</web-resource-name>
            <url-pattern>/administration/*</url-pattern>
            <url-pattern>/operations/*</url-pattern>
            <url-pattern>/applications/*</url-pattern>
            <url-pattern>/entities/*</url-pattern>
        </web-resource-collection>
    </security-constraint>

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

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

</web-app>
Robert Strauch
  • 12,055
  • 24
  • 120
  • 192

2 Answers2

2

I finally got it working without the Keycloak adapter, i.e. using the new built-in Elytron subsystem.

oidc.json (located in the WEB-INF directory)

{
  "realm": "myrealm",
  "client-id": "my-client-app",
  "auth-server-url": "${keycloak.url}/auth",
  "provider-url": "${keycloak.url}/auth/realms/myrealm",
  "bearer-only": true,
  "enable-cors": true,
  "ssl-required": "none"
}

web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <!-- other configuration -->

    <login-config>
        <auth-method>OIDC</auth-method>
    </login-config>

</web-app>
Robert Strauch
  • 12,055
  • 24
  • 120
  • 192
  • 1
    Hi Robert! I tried this solution, but it didn't work for me. The response the client of the web service gets is HTML for the Keycloak login page. Did something like that happened to you? If it did, how did you solve it? – Jorge Campins Mar 17 '22 at 23:12
1

I managed to implement Bearer Token Authorization with Keycloak and Elytron in WildFly 26 in order to control access to RESTful Web services in a Web Module (.war) of an Enterprise Application (.ear), but the solution is not without problems. This is what I did:

Define an elytron token realm

/subsystem=elytron/token-realm=xyz2ap112-token-realm/:add(\
    principal-claim=preferred_username,\
    oauth2-introspection={\
        client-id=xyz2ap112-web-api,\
        client-secret=${env.keycloak_client_secret},\
        introspection-url=${env.keycloak_introspection_url}\
    }\
)

Define an elytron role decoder

/subsystem=elytron/simple-role-decoder=xyz2ap112-realm-access-roles/:add(\
    attribute=realm_access_roles\
)

Warning: the default "Token Claim Name" for Keycloak realms is "realm_access.roles". For this role decoder to work, I had to change it to "realm_access_roles" (no dot). I'll mention this again when I talk about the problems with this solution.

Define an elytron security domain

/subsystem=elytron/security-domain=xyz2ap112-token-security-domain/:add(\
    realms=[{realm="xyz2ap112-token-realm",role-decoder="xyz2ap112-realm-access-roles"}],\
    default-realm=xyz2ap112-token-realm,\
    permission-mapper=default-permission-mapper\
)

Define an elytron HTTP authentication factory

/subsystem=elytron/http-authentication-factory=xyz2ap112-web-api-authentication-factory/:add(\
    security-domain=xyz2ap112-token-security-domain,\
    mechanism-configurations=[{\
        mechanism-name=BEARER_TOKEN,\
        mechanism-realm-configurations=[realm-name=xyz2ap112-token-realm]\
    }],\
    http-server-mechanism-factory=global\
)

Define two application security domains

ejb3 subsystem

/subsystem=ejb3/application-security-domain=xyz2ap112-web-api-security-domain/:add(\
    security-domain=xyz2ap112-token-security-domain\
)

Warning: the war that contains the web services does not contain the EJBs it needs; those are in a separate EJB Module (.jar). I guess that is why I had to define this application security domain in the ejb3 subsystem.

undertow subsystem

/subsystem=undertow/application-security-domain=xyz2ap112-web-api-security-domain/:add(\
    http-authentication-factory=xyz2ap112-web-api-authentication-factory,\
    override-deployment-config=true\
)

Configure application’s jboss-web.xml and web.xml

<jboss-web>
    <context-root>/xyz2ap112-web-api</context-root>
    <resource-ref>
        <res-ref-name>jdbc/xyz2ap112</res-ref-name> <!-- Logical name only. -->
        <jndi-name>java:/jdbc/xyz2ap112</jndi-name> <!-- Real JNDI name. -->
    </resource-ref>
    <security-domain>xyz2ap112-web-api-security-domain</security-domain>
</jboss-web>

The security-domain is the application security domain defined in the undertow subsystem.

<login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>xyz2ap112-web-api-security-domain</realm-name>
</login-config>

The real-name in login-config is the application security domain defined in the undertow subsystem.

PROBLEMS

As I said before, this solution is not without problems. Given that my Enterprise Application (.ear) also has another Web Module (.war), which contains the GUI components of the application and no Web Services, this solution works as long as the auth-method of that second Web Module is FORM or BASIC. And, as you might have already guessed, I want to use OIDC.

Using OIDC to control access to the application is very straightforward, as properly explained by Farah Juma in her article Securing WildFly Apps with OpenID Connect. But that works as long as the "Token Claim Name" of the Keycloak realm is "realm_access.roles" (its default value). With that name the simple-role-decoder doesn’t work. So, I guess a custom role-decoder is required. Given that my application is able to define and administer roles and role assignments on its own, instead of writing a custom role-decoder, I used a constant-role-mapper to get a single role that allows the web services to execute and check permissions using the roles defined within the application. Once again, that works as long as the auth-method of that second Web Module is FORM or BASIC; with OIDC, the web services are not executed; client gets an HTTP 500 (see below). There is no additional information in the logs of any of the running WildFly (Keycloak and application).

This is the oidc.json file of the GUI Web Module:

{
    "client-id": "xyz2ap112-web",
    "confidential-port": 8543,
    "principal-attribute": "preferred_username",
    "provider-url": "http://localhost:8180/auth/realms/jrcam",
    "public-client": true,
    "ssl-required": "external"
}

And this is the client exception:

Exception in thread "main" javax.ws.rs.InternalServerErrorException: HTTP 500 Internal Server Error
    at org.glassfish.jersey.client.JerseyInvocation.convertToException(JerseyInvocation.java:1098)
    at org.glassfish.jersey.client.JerseyInvocation.translate(JerseyInvocation.java:883)
    at org.glassfish.jersey.client.JerseyInvocation.lambda$invoke$1(JerseyInvocation.java:767)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:316)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:298)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:229)
    at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:414)
    at org.glassfish.jersey.client.JerseyInvocation.invoke(JerseyInvocation.java:765)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.method(JerseyInvocation.java:428)
    at org.glassfish.jersey.client.JerseyInvocation$Builder.get(JerseyInvocation.java:324)
    at org.xyz.jax.rs.client.base.AbstractFacadeServiceClient.find(AbstractFacadeServiceClient.java:28)
    at xyz2.BarrioFacadeClient.find(BarrioFacadeClient.java:40)
    at xyz2.BarrioFacadeClient.main(BarrioFacadeClient.java:24)

If the auth-method of the Web Services Web module is OIDC, the response the client gets is html corresponding to the Keycloak login page.

<html xmlns="http://www.w3.org/1999/xhtml" class="login-pf">
    ...
                    <h1 id="kc-page-title">
                        Sign in to your account
                    </h1>
    ...
</html>

This is the oidc.json file of the Web Services Web Module:

{
    "client-id": "xyz2ap112-web-api",
    "confidential-port": 8543,
    "principal-attribute": "preferred_username",
    "provider-url": "http://localhost:8180/auth/realms/jrcam",
    "ssl-required": "external",
    "bearer-only": true,
    "verify-token-audience": true,
    "realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk3PD30r3SQBqnO15g/Jc5z3NFnt9HLA6QlQt2QLtxvGhLcerTD2rVWCst/4NSQev9dBscFnwxXyAoZAqTm7w0oPzlhw1Xbqt1dpKdNjMtbJxmpqzCRLTjmNatPmoAGx+9TWOPKw1qfEwZOy9xOqnCbBeT5eGCAXci+wvt8mpNX9lpAguFxgpFtyVc0at35Lw3BdZ13+6Ljxu6Z+mam1tQ9mwey0ubfhV3NK0eN8jruKWrCyGw6DRbmvKFTwQa5akDbMWt3H/HaSLMXBOrBKq9He6azVL3dkbdd40drgHtI8G+ANC1NhOPzjPtuifo9U2wHD6o8S03o35mm4xjJNcqQIDAQAB",
    "credentials": {
        "secret": "8c98045a-4640-46e7-9f68-74a289e43b7e"
    }
}

I hope this partial solution will help someone and also that someone can tell me how to implement a complete solution.

Jorge Campins
  • 413
  • 1
  • 4
  • 11