Develop a SSO agent

Overview

The integration of an SSO solution with the deployment requires developing an SSO agent and deploying it to the Decision Insight deployment.

To minimize coupling between SSO agents and the deployment, these agents rely only on the OSGi and Servlet APIs.

SSO agents are packaged as OSGi bundles (jar files) and deployed onto the deployment. The installation consists of a simple copy of the jar file into ./plugins directory.

Technically SSO agents are OSGi bundles that register a Servlet Filter upon activation.

Authentication

SSO agents register a servlet filter that must wrap the HttpServletRequest so that any code down the filter chain can access the following properties:

  • HttpServletRequest.getPrincipal()
    Should return a non-null object when the user is authenticated by the SSO. The returned principal should return a non-null and non-empty user name.
  • HttpServletRequest.isUserInRole(String role)
    Should return true if the user has the specified role, false otherwise. If a user authenticated by the SSO tries to enter into the application but has no role, it will not be allowed to enter.

If the SSO system only handles authentication and role management should be managed inside the deployment, set the property com.systar.photon.application.auth.ssoRoleProvisioning to false.

In this case, isUserInRole(String role) is not called and the implementation in the SSO agent may return any value.

platform.properties
com.systar.photon.application.auth.ssoRoleProvisioning=false

User provisioning

Users are created when they first log in. The SSO agent provides at least the user name. It could also provide the roles (via the isUserInRole method).

To provide additional information such as the first name, last name, email address or description, the SSO agent may add values to the following servlet request attributes:

  • com.systar.userPrincipal.firstName
  • com.systar.userPrincipal.lastName
  • com.systar.userPrincipal.email
  • com.systar.userPrincipal.description
Example.java
request.setAttribute("com.systar.userPrincipal.firstName", "James");
request.setAttribute("com.systar.userPrincipal.lastName", "Lewis");
request.setAttribute("com.systar.userPrincipal.email", "jlewis@axway.com");
request.setAttribute("com.systar.userPrincipal.description", "My name is Lewis, James Lewis!");

If a servlet request attribute is not set or null, the value won't be overridden in Decision Insight .

How the filter interacts with the deployment

URLs to filter

URL

SSO plugin action
/login

if user is already logged on SSO, do user provisioning and call the next filter in the chain

if user is not logged on SSO, redirect to the SSO login page

/ssoDisconnected

(default redirection after Decision Insight logout)

if the user has to be disconnected from the SSO, do it here.


The SSO plugin does not need to protect all URLs as the deployment has its own mechanism.

How to invalidate user sessions

On /login you create a HttpSession that you store into a map.

On invalidation, you retrieve the HttpSession that you stored and invalidate it. The deployment listens to HttpSession invalidation events to invalidate its own sessions.

Technical hints

You can rely only on the following libraries that the deployment provides:

  • OSGi Core version 6.0.0
  • OSGi Compendium HTTP Whiteboard version 1.0.0
  • Java Servlet version 3.1.0
  • SLF4j API version 1.7.12

All other libraries should be embedded in the SSO agent bundle even if they already exist in the deployment.

Your bundle should not export any package.

Logging

Logging can be done thanks to SLF4j. It is preferred to the OSGi Logger service.

Configuration

The properties set in <node directory>/conf/platform.properties file can be retrieved through the OSGi API : org.osgi.framework.BundleContext#getProperty The bundle context object is given during the bundle activation.

SSO plugin creators can freely define their own properties to be used in platform.properties as long as they are not clashing with other properties defined by the deployment and its third-party libraries. To achieve this, it is recommended to prefix them with a unique identifier (eg com.mycompany.<...>)

Example

Use case

The user authentication information is set in the HTTP headers by a reverse proxy:

HTTP Headers
USER_NAME: jsmith
USER_ROLES: power-user,supervisor-emea
USER_FIRST_NAME: John
USER_LAST_NAME: Smith
USER_DESCRIPTION: EMEA Supervisor, located in office B245

Java files

The following filter is able to process such headers and forward the authentication information

com/mycompany/authentication/AuthenticationFilter.java
package com.mycompany.authentication;

import java.io.*;
import java.security.Principal;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.mycompany.authentication.AuthenticationConstants.*;

public class AuthenticationFilter implements Filter {
    private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationFilter.class);

    private final String emailSuffix;

    public AuthenticationFilter(String emailSuffix) {
        this.emailSuffix = emailSuffix;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // determine if the request contains authentication information
        String username = trimValue(httpRequest.getHeader("USER_NAME"));
        if (username != null) {
            LOGGER.debug("Authenticated user '{}' found in the HTTP request", username);

            // extract user roles
            String userRoles = trimValue(httpRequest.getHeader("USER_ROLES"));
            Set<String> roles;
            if (userRoles != null) {
                roles = new HashSet<>(Arrays.asList(userRoles.split("[,\\s]+")));
            } else {
                roles = Collections.emptySet();
            }

            // Add user information into request
            request.setAttribute(FIRST_NAME, httpRequest.getHeader("USER_FIRST_NAME"));
            request.setAttribute(LAST_NAME, httpRequest.getHeader("USER_LAST_NAME"));
            request.setAttribute(EMAIL, username + emailSuffix);
            request.setAttribute(DESCRIPTION, httpRequest.getHeader("USER_DESCRIPTION"));

            // Supply user name and role via request wrapping
            request = new AuthenticatedHttpServletRequestWrapper(httpRequest, username, roles);
        } else {
            LOGGER.info("No authenticated user found in the HTTP request");
        }

        // continue request processing in the filter chain
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // should be empty, use initialization through constructor
    }

    @Override
    public void destroy() {
        // should be empty, if a release mechanism is needed, use an ad-hoc method to be called by the activator
    }

    private String trimValue(String value) {
        if (value != null) {
            value = value.trim();
            if (value.isEmpty()) {
                value = null;
            }
        }
        return value;
    }

    private static class AuthenticatedHttpServletRequestWrapper extends HttpServletRequestWrapper {
        private final Principal principal;
        private final Set<String> roles;

        public AuthenticatedHttpServletRequestWrapper(HttpServletRequest request, String username, Set<String> roles) {
            super(request);
            principal = new PrincipalImpl(username);
            this.roles = roles;
        }

        @Override
        public Principal getUserPrincipal() {
            return principal;
        }

        @Override
        public boolean isUserInRole(String role) {
            return roles.contains(role);
        }
    }

    private static class PrincipalImpl implements Principal {
        private final String name;

        private PrincipalImpl(String name) {
            this.name = name;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public boolean equals(Object obj) {
            return (this == obj) || ((obj instanceof PrincipalImpl) && name.equals(((PrincipalImpl) obj).name));
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }

        @Override
        public String toString() {
            return name;
        }
    }
}

com/mycompany/authentication/AuthenticationConstants.java
package com.mycompany.authentication;

/**
 * The HTTP request attribute names to be used to provision user details.
 */
public final class AuthenticationConstants {

    public static final String FIRST_NAME = "com.systar.userPrincipal.firstName";
    public static final String LAST_NAME = "com.systar.userPrincipal.lastName";
    public static final String EMAIL = "com.systar.userPrincipal.email";
    public static final String DESCRIPTION = "com.systar.userPrincipal.description";

    private AuthenticationConstants() {
    }
}


The filter is registered by the following OSGi activator

com/mycompany/authentication/Activator.java
package com.mycompany.authentication;

import java.util.*;
import javax.servlet.*;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.osgi.service.http.whiteboard.HttpWhiteboardConstants.HTTP_WHITEBOARD_FILTER_PATTERN;

public class Activator implements BundleActivator {
    private static final Logger LOGGER = LoggerFactory.getLogger(Activator.class);

    private ServiceRegistration<Filter> registration;

    @Override
    public void start(BundleContext context) throws Exception {
        Filter authenticationFilter = createAuthenticationFilter(context);
        registerAuthenticationFilter(context, authenticationFilter);
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        unregisterAuthenticationFilter();
    }

    private Filter createAuthenticationFilter(BundleContext context) {
        String emailSuffix = context.getProperty("com.mycompany.authentication.email.suffix");
        return new AuthenticationFilter(emailSuffix);
    }

    private void registerAuthenticationFilter(BundleContext context, Filter authenticationFilter) {
        Dictionary<String, Object> props = new Hashtable<>();
        props.put(HTTP_WHITEBOARD_FILTER_PATTERN, "/*");
        registration = context.registerService(Filter.class, authenticationFilter, props);
        LOGGER.info("Authentication filter has been registered");
    }

    private void unregisterAuthenticationFilter() {
        if (registration != null) {
            registration.unregister();
            LOGGER.info("Authentication filter has been unregistered");
        }
    }
}

Jar Manifest

To be a valid OSGi bundle, the jar must have a Manifest like this one

META-INF/MANIFEST.MF
Manifest-Version: 1.0
Bundle-Activator: com.mycompany.authentication.Activator
Bundle-ManifestVersion: 2
Bundle-Name: MyCompany authentication filter
Bundle-SymbolicName: com.mycompany.authentication
Bundle-Version: 1.0.0
Import-Package: javax.servlet;version="[3.1,4)",javax.servlet.http;versi
 on="[3.1,4)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1
 .7,2)"

However, it is recommended to generate it automatically, as in the Maven project below

Maven sample

This example is implemented in a Maven project : com.mycompany.authentication.zip

After unzipping, you can build it with the following command:

> mvn clean package

Related Links