You are here

protected static function SamlService::reformatConfig in SAML Authentication 4.x

Same name and namespace in other branches
  1. 8.3 src/SamlService.php \Drupal\samlauth\SamlService::reformatConfig()
  2. 8.2 src/SamlService.php \Drupal\samlauth\SamlService::reformatConfig()

Returns a configuration array as used by the external library.

Some of these arguments are just added because the method is static (which will change in v4.x).

Parameters

\Drupal\Core\Config\ImmutableConfig $config: The module configuration.

string $base_url: (Optional) base URL to set.

string $purpose: (Optional) purpose for the config: 'metadata' / 'login' / 'acs' / 'logout' / 'sls-request' / 'sls-response'.

\Drupal\key\KeyRepositoryInterface $key_repository: (Optional) the service's Key repository.

Return value

array The library configuration array.

1 call to SamlService::reformatConfig()
SamlService::getSamlAuth in src/SamlService.php
Returns an initialized Auth class from the SAML Toolkit.

File

src/SamlService.php, line 898

Class

SamlService
Governs communication between the SAML toolkit and the IdP / login behavior.

Namespace

Drupal\samlauth

Code

protected static function reformatConfig(ImmutableConfig $config, $base_url = '', $purpose = '', KeyRepositoryInterface $key_repository = NULL) {
  $library_config = [
    'debug' => (bool) $config
      ->get('debug_phpsaml'),
    'sp' => [
      'entityId' => $config
        ->get('sp_entity_id'),
      'assertionConsumerService' => [
        // See ExecuteInRenderContextTrait if curious why the long chained
        // call is necessary.
        'url' => Url::fromRoute('samlauth.saml_controller_acs', [], [
          'absolute' => TRUE,
        ])
          ->toString(TRUE)
          ->getGeneratedUrl(),
      ],
      'singleLogoutService' => [
        'url' => Url::fromRoute('samlauth.saml_controller_sls', [], [
          'absolute' => TRUE,
        ])
          ->toString(TRUE)
          ->getGeneratedUrl(),
      ],
      'NameIDFormat' => $config
        ->get('sp_name_id_format') ?: NULL,
    ],
    'idp' => [
      'entityId' => $config
        ->get('idp_entity_id'),
      'singleSignOnService' => [
        'url' => $config
          ->get('idp_single_sign_on_service'),
      ],
      'singleLogoutService' => [
        'url' => $config
          ->get('idp_single_log_out_service'),
      ],
    ],
    'security' => [
      // Used for metadata (adds a whole signature section):
      'signMetadata' => (bool) $config
        ->get('security_metadata_sign'),
      // Used for metadata (value goes into attribute
      // AuthnRequestsSigned="true/false" of SPSSODescriptor) / login(*):
      'authnRequestsSigned' => (bool) $config
        ->get('security_authn_requests_sign'),
      // Used for logout(*):
      'logoutRequestSigned' => (bool) $config
        ->get('security_logout_requests_sign'),
      // Used for SLO response, sent after processing incoming SLO request(*):
      'logoutResponseSigned' => (bool) $config
        ->get('security_logout_responses_sign'),
      // Used for logout; also influences Settings:__construct() checks for
      // presence of IDP cert:
      'nameIdEncrypted' => (bool) $config
        ->get('security_nameid_encrypt'),
      // Used for acs:
      // TRUE by default AND must be TRUE on existing installations that
      // didn't have the setting before, so it's the first one to get a
      // default value. (If we didn't have the (bool) operator, we wouldn't
      // necessarily need the default - but leaving it out would just invite
      // a bug later on.)
      'wantNameId' => (bool) ($config
        ->get('security_want_name_id') ?? TRUE),
      // Used for login / acs(*); indirectly influences metadata(**):
      'wantNameIdEncrypted' => (bool) $config
        ->get('security_nameid_encrypted'),
      // Used for acs(*); indirectly influences metadata(**):
      'wantAssertionsEncrypted' => (bool) $config
        ->get('security_assertions_encrypt'),
      // Used for metadata (value goes into attribute
      // WantAssertionsSigned="true/false" of SPSSODescriptor) / acs(*):
      'wantAssertionsSigned' => (bool) $config
        ->get('security_assertions_signed'),
      // Used for acs / sls (processing incoming SLO responses and requests):
      'wantMessagesSigned' => (bool) $config
        ->get('security_messages_sign'),
      // Used for login:
      'requestedAuthnContext' => (bool) $config
        ->get('security_request_authn_context'),
      // Used for login / logout / SLO response, sent after processing
      // incoming SLO request; should be deprecated:
      'lowercaseUrlencoding' => (bool) $config
        ->get('security_lowercase_url_encoding'),
    ],
    'strict' => (bool) $config
      ->get('strict'),
  ];
  $sig_alg = $config
    ->get('security_signature_algorithm');
  if ($sig_alg) {
    $library_config['security']['signatureAlgorithm'] = $sig_alg;
  }
  $enc_alg = $config
    ->get('security_encryption_algorithm');
  if ($enc_alg) {

    // Not a typo; this is snake_case in the library too.
    $library_config['security']['encryption_algorithm'] = $enc_alg;
  }
  if ($base_url) {
    $library_config['baseurl'] = $base_url;
  }

  // We want to read cert/key values from whereever they are stored, only
  // when we actually need them. This may lead to us creating a custom
  // \OneLogin\Saml2\Settings child class that contains the logic of 'just in
  // time' reading key/cert values from storage. However (leaving aside the
  // fact that we can't do that in v3.x because it will break compatibility),
  // - the Auth class cannot use Settings subclasses: its constructor only
  //   accepts an array and its $_settings variable is private (so we cannot
  //   get around this by subclassing Auth).
  // - while the design of Settings supports 'just in time reading', (and the
  //   Settings class even does it, in some cases), its setup _in principle_
  //   isn't well suited for handling that. So if we end up doing this, we'll
  //   need to add good comments about e.g. getSPData() not returning
  //   complete data, format*() not working, ... (Basically, my conclusion is
  //   like my conclusion elsewhere about the Auth object: from the design of
  //   the Settings object it isn't clear whether it wants to be a value
  //   object or whether it wants to contain the logic of how to read the
  //   key/cert values. It would be nice if the code was... clearer.)
  // So for now, we are adding logic to this method that 'knows' when the key
  // / certs are used.
  $add_key = $add_cert = $add_idp_cert = TRUE;
  $add_new_cert = in_array($purpose, [
    'metadata',
    '',
  ]);
  $add_idp_encryption_cert = FALSE;
  switch ($purpose) {
    case 'metadata':

      // signMetadata / wantNameIdEncrypted are not implemented yet but that
      // doesn't mean we can't already use them here: they potentially
      // influence metadata but then we can't turn off $add_key.
      $add_key = !empty($library_config['security']['signMetadata']) || !empty($library_config['security']['wantNameIdEncrypted']) || !empty($library_config['security']['wantAssertionsEncrypted']);

      // We cannot prevent Settings::checkIdPSettings() from being called
      // while using the standard Auth class, so cannot do this:
      // $add_idp_cert = FALSE;.
      // @todo this is a good enough reason for opening a PR to amend
      //   Auth::construct() (to accept a Settings that was constructed with
      //   (, TRUE)), regardless of considerations outlined in AuthVolatile.
      break;
    case 'login':

      // We don't need a cert for operation; we just need to set it to a
      // value if certain security settings are set (which need the private
      // key), or otherwise checkSPCerts() will freak out.
      $add_cert = !empty($library_config['security']['authnRequestsSigned']) || !empty($library_config['security']['wantNameIdEncrypted']) ? 'FAKE' : FALSE;

      // We cannot prevent Settings::checkIdPSettings() from being called
      // while using the standard Auth class, so cannot do this:
      // $add_idp_cert = FALSE;.
      break;
    case 'logout':

      // We don't need a cert for operation; we just need to set it to a
      // value if certain security settings are set (which need the private
      // key), or otherwise checkSPCerts() will freak out.
      $add_cert = !empty($library_config['security']['logoutRequestSigned']) ? 'FAKE' : FALSE;

      // We cannot prevent Settings::checkIdPSettings() from being called
      // while using the standard Auth class, so cannot do this:
      // $add_idp_cert=!empty($library_config['security']['nameIdEncrypted']);
      // ^ This would also need the 2nd parameter to Settings::__construct()
      // to be !$add_idp_cert.
      // That just means we should generally add at least 1 IdP cert, though.
      // We don't need to add the encryption cert specifically - only if:
      $add_idp_encryption_cert = !empty($library_config['security']['nameIdEncrypted']);
      break;
    case 'acs':

      // $add_key depends on the presence of any EncryptedAssertion elements,
      // which is too bothersome for us to check (and which is a reason to
      // have the key reading inside a child Settings object instead) so
      // we'll assume it's TRUE. Same with $add_idp_cert which depends on
      // the presence of various signed elements.
      $add_cert = !empty($library_config['security']['wantAssertionsEncrypted']) || !empty($library_config['security']['wantAssertionsSigned']) || !empty($library_config['security']['wantNameIdEncrypted']) ? 'FAKE' : FALSE;
      break;
    case 'sls-request':

      // We need to both interpret the incoming request and probably create a
      // new outgoing one, so we need key and cert.
      // We cannot prevent Settings::checkIdPSettings() from being called
      // while using the standard Auth class, so cannot do this:
      // $add_idp_cert = isset($_GET['Signature']); // This would also need
      // the 2nd parameter to Settings::__construct() to be !$add_idp_cert.
      break;
    case 'sls-response':
      $add_key = $add_cert = FALSE;
  }
  if (!$add_key || !$add_cert) {

    // If these are set, checkSPCerts() will get called, which requires key +
    // cert presence. Neither of these two variables are (should have been)
    // set to FALSE if any of these security settings is both true and
    // relevant for our $purpose. (The full logic including the switch{}
    // implies the library default is FALSE for all these security settings.)
    unset($library_config['security']['authnRequestsSigned']);
    unset($library_config['security']['logoutRequestSigned']);
    unset($library_config['security']['logoutResponseSigned']);
    unset($library_config['security']['wantAssertionsEncrypted']);
    unset($library_config['security']['wantNameIdEncrypted']);
  }
  if (!$add_idp_cert) {

    // Same for IdP cert.
    unset($library_config['security']['nameIdEncrypted']);
  }

  // Check if we want to load the certificates from a folder. Either folder
  // or cert+key settings should be defined. If both are defined, "folder" is
  // the preferred method and we ignore cert/path values; we don't do more
  // complicated validation like checking whether the cert/key files exist.
  // Initializing cert/key properties to '' (rather than not setting them at
  // all) should be fine, because the standard Settings also does that.
  $cert_folder = $config
    ->get('sp_cert_folder');
  if ($cert_folder) {

    // Set the folder so the SAML toolkit knows where to look. This is the
    // old method which only reads key/cert when we actually need it, because
    // the logic for it is inside the Settings class. We're phasing it out in
    // favor of the hardcoded above $add_* logic and reading the key/cert
    // files inside this function, because this old method relies on a
    // specific hardcoded folder name / file names in the Settings class.
    // @todo remove in 4.x: not applicable after samlauth_update_8304().
    if (!defined('ONELOGIN_CUSTOMPATH')) {
      define('ONELOGIN_CUSTOMPATH', "{$cert_folder}/");
    }
  }
  else {
    if ($add_key) {
      $key = $config
        ->get('sp_private_key');
      if (isset($key) && !is_string($key)) {
        throw new SamlError('SP private key setting is not a string.', SamlError::SETTINGS_INVALID);
      }
      $type = strstr($key, ':', TRUE);
      if ($type === 'key') {
        if ($key_repository) {
          $key = substr($key, 4);
          $key_entity = $key_repository
            ->getKey($key);
          if (!$key_entity) {
            throw new SamlError("SP private key '{$key}' not found.", SamlError::SETTINGS_INVALID);
          }
          $key = $key_entity
            ->getKeyValue();
        }
        else {
          throw new SamlError('SP private key setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
        }
      }
      elseif ($type === 'file') {
        $key = file_get_contents(substr($key, 5));
        if ($key === FALSE) {
          throw new SamlError('SP private key not found.', SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
        }
      }
      $library_config['sp']['privateKey'] = $key;
    }
    if ($add_cert) {
      $cert = $add_cert === 'FAKE' ? 'dummy-value-to-subvert-validation' : $config
        ->get('sp_x509_certificate');
      if (isset($cert) && !is_string($cert)) {
        throw new SamlError('SP public cert setting is not a string.', SamlError::SETTINGS_INVALID);
      }
      if ($cert) {
        $type = strstr($cert, ':', TRUE);
        if ($type === 'key') {
          if ($key_repository) {
            $cert = substr($cert, 4);
            $key_entity = $key_repository
              ->getKey($cert);
            if (!$key_entity) {
              throw new SamlError("SP public cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
            }
            $cert = $key_entity
              ->getKeyValue();
          }
          else {
            throw new SamlError('SP public cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
          }
        }
        elseif ($type === 'file') {
          $cert = file_get_contents(substr($cert, 5));
          if ($cert === FALSE) {
            throw new SamlError('SP public cert not found.', SamlError::PUBLIC_CERT_FILE_NOT_FOUND);
          }
        }
        $library_config['sp']['x509cert'] = $cert;
      }
    }
    if ($add_new_cert) {
      $cert = $config
        ->get('sp_new_certificate');
      if (isset($cert) && !is_string($cert)) {
        throw new SamlError('SP new public cert setting is not a string.', SamlError::SETTINGS_INVALID);
      }
      if ($cert) {
        $type = strstr($cert, ':', TRUE);
        if ($type === 'key') {
          if ($key_repository) {
            $cert = substr($cert, 4);
            $key_entity = $key_repository
              ->getKey($cert);
            if (!$key_entity) {
              throw new SamlError("SP new public cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
            }
            $cert = $key_entity
              ->getKeyValue();
          }
          else {
            throw new SamlError('SP new public cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
          }
        }
        elseif ($type === 'file') {
          $cert = file_get_contents(substr($cert, 5));
          if ($cert === FALSE) {
            throw new SamlError('SP new public cert not found.', SamlError::PUBLIC_CERT_FILE_NOT_FOUND);
          }
        }
        $library_config['sp']['x509certNew'] = $cert;
      }
    }
  }
  $encryption_cert = '';
  $certs = [];
  if ($add_idp_encryption_cert) {
    $encryption_cert = $config
      ->get('idp_cert_encryption');
    if (isset($encryption_cert) && !is_string($encryption_cert)) {
      throw new SamlError('IdP encryption cert setting is not a string.', SamlError::SETTINGS_INVALID);
    }
    $type = strstr($encryption_cert, ':', TRUE);
    if ($type === 'key') {
      if ($key_repository) {
        $encryption_cert = substr($encryption_cert, 4);
        $key_entity = $key_repository
          ->getKey($encryption_cert);
        if (!$key_entity) {
          throw new SamlError("IdP encryption cert '{$encryption_cert}' not found.", SamlError::SETTINGS_INVALID);
        }
        $encryption_cert = $key_entity
          ->getKeyValue();
      }
      else {
        throw new SamlError('IdP encryption cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
      }
    }
    elseif ($type === 'file') {
      $encryption_cert = file_get_contents(substr($encryption_cert, 5));
      if ($encryption_cert === FALSE) {
        throw new SamlError('IdP encryption cert not found.', SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
      }
    }
  }
  if ($add_idp_cert || $add_idp_encryption_cert && !$encryption_cert) {
    $certs = $config
      ->get('idp_certs');
    foreach ($certs as $i => $cert) {
      if (isset($certs[$i]) && !is_string($certs[$i])) {
        $nr = $i ? " {$i}" : '';
        throw new SamlError("IdP cert setting{$nr} is not a string.", SamlError::SETTINGS_INVALID);
      }
      $type = strstr($cert, ':', TRUE);
      if ($type === 'key') {
        if ($key_repository) {
          $cert = substr($cert, 4);
          $key_entity = $key_repository
            ->getKey($cert);
          if (!$key_entity) {
            throw new SamlError("IdP cert '{$cert}' not found.", SamlError::SETTINGS_INVALID);
          }
          $certs[$i] = $key_entity
            ->getKeyValue();
        }
        else {
          throw new SamlError('IdP cert setting is of type "key" but the Key module is not installed.', SamlError::SETTINGS_INVALID);
        }
      }
      elseif ($type === 'file') {
        $certs[$i] = file_get_contents(substr($cert, 5));
        if ($certs[$i] === FALSE) {
          $nr = $i ? " {$i}" : '';
          throw new SamlError("IdP cert{$nr} not found.", SamlError::PRIVATE_KEY_FILE_NOT_FOUND);
        }
      }
    }
  }

  // @todo remove in 4.x: not applicable after samlauth_update_8304().
  // @todo at the same time as removing this, uncomment samlauth_update_8400.
  if (!$certs && !$encryption_cert) {
    $old_cert = $config
      ->get('idp_x509_certificate');
    $old_cert_multi = $config
      ->get('idp_x509_certificate_multi');
    if ($old_cert || $old_cert_multi) {
      $certs = $old_cert ? [
        $old_cert,
      ] : [];
      if ($old_cert_multi) {
        if ($config
          ->get('idp_cert_type') === 'encryption') {
          $encryption_cert = $old_cert_multi;
        }
        else {
          $certs[] = $old_cert_multi;
        }
      }
    }
  }

  // If we don't set a separate 'x509certMulti > encryption' cert, the
  // 'main' cert (not 'x509certMulti > signing') is used for encryption so
  // it must be set. If we have a single 'main/signing' cert, we can set it
  // in either 'x509certMulti > signing' or as the main cert - both is not
  // necessary. This can be encoded in several arbitrary ways, e.g.:
  if ($encryption_cert) {
    $library_config['idp']['x509certMulti'] = [
      // This is an array, but the library never uses anything but the
      // first value.
      'encryption' => [
        $encryption_cert,
      ],
      'signing' => $certs,
    ];
  }
  elseif ($certs) {
    if (count($certs) == 1 || $add_idp_encryption_cert) {
      $library_config['idp']['x509cert'] = reset($certs);
    }
    if (count($certs) > 1) {
      $library_config['idp']['x509certMulti']['signing'] = $certs;
    }
  }
  return $library_config;
}