Google Workspace (aka G-Suite) enable both SP- and IdP-initiated authentication SSO with OpenSearch

Versions (relevant - OpenSearch/Dashboard/Server OS/Browser):

OpenSearch 2.4.0

Describe the issue:

I would like to setup Google Workspace (aka G-Suite) to enable both SP- and IdP-initiated authentication SSO at the same time with OpenSearch.

I know with Okta it can be done using Requestable SSO URLs as described in AWS’ documentation: SAML authentication for OpenSearch Dashboards - Amazon OpenSearch Service

However when creating a SAML app in Google Workspace, I can only set ACS URL, Entity ID and Start URL. That means I need to decide whether I want to create an SP- or IdP-initiated authentication, but can’t do both at the same time (?). Just for clarity; when setting IdP-initiated authentication is when I’m logged into my gmail account that is part of the Google Workspace where the SAML app is created / configured and clicking on the custom SAML app in Google’s application drawer (top right corner). That will initiate a successful SAML authentication, as long as the ACS URL is set to https://domain.com/_opendistro/_security/saml/acs/idpinitiated. Now if I want SP-initiated authentication, I would have to change the ACS URL to https://domain.com/_opendistro/_security/saml/acs, which means I can go directly to the hosted domain for letting the service provider (SP) to initiate the authentication.

How can I configure both at the same time? Important to note that I already successfully configured either SP- or IdP-initiated SSO authentication, but I’m trying to find out how can both be set at the same time using Google Workspace, considering the fact that Google Workspace doesn’t have something like Okta does with the additional Requestable SSO URLs.

At the moment if I do IdP-initiated authentication, if I try via SP-initiated, it throws the following error, as expected:

{"statusCode":500,"error":"Internal Server Error","message":"Internal Error"}

and server logs throwing the following error:

[ERROR][c.o.s.a.SamlResponse     ] [node-1] The status code of the Response was not Success, was urn:oasis:names:tc:SAML:2.0:status:Requester -> Invalid request, ACS Url in request https://domain.com/_opendistro/_security/saml/acs doesn't match configured ACS Url https://domain.com/_opendistro/_security/saml/acs/idpinitiated.
[WARN ][c.a.d.a.h.s.AuthTokenProcessorHandler] [node-1] Error while validating SAML response in /_opendistro/_security/api/authtoken

Now if I change my SAML app to SP-initiated authentication by setting ACS URL to https://domain.com/_opendistro/_security/saml/acs. Trying to click the SAML app icon using Google’s interface throws the following error:

{"statusCode":400,"error":"Bad Request","message":"Invalid requestId"}

Of course, because the SAML authentication hasn’t initiated yet to let the routes.js extract the requestId from the cookie:

routes.js
    this.router.post({
      path: `/_opendistro/_security/saml/acs`,
      validate: {
        body: _configSchema.schema.any()
      },
      options: {
        authRequired: false
      }
    }, async (context, request, response) => {
      let requestId = '';
      let nextUrl = '/';

      try {
        const cookie = await this.sessionStorageFactory.asScoped(request).get();

        if (cookie) {
          var _cookie$saml, _cookie$saml2;

          requestId = ((_cookie$saml = cookie.saml) === null || _cookie$saml === void 0 ? void 0 : _cookie$saml.requestId) || '';
          nextUrl = ((_cookie$saml2 = cookie.saml) === null || _cookie$saml2 === void 0 ? void 0 : _cookie$saml2.nextUrl) || `${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`;
        }

        if (!requestId) {
          return response.badRequest({
            body: 'Invalid requestId'
          });
        }
      } catch (error) {
        context.security_plugin.logger.error(`Failed to parse cookie: ${error}`);
        return response.badRequest();
      }

Configuration:

This is my current config.yml configurations:

config.yml
config:
  dynamic:
    http:
      anonymous_auth_enabled: false
    authc:
      basic_internal_auth_domain:
        description: "Authenticate via HTTP Basic against internal users database"
        http_enabled: true
        transport_enabled: true
        order: 0
        http_authenticator:
          type: "basic"
          challenge: false
        authentication_backend:
          type: "intern"
      saml_auth_domain:
        http_enabled: true
        transport_enabled: false
        order: 1
        http_authenticator:
          type: saml
          challenge: true
          config:
            idp:
              metadata_file: "path to google metadata.xml"
              entity_id: "https://accounts.google.com/o/saml2?idpid=<token>"
            sp:
              entity_id: "my-entity-id"
            kibana_url: https://domain.com/
            roles_key: roles
            exchange_key: 'X509Certificate'
        authentication_backend:
          type: noop

@gilad.reich Did you resolve your issue?

Hi @pablo, yes and no.

Yes, because I had to manually modify OpenSearch’s routes.js to simulate “SP initiated” SSO to do IdP initiated instead. First I configured Google Workspace ACS URL to /_opendistro/_security/saml/acs/idpinitiated and then hardcoding in routes.js the url to redirect for IdP initiated path on /auth/saml/login, e.g.:

    }, async (context, request, response) => {
      if (request.auth.isAuthenticated) {
        return response.redirected({
          headers: {
            location: `${this.coreSetup.http.basePath.serverBasePath}/app/wazuh`
          }
        });
      }
      // NOTE(Gilad): Fix this... I added this line because Google Workspace SAML apps can't configure multiple SSO urls
      // So the idea is to simulate SP SSO Initiated (when the url is given in the browser), whilst we actually force here IdP.
      return response.redirected({
        headers: {
          location: `https://accounts.google.com/o/saml2/initsso?idpid=****&spid=****&forceauthn=false`
        }
      });

Not ideal solution, but it works for now at least. Also note that there are other issues around this kind of setup, where the session that expires doesn’t renew the JWT auth cookie. There are still pile of ongoing GitHub issues around this:

and following mvanderlee’s suggestion there, helped making things better by setting in opensearch_dashboard.yml the session keepalive to false:

opensearch_security.session.keepalive: false

No, because these workarounds I had to figure out myself shouldn’t exists in first place, unfortunately.