Search code examples
authenticationsingle-sign-onpentahomulti-tenantspring-saml

Redirect after multi-tenant SSO login in Pentaho with spring-SAML


I would like to configure Pentaho with a multi-tenant SSO login using the SAML Plugin Link (that extends the Spring SAML).

Right now I have declared multiple Service Providers (SPs) and Identity Providers (IDPs) in the blueprint.xml (one for each tenant plus a common SP). However at the end of the login flow I am not redirected to the home page but to a generic error page.


Here is an example of the blueprint.xml setup in the SAML Plugin:

  <bean id="spResourceFactoryCommon" class="org.pentaho.platform.spring.security.saml.resources.MetadataResourceFactory">
    <argument>
      <map key-type="java.lang.String" value-type="java.lang.String">
        <entry key="org.opensaml.util.resource.FilesystemResource" value="${saml.sp.metadata.filesystem.common}" />
      </map>
    </argument>
    <argument value="${saml.sp.metadata.classpath.fallback}" />
  </bean>

  <bean id="spResourceFactoryTenant1" class="org.pentaho.platform.spring.security.saml.resources.MetadataResourceFactory">
    <argument>
      <map key-type="java.lang.String" value-type="java.lang.String">
        <entry key="org.opensaml.util.resource.FilesystemResource" value="${saml.sp.metadata.filesystem.tenant1}" />
      </map>
    </argument>
    <argument value="${saml.sp.metadata.classpath.fallback}" />
  </bean>

  <bean id="spResourceFactoryTenant2" class="org.pentaho.platform.spring.security.saml.resources.MetadataResourceFactory">
    <argument>
      <map key-type="java.lang.String" value-type="java.lang.String">
        <entry key="org.opensaml.util.resource.FilesystemResource" value="${saml.sp.metadata.filesystem.tenant2}" />
      </map>
    </argument>
    <argument value="${saml.sp.metadata.classpath.fallback}" />
  </bean>

  <bean id="idpResourceFactoryTenant1" class="org.pentaho.platform.spring.security.saml.resources.MetadataResourceFactory">
    <argument>
      <map key-type="java.lang.String" value-type="java.lang.String">
        <entry key="org.opensaml.util.resource.FilesystemResource" value="${saml.idp.metadata.filesystem.tenant1}" />
      </map>
    </argument>
    <argument value="${saml.idp.metadata.classpath.fallback}" />
  </bean>

  <bean id="idpResourceFactoryTenant2" class="org.pentaho.platform.spring.security.saml.resources.MetadataResourceFactory">
    <argument>
      <map key-type="java.lang.String" value-type="java.lang.String">
        <entry key="org.opensaml.util.resource.FilesystemResource" value="${saml.idp.metadata.filesystem.tenant2}" />
      </map>
    </argument>
    <argument value="${saml.idp.metadata.classpath.fallback}" />
  </bean>

  <!-- MetadataManager configuration - paths to metadata of IDPs and SP's  -->
  <bean id="metadata" class="org.springframework.security.saml.metadata.CachingMetadataManager" depends-on="pentahoSamlBootstrap">
    <argument>
      <list>
        <!-- sp metadata with extended metadata -->
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
          <argument>
            <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
              <argument>
                <bean class="java.util.Timer"/>
              </argument>
              <argument>
                <bean factory-ref="spResourceFactoryCommon" factory-method="factoryResource" />
              </argument>
              <property name="parserPool" ref="parserPool"/>
            </bean>
          </argument>
          <argument>
            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
              <property name="idpDiscoveryEnabled" value="${saml.discovery.idp.enabled}"/>
              <property name="requireLogoutRequestSigned" value="${ensure.incoming.logout.request.signed}"/>
              <property name="alias" value="pentahoCommon"/>
              <property name="local" value="true"/>
            </bean>
          </argument>
        </bean>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
          <argument>
            <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
              <argument>
                <bean class="java.util.Timer"/>
              </argument>
              <argument>
                <bean factory-ref="spResourceFactoryTenant1" factory-method="factoryResource" />
              </argument>
              <property name="parserPool" ref="parserPool"/>
            </bean>
          </argument>
          <argument>
            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
              <property name="idpDiscoveryEnabled" value="${saml.discovery.idp.enabled}"/>
              <property name="requireLogoutRequestSigned" value="${ensure.incoming.logout.request.signed}"/>
              <property name="alias" value="tenant1sp"/>
              <property name="local" value="true"/>
            </bean>
          </argument>
        </bean>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
          <argument>
            <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
              <argument>
                <bean class="java.util.Timer"/>
              </argument>
              <argument>
                <bean factory-ref="spResourceFactoryTenant2" factory-method="factoryResource" />
              </argument>
              <property name="parserPool" ref="parserPool"/>
            </bean>
          </argument>
          <argument>
            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
              <property name="idpDiscoveryEnabled" value="${saml.discovery.idp.enabled}"/>
              <property name="requireLogoutRequestSigned" value="${ensure.incoming.logout.request.signed}"/>
              <property name="alias" value="tenant2sp"/>
              <property name="local" value="true"/>
            </bean>
          </argument>
        </bean>

        <!-- idp metadata -->
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
          <argument>
            <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
              <argument>
                <bean class="java.util.Timer"/>
              </argument>
              <argument>
                <bean factory-ref="idpResourceFactoryTenant1" factory-method="factoryResource" />
              </argument>
              <property name="parserPool" ref="parserPool"/>
            </bean>
          </argument>
          <argument>
            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
              <property name="idpDiscoveryEnabled" value="${saml.discovery.idp.enabled}"/>
              <property name="requireLogoutRequestSigned" value="${ensure.outgoing.logout.request.signed}"/>
              <property name="requireLogoutResponseSigned" value="${ensure.outgoing.logout.response.signed}"/>
              <property name="alias" value="tenant1idp"/>
              <property name="local" value="true"/>
            </bean>
          </argument>
        </bean>
        <bean class="org.springframework.security.saml.metadata.ExtendedMetadataDelegate">
          <argument>
            <bean class="org.opensaml.saml2.metadata.provider.ResourceBackedMetadataProvider">
              <argument>
                <bean class="java.util.Timer"/>
              </argument>
              <argument>
                <bean factory-ref="idpResourceFactoryTenant2" factory-method="factoryResource" />
              </argument>
              <property name="parserPool" ref="parserPool"/>
            </bean>
          </argument>
          <argument>
            <bean class="org.springframework.security.saml.metadata.ExtendedMetadata">
              <property name="idpDiscoveryEnabled" value="${saml.discovery.idp.enabled}"/>
              <property name="requireLogoutRequestSigned" value="${ensure.outgoing.logout.request.signed}"/>
              <property name="requireLogoutResponseSigned" value="${ensure.outgoing.logout.response.signed}"/>
              <property name="alias" value="tenant2idp"/>
              <property name="local" value="true"/>
            </bean>
          </argument>
        </bean>

      </list>
    </argument>
    <property name="keyManager" ref="keyManager" />
    <property name="defaultIDP" value="${saml.idp.url}" />
  </bean>

With this configuration, when I go to the url https://my.application.com/pentaho/alias/tenant1sp/sp?idp=tenant.1.name I am redirected to the login page exposed by the IDP for the tenant1. After the login I am redirected to the previous url getting a generic error: see the screenshot

Sorry, something went wrong. Please try again or contact your system administrator.

If I go to the URL https://my.application.com/pentaho/Home I am logged to the Pentaho dashboard. This makes me think that the login process has been successful but something went wrong with the redirect at the end of the flow. Indeed I would expect to be redirected to the URL https://my.application.com/pentaho/Home . Can I configure this redirect somewhere/somehow?


Solution

  • A work around seems to solve this issue, however I see that it creates a conflict with the SAML authentication when using the exposed APIs of Pentaho. To use those APIs, this solution is not good.


    WORK AROUND


    Change the defaultTargetUrl and alwaysUseDefaultTargetUrl property of the successRedirectHandler bean (declared in the blueprint.xml file of the Pentaho-SAML Plugin)

      <!-- Handler deciding where to redirect user after successful login -->
      <bean id="successRedirectHandler" class="org.pentaho.platform.spring.security.saml.PentahoSamlAuthenticationSuccessHandler" 
                            init-method="afterPropertiesSet">
        <property name="defaultTargetUrl" value="https://my.application.com/pentaho/Home"/>
        <property name="alwaysUseDefaultTargetUrl" value="true"/>
        <property name="requireProxyWrapping" value="false"/>
      </bean>
    

    Edit: Resolution ofAPIs login issue I extended the org.pentaho.platform.spring.security.saml.PentahoSamlAuthenticationSuccessHandler class with a custom one that overrides the onAuthenticationSuccess method

    private String contextPath;
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
    
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        
        //Apply the redirect to the Pentaho console Home if and only if the original targetUrl is not a Pentaho exposed API but the home (contextPath/Home)
        if (!savedRequest.getRedirectUrl().contains("API")
                && savedRequest.getRedirectUrl().contains(contextPath+"/Home")) {
            
            //The Pentaho console Home is set as defaultTargetUrl in the blueprint.xml
            this.setAlwaysUseDefaultTargetUrl(true);
            log.info("The request is not on a Pentaho API. Forcing the target URL to redirect to the defaultTargetUrl");
        }
        super.onAuthenticationSuccess(request, response, authentication);
        
        //retore the original value of alwaysUseDefaultTargetUrl
        this.setAlwaysUseDefaultTargetUrl(false);
    }