Configure multi-realm federated authentication

Your users can log in to the Dashboard and select from multiple federated external Identity Providers (IdPs) by using OpenID Connect (OIDC).

Enable multiple federated IdP authentication

Configure Red Hat OpenStack Services on OpenShift (RHOSO) to support multiple federated Identity Providers (IdPs) so that users can log in to the Dashboard by using single sign-on (SSO) and select their authentication realm.

Before you begin

  • You have installed RHOSO.
  • You have multiple external OpenID Connect (OIDC) IdPs configured in your environment.

About this task

The RHOSO deployment of multiple federated IdPs implements the Web SSO authentication flow because the openstack CLI does not support multiple IdPs.

Important
When a federated user uses an application credential for authentication, this application credential remains valid for its configured lifetime even when its associated federated user account is disabled.

Procedure

  1. Choose a name to uniquely identify each IdP.

    In this example there are two IdPs, whose names are referenced as <idp_name_1> and <idp_name_2>.

  2. Obtain the following settings from each IdP administrator:
    • The FQDN for each IdP that is referenced in this procedure as <fqdn_1> and <fqdn_2>.
    • The federation Realm Name for each IdP that is referenced in this procedure as <realm_name_1> and <realm_name_2>.
    • The Client ID for each IdP that is referenced in this procedure as <client_id_1> and <client_id_2>.
    • The Client Secret for each IdP that is referenced in this procedure as <client_secret_1> and <client_secret_2>.
    • The Provider Metadata URL for each IdP that is referenced in this procedure as <provider_metadata_url_1> and <provider_metadata_url_2>.
  3. Retrieve the Identity service (keystone) public endpoint:
    $ oc get keystoneapis.keystone.openstack.org -o json | jq '.items[0].status.apiEndpoints.public'

    This Identity service endpoint is referenced in this procedure as <keystone_url>.

  4. Provide the IdP administrators with the following information:
    • Web origin:
      https://<keystone_url>
    • Redirect URIs:
      https://<keystone_url>/v3/auth/OS-FEDERATION/websso/openid

      Provide a URI for each IdP containing their unique IdP name, which must end in a trailing /. You must send each URI to the corresponding IdP administrator:

      https://<keystone_url>/v3/auth/OS-FEDERATION/identity_providers/<idp_name_1>/protocols/openid/websso/
      https://<keystone_url>/v3/auth/OS-FEDERATION/identity_providers/<idp_name_2>/protocols/openid/websso/
    • Each federation client must have Implicit flow enabled and not Authorization code flow.
  5. Create a custom resource (CR) file for a secret called keystone-httpd-override:

    The following specified OIDC parameter and associated Apache configuration is added to provide the most failsafe solution that supports the login of users from multiple IdPs. Consequently, previous sessions are not saved and users must reauthenticate themselves after they log out of the Dashboard:

     OIDCAuthRequestParams "prompt=login"
    
     <IfModule headers_module>
       <Location "/v3/local-logout/clear">
         Header always add Set-Cookie "mod_auth_openidc_session=deleted; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=None"
       </Location>
     </IfModule>
      RewriteEngine On
      RewriteRule ^/v3/auth/OS-FEDERATION/identity_providers/(<idp_name_1>|<idp_name_2>)/protocols/openid/websso$ \
       /v3/local-logout/clear [R=302,L]
      RewriteRule ^/v3/local-logout/clear$ \
       /v3/auth/OS-FEDERATION/websso/openid [R=302,L,QSA,NE]

    If users in your multiple federated IdP deployment do not belong to more than one IdP then you can allow users to reopen the Dashboard they have closed without providing any authentication. In this case, you must remove this OIDC parameter and provide a different Apache LocationMatch configuration to save the previous sessions.

    apiVersion: v1
    kind: Secret
    metadata:
     name: keystone-httpd-override
     namespace: openstack
    type: Opaque
    stringData:
     federation.conf: |
       # Example OIDC directives for the *public* endpoint
       OIDCClaimPrefix "OIDC-"
       OIDCResponseType "id_token"
       OIDCScope "openid email profile"
       OIDCClaimDelimiter ";"
       OIDCPassUserInfoAs "claims"
       OIDCPassClaimsAs "both"
       OIDCCryptoPassphrase "<crypto_pass>"
       OIDCRedirectURI "<keystone_url>/v3/redirect_uri/"
                            OIDCMetadataDir "/var/lib/httpd/metadata"
       OIDCAuthRequestParams "prompt=login"
       <IfModule headers_module>
         <Location "/v3/local-logout/clear">
           Header always add Set-Cookie "mod_auth_openidc_session=deleted; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=None"
         </Location>
       </IfModule>
    
       RewriteEngine On
    
       RewriteRule ^/v3/auth/OS-FEDERATION/identity_providers/(<idp_name_1>|<idp_name_2>)/protocols/openid/websso$ \
         /v3/local-logout/clear [R=302,L]
    
       RewriteRule ^/v3/local-logout/clear$ \
         /v3/auth/OS-FEDERATION/websso/openid [R=302,L,QSA,NE]
    
       <Location "/v3/auth/OS-FEDERATION/websso/openid">
         AuthType openid-connect
         Require  valid-user
       </Location>
    
       <Location "/v3/redirect_uri">
         AuthType openid-connect
         Require  valid-user
       </Location>
    Important
    The full value of the OIDCRedirectURI parameter must end in a trailing /.

    where:

    <crypto_pass>
    Specifies a user-defined passphrase to use when encrypting data for the OpenID Connect handshake.
    <keystone_url>
    Specifies the Identity service endpoint value that you retrieved in step 3.
    <idp_name_1>
    Specifies the unique IdP name that you specified in step 1.
    <idp_name_2>
    Specifies the unique IdP name that you specified in step 1.
  6. Create the keystone-httpd-override secret:
    $ oc create -f keystone-httpd-override.yaml
  7. Retrieve the URL for the Dashboard:
    $ oc get horizons.horizon.openstack.org -o json | jq -r '.items[0].status.endpoint'
  8. Use the following Ansible Playbook to create a secret called federation-realm-data:
    - name: Download realm1 OpenID configuration
      ansible.builtin.uri:
        url: "<provider_metadata_url_1>"
        method: GET
        return_content: true
        validate_certs: false
      register: openid_wellknown_config1
    
    - name: Download realm2 OpenID configuration
      ansible.builtin.uri:
        url: "<provider_metadata_url_2>"
        method: GET
        return_content: true
        validate_certs: false
      register: openid_wellknown_config2
    
    - name: Set federation_config_items
      ansible.builtin.set_fact:
        federation_config_items:
          - filename: "<fqdn_1>%2Fauth%2Frealms%2F<realm_name_1>.conf"
            contents: |
              {
                "scope" : "openid email profile"
              }
          - filename: "<fqdn_1>%2Fauth%2Frealms%2F<realm_name_1>.client"
            contents: "{{ {'client_id': <client_id_1>, 'client_secret': <client_secret_1> } | to_json }}"
          - filename: "<fqdn_1>%2Fauth%2Frealms%2F<realm_name_1>.provider"
            contents: |
              {{ openid_wellknown_config1.content }}
          - filename: "<fqdn_2>%2Fauth%2Frealms%2F<realm_name_2>.conf"
            contents: |
              {
                "scope" : "openid email profile"
              }
          - filename: "<fqdn_2>%2Fauth%2Frealms%2F<realm_name_2>.client"
            contents: "{{ {'client_id': <client_id_2>, 'client_secret': <client_secret_2>} | to_json }}"
          - filename: "<fqdn_2>%2Fauth%2Frealms%2F<realm_name_2>.provider"
            contents: |
              {{ openid_wellknown_config2.content }}
    - name: Generate the final federation_config.json string (as a dictionary)
      ansible.builtin.set_fact:
        _raw_federation_config_json_value: |
          {
          {% for item in federation_config_items %}
            "{{ item.filename }}": {{ item.contents }}{% if not loop.last %},{% endif %}
          {% endfor %}
          }
    - name: Final JSON string for Secret stringData
      ansible.builtin.set_fact:
        federation_config_json_string: "{{ _raw_federation_config_json_value }}"
    
    - name: Create a Kubernetes Secret with federation metadata
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Secret
          type: Opaque
          metadata:
            name: federation-realm-data
            namespace: openstack
          stringData:
            federation-config.json: "{{ federation_config_json_string }}"
    • Replace the IdP variables with the values you obtained from the IdP administrators in step 2.
  9. Open your OpenStackControlPlane custom resource (CR) file, openstack_control_plane.yaml, on your workstation.
  10. Edit the keystone section of the OpenStackControlPlane CR:
    • Remove external from the methods = comma-delimited list.
    • Add the httpdCustomization.customConfigSecret parameter and set this value to the key created in the keystone-httpd-override.yaml CR file in step 5.
    • Add the httpdCustomization.federatedRealmConfig parameter and set this value to the federation-realm-data secret created by the Ansible Playbook in step 8.
      keystone:
       template:
         customServiceConfig: |
           [federation]
           trusted_dashboard=<horizon_endpoint>/dashboard/auth/websso/
           [openid]
           remote_id_attribute=HTTP_OIDC_ISS
           [auth]
           methods = password,token,oauth1,mapped,application_credential,openid
         httpdCustomization:
           customConfigSecret: keystone-httpd-override
           federatedRealmConfig: federation-realm-data

      where:

      <horizon_endpoint>
      Specifies the Dashboard URL you retrieved in step 7.
  11. Edit the horizon section of the OpenStackControlPlane CR:
    horizon:
     template:
     customServiceConfig: |
       # Point horizon to the keystone public endpoint
       OPENSTACK_KEYSTONE_URL = "<keystone_endpoint>/v3"
    
       # Enable WebSSO in horizon
       WEBSSO_ENABLED = True
    
       # Provide login options in the horizon dropdown menu
       WEBSSO_CHOICES = (
         ("credentials", _("Keystone Credentials")),
         ("OIDC1", _("OpenID Connect IdP1")),
         ("OIDC2", _("OpenID Connect IdP2")),
       )
    
       # Map the "OIDC" choice of horizon to the keystone IDP and protocol
       WEBSSO_IDP_MAPPING = {
         "OIDC1": ("<idp_name1>", "openid"),
         "OIDC2": ("<idp_name2>", "openid"),
       }
  12. Update the control plane:
    $ oc apply -f openstack_control_plane.yaml
  13. Optional: If your federated users obtain their project roles through mapped groups and you must create OpenStack Orchestration service (heat) stacks or use other trust-based workflows, then specify the authorization TTL for each IdP:
    $ openstack identity provider set --authorization-ttl <ttl_minutes> <idp_name_1>
    $ openstack identity provider set --authorization-ttl <ttl_minutes> <idp_name_2>

    where:

    <ttl_minutes>

    Specifies the time in minutes that the Identity service retains the group memberships of a federated user. Set the shortest TTL that covers the interval between the user logging in and the trust-based operation completing.

    Note
    Each federated login retains the memberships for another full TTL window.