1 Reply Latest reply on May 10, 2019 1:22 PM by Stephan Schu

    How to use keycloak authentication for ejb remote client with wildfly elytron security?

    Marco Benuzzi Newbie

      I've some web application which authenticates on Keycloak and call EJBs. The security in Wildfly is configured with Elytron and the security context is propagated correctly between web and EJB.

       

      Now I've to write a java client (desktop application) which have to call the same EJBs on the server using Wildfly http+remoting. I suppose I need to configure SASL authentication against Keycloak on Wildfly, probably using oauth bearer token obtained by java client logging in with Keycloak.

       

      I can't find any information about how to configure SASL with Keycloak on Wildfly. Does someone have some hit about that?

        • 1. Re: How to use keycloak authentication for ejb remote client with wildfly elytron security?
          Stephan Schu Newbie

          I was researching a similar use case today. The information available online about this is pretty sparse but perhaps the following will be helpful:

           

          As you already mentioned, the remote EJB calls use the remoting subsystem. The remoting connectors use /server/management/security-realms for authentication and authorization instead of the security-realms in the Elytron subsystem. The authenticators included in the Keycloak adapters, however, seem to be geared towards the Elytron security system, so no configuration I tried worked out of the box.

           

          Fortunately you can just write a small custom authenticator that delegates everything to the existing Keycloak adapters. You can find a pretty comprehensive guide on writing an authentication/ authorization plugin here: https://docs.wildfly.org/14/Admin_Guide.html#Security_Realms_Plugins  and some documentation on Keycloak Login Modules this works with here: https://www.keycloak.org/docs/3.2/securing_apps/topics/oidc/java/jaas.html

           

          Here is a small spaghetti-coded proof of concept. You will have to install the two below classes as a module on Wildfly. (as described in the first link)

          The keycloak.json file referenced in the standalone.xml is generated by Keycloak (Client > Installation in the web interface)

          import java.io.IOException;
          import java.security.Principal;
          import java.util.Map;
          import javax.security.auth.Subject;
          import javax.security.auth.callback.Callback;
          import javax.security.auth.callback.CallbackHandler;
          import javax.security.auth.callback.NameCallback;
          import javax.security.auth.callback.PasswordCallback;
          import javax.security.auth.callback.TextOutputCallback;
          import javax.security.auth.callback.UnsupportedCallbackException;
          import javax.security.auth.login.LoginException;
          import org.jboss.as.domain.management.plugin.AbstractPlugIn;
          import org.jboss.as.domain.management.plugin.AuthorizationPlugIn;
          import org.jboss.as.domain.management.plugin.Credential;
          import org.jboss.as.domain.management.plugin.Identity;
          import org.jboss.as.domain.management.plugin.ValidatePasswordCredential;
          import org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule;
          
          public class KeycloakManagementAuthenticator extends AbstractPlugIn {
          
              private DirectAccessGrantsLoginModule keycloakDelegate = new DirectAccessGrantsLoginModule();
              private Map<String, String> configuration;
          
              @Override
              public void init(Map<String, String> configuration, Map<String, Object> sharedState) throws IOException {
                  this.configuration = configuration;
                  // This will allow an AuthorizationPlugIn to delegate back to this instance.
                  sharedState.put(AuthorizationPlugIn.class.getName(), this);
              }
          
              @Override
              public Identity loadIdentity(final String userName, String realm) throws IOException {
                  System.out.println("loadIdentity for " + userName + "@" + realm);
                  return new SampleIdentity(userName, realm);
              }
          
              @Override
              public String[] loadRoles(String userName, String realm) throws IOException {
                  //TODO
                  return new String[0];
              }
          
              private class SampleIdentity implements Identity {
          
                  private final String userName;
                  private final Credential credential;
          
                  private SampleIdentity(final String userName, final String password) {
                      System.out.println("creating new SampleIdentity");
                      this.userName = userName;
                      this.credential = new ValidatePasswordCredential() {
                          @Override
                          public boolean validatePassword(final char[] password) {
                              System.out.println("SampleIdentity: validating password: " + String.valueOf(password));
                              try {
                                  Subject subject = new Subject();
                                  subject.getPrincipals().add(new Principal() {
                                      private final String name = userName;
          
                                      @Override
                                      public String getName() {
                                          return name;
                                      }
                                  });
                                  subject.getPublicCredentials().add(password);
                                  subject.setReadOnly();
                                  System.out.println("assembeled subject");
                                  CallbackHandler callbackHandler = new CallbackHandler() {
                                      @Override
                                      public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
                                          for (int i = 0; i < callbacks.length; i++) {
                                              if (callbacks[i] instanceof TextOutputCallback) {
          
                                                  // display the message according to the specified type
                                                  TextOutputCallback toc = (TextOutputCallback) callbacks[i];
                                                  switch (toc.getMessageType()) {
                                                      case TextOutputCallback.INFORMATION:
                                                          System.out.println(toc.getMessage());
                                                          break;
                                                      case TextOutputCallback.ERROR:
                                                          System.out.println("ERROR: " + toc.getMessage());
                                                          break;
                                                      case TextOutputCallback.WARNING:
                                                          System.out.println("WARNING: " + toc.getMessage());
                                                          break;
                                                      default:
                                                          throw new IOException("Unsupported message type: "
                                                                  + toc.getMessageType());
                                                  }
          
                                              } else if (callbacks[i] instanceof NameCallback) {
          
                                                  // prompt the user for a username
                                                  NameCallback nc = (NameCallback) callbacks[i];
          
                                                  // ignore the provided defaultName
                                                  nc.setName(userName);
          
                                              } else if (callbacks[i] instanceof PasswordCallback) {
          
                                                  // prompt the user for sensitive information
                                                  PasswordCallback pc = (PasswordCallback) callbacks[i];
                                                  pc.setPassword(password);
          
                                              } else {
                                                  throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
                                              }
                                          }
                                      }
                                  };
                                  System.out.println("before init keycloak delegate");
                                  keycloakDelegate.initialize(subject, callbackHandler, sharedState, configuration);
                                  System.out.println("before login");
                                  boolean result = keycloakDelegate.login();
                                  System.out.println("after login: " + result);
                                  return result;
                              } catch (LoginException ex) {
                                  System.out.println("Login Exception");
                                  ex.printStackTrace();
                                  return false;
                              }
                          }
                      };
                  }
          
                  @Override
                  public String getUserName() {
                      return userName;
                  }
          
                  @Override
                  public Credential getCredential() {
                      return credential;
                  }
              }
          }
          import org.jboss.as.domain.management.plugin.AuthenticationPlugIn;
          import org.jboss.as.domain.management.plugin.AuthorizationPlugIn;
          import org.jboss.as.domain.management.plugin.Credential;
          import org.jboss.as.domain.management.plugin.PlugInProvider;
          
          public class KeycloakManagementPluginProvider implements PlugInProvider{
             
              @Override
              public AuthenticationPlugIn<Credential> loadAuthenticationPlugIn(String name) {
                  if ("Keycloak".equals(name)) {
                      return new KeycloakManagementAuthenticator();
                  }
                  return null;
              }
          
              @Override
              public AuthorizationPlugIn loadAuthorizationPlugIn(String name) {
                  if ("Keycloak".equals(name)) {
                      return new KeycloakManagementAuthenticator();
                  }
                  return null;
              }
          }
          
          
          

           

          module.xml

          <?xml version="1.0" encoding="UTF-8"?>
          
          <module xmlns="urn:jboss:module:1.1" name="my.keycloak.auth.module">
              <properties>
              </properties>
          
              <resources>
                  <resource-root path="KeycloakManagementAuthenticator-1.0.0.jar"/>
              </resources>
          
              <dependencies>
                  <module name="org.jboss.as.domain-management" />
                  <module name="org.keycloak.keycloak-adapter-core" />
              </dependencies>
          </module>
          

           

          standalone.xml

               <management>
                  <security-realms>
                    ...
                    <security-realm name="KeycloakRealm">
                      <plug-ins>
                          <plug-in module="my.keycloak.auth.module">          
                          </plug-in>
                          </plug-ins>
                          <authentication>
                              <plug-in name="Keycloak" mechanism="PLAIN">
                              <properties>
                              <property name="keycloak-config-file" value="/path/to/keycloak.json"></property>
                              </properties>
                              </plug-in>
                          </authentication>
                      </security-realm>
                  </security-realms>
          ...
                  <subsystem xmlns="urn:jboss:domain:remoting:4.0">
                      <http-connector name="http-remoting-connector" connector-ref="default" security-realm="KeycloakRealm"/>
                  </subsystem>
          

           

           

          This approach certainly needs some adjustments since, among other atrocities, it currently sends the plain credentials via http but it was the first proof of concept that worked for me.

           

          Currently this works with user/pass but you should be able to switch to bearer tokens by simply delegating to org.keycloak.adapters.jaas.BearerTokenLoginModule instead of org.keycloak.adapters.jaas.DirectAccessGrantsLoginModule.

           

          Would appreciate any hints if you come up with a better solution.