Spring Security/OAuth 2.0 SSO Integration

Introduction

Goal

Integrate Bloomreach Experience Manager using Spring Security and an OAuth 2.0 single sign-on solution.

Background

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications. OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows. This page describes how Bloomreach Experience Manager can be integrated using Spring Security and an OAuth 2.0 identity provider for a seamless SSO integration.

Spring Boot and Spring Security

Since version 14.6, the CMS by default ships with Spring Boot. With Spring Boot we can leverage Spring Security to setup the SSO configuration. To make dependency management easier it is possible to add the Bill of Materials for Spring to your project.

      <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-framework-bom</artifactId>
        <version>${spring.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-bom</artifactId>
        <version>${spring-security.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
However, some projects have a Spring ContextLoaderListener configured in their web.xml and have Spring Boot disabled. More information is available here.
In the past the Spring Security Extension has been used to setup SSO with SAML. The Spring Security extension is however no longer being maintained. It is also very likely that you will encounter compatibility issues with Java 11, so it is strongly advised to use Spring Security instead.

You can add you own project specific Spring Configuration in the CMS. It is important that any Java-based Spring Boot configuration resides in the package: org.bloomreach.xm.cms. You can add your Spring Security configuration within that package.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    SecurityFilterChain configure(HttpSecurity http) throws Exception {
        // Your SSO configuration
        return http.build();
    }

}
Any Java-based configuration located at org.bloomreach.xm.cms will be auto located and used by Spring Boot due to framework conventions.

CMS (repository) login integration

When a user is logged into Spring Security, it is not automatically logged in/known to the CMS. The CMS needs to be informed about the current logged in user, and makes sure this user will be logged in the repository as well. This is required to apply the internal security model, which is internally used for authorization of different parts of the system, to the logged in user. This can be achieved by adding a filter to the security configuration, which will be applied after the authorization configuration and makes sure that when a user is authenticated in Spring Security the user information (username) will be passed on to the CMS. The user's session will then be authorised to the repository with the provided username.

final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!authentication.isAuthenticated()) {
    log.error("User not authenticated");
    chain.doFilter(request, response);
    return;
}

// Check if the user already has a SSO user state stored in HttpSession before.
final Object principal = authentication.getPrincipal();
if (principal instanceof AuthenticatedPrincipal) {
    final String username = ((AuthenticatedPrincipal) principal).getName();
    if (StringUtils.isNotBlank(username)) {
        SimpleCredentials credentials = new SimpleCredentials(username, "DUMMY".toCharArray());
        credentials.setAttribute("type", "sso");
        req.setAttribute(UserCredentials.class.getName(), new UserCredentials(credentials));
    }
}

CMS authentication

The default security provider used in the CMS has a user manager which by default authenticates users by repository authentication. To leverage the authentication mechanism of Spring Security an additional security provider can be configured, which overrides the authentication method to validate the authentication in Spring Security.

The custom security provider can be added using the below configuration:

definitions:
  config:
    /hippo:configuration/hippo:security/sso:
      jcr:primaryType: hipposys:securityprovider
      hipposys:classname: org.bloomreach.xm.cms.sso.SSODelegatingSecurityProvider
      /hipposys:userprovider:
        jcr:primaryType: hipposys:userprovider
        hipposys:dirlevels: 0
      /hipposys:groupprovider:
        jcr:primaryType: hipposys:groupprovider
        hipposys:dirlevels: 0

By introducing a custom DelegatingSecurityProvider the authentication method can be overridden to perform the authentication with Spring Security, but delegating all other interaction to the configured user and group manager.

/**
* Custom <code>org.hippoecm.repository.security.SecurityProvider</code> implementation.
* <p>
* Hippo Repository allows to set a custom security provider for various reasons (e.g, SSO) for specific users.
* If a user is associated with a custom security provider, then Hippo Repository invokes
* the custom security provider to do authentication and authorization.
* </P>
*/
public class SSODelegatingSecurityProvider extends DelegatingSecurityProvider {

    private static Logger log = LoggerFactory.getLogger(SSODelegatingSecurityProvider.class);

    private HippoUserManager userManager;

    /**
     * Constructs by creating the default <code>RepositorySecurityProvider</code> to delegate all the other calls
     * except of authentication calls.
     *
     * @throws RepositoryException
     */

    public SSODelegatingSecurityProvider() throws RepositoryException {
        super(new RepositorySecurityProvider());
    }

    /**
     * Returns a custom (delegating) HippoUserManager to authenticate a user by Spring Security.
     */
    @Override
    public UserManager getUserManager() throws RepositoryException {
        if (userManager == null) {
            userManager = new DelegatingHippoUserManager((HippoUserManager) super.getUserManager()) {
                @Override
                public boolean authenticate(SimpleCredentials creds) {
                    return validateAuthentication(creds);
                }
            };
        }
        return userManager;
    }

    /**
     * Returns a custom (delegating) HippoUserManager to authenticate a user by Spring Security.
     */
    @Override
    public UserManager getUserManager(Session session) throws RepositoryException {
        return new DelegatingHippoUserManager((HippoUserManager) super.getUserManager(session)) {

            @Override
            public boolean authenticate(SimpleCredentials creds) {
                return validateAuthentication(creds);
            }
        };
    }

    /**
     * Validates authentication in Spring Security.
     *
     * @param creds
     * @return
     * @throws RepositoryException
     */
    protected boolean validateAuthentication(SimpleCredentials creds) {
        log.debug("Spring security context validates authentication for credentials: {}", creds);
        final SecurityContext context = SecurityContextHolder.getContext();
        if(context != null) {
            final Authentication authentication = context.getAuthentication();
            if(authentication != null) {
                log.debug("User {} authenticated: {}", creds.getUserID(), authentication.isAuthenticated());
                return authentication.isAuthenticated();
            }
        }
        return false;
    }

}

User logout

When the user clicks the Logout button in the CMS, the internal logout process happens as usual, but it is also important to trigger the logout procedure within Spring Security. This can be done by adding a CMS logout service that redirects the request to the desired endpoint:

/**
 * Logout service to redirect user after internal logout is done.
 */
public class LogoutService extends CmsLogoutService {
  public LogoutService(IPluginContext context, IPluginConfig config) {
    super(context, config);
  }

  @Override
  protected void redirectPage() {
    throw new RedirectToUrlException("/logout");
  }
}

Spring Security handles logout automatically when a request to /logout is received. From the documentaton:

The default is that accessing the URL "/logout" will log the user out by invalidating the HTTP Session, cleaning up any rememberMe() authentication that was configured, clearing the SecurityContextHolder, and then redirect to "/login?success".

If further customisation is needed to the logout process, a LogoutSuccessHandler can be added to the Security Config. This depends on the Identity Provider, see the examples for more information.

Azure Entra ID example

A sample project based on a v15 brXM archetype is available at https://github.com/bloomreach/brxm-azure-sso-oauth2. The project largely follows the approach documented in this page, using LDAP for authentication and the Spring Boot starter for Azure to integrate with Entra ID. Please read the guide for integrating a generic Spring web application with Entra ID for more information, and the README in the project for an overview of the implementation classes.

Did you find this page helpful?
How could this documentation serve you better?
On this page
    Did you find this page helpful?
    How could this documentation serve you better?