Weblogic Standard SSO solution

Weblogic provides the possibility to configure an SSO infrastructure through the means of an SAML Identity Asserter. You can have a look at an example of how to achieve this in the following blog article:

https://blogs.oracle.com/blogbypuneeth/steps-to-configure-saml-20-with-weblogic-server-using-embedded-ldap-as-a-security-store-only-for-dev-environment

However, as you can see, it does take a lot of configuration steps to achieve and in the end it restricts you in certain ways. The most obvious which we encountered while working with it is the inability to provide another path for the SAML servlet, which is hardcoded to “/”. This means that you always need to set your endpoint for Service Providers to <domain>/saml2, this in turn means that it is very difficult to have multiple applications using the same domain and grouped with URL paths. For example, you will have issues if you have applications <domain>/application1 and <domain>/application2. The workaround for this would be to have the applications under subdomains, like: http://application1.domain.localhttp://application2.domain.local.

Custom SSO on Weblogic

We were recently asked by a customer to come up with a SSO solution which requires slightly more flexibility than that of the standard way. Plus, we wanted to have more control on the entire process, on the encryption algorithms and so on.

So we ended up building our own Identity Provider. That is, the part of the SSO flow which will authenticate users and respond to requests of weather the user is authenticated or not. The Identity Provider is implemented as a SOAP webservice. This means that every application which needs to be authenticated, will make a request to the Identity Provider with the username, password of the user and in turn it will get a token back which it will set in a cookie. Then, on the application’s side, we have an Weblogic IdentityAsserter which will intercept unauthorized requests and ask the remote WebService if the user is authenticated or not.

We will delve into details in what follows. First, let’s look at the high-level architecture and then look down piece by piece.

[Click on the image to enlarge]

Let’s summarize the flow in a few sentences: When a user first visits one of the applications, he will be prompted to login. He enters the credentials. Then, the application will make a request to the Identity Provider, with this username and password, the Identity Provider will authenticate the user on its side and it will send back a token which is associated with this user. Then, the application will set this token in a cookie.

Now, consider the case that the user accesses a protected resource on a second application. This time, because we have the token in the cookie, the Identity Asserter will intercept the request, and will not redirect to login page, instead, it will send a request to the Identity Provider with this token. The Identity Provider will respond with the name of the user which is associated with the token. Then, the Identity Asserter will authenticate the user also locally and redirect the user to the originally accessed protected resource.

A few notes worth mentioning:

  • All the applications which are under SSO are ADF applications; thus, the architecture had allowed us to place all the login related code into one place, a Login Taskflow which is used by all applications. Consequently, code changes required to request the Identity Provider were all done in a single place.
  • Although we could have easily done so, we chose to not use a central Identity Provider login page. This would mean, that the user’s browser would be redirected to the login page (which could potentially be on another domain) and then come back to the initial application. Instead, we rely that all applications will be on the same domain, thus the cookie set by one application will be sent by the browser also when requesting the second application.

Now, let’s break it down piece by piece.

Identity Provider WebService

For this part, we need two elements: The WebService itself and a Database connection where we will store the associations between users and their respective tokens.

For the WebService, we generated a SOAP one using the JDeveloper wizard and implemented 4 endpoints:

@WebService
public class SsoSession {
    private Context ctx = Context.getInstance();

    public SsoSession() {
    }

    @WebMethod
    public String getToken(String username, String password) {
        //Check if the user/password are correct
        boolean valid = false;
        CallbackHandler handler = new SimpleCallbackHandler(username, password == null ? null : password.getBytes(), "UTF-8");
        try {
            Subject mySubject = Authentication.login(handler);
            valid = true;
        } catch (LoginException le) {
            valid = false;
        }

        //If user/pass are valid, encode the token and send it to the user
        //I will not show the algorithm to use for encoding, you can go wild with the solution here
        //although I would advice to use SHA-256 for hashing
        if (valid) {
            String token = ctx.encodeSSOToken(username, password);
            return token;
         }
      return null;
    }

    @WebMethod
    public String getUsername(String token){
      //Retrieve username from database, corresponding to the token
      boolean valid = checkifTokenisExpired( token );
      if( valid ){
       return reitrveUsernameFromDB(token);
      }
    }
    @WebMethod
    public String ping() {
      //String to indicate that WebService is online
    }
}

I hope the code and comments are self-explanatory. It is beyond the scope of the article to provide full code for the implementation (that would immediately bloat the article), but instead it is to give you an idea of the approach we took.

For the database side, we need a structure like the following:

 

Application Login 

In the Login flow of our application, we need a slight modification. This code will cal our WebService to authenticate the user and then will set the cookie for the client.

    private void invokeSSOLogin(HttpServletResponse response) {
         log.info("SSO is activated. Processing SSO request...");
         response.addCookie(getSsoCookie());
    }
    private Cookie getSsoCookie() {
        Cookie ssoCookie = new Cookie(ssoCookieName, getSsoToken());
        System.out.println("Got the token. Setting cookie...");
        ssoCookie.setPath("/");
        ssoCookie.setMaxAge(-1);
        return ssoCookie;
    }
    private String getSsoToken() {
        log.info("Logging in against remote service and getting token");
        // SSO Service Client
        Properties props = new Properties();
        props.put("UrlSSOServiceWSDL", ssoWsdlUrl);
        log.fine("SSO Endpoint is at: " + props.get("UrlSSOServiceWSDL"));

        Session ssoServiceClient = new Session(props);
        String ssoToken;
        try {
            ssoToken = ssoServiceClient.getToken(getUsername(), getPassword());
        } catch (Exception e) {
            ssoToken = "";
        }
        return ssoToken;
    }

 

IdentityAsserter

To build this, we need a few more things besides the source code

  • Write the actual code for the Identity Asserter
  • Place a jar library which is used to call the SSO WebService into the classpath of the Weblogic server so that it is available at compile & runtime
  • Use the Weblogic MBean generation tool to build the asserter
  • Install the asserter into Weblogic

In a separate article, I will describe how we managed to automate this entire process into a single maven goal. But for now, let’s focus on the raw steps.

For a good explanation into how to build an asserter, use this article: http://weblogic-wonders.com/weblogic/2014/01/13/simple-sample-custom-identity-asserter-weblogic-server-12c/

I will only show the relevant part of the code for our SSO implementation.

For the source code of the asserter, we need 3 files: The actual implemention of the asserter, a callback and an asserter descriptor in the form of an XML.

In the class which implements the Identity Asserter, in the initialize() method, we make some logic and put the parts together. It can look like this:

    public void initialize(ProviderMBean providerMBean, SecurityServices securityServices) {
        System.out.println("Initialize SSO Identity Asserter ");
        SSOIdentityAsserterMBean ssoMBean = (SSOIdentityAsserterMBean)providerMBean;
        description = ssoMBean.getDescription() + " - Version " + ssoMBean.getVersion();
        
        //This is a property which we set in the descriptor of the asserter.
        //It is the WSDL URL used 
        urlSSOServiceWSDL = ssoMBean.getUrlSSOServiceWSDL();
        debug = ssoMBean.getDebug();
        activeTypes = ssoMBean.getActiveTypes();

        // Properties eintragen
        props.setProperty(Session.SSO_URL_PROPERTY, urlSSOServiceWSDL);
        props.setProperty("debug", debug ? "true" : "false");

        // Initialize the client of the webservice
        ssoSessionService = new Session(props);
    }

Then, we must implement assertIdentity() method. This is the method that the Weblogic invokes when a user tries to request a protected resource and has the above mentioned token in the cookie.

Here, it is important to understand how Weblogic figures out when to call our Identity Asserter and when not to. For this, activeTypes property of the MBean (this is standard property for an Identity Asserter) plays a crucial role. In this property, you can put one or more strings which will be used by Weblogic to identify request that it should map to our Identity Asserter. So, if the has, in his request, either a Header name or a Cookie name with one of the activeTypes which we configure, then it will call our Identity asserter. In our case, we have our token sent as Cookie name.

Let’s see the code the the method

    public CallbackHandler assertIdentity(String type, Object token, ContextHandler contextHandler) throws IdentityAssertionException {
        System.out.println("Asserting identity for type " + type);

        // convert the array of bytes to a string
        byte[] tokenBytes = (byte[])token;
        if (tokenBytes == null || tokenBytes.length < 1) {
            String error = "SSOIdentityAsserterImpl received empty token byte array";
            System.out.println("\tError: " + error);
            throw new IdentityAssertionException(error);
        }

        //Call the WebService to check the token against
        String tokenStr = new String(tokenBytes);
        String username;
        try {
            username = ssoSessionService.getUsername(tokenStr);
        } catch (Exception e) {
            String error = "SSOIdentityAsserterImpl error calling SSO-Service. " + e.getMessage();
            throw new IdentityAssertionException(error);
        }
        
        //We return null if the user does not correspond.
        if (username == null) {
            return null;
        }
        
        //If the user matches, we call a callback to handle further the local authentication
        return new SSOCallbackHandlerImpl(username);
    }

Then, the callback implementation has the sole purpose of putting the username in a wrapper payload and send it further so that the Weblogic infrastructure authenticates it. The code is shown below

class SSOCallbackHandlerImpl implements CallbackHandler {
  // Username
  private String username;

  public SSOCallbackHandlerImpl(String username) {
    this.username = username;
  }

  public void handle(Callback[] callbacks) throws UnsupportedCallbackException {
    //We need to iterate the callback to handle only NameCallback
    for (int i = 0; i < callbacks.length; i++) {
      Callback callback = callbacks[i];
      if (!(callback instanceof NameCallback)) {
        throw new UnsupportedCallbackException(callback, "Unrecognized Callback");
      }
      // Put the username in the callback payload
      System.out.println("SSO: Sending username in the callback payload. Username is: " + username);
      NameCallback nameCallback = (NameCallback)callback;
      nameCallback.setName(username);
    }
  }
}

There is one more aspect that we need to take care. The XML descriptor for this Identity Asserter

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE MBeanType SYSTEM "commo.dtd">
<MBeanType Name="SSOIdentityAsserter" DisplayName="SSOIdentityAsserter" Package="de.sso.wls"
           Extends="weblogic.management.security.authentication.IdentityAsserter" PersistPolicy="OnUpdate">
  <MBeanAttribute Name="Base64DecodingRequired" Type="boolean" Writeable="false" Default="false"/>
  <MBeanAttribute Name="ProviderClassName" Type="java.lang.String" Writeable="false"
                  Preprocessor="weblogic.management.configuration.LegalHelper.checkClassName(value)"
                  Default='"de.sso.wls.SSOIdentityAsserterProviderImpl"'/>
  <MBeanAttribute Name="Description" Type="java.lang.String" Writeable="false"
                  Default='" SSO Identity Asserter Provider for WebLogic"'/>
  <MBeanAttribute Name="SupportedTypes" Type="java.lang.String[]" Writeable="false" Default='new String[] { "de.sso" }'/>
  <MBeanAttribute Name="ActiveTypes" Type="java.lang.String[]" Default='new String[] { "de.sso" }'/>
  <MBeanAttribute Name="Version" Type="java.lang.String" Writeable="false" Default='"8.0.0.0"'/>
  <MBeanAttribute Name="Debug" Type="boolean" Default="true" Description="Set to true to enable Debug output"/>
  <MBeanAttribute Name="UrlSSOServiceWSDL" Type="java.lang.String" Default='"http://intranet.de/SSO/SessionPort"'
                  Description="URL für SSO Service."/>
</MBeanType>

Build client jar library

Since we are building our IdentityAsserter using a Weblogic tool which we will call from command line and also because we need it at runtime, we have to create a library which acts as a client for our WebService and place it in the “/lib” directory of the server so that it will be in the classpath at runtime.

For this, we simply take the WSDL of the WebService created in the first step and use JDeveloper wizard to create a proxy client implementation.

 

Build the asserter

We used maven to build our project, so I will show the relevant part from the pom.

What is important to mention is that the MBean maker which we will call needs a weblogic installation for it to work. For this purpose, we preferred to build the MBean on the remote server when it will be in the end installed. That is also to avoid eventual version differences between our local instance and the remote installation

We used Maven ant run plugin to do most of the work.

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>run-mbean-maker</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <tasks>
                    <!-- scp and sshexec are not standard in ANT, so we need to define the tasks here, along with the classes that implement then -->
                    <taskdef name="scp" classname="org.apache.tools.ant.taskdefs.optional.ssh.Scp" classpathref="maven.compile.classpath" />
                    <taskdef name="sshexec" classname="org.apache.tools.ant.taskdefs.optional.ssh.SSHExec" classpathref="maven.compile.classpath" />

                    <!-- Create a remote folder structure on the host to act as a workspace -->
                    <echo message="Preparing remote host folder"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="rm -rf ${scp.remote.project.dir}" trust="true"/>
                    <echo message="Deleted folder ${scp.remote.project.dir}"/>

                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="mkdir -p ${scp.remote.project.dir}" trust="true"/>
                    <echo message="Created folder ${scp.remote.project.dir}"/>

                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="mkdir -p ${scp.remote.project.dir}/lib" trust="true"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="mkdir -p ${scp.remote.project.dir}/src" trust="true"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="mkdir -p ${scp.remote.project.dir}/temp" trust="true"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="mkdir -p ${scp.remote.project.dir}/wlst" trust="true"/>

                    <!-- Copied the sources and the client library remotely -->   
                    <scp todir="${scp.user}:${scp.password}@${scp.host}:${scp.remote.project.dir}/src" trust="true" failonerror="false">
                        <fileset dir="${src.dir}" />
                    </scp>
                    <scp todir="${scp.user}:${scp.password}@${scp.host}:${scp.remote.project.dir}/lib" trust="true" failonerror="false">
                        <fileset file="${client.lib.path}" />
                    </scp>

                    <!-- We need also the como.dtd which can be found in the weblogic installation -->
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="cp ${wls.lib.dir}/commo.dtd ${scp.remote.project.dir}/temp" trust="true"/>
                    <echo message="Copied commo.dtd from weblogic lib folder to working directory"/>

                    <!-- The sources need to be flattened, not in a directory structure. -->
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="find ${scp.remote.project.dir}/src -type f -exec cp '{}'  ${scp.remote.project.dir}/temp/ \;" trust="true"/>
                    <echo message="Copied sources into a flattened manner into the temp working folder"/>

                    <!-- Execute MBean maker class -->
                    <echo message="Executing MBean maker class..."/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="java -classpath ${wlst.wls.classpath.jar.path}:${java.tools.jar.path}:${scp.remote.project.dir}/lib/ServiceClient-${client.jar.version}.jar:. -Dfiles=${scp.remote.project.dir}/temp -DMDFDIR=${scp.remote.project.dir}/temp -DMJF=${scp.remote.project.dir}/temp/${wls.asserter.jar.name} -DpreserveStubs=true -DcreateStubs=true weblogic.management.commo.WebLogicMBeanMaker" trust="true"/>

                    <echo message="Copying the jar of IdentityAsserter into mbeantypes folder"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="yes | cp ${scp.remote.project.dir}/temp/${wls.asserter.jar.name} ${wls.lib.dir}/mbeantypes" trust="true"/>

                    <echo message="Copying client lib jar file into libs of weblogic"/>
                    <sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="yes | cp ${scp.remote.project.dir}/lib/ServiceClient-${client.jar.version}.jar ${wls.domain.home.path}/lib" trust="true"/>

                </tasks>
            </configuration>
        </execution>
    </executions>
</plugin>

A few things are important here. First, we need to use extensively SSH and SCP to copy and execute things remotely. For this, we need to define the tasks at the beginning, since they are not in ant’s default installation. This is done with

<taskdef name="scp" classname="org.apache.tools.ant.taskdefs.optional.ssh.Scp" classpathref="maven.compile.classpath" />
<taskdef name="sshexec" classname="org.apache.tools.ant.taskdefs.optional.ssh.SSHExec" classpathref="maven.compile.classpath" />

You notice that we put maven.compile.classpath in the classpathref, this is for the task to look the current maven classpath to search to the needed classes. This means that we must put the jars as dependencies in maven.

        <dependency>
            <groupId>org.apache.ant</groupId>
            <artifactId>ant-jsch</artifactId>
            <version>1.8.4</version>
        </dependency>
        <dependency>
            <groupId>com.jcraft</groupId>
            <artifactId>jsch</artifactId>
            <version>0.1.54</version>
        </dependency>

Then you can see that there is a need for commo.dtd from the lib directory of weblogic. We copy this using:

<sshexec host="${scp.host}" username="${scp.user}" password="${scp.password}" command="cp ${wls.lib.dir}/commo.dtd ${scp.remote.project.dir}/temp" trust="true"/>

Then we execute the MBean maker. This is a jar on the remote host which we trigger by java. One thing to notice is that we set the classpath for the execution using the -classpath option. This is because MBean Maker needs to be aware of a few things. First we add path of the wlst.wls.classpath.jar. This jar has the role of adding to the classpath what we need and it is located in <domain_home>/wlserver/modules/features/wlst.wls.classpath.jar. Then we need the path to the JDK Tools. This is <JDK>/lib/tools.jar. The rest of the classpath is completed with the addition of our client jar library

Now, what’s left is to install the asserter. This is easily shown at the end of the already mentioned article: http://weblogic-wonders.com/weblogic/2014/01/13/simple-sample-custom-identity-asserter-weblogic-server-12c/

That’s all. I will come back with an article describing the automation of these tasks and I will link it here.