diff --git a/saml/README.md b/saml/README.md new file mode 100755 index 00000000..6a2ee8f7 --- /dev/null +++ b/saml/README.md @@ -0,0 +1,17 @@ +SAML Addon +============= + +This addon replaces the normal login and registration mechanism with SSO and SLO via a SAML identity provider. + +New users are created in the Friendica database when they log in via SAML for the first time. They are given a random password at least 24 characters long. + +SAML users with the same usernames/nicknames as existing users will be able to log in as those existing users. Make sure to create SAML accounts for any existing users before activating this addon, or you'll create a situation where a person may claim someone else's account by registering a SAML account with their username. + +SSO is triggered when the user visits the Friendica homepage while logged out. + +If using KeyCloak as your IdP, make sure the "role_list" scope is either set up to return a single "Role" attribute or to not return one at all. (This addon doesn't need it.) The SAML library used here does not allow multiple attributes with the same name. + +To remove the "role_list" from your client in Keycloak, edit the client you created for this addon, click the "Client Scopes" tab, select "role_list" under "Assigned Default Client Scopes," and click "Remove Selected." + +For more details on the Keycloak "role_list" issue: +https://help.nextcloud.com/t/solved-nextcloud-saml-keycloak-as-identity-provider-issues/19293/9 diff --git a/saml/composer.json b/saml/composer.json new file mode 100644 index 00000000..2418c2f1 --- /dev/null +++ b/saml/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "onelogin/php-saml": "^4.0", + "robrichards/xmlseclibs": "^3.1" + } +} diff --git a/saml/composer.lock b/saml/composer.lock new file mode 100644 index 00000000..4ae19c1b --- /dev/null +++ b/saml/composer.lock @@ -0,0 +1,117 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3b4cc13e4ad8a08e53069e39d86dd86f", + "packages": [ + { + "name": "onelogin/php-saml", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/onelogin/php-saml.git", + "reference": "f30f5062f3653c4d2082892d207f4dc3e577d979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/f30f5062f3653c4d2082892d207f4dc3e577d979", + "reference": "f30f5062f3653c4d2082892d207f4dc3e577d979", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "robrichards/xmlseclibs": ">=3.1.1" + }, + "require-dev": { + "pdepend/pdepend": "^2.8.0", + "php-coveralls/php-coveralls": "^2.0", + "phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpunit/phpunit": "^9.5", + "sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ", + "squizlabs/php_codesniffer": "^3.5.8" + }, + "suggest": { + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-dom": "Install xml lib", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)", + "ext-zlib": "Install zlib" + }, + "type": "library", + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "OneLogin PHP SAML Toolkit", + "homepage": "https://developers.onelogin.com/saml/php", + "keywords": [ + "SAML2", + "onelogin", + "saml" + ], + "support": { + "email": "sixto.garcia@onelogin.com", + "issues": "https://github.com/onelogin/php-saml/issues", + "source": "https://github.com/onelogin/php-saml/" + }, + "time": "2021-03-02T10:19:19+00:00" + }, + { + "name": "robrichards/xmlseclibs", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/robrichards/xmlseclibs/issues", + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1" + }, + "time": "2020-09-05T13:00:25+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.0.0" +} diff --git a/saml/saml.css b/saml/saml.css new file mode 100644 index 00000000..087633cd --- /dev/null +++ b/saml/saml.css @@ -0,0 +1 @@ +#settings-form > div:first-of-type, #settings-form > h2:first-of-type, #wrapper_mpassword, #wrapper_email { display: none !important; } diff --git a/saml/saml.php b/saml/saml.php new file mode 100755 index 00000000..03f4bffe --- /dev/null +++ b/saml/saml.php @@ -0,0 +1,428 @@ + + */ +use Friendica\Core\Hook; +use Friendica\Core\Logger; +use Friendica\Core\Renderer; +use Friendica\Core\Session; +use Friendica\Database\DBA; +use Friendica\DI; +use Friendica\Model\User; +use Friendica\Util\Strings; +use OneLogin\Saml2\Auth; + +require_once(__DIR__ . '/vendor/autoload.php'); + +define("PW_LEN", 32); // number of characters to use for random passwords + +function saml_module($a) {} + +function saml_init($a) { + if ($a->argc < 2) return; + + switch ($a->argv[1]) { + case "metadata.xml": + saml_metadata(); + break; + case "sso": + saml_sso_reply($a); + break; + case "slo": + saml_slo_reply(); + break; + case "moo": + echo DI::baseUrl(); + echo $_SERVER['REQUEST_URI']; + break; + } + exit(); +} + +function saml_metadata() { + try { + $settings = new \OneLogin\Saml2\Settings(saml_settings()); + $metadata = $settings->getSPMetadata(); + $errors = $settings->validateMetadata($metadata); + + if (empty($errors)) { + header('Content-Type: text/xml'); + echo $metadata; + } else { + throw new \OneLogin\Saml2\Error( + 'Invalid SP metadata: '.implode(', ', $errors), + \OneLogin\Saml2\Error::METADATA_SP_INVALID + ); + } + } catch (Exception $e) { + Logger::error($e->getMessage()); + } +} + +function saml_install() { + Hook::register('login_hook', 'addon/saml/saml.php', 'saml_sso_initiate'); + Hook::register('logging_out', 'addon/saml/saml.php', 'saml_slo_initiate'); + Hook::register('head', 'addon/saml/saml.php', 'saml_head'); + Hook::register('footer', 'addon/saml/saml.php', 'saml_footer'); +} + +function saml_head(&$a, &$b) { + DI::page()->registerStylesheet(__DIR__ . '/saml.css'); +} + +function saml_footer(&$a, &$b) { + $fragment = addslashes(DI::config()->get('saml', 'settings_statement')); + $b .= << +var target=$("#settings-nickname-desc"); +if (target.length) { target.append("$fragment"); } + +EOL; +} + +function saml_is_configured() { + return + DI::config()->get('saml', 'idp_id') && + DI::config()->get('saml', 'client_id') && + DI::config()->get('saml', 'sso_url') && + DI::config()->get('saml', 'slo_request_url') && + DI::config()->get('saml', 'slo_response_url') && + DI::config()->get('saml', 'sp_key') && + DI::config()->get('saml', 'sp_cert') && + DI::config()->get('saml', 'idp_cert'); + +} + +function saml_sso_initiate(&$a, &$b) { + if (!saml_is_configured()) return; + + $auth = new \OneLogin\Saml2\Auth(saml_settings()); + $ssoBuiltUrl = $auth->login(null, array(), false, false, true); + $_SESSION['AuthNRequestID'] = $auth->getLastRequestID(); + header('Pragma: no-cache'); + header('Cache-Control: no-cache, must-revalidate'); + header('Location: ' . $ssoBuiltUrl); + exit(); +} + +function saml_sso_reply($a) { + $auth = new \OneLogin\Saml2\Auth(saml_settings()); + $requestID = null; + + if (isset($_SESSION) && isset($_SESSION['AuthNRequestID'])) { + $requestID = $_SESSION['AuthNRequestID']; + } + + $auth->processResponse($requestID); + unset($_SESSION['AuthNRequestID']); + + $errors = $auth->getErrors(); + + if (!empty($errors)) { + echo "Errors encountered."; + exit(); + } + + if (!$auth->isAuthenticated()) { + echo "Not authenticated"; + exit(); + } + + $username = $auth->getNameId(); + $email = $auth->getAttributeWithFriendlyName('email')[0]; + $name = $auth->getAttributeWithFriendlyName('givenName')[0]; + $last_name = $auth->getAttributeWithFriendlyName('surname')[0]; + + if (strlen($last_name)) { + $name .= " $last_name"; + } + + if (!DBA::exists('user', ['nickname' => $username])) { + $user = saml_create_user($username, $email, $name); + } else { + $user = User::getByNickname($username); + } + + if (!empty($user['uid'])) { + DI::auth()->setForUser($a, $user); + } + + if (isset($_POST['RelayState']) + && \OneLogin\Saml2\Utils::getSelfURL() != $_POST['RelayState']) + { + $auth->redirectTo($_POST['RelayState']); + } +} + +function saml_slo_initiate(&$a, &$b) { + $auth = new \OneLogin\Saml2\Auth(saml_settings()); + + $sloBuiltUrl = $auth->logout(); + $_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); + header('Pragma: no-cache'); + header('Cache-Control: no-cache, must-revalidate'); + header('Location: ' . $sloBuiltUrl); + exit(); +} + +function saml_slo_reply() { + $auth = new \OneLogin\Saml2\Auth(saml_settings()); + + if (isset($_SESSION) && isset($_SESSION['LogoutRequestID'])) { + $requestID = $_SESSION['LogoutRequestID']; + } else { + $requestID = null; + } + + $auth->processSLO(false, $requestID); + + $errors = $auth->getErrors(); + + if (empty($errors)) { + $auth->redirectTo(DI::baseUrl()); + } else { + Logger::error(implode(', ', $errors)); + } +} + +function saml_input($key, $label, $description) { + return [ + '$' . $key => [ + $key, + DI::l10n()->t($label), + DI::config()->get('saml', $key), + DI::l10n()->t($description), + true, // all the fields are required + ] + ]; +} + +function saml_addon_admin (&$a, &$o) { + $form = + saml_input( + 'settings_statement', + 'Settings statement', + 'A statement on the settings page explaining where the user should go to change their e-mail and password. HTML allowed.' + ) + + saml_input( + 'idp_id', + 'IdP ID', + 'Identity provider (IdP) entity URI (e.g., https://example.com/auth/realms/user).' + ) + + saml_input( + 'client_id', + 'Client ID', + 'Identifier assigned to client by the identity provider (IdP).' + ) + + saml_input( + 'sso_url', + 'IdP SSO URL', + 'The URL for your identity provider\'s SSO endpoint.' + ) + + saml_input( + 'slo_request_url', + 'IdP SLO request URL', + 'The URL for your identity provider\'s SLO request endpoint.' + ) + + saml_input( + 'slo_response_url', + 'IdP SLO response URL', + 'The URL for your identity provider\'s SLO response endpoint.' + ) + + saml_input( + 'sp_key', + 'SP private key', + 'The private key the addon should use to authenticate.' + ) + + saml_input( + 'sp_cert', + 'SP certificate', + 'The certficate for the addon\'s private key.' + ) + + saml_input( + 'idp_cert', + 'IdP certificate', + 'The x509 certficate for your identity provider.' + ) + + [ + '$submit' => DI::l10n()->t('Save Settings'), + ]; + $t = Renderer::getMarkupTemplate( "admin.tpl", "addon/saml/" ); + $o = Renderer::replaceMacros( $t, $form); +} + +function saml_addon_admin_post (&$a) { + $safeset = function ($key) { + $val = (!empty($_POST[$key]) ? Strings::escapeTags(trim($_POST[$key])) : ''); + DI::config()->set('saml', $key, $val); + }; + $safeset('idp_id'); + $safeset('client_id'); + $safeset('sso_url'); + $safeset('slo_request_url'); + $safeset('slo_response_url'); + $safeset('sp_key'); + $safeset('sp_cert'); + $safeset('idp_cert'); + + // Not using safeset here since settings_statement is *meant* to include HTML tags. + DI::config()->set('saml', 'settings_statement', $_POST['settings_statement']); +} + +function saml_create_user($username, $email, $name) { + if (!strlen($email) || !strlen($name)) { + Logger::error('Could not create user: no email or username given.'); + return false; + } + + try { + $strong = false; + $bytes = openssl_random_pseudo_bytes(intval(ceil(PW_LEN * 0.75)), $strong); + + if (!$strong) { + throw new Exception('Strong algorithm not available for PRNG.'); + } + + $user = User::create([ + 'username' => $name, + 'nickname' => $username, + 'email' => $email, + 'password' => base64_encode($bytes), // should be at least PW_LEN long + 'verified' => true + ]); + + return $user; + + } catch (Exception $e) { + Logger::error( + 'Exception while creating user', + [ + 'username' => $username, + 'email' => $email, + 'name' => $name, + 'exception' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + return false; + } +} + +function saml_settings() { + return array( + // If 'strict' is True, then the PHP Toolkit will reject unsigned + // or unencrypted messages if it expects them to be signed or encrypted. + // Also it will reject the messages if the SAML standard is not strictly + // followed: Destination, NameId, Conditions ... are validated too. + // Should never be set to anything else in production! + 'strict' => true, + + // Enable debug mode (to print errors). + 'debug' => false, + + // Set a BaseURL to be used instead of try to guess + // the BaseURL of the view that process the SAML Message. + // Ex http://sp.example.com/ + // http://example.com/sp/ + 'baseurl' => DI::baseUrl() . "/saml", + + // Service Provider Data that we are deploying. + 'sp' => array( + // Identifier of the SP entity (must be a URI) + 'entityId' => DI::config()->get('saml','client_id'), + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'assertionConsumerService' => array( + // URL Location where the from the IdP will be returned + 'url' => DI::baseUrl() . "/saml/sso", + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports this endpoint for the + // HTTP-POST binding only. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + ), + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be omitted + "attributeConsumingService"=> array( + "serviceName" => "Friendica SAML SSO and SLO Addon", + "serviceDescription" => "SLO and SSO support for Friendica", + "requestedAttributes" => array( + array( + "uid" => "", + "isRequired" => false, + ) + ) + ), + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'singleLogoutService' => array( + // URL Location where the from the IdP will be returned + 'url' => DI::baseUrl() . "/saml/slo", + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Specifies the constraints on the name identifier to be used to + // represent the requested subject. + // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported. + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + // Usually x509cert and privateKey of the SP are provided by files placed at + // the certs folder. But we can also provide them with the following parameters + 'x509cert' => DI::config()->get('saml','sp_cert'), + 'privateKey' => DI::config()->get('saml','sp_key'), + ), + + // Identity Provider Data that we want connected with our SP. + 'idp' => array( + // Identifier of the IdP entity (must be a URI) + 'entityId' => DI::config()->get('saml','idp_id'), + // SSO endpoint info of the IdP. (Authentication Request protocol) + 'singleSignOnService' => array( + // URL Target of the IdP where the Authentication Request Message + // will be sent. + 'url' => DI::config()->get('saml','sso_url'), + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // SLO endpoint info of the IdP. + 'singleLogoutService' => array( + // URL Location of the IdP where SLO Request will be sent. + 'url' => DI::config()->get('saml','slo_request_url'), + // URL location of the IdP where SLO Response will be sent (ResponseLocation) + // if not set, url for the SLO Request will be used + 'responseUrl' => DI::config()->get('saml','slo_response_url'), + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Public x509 certificate of the IdP + 'x509cert' => DI::config()->get('saml','idp_cert'), + ), + 'security' => array ( + 'wantXMLValidation' => false, + + // Indicates whether the messages sent by this SP + // will be signed. [Metadata of the SP will offer this info] + 'authnRequestsSigned' => true, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutRequestSigned' => true, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutResponseSigned' => true, + + /* Sign the Metadata */ + 'signMetadata' => true, + ) + ); +} +?> diff --git a/saml/templates/admin.tpl b/saml/templates/admin.tpl new file mode 100755 index 00000000..0ba4601d --- /dev/null +++ b/saml/templates/admin.tpl @@ -0,0 +1,36 @@ +{{include file="field_textarea.tpl" field=$settings_statement}} +{{include file="field_input.tpl" field=$idp_id}} +{{include file="field_input.tpl" field=$client_id}} +{{include file="field_input.tpl" field=$sso_url}} +{{include file="field_input.tpl" field=$slo_request_url}} +{{include file="field_input.tpl" field=$slo_response_url}} +{{include file="field_textarea.tpl" field=$sp_key}} +{{include file="field_textarea.tpl" field=$sp_cert}} +{{include file="field_textarea.tpl" field=$idp_cert}} + + +
diff --git a/saml/vendor/autoload.php b/saml/vendor/autoload.php new file mode 100644 index 00000000..c3e11136 --- /dev/null +++ b/saml/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + private $vendorDir; + + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/saml/vendor/composer/InstalledVersions.php b/saml/vendor/composer/InstalledVersions.php new file mode 100644 index 00000000..c71929ad --- /dev/null +++ b/saml/vendor/composer/InstalledVersions.php @@ -0,0 +1,301 @@ + + array ( + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'aliases' => + array ( + ), + 'reference' => '559c85775e78669a168f48a4302e807d213f6d9f', + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'aliases' => + array ( + ), + 'reference' => '559c85775e78669a168f48a4302e807d213f6d9f', + ), + 'onelogin/php-saml' => + array ( + 'pretty_version' => '4.0.0', + 'version' => '4.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'f30f5062f3653c4d2082892d207f4dc3e577d979', + ), + 'robrichards/xmlseclibs' => + array ( + 'pretty_version' => '3.1.1', + 'version' => '3.1.1.0', + 'aliases' => + array ( + ), + 'reference' => 'f8f19e58f26cdb42c54b214ff8a820760292f8df', + ), + ), +); +private static $canGetVendors; +private static $installedByVendor = array(); + + + + + + + +public static function getInstalledPackages() +{ +$packages = array(); +foreach (self::getInstalled() as $installed) { +$packages[] = array_keys($installed['versions']); +} + + +if (1 === \count($packages)) { +return $packages[0]; +} + +return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); +} + + + + + + + + + +public static function isInstalled($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (isset($installed['versions'][$packageName])) { +return true; +} +} + +return false; +} + + + + + + + + + + + + + + +public static function satisfies(VersionParser $parser, $packageName, $constraint) +{ +$constraint = $parser->parseConstraints($constraint); +$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + +return $provided->matches($constraint); +} + + + + + + + + + + +public static function getVersionRanges($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +$ranges = array(); +if (isset($installed['versions'][$packageName]['pretty_version'])) { +$ranges[] = $installed['versions'][$packageName]['pretty_version']; +} +if (array_key_exists('aliases', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); +} +if (array_key_exists('replaced', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); +} +if (array_key_exists('provided', $installed['versions'][$packageName])) { +$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); +} + +return implode(' || ', $ranges); +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['version'])) { +return null; +} + +return $installed['versions'][$packageName]['version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getPrettyVersion($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['pretty_version'])) { +return null; +} + +return $installed['versions'][$packageName]['pretty_version']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getReference($packageName) +{ +foreach (self::getInstalled() as $installed) { +if (!isset($installed['versions'][$packageName])) { +continue; +} + +if (!isset($installed['versions'][$packageName]['reference'])) { +return null; +} + +return $installed['versions'][$packageName]['reference']; +} + +throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); +} + + + + + +public static function getRootPackage() +{ +$installed = self::getInstalled(); + +return $installed[0]['root']; +} + + + + + + + +public static function getRawData() +{ +return self::$installed; +} + + + + + + + + + + + + + + + + + + + +public static function reload($data) +{ +self::$installed = $data; +self::$installedByVendor = array(); +} + + + + +private static function getInstalled() +{ +if (null === self::$canGetVendors) { +self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); +} + +$installed = array(); + +if (self::$canGetVendors) { +foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { +if (isset(self::$installedByVendor[$vendorDir])) { +$installed[] = self::$installedByVendor[$vendorDir]; +} elseif (is_file($vendorDir.'/composer/installed.php')) { +$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; +} +} +} + +$installed[] = self::$installed; + +return $installed; +} +} diff --git a/saml/vendor/composer/LICENSE b/saml/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/saml/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/saml/vendor/composer/autoload_classmap.php b/saml/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..b26f1b13 --- /dev/null +++ b/saml/vendor/composer/autoload_classmap.php @@ -0,0 +1,10 @@ + $vendorDir . '/composer/InstalledVersions.php', +); diff --git a/saml/vendor/composer/autoload_namespaces.php b/saml/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..b7fc0125 --- /dev/null +++ b/saml/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($vendorDir . '/robrichards/xmlseclibs/src'), + 'OneLogin\\' => array($vendorDir . '/onelogin/php-saml/src'), +); diff --git a/saml/vendor/composer/autoload_real.php b/saml/vendor/composer/autoload_real.php new file mode 100644 index 00000000..617b7cd1 --- /dev/null +++ b/saml/vendor/composer/autoload_real.php @@ -0,0 +1,57 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit6ecac07d47dd9d108b36bee3eda76704::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/saml/vendor/composer/autoload_static.php b/saml/vendor/composer/autoload_static.php new file mode 100644 index 00000000..cc5e7f93 --- /dev/null +++ b/saml/vendor/composer/autoload_static.php @@ -0,0 +1,44 @@ + + array ( + 'RobRichards\\XMLSecLibs\\' => 23, + ), + 'O' => + array ( + 'OneLogin\\' => 9, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'RobRichards\\XMLSecLibs\\' => + array ( + 0 => __DIR__ . '/..' . '/robrichards/xmlseclibs/src', + ), + 'OneLogin\\' => + array ( + 0 => __DIR__ . '/..' . '/onelogin/php-saml/src', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit6ecac07d47dd9d108b36bee3eda76704::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit6ecac07d47dd9d108b36bee3eda76704::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit6ecac07d47dd9d108b36bee3eda76704::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/saml/vendor/composer/installed.json b/saml/vendor/composer/installed.json new file mode 100644 index 00000000..689f9b11 --- /dev/null +++ b/saml/vendor/composer/installed.json @@ -0,0 +1,110 @@ +{ + "packages": [ + { + "name": "onelogin/php-saml", + "version": "4.0.0", + "version_normalized": "4.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/onelogin/php-saml.git", + "reference": "f30f5062f3653c4d2082892d207f4dc3e577d979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/onelogin/php-saml/zipball/f30f5062f3653c4d2082892d207f4dc3e577d979", + "reference": "f30f5062f3653c4d2082892d207f4dc3e577d979", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "robrichards/xmlseclibs": ">=3.1.1" + }, + "require-dev": { + "pdepend/pdepend": "^2.8.0", + "php-coveralls/php-coveralls": "^2.0", + "phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "phpunit/phpunit": "^9.5", + "sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ", + "squizlabs/php_codesniffer": "^3.5.8" + }, + "suggest": { + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-dom": "Install xml lib", + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)", + "ext-zlib": "Install zlib" + }, + "time": "2021-03-02T10:19:19+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "OneLogin PHP SAML Toolkit", + "homepage": "https://developers.onelogin.com/saml/php", + "keywords": [ + "SAML2", + "onelogin", + "saml" + ], + "support": { + "email": "sixto.garcia@onelogin.com", + "issues": "https://github.com/onelogin/php-saml/issues", + "source": "https://github.com/onelogin/php-saml/" + }, + "install-path": "../onelogin/php-saml" + }, + { + "name": "robrichards/xmlseclibs", + "version": "3.1.1", + "version_normalized": "3.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", + "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "time": "2020-09-05T13:00:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/robrichards/xmlseclibs/issues", + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1" + }, + "install-path": "../robrichards/xmlseclibs" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/saml/vendor/composer/installed.php b/saml/vendor/composer/installed.php new file mode 100644 index 00000000..91dbd460 --- /dev/null +++ b/saml/vendor/composer/installed.php @@ -0,0 +1,42 @@ + + array ( + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'aliases' => + array ( + ), + 'reference' => '559c85775e78669a168f48a4302e807d213f6d9f', + 'name' => '__root__', + ), + 'versions' => + array ( + '__root__' => + array ( + 'pretty_version' => 'dev-develop', + 'version' => 'dev-develop', + 'aliases' => + array ( + ), + 'reference' => '559c85775e78669a168f48a4302e807d213f6d9f', + ), + 'onelogin/php-saml' => + array ( + 'pretty_version' => '4.0.0', + 'version' => '4.0.0.0', + 'aliases' => + array ( + ), + 'reference' => 'f30f5062f3653c4d2082892d207f4dc3e577d979', + ), + 'robrichards/xmlseclibs' => + array ( + 'pretty_version' => '3.1.1', + 'version' => '3.1.1.0', + 'aliases' => + array ( + ), + 'reference' => 'f8f19e58f26cdb42c54b214ff8a820760292f8df', + ), + ), +); diff --git a/saml/vendor/composer/platform_check.php b/saml/vendor/composer/platform_check.php new file mode 100644 index 00000000..92370c5a --- /dev/null +++ b/saml/vendor/composer/platform_check.php @@ -0,0 +1,26 @@ += 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + trigger_error( + 'Composer detected issues in your platform: ' . implode(' ', $issues), + E_USER_ERROR + ); +} diff --git a/saml/vendor/onelogin/php-saml/CHANGELOG b/saml/vendor/onelogin/php-saml/CHANGELOG new file mode 100644 index 00000000..27a4b2bd --- /dev/null +++ b/saml/vendor/onelogin/php-saml/CHANGELOG @@ -0,0 +1,318 @@ +CHANGELOG +========= +v4.0.0 +* Supports PHP 8.X + +v3.6.1 +* [#467](https://github.com/onelogin/php-saml/issues/467) Fix bug on getSelfRoutedURLNoQuery method + +v3.6.0 +* Add AES128_GCM encryption on generateNameId method. New setting parameter encryption_algorithm. If you set a encryption method different than AES128_CBC then the algorithm RSA_OAEP_MGF1P will be used as well instead RSA_1_5 +* PHP 8.0 support + +v3.5.1 +* 3.5.0 packagist/github release due a confusion were using the master (2.X branch). I'm releasing 3.5.1 to fix this issue and go back to 3.X branch + +v3.5.0 +* [#412](https://github.com/onelogin/php-saml/pull/412) Empty instead of unset the $_SESSION variable +* [#433](https://github.com/onelogin/php-saml/issues/443) Fix Incorrect Destination in LogoutResponse when using responseUrl #443 +* Update xmlseclibs to 3.1.1 +* Add support for SMARTCARD_PKI and RSA_TOKEN Auth Contexts +* Get lib path dinamically +* Check for x509Cert of the IdP when loading settings, even if the security index was not provided +* Support Statements with Attribute elements with the same name enabling the allowRepeatAttributeName setting + +v3.4.1 +* Add setSchemasPath to Auth class and fix backward compatibility + +v.3.4.0 +* Support rejecting unsolicited SAMLResponses. +* Support stric destination matching. +* Reject SAMLResponse if requestID was provided to the validotr but the InResponseTo attributeof the SAMLResponse is missing +* Check destination against the getSelfURLNoQuery as well on LogoutRequest and LogoutResponse as we do on Response +* Improve getSelfRoutedURLNoQuery method +* Only add responseUrl to the settings if ResponseLocation present in the IdPMetadataParser +* Remove use of $_GET on static method validateBinarySign +* Fix error message when Assertion and NameId are both encrypted (not supported) + +v.3.3.1 +* Update xmlseclibs to 3.0.4 +* Remove Comparison atribute from RequestedAuthnContext when setting has empty value + +v.3.3.0 +* Set true as the default value for strict setting +* Relax comparision of false on SignMetadata +* Fix CI + +v.3.2.1 +* Add missed nameIdValueReq parameter to buildAuthnRequest method + +v.3.2.0 +* Add support for Subjects on AuthNRequests by the new parameter nameIdValueReq +* Support SLO ResponseLocation +* [#344](https://github.com/onelogin/php-saml/issues/344) Raise errors on IdPMetadataParser::parseRemoteXML and IdPMetadataParser::parseFileXML +* [#356](https://github.com/onelogin/php-saml/issues/356) Support 'x509cert' and 'privateKey' on signMetadata security setting + +v.3.1.1 +* Force to use at least xmlseclibs 3.0.3 for security reasons +* [#367](https://github.com/onelogin/php-saml/pull/367) Move the creation of the AuthnRequest to separate function +* Set strict=true on config examples +* Move phpunit.xml + +v.3.1.0 +* Security improvement suggested by Nils Engelbertz to prevent DDOS by expansion of internally defined entities (XEE) +* Fix setting_example.php servicename parameter + +v.3.0.0 +* Remove mcrypt dependency. Compatible with PHP 7.2 +* xmlseclibs now is not part of the toolkit and need to be installed from original source + +v.2.18.0 +* Support rejecting unsolicited SAMLResponses. +* Support stric destination matching. +* Reject SAMLResponse if requestID was provided to the validotr but the InResponseTo attributeof the SAMLResponse is missing +* Check destination against the getSelfURLNoQuery as well on LogoutRequest and LogoutResponse as we do on Response +* Improve getSelfRoutedURLNoQuery method +* Only add responseUrl to the settings if ResponseLocation present in the IdPMetadataParser +* Remove use of $_GET on static method validateBinarySign +* Fix error message when Assertion and NameId are both encrypted (not supported) + +v.2.17.1 +* Update xmlseclibs to 3.0.4 +* Remove Comparison atribute from RequestedAuthnContext when setting has empty value + +v.2.17.0 +* Set true as the default value for strict setting +* Support 'x509cert' and 'privateKey' on signMetadata security settings +* Relax comparision of false on SignMetadata +* Fix CI + +v.2.16.0 +* Support SLO ResponseLocation +* [#344](https://github.com/onelogin/php-saml/issues/344) Raise errors on IdPMetadataParser::parseRemoteXML and IdPMetadataParser::parseFileXML +* Adjusted acs endpoint to extract NameQualifier and SPNameQualifier from SAMLResponse. Adjusted single logout service to provide NameQualifier and SPNameQualifier to logout method. Add getNameIdNameQualifier to Auth and SamlResponse. Extend logout method from Auth and LogoutRequest constructor to support SPNameQualifier parameter. Align LogoutRequest constructor with SAML specs +* Add support for Subjects on AuthNRequests by the new parameter +* Set strict=true on config examples + +v.2.15.0 +* Security improvement suggested by Nils Engelbertz to prevent DDOS by expansion of internally defined entities (XEE) +* Fix bug on settings_example.php + +v.2.14.0 +* Add parameter to the decryptElement method to make optional the formatting +* [#283](https://github.com/onelogin/php-saml/pull/283) New method of importing a decrypted assertion into the XML document to replace the EncryptedAssertion. Fix signature issues on Signed Encrypted Assertions with default namespace +* Allow the getSPMetadata() method to always include the encryption Key Descriptor +* Change some Fatal Error to Exceptions +* [#265](https://github.com/onelogin/php-saml/issues/265) Support parameters at getSPMetadata method +* Avoid calling static method using this + +v.2.13.0 +* Update xmlseclibs with some fixes. +* Add extra protection verifying the Signature algorithm used on SignedInfo element, not only rely on the xmlseclibs verify / verifySignature methods. +* Add getAttributesWithFriendlyName method which returns the set of SAML attributes indexed by FriendlyName +* Fix bug on parseRemoteXML and parseFileXML. Internal calls to parseXML missed the desiredNameIdFormat parameter + +v.2.12.0 +* Improve Time management. Use DateTime/DateTimeZone classes. +* Escape error messages in debug mode +* Improve phpdoc +* Add an extra filter to the url to be used on redirection + +* [#242](https://github.com/onelogin/php-saml/pull/242) Document that SHA-1 must not be used +* [#250](https://github.com/onelogin/php-saml/pull/250) Fixed issue with IdPMetadataParser only keeping 1 certificate when multiple certificates of a single type were provided. +* [#263](https://github.com/onelogin/php-saml/issues/263) Fix incompatibility with ADFS on SLO. When on php saml settings NameID Format is set as unspecified but the SAMLResponse has no NameID Format, no NameID Format should be specified on LogoutRequest. + +v.2.11.0 +* [#236](https://github.com/onelogin/php-saml/pull/236) Exclude unnecesary files from Composer production downloads +* [#226](https://github.com/onelogin/php-saml/pull/226) Add possibility to handle nameId NameQualifier attribute in SLO Request +* Improve logout documentation on Readme. +* Improve multi-certificate support + +v.2.10.7 +* Fix IdPMetadataParser. The SingleLogoutService retrieved method was wrong +* [#201](https://github.com/onelogin/php-saml/issues/201) Fix issues with SP entity_id, acs url and sls url that contains & + +v.2.10.6 +* [#206](https://github.com/onelogin/php-saml/pull/206)Be able to register future SP x509cert on the settings and publish it on SP metadata +* [#206](https://github.com/onelogin/php-saml/pull/206) Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption) +* [#206](https://github.com/onelogin/php-saml/pull/206) Support the ability to parse IdP XML metadata (remote url or file) and be able to inject the data obtained on the settings. + +v.2.10.5 +* Be able to get at the auth object the last processed ID +* Improve NameID Format support +* Reset errorReason attribute of the auth object after each Process method +* Validate serial number as string to work around libxml2 limitation +* Make the Issuer on the Response Optional + +v.2.10.4 +* [+](https://github.com/onelogin/php-saml/commit/949359f5cad5e1d085c4e5447d9aa8f49a6e82a1) Security update for signature validation on LogoutRequest/LogoutResponse +* [#192](https://github.com/onelogin/php-saml/pull/192) Added ability to configure DigestAlgorithm in settings +* [#183](https://github.com/onelogin/php-saml/pull/183) Fix strpos bug when decrypting assertions +* [#186](https://github.com/onelogin/php-saml/pull/186) Improve info on entityId validation Exception +* [#188](https://github.com/onelogin/php-saml/pull/188) Fixed issue with undefined constant of UNEXPECTED_SIGNED_ELEMENT +* Read ACS binding on AuthNRequest builder from settings +* Be able to relax Destination validation on SAMLResponses and let this + attribute to be empty with the 'relaxDestinationValidation' setting + +v.2.10.3 +* Implement a more specific exception class for handling some validation errors +* Minor changes on time validation/exceptions +* Add hooks to retrieve last-sent and last-received requests and responses +* Improve/Fix tests +* Add DigestAlgorithm support on addSign +* [#177](https://github.com/onelogin/php-saml/pull/177) Add error message for bad OneLogin_Saml2_Settings argument + +v.2.10.2 +* [#175](https://github.com/onelogin/php-saml/pull/175) Allow overriding of host, port, protocol and url path for URL building +* [#173](https://github.com/onelogin/php-saml/pull/173) Provide better support to NameIdFormat +* Fix another issue on Assertion Signature validation when the assertion contains no namespace, container has saml2 namespace and it was encrypted + +v.2.10.1 +* Fix error message on SignMetadata process +* Fix issue on Assertion Signature validation when the assertion contains no namespace and it was encrypted + +v.2.10.0 +* Several security improvements: + * Conditions element required and unique. + * AuthnStatement element required and unique. + * SPNameQualifier must math the SP EntityID + * Reject saml:Attribute element with same “Name” attribute + * Reject empty nameID + * Require Issuer element. (Must match IdP EntityID). + * Destination value can't be blank (if present must match ACS URL). + * Check that the EncryptedAssertion element only contains 1 Assertion element. +* Improve Signature validation process +* AttributeConsumingService support +* Support lowercase Urlencoding (ADFS compatibility). +* [#154](https://github.com/onelogin/php-saml/pull/154) getSelfHost no longer returns a port number +* [#156](https://github.com/onelogin/php-saml/pull/156) Use correct host on response destination fallback check +* [#158](https://github.com/onelogin/php-saml/pull/158) NEW Control usage of X-Forwarded-* headers +* Fix issue with buildRequestSignature. Added RelayState to the SignQuery only if is not null. +* Add Signature Wrapping prevention Test +* Improve _decryptAssertion in order to take care of Assertions with problems with namespaces +* Improve documentation + +v.2.9.1 +....... +* [134](https://github.com/onelogin/php-saml/pull/134) PHP7 production settings compiles out assert(), throw an exception explicitly +* [132](https://github.com/onelogin/php-saml/pull/132) Add note for "wantAssertionsEncrypted" +* Update copyright on LICENSE + +v.2.9.0 +------- +* Change the decrypt assertion process. +* Add 2 extra validations to prevent Signature wrapping attacks. +* Remove reference to wrong NameIDFormat: urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified should be urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified +* [128](https://github.com/onelogin/php-saml/pull/128) Test php7 and upgrade phpunit +* Update Readme with more descriptive requestedAuthnContext description and Security Guidelines + +v.2.8.0 +------- +* Make NameIDPolicy of AuthNRequest optional +* Make nameID requirement on SAMLResponse optional +* Fix empty URI support +* Symmetric encryption key support +* Add more Auth Context options to the constant class +* Fix DSA_SHA1 constant on xmlseclibs +* Set none requestedAuthnContext as default behaviour +* Update xmlseclibs lib +* Improve formatPrivateKey method +* Fix bug when signing metadata, the SignatureMethod was not provided +* Fix getter for lastRequestID parameter in OneLogin_Saml2_Auth class +* Add $wantEncrypted parameter on addX509KeyDescriptors method that will allow to set KeyDescriptor[use='encryption'] if wantNameIdEncrypted or wantAssertionsEncrypted enabled +* Add $stay parameter on redirectTo method. (login/logout supports $stay but I forgot add this on previous 2.7.0 version) +* Improve code style + +v.2.7.0 +------- +* Trim acs, slo and issuer urls. +* Fix PHP 7 error (used continue outside a loop/switch). +* Fix bug on organization element of the SP metadata builder. +* Fix typos on documentation. Fix ALOWED Misspell. +* Be able to extract RequestID. Add RequestID validation on demo1. +* Add $stay parameter to login, logout and processSLO method. + +v.2.6.1 +------- +* Fix bug on cacheDuration of the Metadata XML generated. +* Make SPNameQualifier optional on the generateNameId method. Avoid the use of SPNameQualifier when generating the NameID on the LogoutRequest builder. +* Allows the authn comparsion attribute to be set via config. +* Retrieve Session Timeout after processResponse with getSessionExpiration(). +* Improve readme readability. +* Allow single log out to work for applications not leveraging php session_start. Added a callback parameter in order to close the session at processSLO. + +v.2.6.0 +------- +* Set NAMEID_UNSPECIFIED as default NameIDFormat to prevent conflicts with IdPs that don't support NAMEID_PERSISTENT. +* Now the SP is able to select the algorithm to be used on signatures (DSA_SHA1, RSA_SHA1, RSA_SHA256, RSA_SHA384, RSA_SHA512). +* Change visibility of _decryptAssertion to protected. +* Update xmlseclibs library. +* Handle valid but uncommon dsig block with no URI in the reference. +* login, logout and processSLO now return ->redirectTo instead of just call it. +* Split the setting check methods. Now 1 method for IdP settings and other for SP settings. +* Let the setting object to avoid the IdP setting check. required if we want to publish SP SAML Metadata when the IdP data is still not provided. + +v.2.5.0 +------- +* Do accesible the ID of the object Logout Request (id attribute). +* Add note about the fact that PHP 5.3 is unssuported. +* Add fingerprint algorithm support. +* Add dependences to composer. + +v.2.4.0 +------- +* Fix wrong element order in generated metadata. +* Added SLO with nameID and SessionIndex in demo1. +* Improve isHTTPS method in order to support HTTP_X_FORWARDED_PORT. +* Set optional the XMLvalidation (enable/disable it with wantXMLValidation security setting). + +v.2.3.0 +------- +* Resolve namespace problem. Some IdPs uses saml2p:Response and saml2:Assertion instead of samlp:Response saml:Assertion. +* Improve test and documentation. +* Improve ADFS compatibility. +* Remove unnecessary XSDs files. +* Make available the reason for the saml message invalidation. +* Adding ability to set idp cert once the Setting object initialized. +* Fix status info issue. +* Reject SAML Response if not signed and strict = false. +* Support NameId and SessionIndex in LogoutRequest. +* Add ForceAuh and IsPassive support. + +v.2.2.0 +------- +* Fix bug with Encrypted nameID on LogoutRequest. +* Fixed usability bug. SP will inform about AuthFail status after process a Response. +* Added SessionIndex support on LogoutRequest, and know is accesible from the Auth class. +* LogoutRequest and LogoutResponse classes now accept non deflated xml. +* Improved the XML metadata/ Decrypted Assertion output. (prettyprint). +* Fix bug in formatPrivateKey method, the key could be not RSA. +* Explicit warning message for signed element problem. +* Decrypt method improved. +* Support more algorithm at the SigAlg in the Signed LogoutRequests and LogoutResponses +* AuthNRequest now stores ID (it can be retrieved later). +* Fixed a typo on the 'NameIdPolicy' attribute that appeared at the README and settings_example file. + + +v.2.1.0 +------- + +* The isValid method of the Logout Request is now non-static. (affects processSLO method of Auth.php). +* Logout Request constructor now accepts encoded logout requests. +* Now after validate a message, if fails a method getError of the object will return the cause. +* Fix typos. +* Added extra parameters option to login and logout methods. +* Improve Test (new test, use the new getError method for testing). +* Bugfix namespace problem when getting Attributes. + + +v.2.0.0 +------- + +* New PHP SAML Toolkit (SLO, Sign, Encryptation). + + +v.1.0.0 +------- + +* Old PHP SAML Toolkit. diff --git a/saml/vendor/onelogin/php-saml/LICENSE b/saml/vendor/onelogin/php-saml/LICENSE new file mode 100644 index 00000000..dbbca9c6 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2010-2016 OneLogin, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + diff --git a/saml/vendor/onelogin/php-saml/README.md b/saml/vendor/onelogin/php-saml/README.md new file mode 100644 index 00000000..d2199ec3 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/README.md @@ -0,0 +1,1548 @@ +# OneLogin's SAML PHP Toolkit Compatible with PHP 7.X & 8.X + +[![Build Status](https://api.travis-ci.org/onelogin/php-saml.png?branch=master)](http://travis-ci.org/onelogin/php-saml) [![Coverage Status](https://coveralls.io/repos/onelogin/php-saml/badge.png)](https://coveralls.io/r/onelogin/php-saml) [![License](https://poser.pugx.org/onelogin/php-saml/license.png)](https://packagist.org/packages/onelogin/php-saml) + +Add SAML support to your PHP software using this library. +Forget those complicated libraries and use this open source library provided +and supported by OneLogin Inc. + + +Warning +------- + +This version is compatible with PHP >=7.3 and 8.X and does not include xmlseclibs (you will need to install it via composer, dependency described in composer.json) + +Security Guidelines +------------------- + +If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution. + + +Why add SAML support to my software? +------------------------------------ + +SAML is an XML-based standard for web browser single sign-on and is defined by +the OASIS Security Services Technical Committee. The standard has been around +since 2002, but lately it is becoming popular due its advantages: + + * **Usability** - One-click access from portals or intranets, deep linking, + password elimination and automatically renewing sessions make life + easier for the user. + * **Security** - Based on strong digital signatures for authentication and + integrity, SAML is a secure single sign-on protocol that the largest + and most security conscious enterprises in the world rely on. + * **Speed** - SAML is fast. One browser redirect is all it takes to securely + sign a user into an application. + * **Phishing Prevention** - If you don’t have a password for an app, you + can’t be tricked into entering it on a fake login page. + * **IT Friendly** - SAML simplifies life for IT because it centralizes + authentication, provides greater visibility and makes directory + integration easier. + * **Opportunity** - B2B cloud vendor should support SAML to facilitate the + integration of their product. + + +General description +------------------- + +OneLogin's SAML PHP toolkit let you build a SP (Service Provider) over +your PHP application and connect it to any IdP (Identity Provider). + +Supports: + + * SSO and SLO (SP-Initiated and IdP-Initiated). + * Assertion and nameId encryption. + * Assertion signature. + * Message signature: AuthNRequest, LogoutRequest, LogoutResponses. + * Enable an Assertion Consumer Service endpoint. + * Enable a Single Logout Service endpoint. + * Publish the SP metadata (which can be signed). + +Key features: + + * **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile. + * **Session-less** - Forget those common conflicts between the SP and + the final app, the toolkit delegate session in the final app. + * **Easy to use** - Programmer will be allowed to code high-level and + low-level programming, 2 easy to use APIs are available. + * **Tested** - Thoroughly tested. + * **Popular** - OneLogin's customers use it. Many PHP SAML plugins uses it. + +Integrate your PHP toolkit at OneLogin using this guide: [https://developers.onelogin.com/page/saml-toolkit-for-php](https://developers.onelogin.com/page/saml-toolkit-for-php) + +Installation +------------ + +### Dependencies ### + + * `php >= 5.4` and some core extensions like `php-xml`, `php-date`, `php-zlib`. + * `openssl`. Install the openssl library. It handles x509 certificates. + * `gettext`. Install that library and its php driver. It handles translations. + * `curl`. Install that library and its php driver if you plan to use the IdP Metadata parser. + +### Code ### + +#### Option 1. clone the repository from github #### + +git clone git@github.com:onelogin/php-saml.git + +Then pull the 3.X.X branch/tag + +#### Option 2. Download from github #### + +The toolkit is hosted on github. You can download it from: + + * https://github.com/onelogin/php-saml/releases + +Search for 3.X.X releases + +Copy the core of the library inside the php application. (each application has its +structure so take your time to locate the PHP SAML toolkit in the best place). +See the "Guide to add SAML support to my app" to know how. + +Take in mind that the compressed file only contains the main files. +If you plan to play with the demos, use the Option 1. + +#### Option 3. Composer #### + +The toolkit supports [composer](https://getcomposer.org/). You can find the `onelogin/php-saml` package at https://packagist.org/packages/onelogin/php-saml + +In order to import the saml toolkit to your current php project, execute +``` +composer require onelogin/php-saml +``` + +Remember to select the 3.X.X branch + +After installation has completed you will find at the `vendor/` folder a new folder named `onelogin` and inside the `php-saml`. Make sure you are including the autoloader provided by composer. It can be found at `vendor/autoload.php`. + +**Important** In this option, the x509 certs must be stored at `vendor/onelogin/php-saml/certs` +and settings file stored at `vendor/onelogin/php-saml`. + +Your settings are at risk of being deleted when updating packages using `composer update` or similar commands. So it is **highly** recommended that instead of using settings files, you pass the settings as an array directly to the constructor (explained later in this document). If you do not use this approach your settings are at risk of being deleted when updating packages using `composer update` or similar commands. + +Compatibility +------------- + +This 4.X.X supports PHP >=7.3 . + +It is not compatible with PHP5.6 or PHP7.0. + +Namespaces +---------- + +If you are using the library with a framework like Symfony that contains +namespaces, remember that calls to the class must be done by adding a backslash (`\`) to the +start, for example to use the static method getSelfURLNoQuery use: + + \OneLogin\Saml2\Utils::getSelfURLNoQuery() + + +Security warning +---------------- + +In production, the `strict` parameter **MUST** be set as `"true"` and the +`signatureAlgorithm` and `digestAlgorithm` under `security` must be set to +something other than SHA1 (see https://shattered.io/ ). Otherwise your +environment is not secure and will be exposed to attacks. + +In production also we highly recommended to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment. + +Getting started +--------------- + +### Knowing the toolkit ### + +The new OneLogin SAML Toolkit contains different folders (`certs`, `endpoints`, +`lib`, `demo`, etc.) and some files. + +Let's start describing the folders: + +#### `certs/` #### + +SAML requires a x509 cert to sign and encrypt elements like `NameID`, `Message`, +`Assertion`, `Metadata`. + +If our environment requires sign or encrypt support, this folder may contain +the x509 cert and the private key that the SP will use: + + * `sp.crt` - The public cert of the SP + * `sp.key` - The private key of the SP + +Or also we can provide those data in the setting file at the `$settings['sp']['x509cert']` +and the `$settings['sp']['privateKey']`. + +Sometimes we could need a signature on the metadata published by the SP, in +this case we could use the x509 cert previously mentioned or use a new x.509 +cert: `metadata.crt` and `metadata.key`. + +Use `sp_new.crt` if you are in a key rollover process and you want to +publish that x509 certificate on Service Provider metadata. + +#### `src/` #### + +This folder contains the heart of the toolkit, the libraries: + + * `Saml2` folder contains the new version of the classes and methods that + are described in a later section. + + +#### `doc/` #### + +This folder contains the API documentation of the toolkit. + + +#### `endpoints/` #### + +The toolkit has three endpoints: + + * `metadata.php` - Where the metadata of the SP is published. + * `acs.php` - Assertion Consumer Service. Processes the SAML Responses. + * `sls.php` - Single Logout Service. Processes Logout Requests and Logout + Responses. + +You can use the files provided by the toolkit or create your own endpoints +files when adding SAML support to your applications. Take in mind that those +endpoints files uses the setting file of the toolkit's base folder. + + +#### `locale/` #### + +Locale folder contains some translations: `en_US` and `es_ES` as a proof of concept. +Currently there are no translations but we will eventually localize the messages +and support multiple languages. + + +#### Other important files #### + +* `settings_example.php` - A template to be used in order to create a + settings.php file which contains the basic configuration info of the toolkit. +* `advanced_settings_example.php` - A template to be used in order to create a + advanced_settings.php file which contains extra configuration info related to + the security, the contact person, and the organization associated to the SP. +* `_toolkit_loader.php` - This file load the toolkit libraries (The SAML2 lib). + + +#### Miscellaneous #### + +* `tests/` - Contains the unit test of the toolkit. +* `demo1/` - Contains an example of a simple PHP app with SAML support. + Read the `Readme.txt` inside for more info. +* `demo2/` - Contains another example. + + +### How it works ### + +#### Settings #### + +First of all we need to configure the toolkit. The SP's info, the IdP's info, +and in some cases, configure advanced security issues like signatures and +encryption. + +There are two ways to provide the settings information: + + * Use a `settings.php` file that we should locate at the base folder of the + toolkit. + * Use an array with the setting data and provide it directly to the + constructor of the class. + + +There is a template file, `settings_example.php`, so you can make a copy of this +file, rename and edit it. + +```php + true, + + // Enable debug mode (to print errors). + 'debug' => false, + + // Set a BaseURL to be used instead of try to guess + // the BaseURL of the view that process the SAML Message. + // Ex http://sp.example.com/ + // http://example.com/sp/ + 'baseurl' => null, + + // Service Provider Data that we are deploying. + 'sp' => array( + // Identifier of the SP entity (must be a URI) + 'entityId' => '', + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'assertionConsumerService' => array( + // URL Location where the from the IdP will be returned + 'url' => '', + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports this endpoint for the + // HTTP-POST binding only. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + ), + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be omitted + "attributeConsumingService"=> array( + "serviceName" => "SP test", + "serviceDescription" => "Test Service", + "requestedAttributes" => array( + array( + "name" => "", + "isRequired" => false, + "nameFormat" => "", + "friendlyName" => "", + "attributeValue" => array() + ) + ) + ), + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'singleLogoutService' => array( + // URL Location where the from the IdP will be returned + 'url' => '', + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Specifies the constraints on the name identifier to be used to + // represent the requested subject. + // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported. + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + // Usually x509cert and privateKey of the SP are provided by files placed at + // the certs folder. But we can also provide them with the following parameters + 'x509cert' => '', + 'privateKey' => '', + + /* + * Key rollover + * If you plan to update the SP x509cert and privateKey + * you can define here the new x509cert and it will be + * published on the SP metadata so Identity Providers can + * read them and get ready for rollover. + */ + // 'x509certNew' => '', + ), + + // Identity Provider Data that we want connected with our SP. + 'idp' => array( + // Identifier of the IdP entity (must be a URI) + 'entityId' => '', + // SSO endpoint info of the IdP. (Authentication Request protocol) + 'singleSignOnService' => array( + // URL Target of the IdP where the Authentication Request Message + // will be sent. + 'url' => '', + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // SLO endpoint info of the IdP. + 'singleLogoutService' => array( + // URL Location of the IdP where SLO Request will be sent. + 'url' => '', + // URL location of the IdP where SLO Response will be sent (ResponseLocation) + // if not set, url for the SLO Request will be used + 'responseUrl' => '', + // SAML protocol binding to be used when returning the + // message. OneLogin Toolkit supports the HTTP-Redirect binding + // only for this endpoint. + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Public x509 certificate of the IdP + 'x509cert' => '', + /* + * Instead of use the whole x509cert you can use a fingerprint in order to + * validate a SAMLResponse, but we don't recommend to use that + * method on production since is exploitable by a collision attack. + * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, + * or add for example the -sha256 , -sha384 or -sha512 parameter) + * + * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to + * let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512 + * 'sha1' is the default value. + * + * Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you + * will need to provide the whole x509cert. + */ + // 'certFingerprint' => '', + // 'certFingerprintAlgorithm' => 'sha1', + + /* In some scenarios the IdP uses different certificates for + * signing/encryption, or is under key rollover phase and + * more than one certificate is published on IdP metadata. + * In order to handle that the toolkit offers that parameter. + * (when used, 'x509cert' and 'certFingerprint' values are + * ignored). + */ + // 'x509certMulti' => array( + // 'signing' => array( + // 0 => '', + // ), + // 'encryption' => array( + // 0 => '', + // ) + // ), + ), +); +``` +In addition to the required settings data (IdP, SP), there is extra +information that could be defined. In the same way that a template exists +for the basic info, there is a template for that advanced info located +at the base folder of the toolkit and named `advanced_settings_example.php` +that you can copy and rename it as `advanced_settings.php` + +```php + array( + 'requests' => true, + 'responses' => true + ), + // Security settings + 'security' => array( + + /** signatures and encryptions offered */ + + // Indicates that the nameID of the sent by this SP + // will be encrypted. + 'nameIdEncrypted' => false, + + // Indicates whether the messages sent by this SP + // will be signed. [Metadata of the SP will offer this info] + 'authnRequestsSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutRequestSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutResponseSigned' => false, + + /* Sign the Metadata + False || True (use sp certs) || array ( + 'keyFileName' => 'metadata.key', + 'certFileName' => 'metadata.crt' + ) + || array ( + 'x509cert' => '', + 'privateKey' => '' + ) + */ + 'signMetadata' => false, + + /** signatures and encryptions required **/ + + // Indicates a requirement for the , + // and elements received by this SP to be signed. + 'wantMessagesSigned' => false, + + // Indicates a requirement for the elements received by + // this SP to be encrypted. + 'wantAssertionsEncrypted' => false, + + // Indicates a requirement for the elements received by + // this SP to be signed. [Metadata of the SP will offer this info] + 'wantAssertionsSigned' => false, + + // Indicates a requirement for the NameID element on the SAMLResponse + // received by this SP to be present. + 'wantNameId' => true, + + // Indicates a requirement for the NameID received by + // this SP to be encrypted. + 'wantNameIdEncrypted' => false, + + // Authentication context. + // Set to false and no AuthContext will be sent in the AuthNRequest. + // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'. + // Set an array with the possible auth context values: array('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'). + 'requestedAuthnContext' => false, + + // Indicates if the SP will validate all received xmls. + // (In order to validate the xml, 'strict' and 'wantXMLValidation' must be true). + 'wantXMLValidation' => true, + + // If true, SAMLResponses with an empty value at its Destination + // attribute will not be rejected for this fact. + 'relaxDestinationValidation' => false, + + // If true, Destination URL should strictly match to the address to + // which the response has been sent. + // Notice that if 'relaxDestinationValidation' is true an empty Destintation + // will be accepted. + 'destinationStrictlyMatches' => false, + + // If true, the toolkit will not raised an error when the Statement Element + // contain atribute elements with name duplicated + 'allowRepeatAttributeName' => false, + + // If true, SAMLResponses with an InResponseTo value will be rejectd if not + // AuthNRequest ID provided to the validation method. + 'rejectUnsolicitedResponsesWithInResponseTo' => false, + + // Algorithm that the toolkit will use on signing process. Options: + // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + // Notice that rsa-sha1 is a deprecated algorithm and should not be used + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + + // Algorithm that the toolkit will use on digest process. Options: + // 'http://www.w3.org/2000/09/xmldsig#sha1' + // 'http://www.w3.org/2001/04/xmlenc#sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#sha384' + // 'http://www.w3.org/2001/04/xmlenc#sha512' + // Notice that sha1 is a deprecated algorithm and should not be used + 'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256', + + // Algorithm that the toolkit will use for encryption process. Options: + // 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + // 'http://www.w3.org/2009/xmlenc11#aes128-gcm' + // 'http://www.w3.org/2009/xmlenc11#aes192-gcm' + // 'http://www.w3.org/2009/xmlenc11#aes256-gcm'; + // Notice that aes-cbc are not consider secure anymore so should not be used + 'encryption_algorithm' => 'http://www.w3.org/2009/xmlenc11#aes128-gcm', + + // ADFS URL-Encodes SAML data as lowercase, and the toolkit by default uses + // uppercase. Turn it True for ADFS compatibility on signature verification + 'lowercaseUrlencoding' => false, + ), + + // Contact information template, it is recommended to supply a + // technical and support contacts. + 'contactPerson' => array( + 'technical' => array( + 'givenName' => '', + 'emailAddress' => '' + ), + 'support' => array( + 'givenName' => '', + 'emailAddress' => '' + ), + ), + + // Organization information template, the info in en_US lang is + // recomended, add more if required. + 'organization' => array( + 'en-US' => array( + 'name' => '', + 'displayname' => '', + 'url' => '' + ), + ), +); +``` + +The compression settings allow you to instruct whether or not the IdP can accept +data that has been compressed using [gzip](gzip) ('requests' and 'responses'). +But if we provide a `$deflate` boolean parameter to the `getRequest` or `getResponse` method it will have priority over the compression settings. + +In the security section, you can set the way that the SP will handle the messages +and assertions. Contact the admin of the IdP and ask him what the IdP expects, +and decide what validations will handle the SP and what requirements the SP will have +and communicate them to the IdP's admin too. + +Once we know what kind of data could be configured, let's talk about the way +settings are handled within the toolkit. + +The settings files described (`settings.php` and `advanced_settings.php`) are loaded +by the toolkit if no other array with settings info is provided in the constructor of the toolkit. Let's see some examples. + +```php +// Initializes toolkit with settings.php & advanced_settings files. +$auth = new OneLogin\Saml2\Auth(); +//or +$settings = new OneLogin\Saml2\Settings(); + +// Initializes toolkit with the array provided. +$auth = new OneLogin\Saml2\Auth($settingsInfo); +//or +$settings = new OneLogin\Saml2\Settings($settingsInfo); +``` + +You can declare the `$settingsInfo` in the file that contains the constructor +execution or locate them in any file and load the file in order to get the +array available as we see in the following example: + +```php +login(); // Method that sent the AuthNRequest +``` + +The `AuthNRequest` will be sent signed or unsigned based on the security info +of the `advanced_settings.php` (`'authnRequestsSigned'`). + + +The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Attribute Consumer Service of the SP with this information. If we do not set a `'url'` param in the login method and we are using the default ACS provided by the toolkit (`endpoints/acs.php`), then the ACS endpoint will redirect the user to the file that launched the SSO request. + +We can set a `'returnTo'` url to change the workflow and redirect the user to the other PHP file. + +```php +$newTargetUrl = 'http://example.com/consume2.php'; +$auth = new OneLogin\Saml2\Auth(); +$auth->login($newTargetUrl); +``` + +The login method can receive other six optional parameters: + +* `$parameters` - An array of parameters that will be added to the `GET` in the HTTP-Redirect. +* `$forceAuthn` - When true the `AuthNRequest` will set the `ForceAuthn='true'` +* `$isPassive` - When true the `AuthNRequest` will set the `Ispassive='true'` +* `$strict` - True if we want to stay (returns the url string) False to redirect +* `$setNameIdPolicy` - When true the AuthNRequest will set a nameIdPolicy element. +* `$nameIdValueReq` - Indicates to the IdP the subject that should be authenticated. + +If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and saved. + +```php +$ssoBuiltUrl = $auth->login(null, array(), false, false, true); +$_SESSION['AuthNRequestID'] = $auth->getLastRequestID(); +header('Pragma: no-cache'); +header('Cache-Control: no-cache, must-revalidate'); +header('Location: ' . $ssoBuiltUrl); +exit(); +``` + +#### The SP Endpoints #### + +Related to the SP there are three important views: The metadata view, the ACS view and the SLS view. The toolkit +provides examples of those views in the endpoints directory. + +##### SP Metadata `endpoints/metadata.php` ##### + +This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files. + +```php +getSettings(); + $metadata = $settings->getSPMetadata(); + $errors = $settings->validateMetadata($metadata); + if (empty($errors)) { + header('Content-Type: text/xml'); + echo $metadata; + } else { + throw new OneLogin\Saml2\Error( + 'Invalid SP metadata: '.implode(', ', $errors), + OneLogin\Saml2\Error::METADATA_SP_INVALID + ); + } +} catch (Exception $e) { + echo $e->getMessage(); +} +``` +The `getSPMetadata` will return the metadata signed or not based +on the security info of the `advanced_settings.php` (`'signMetadata'`). + +Before the XML metadata is exposed, a check takes place to ensure +that the info to be provided is valid. + +Instead of use the Auth object, you can directly use + +```php +$settings = new OneLogin\Saml2\Settings($settingsInfo, true); +``` +to get the settings object and with the true parameter we will avoid the IdP Settings validation. + + +##### Attribute Consumer Service(ACS) `endpoints/acs.php` ##### + +This code handles the SAML response that the IdP forwards to the SP through the user's client. + +```php +processResponse($requestID); +unset($_SESSION['AuthNRequestID']); + +$errors = $auth->getErrors(); + +if (!empty($errors)) { + echo '

' . implode(', ', $errors) . '

'; + exit(); +} + +if (!$auth->isAuthenticated()) { + echo "

Not authenticated

"; + exit(); +} + +$_SESSION['samlUserdata'] = $auth->getAttributes(); +$_SESSION['samlNameId'] = $auth->getNameId(); +$_SESSION['samlNameIdFormat'] = $auth->getNameIdFormat(); +$_SESSION['samlNameidNameQualifier'] = $auth->getNameIdNameQualifier(); +$_SESSION['samlNameidSPNameQualifier'] = $auth->getNameIdSPNameQualifier(); +$_SESSION['samlSessionIndex'] = $auth->getSessionIndex(); + +if (isset($_POST['RelayState']) && OneLogin\Saml2\Utils::getSelfURL() != $_POST['RelayState']) { + $auth->redirectTo($_POST['RelayState']); +} + +$attributes = $_SESSION['samlUserdata']; +$nameId = $_SESSION['samlNameId']; + +echo '

Identified user: '. htmlentities($nameId) .'

'; + +if (!empty($attributes)) { + echo '

' . _('User attributes:') . '

'; + echo ''; + foreach ($attributes as $attributeName => $attributeValues) { + echo ''; + } + echo '
' . _('Name') . '' . _('Values') . '
' . htmlentities($attributeName) . '
    '; + foreach ($attributeValues as $attributeValue) { + echo '
  • ' . htmlentities($attributeValue) . '
  • '; + } + echo '
'; +} else { + echo _('No attributes found.'); +} +``` + +The SAML response is processed and then checked that there are no errors. +It also verifies that the user is authenticated and stored the userdata in session. + +At that point there are two possible alternatives: + + 1. If no `RelayState` is provided, we could show the user data in this view + or however we wanted. + + 2. If `RelayState` is provided, a redirection takes place. + +Notice that we saved the user data in the session before the redirection to +have the user data available at the `RelayState` view. + + +###### The `getAttributes` method ###### + +In order to retrieve attributes we can use: + +```php +$attributes = $auth->getAttributes(); +``` + +With this method we get all the user data provided by the IdP in the Assertion +of the SAML Response. + +If we execute ```print_r($attributes)``` we could get: + +```php +Array +( + [cn] => Array + ( + [0] => John + ) + [sn] => Array + ( + [0] => Doe + ) + [mail] => Array + ( + [0] => john.doe@example.com + ) + [groups] => Array + ( + [0] => users + [1] => members + ) +) +``` + +Each attribute name can be used as an index into `$attributes` to obtain the value. Every attribute value +is an array - a single-valued attribute is an array of a single element. + + +The following code is equivalent: + +```php +$attributes = $auth->getAttributes(); +print_r($attributes['cn']); +``` + +```php +print_r($auth->getAttribute('cn')); +``` + + +Before trying to get an attribute, check that the user is +authenticated. If the user isn't authenticated or if there were +no attributes in the SAML assertion, an empty array will be +returned. For example, if we call to `getAttributes` before a +`$auth->processResponse`, the `getAttributes()` will return an +empty array. + + +##### Single Logout Service (SLS) `endpoints/sls.php` ##### + +This code handles the Logout Request and the Logout Responses. + +```php +processSLO(false, $requestID); + +$errors = $auth->getErrors(); + +if (empty($errors)) { + echo 'Sucessfully logged out'; +} else { + echo implode(', ', $errors); +} +``` + +If the SLS endpoints receives a Logout Response, the response is +validated and the session could be closed + + + +```php +// part of the processSLO method + +$logoutResponse = new OneLogin\Saml2\LogoutResponse($this->_settings, $_GET['SAMLResponse']); +if (!$logoutResponse->isValid($requestId)) { + $this->_errors[] = 'invalid_logout_response'; +} else if ($logoutResponse->getStatus() !== OneLogin\Saml2\Constants::STATUS_SUCCESS) { + $this->_errors[] = 'logout_not_success'; +} else { + if (!$keepLocalSession) { + OneLogin\Saml2\Utils::deleteLocalSession(); + } +} +``` + +If the SLS endpoints receives an Logout Request, the request is validated, +the session is closed and a Logout Response is sent to the SLS endpoint of +the IdP. + +```php +// part of the processSLO method + +$decoded = base64_decode($_GET['SAMLRequest']); +$request = gzinflate($decoded); +if (!OneLogin\Saml2\LogoutRequest::isValid($this->_settings, $request)) { + $this->_errors[] = 'invalid_logout_request'; +} else { + if (!$keepLocalSession) { + OneLogin\Saml2\Utils::deleteLocalSession(); + } + + $inResponseTo = $request->id; + $responseBuilder = new OneLogin\Saml2\LogoutResponse($this->_settings); + $responseBuilder->build($inResponseTo); + $logoutResponse = $responseBuilder->getResponse(); + + $parameters = array('SAMLResponse' => $logoutResponse); + if (isset($_GET['RelayState'])) { + $parameters['RelayState'] = $_GET['RelayState']; + } + + $security = $this->_settings->getSecurityData(); + if (isset($security['logoutResponseSigned']) && $security['logoutResponseSigned']) { + $signature = $this->buildResponseSignature($logoutResponse, $parameters['RelayState'], $security['signatureAlgorithm']); + $parameters['SigAlg'] = $security['signatureAlgorithm']; + $parameters['Signature'] = $signature; + } + + $this->redirectTo($this->getSLOurl(), $parameters); +} +``` + +If you aren't using the default PHP session, or otherwise need a manual +way to destroy the session, you can pass a callback method to the +`processSLO` method as the fourth parameter + +```php +$keepLocalSession = False; +$callback = function () { + // Destroy user session +}; + +$auth->processSLO($keepLocalSession, null, false, $callback); +``` + + +If we don't want that `processSLO` to destroy the session, pass a true +parameter to the `processSLO` method + +```php +$keepLocalSession = True; +$auth->processSLO($keepLocalSession); +``` + +#### Initiate SLO #### + +In order to send a Logout Request to the IdP: + +```php +logout(); // Method that sent the Logout Request. +``` + +Also there are eight optional parameters that can be set: +* `$returnTo` - The target URL the user should be returned to after logout. +* `$parameters` - Extra parameters to be added to the GET. +* `$name_id` - That will be used to build the LogoutRequest. If `name_id` parameter is not set and the auth object processed a +SAML Response with a `NameId`, then this `NameId` will be used. +* `$session_index` - SessionIndex that identifies the session of the user. +* `$stay` - True if we want to stay (returns the url string) False to redirect. +* `$nameIdFormat` - The NameID Format will be set in the LogoutRequest. +* `$nameIdNameQualifier` - The NameID NameQualifier will be set in the LogoutRequest. +* `$nameIdSPNameQualifier` - The NameID SP NameQualifier will be set in the LogoutRequest. + +The Logout Request will be sent signed or unsigned based on the security +info of the `advanced_settings.php` (`'logoutRequestSigned'`). + +The IdP will return the Logout Response through the user's client to the +Single Logout Service of the SP. +If we do not set a `'url'` param in the logout method and are using the +default SLS provided by the toolkit (`endpoints/sls.php`), then the SLS +endpoint will redirect the user to the file that launched the SLO request. + +We can set an `'returnTo'` url to change the workflow and redirect the user +to other php file. + +```php +$newTargetUrl = 'http://example.com/loggedOut.php'; +$auth = new OneLogin\Saml2\Auth(); +$auth->logout($newTargetUrl); +``` +A more complex logout with all the parameters: +``` +$auth = new OneLogin\Saml2\Auth(); +$returnTo = null; +$parameters = array(); +$nameId = null; +$sessionIndex = null; +$nameIdFormat = null; +$nameIdNameQualifier = null; +$nameIdSPNameQualifier = null; + +if (isset($_SESSION['samlNameId'])) { + $nameId = $_SESSION['samlNameId']; +} +if (isset($_SESSION['samlSessionIndex'])) { + $sessionIndex = $_SESSION['samlSessionIndex']; +} +if (isset($_SESSION['samlNameIdFormat'])) { + $nameIdFormat = $_SESSION['samlNameIdFormat']; +} +if (isset($_SESSION['samlNameIdNameQualifier'])) { + $nameIdNameQualifier = $_SESSION['samlNameIdNameQualifier']; +} +if (isset($_SESSION['samlNameIdSPNameQualifier'])) { + $nameIdSPNameQualifier = $_SESSION['samlNameIdSPNameQualifier']; +} +$auth->logout($returnTo, $parameters, $nameId, $sessionIndex, false, $nameIdFormat, $nameIdNameQualifier, $nameIdSPNameQualifier); +``` + +If a match on the future LogoutResponse ID and the LogoutRequest ID to be sent is required, that LogoutRequest ID must to be extracted and stored. + +```php +$sloBuiltUrl = $auth->logout(null, $parameters, $nameId, $sessionIndex, true); +$_SESSION['LogoutRequestID'] = $auth->getLastRequestID(); +header('Pragma: no-cache'); +header('Cache-Control: no-cache, must-revalidate'); +header('Location: ' . $sloBuiltUrl); +exit(); +``` + +#### Example of a view that initiates the SSO request and handles the response (is the acs target) #### + +We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate +the SLO and processes the logout response. + +Note: Review the `demo1` folder that contains that use case; in a later section we +explain the demo1 use case further in detail. + +```php +login(); +} else if (isset($_GET['sso2'])) { // Another SSO action + $returnTo = $spBaseUrl.'/demo1/attrs.php'; // but set a custom RelayState URL + $auth->login($returnTo); +} else if (isset($_GET['slo'])) { // SLO action. Will sent a Logout Request to IdP + $auth->logout(); +} else if (isset($_GET['acs'])) { // Assertion Consumer Service + $auth->processResponse(); // Process the Response of the IdP, get the + // attributes and put then at + // $_SESSION['samlUserdata'] + + $errors = $auth->getErrors(); // This method receives an array with the errors + // that could took place during the process + + if (!empty($errors)) { + echo '

' . implode(', ', $errors) . '

'; + } + // This check if the response was + if (!$auth->isAuthenticated()) { // sucessfully validated and the user + echo '

Not authenticated

'; // data retrieved or not + exit(); + } + + $_SESSION['samlUserdata'] = $auth->getAttributes(); // Retrieves user data + if (isset($_POST['RelayState']) && OneLogin\Saml2\Utils::getSelfURL() != $_POST['RelayState']) { + $auth->redirectTo($_POST['RelayState']); // Redirect if there is a + } // relayState set +} else if (isset($_GET['sls'])) { // Single Logout Service + $auth->processSLO(); // Process the Logout Request & Logout Response + $errors = $auth->getErrors(); // Retrieves possible validation errors + if (empty($errors)) { + echo '

Sucessfully logged out

'; + } else { + echo '

' . implode(', ', $errors) . '

'; + } +} + +if (isset($_SESSION['samlUserdata'])) { // If there is user data we print it. + if (!empty($_SESSION['samlUserdata'])) { + $attributes = $_SESSION['samlUserdata']; + echo 'You have the following attributes:
'; + echo ''; + foreach ($attributes as $attributeName => $attributeValues) { + echo ''; + } + echo '
NameValues
' . htmlentities($attributeName) . '
    '; + foreach ($attributeValues as $attributeValue) { + echo '
  • ' . htmlentities($attributeValue) . '
  • '; + } + echo '
'; + } else { // If there is not user data, we notify + echo "

You don't have any attribute

"; + } + + echo '

Logout

'; // Print some links with possible +} else { // actions + echo '

Login

'; + echo '

Login and access to attrs.php page

'; +} +``` + +#### URL-guessing methods #### + +php-saml toolkit uses a bunch of methods in OneLogin\Saml2\Utils that try to guess the URL where the SAML messages are processed. + +* `getSelfHost` Returns the current host. +* `getSelfPort` Return the port number used for the request +* `isHTTPS` Checks if the protocol is https or http. +* `getSelfURLhost` Returns the protocol + the current host + the port (if different than common ports). +* `getSelfURL` Returns the URL of the current host + current view + query. +* `getSelfURLNoQuery` Returns the URL of the current host + current view. +* `getSelfRoutedURLNoQuery` Returns the routed URL of the current host + current view. + +getSelfURLNoQuery and getSelfRoutedURLNoQuery are used to calculate the currentURL in order to validate SAML elements like Destination or Recipient. + +When the PHP application is behind a proxy or a load balancer we can execute `setProxyVars(true)` and `setSelfPort` and `isHTTPS` will take care of the `$_SERVER["HTTP_X_FORWARDED_PORT"]` and `$_SERVER['HTTP_X_FORWARDED_PROTO']` vars (otherwise they are ignored). + +Also a developer can use `setSelfProtocol`, `setSelfHost`, `setSelfPort` and `getBaseURLPath` to define a specific value to be returned by `isHTTPS`, `getSelfHost`, `getSelfPort` and `getBaseURLPath`. And define a `setBasePath` to be used on the `getSelfURL` and `getSelfRoutedURLNoQuery` to replace the data extracted from `$_SERVER["REQUEST_URI"]`. + +At the settings the developer will be able to set a `'baseurl'` parameter that automatically will use `setBaseURL` to set values for `setSelfProtocol`, `setSelfHost`, `setSelfPort` and `setBaseURLPath`. + + +### Working behind load balancer ### + +Is possible that asserting request URL and Destination attribute of SAML response fails when working behind load balancer with SSL offload. + +You should be able to workaround this by configuring your server so that it is aware of the proxy and returns the original url when requested. + +Or by using the method described on the previous section. + + +### SP Key rollover ### + +If you plan to update the SP x509cert and privateKey you can define the new x509cert as `$settings['sp']['x509certNew']` and it will be +published on the SP metadata so Identity Providers can read them and get ready for rollover. + + +### IdP with multiple certificates ### + +In some scenarios the IdP uses different certificates for +signing/encryption, or is under key rollover phase and more than one certificate is published on IdP metadata. + +In order to handle that the toolkit offers the `$settings['idp']['x509certMulti']` parameter. + +When that parameter is used, `'x509cert'` and `'certFingerprint'` values will be ignored by the toolkit. + +The `x509certMulti` is an array with 2 keys: +- `signing`. An array of certs that will be used to validate IdP signature +- `encryption` An array with one unique cert that will be used to encrypt data to be sent to the IdP + + +### Replay attacks ### + +In order to avoid replay attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting. + +Get the ID of the last processed message/assertion with the `getLastMessageId`/`getLastAssertionId` methods of the Auth object. + + +### Main classes and methods ### + +Described below are the main classes and methods that can be invoked. + +#### Saml2 library #### + +Lets describe now the classes and methods of the SAML2 library. + +##### OneLogin\Saml2\Auth - Auth.php ##### + +Main class of OneLogin PHP Toolkit + + * `Auth` - Initializes the SP SAML instance + * `login` - Initiates the SSO process. + * `logout` - Initiates the SLO process. + * `processResponse` - Process the SAML Response sent by the IdP. + * `processSLO` - Process the SAML Logout Response / Logout Request sent by the + IdP. + * `redirectTo` - Redirects the user to the url past by parameter or to the url + that we defined in our SSO Request. + * `isAuthenticated` - Checks if the user is authenticated or not. + * `getAttributes` - Returns the set of SAML attributes. + * `getAttribute` - Returns the requested SAML attribute + * `getNameId` - Returns the nameID + * `getNameIdFormat` - Gets the NameID Format provided by the SAML response from the IdP. + * `getNameIdNameQualifier` - Gets the NameID NameQualifier provided from the SAML Response String. + * `getNameIdSPNameQualifier` - Gets the NameID SP NameQualifier provided from the SAML Response String. + * `getSessionIndex` - Gets the SessionIndex from the AuthnStatement. + * `getErrors` - Returns if there were any error + * `getSSOurl` - Gets the SSO url. + * `getSLOurl` - Gets the SLO url. + * `getLastRequestID` - The ID of the last Request SAML message generated. + * `buildRequestSignature` - Generates the Signature for a SAML Request + * `buildResponseSignature` - Generates the Signature for a SAML Response + * `getSettings` - Returns the settings info + * `setStrict` - Set the strict mode active/disable + * `getLastRequestID` - Gets the ID of the last AuthNRequest or LogoutRequest generated by the Service Provider. + * `getLastRequestXML` - Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest) + * `getLastResponseXML` - Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it. + + +##### OneLogin\Saml2\AuthnRequest - `AuthnRequest.php` ##### + +SAML 2 Authentication Request class + + * `AuthnRequest` - Constructs the `AuthnRequest` object. + * `getRequest` - Returns deflated, base64 encoded, unsigned `AuthnRequest`. + * `getId` - Returns the `AuthNRequest` ID. + * `getXML` - Returns the XML that will be sent as part of the request. + +##### OneLogin\Saml2\Response - `Response.php` ##### + +SAML 2 Authentication Response class + + * `Response` - Constructs the SAML Response object. + * `isValid` - Determines if the SAML Response is valid using the certificate. + * `checkStatus` - Checks if the Status is success. + * `getAudiences` - Gets the audiences. + * `getIssuers` - Gets the Issuers (from Response and Assertion) + * `getNameIdData` - Gets the NameID Data provided by the SAML response from the + IdP. + * `getNameId` - Gets the NameID provided by the SAML response from the IdP. + * `getNameIdFormat` - Gets the NameID Format provided by the SAML response from the IdP. + * `getNameIdNameQualifier` - Gets the NameID NameQualifier provided from the SAML Response String. + * `getNameIdSPNameQualifier` - Gets the NameID SP NameQualifier provided from the SAML Response String. + * `getSessionNotOnOrAfter` - Gets the SessionNotOnOrAfter from the + AuthnStatement + * `getSessionIndex` - Gets the SessionIndex from the AuthnStatement. + * `getAttributes` - Gets the Attributes from the AttributeStatement element. + * `validateNumAssertions` - Verifies that the document only contains a single + Assertion (encrypted or not). + * `validateTimestamps` - Verifies that the document is still valid according + Conditions Element. + * `getError` - After executing a validation process, if it fails, this method returns the cause + * `getXMLDocument` - Returns the SAML Response document (If contains an encrypted assertion, decrypts it) + +##### OneLogin\Saml2\LogoutRequest - `LogoutRequest.php` ##### + +SAML 2 Logout Request class + + * `LogoutRequest` - Constructs the Logout Request object. + * `getRequest` - Returns the Logout Request defated, base64encoded, unsigned + * `getID` - Returns the ID of the Logout Request. (If you have the object you can access to the id attribute) + * `getNameIdData` - Gets the NameID Data of the the Logout Request. + * `getNameId` - Gets the NameID of the Logout Request. + * `getIssuer` - Gets the Issuer of the Logout Request. + * `getSessionIndexes` - Gets the SessionIndexes from the Logout Request. + * `isValid` - Checks if the Logout Request received is valid. + * `getError` - After executing a validation process, if it fails, this method returns the cause + * `getXML` - Returns the XML that will be sent as part of the request or that was received at the SP. + +##### OneLogin\Saml2\LogoutResponse - `LogoutResponse.php` ##### + +SAML 2 Logout Response class + + * `LogoutResponse` - Constructs a Logout Response object + (Initialize params from settings and if provided load the Logout Response) + * `getIssuer` - Gets the Issuer of the Logout Response. + * `getStatus` - Gets the Status of the Logout Response. + * `isValid` - Determines if the SAML LogoutResponse is valid + * `build` - Generates a Logout Response object. + * `getResponse` - Returns a Logout Response object. + * `getError` - After executing a validation process, if it fails, this method returns the cause. + * `getXML` - Returns the XML that will be sent as part of the response or that was received at the SP. + +##### OneLogin\Saml2\Settings - `Settings.php` ##### + +Configuration of the OneLogin PHP Toolkit + + * `Settings` - Initializes the settings: Sets the paths of + the different folders and Loads settings info from settings file or + array/object provided + * `checkSettings` - Checks the settings info. + * `getBasePath` - Returns base path. + * `getCertPath` - Returns cert path. + * `getLibPath` - Returns lib path. + * `getExtLibPath` - Returns external lib path. + * `getSchemasPath` - Returns schema path. + * `checkSPCerts` - Checks if the x509 certs of the SP exists and are valid. + * `getSPkey` - Returns the x509 private key of the SP. + * `getSPcert` - Returns the x509 public cert of the SP. + * `getSPcertNew` - Returns the future x509 public cert of the SP. + * `getIdPData` - Gets the IdP data. + * `getSPData`Gets the SP data. + * `getSecurityData` - Gets security data. + * `getContacts` - Gets contact data. + * `getOrganization` - Gets organization data. + * `getSPMetadata` - Gets the SP metadata. The XML representation. + * `validateMetadata` - Validates an XML SP Metadata. + * `formatIdPCert` - Formats the IdP cert. + * `formatSPCert` - Formats the SP cert. + * `formatSPCertNew` - Formats the SP cert new. + * `formatSPKey` - Formats the SP private key. + * `getErrors` - Returns an array with the errors, the array is empty when + the settings is ok. + * `getLastErrorReason` - Returns the reason of the last error + * `getBaseURL` - Returns the baseurl set on the settings if any. + * `setBaseURL` - Set a baseurl value + * `setStrict` - Activates or deactivates the strict mode. + * `isStrict` - Returns if the 'strict' mode is active. + * `isDebugActive` - Returns if the debug is active. + +##### OneLogin\Saml2\Metadata - `Metadata.php` ##### + +A class that contains functionality related to the metadata of the SP + +* `builder` - Generates the metadata of the SP based on the settings. +* `signmetadata` - Signs the metadata with the key/cert provided +* `addX509KeyDescriptors` - Adds the x509 descriptors (sign/encriptation) to + the metadata + +##### OneLogin\Saml2\Utils - `Utils.php` ##### + +Auxiliary class that contains several methods + + * `validateXML` - This function attempts to validate an XML string against + the specified schema. + * `formatCert` - Returns a x509 cert (adding header & footer if required). + * `formatPrivateKey` - returns a RSA private key (adding header & footer if required). + * `redirect` - Executes a redirection to the provided url (or return the + target url). + * `isHTTPS` - Checks if https or http. + * `getSelfHost` - Returns the current host. + * `getSelfURLhost` - Returns the protocol + the current host + the port + (if different than common ports). + * `getSelfURLNoQuery` - Returns the URL of the current host + current view. + * `getSelfURL` - Returns the URL of the current host + current view + query. + * `generateUniqueID` - Generates a unique string (used for example as ID + for assertions). + * `parseTime2SAML` - Converts a UNIX timestamp to SAML2 timestamp on the + form `yyyy-mm-ddThh:mm:ss(\.s+)?Z`. + * `parseSAML2Time` - Converts a SAML2 timestamp on the form + `yyyy-mm-ddThh:mm:ss(\.s+)?Z` to a UNIX timestamp. The sub-second part is + ignored. + * `parseDuration` - Interprets a ISO8601 duration value relative to a given + timestamp. + * `getExpireTime` - Compares two dates and returns the earliest. + * `query` - Extracts nodes from the DOMDocument. + * `isSessionStarted` - Checks if the session is started or not. + * `deleteLocalSession` - Deletes the local session. + * `calculateX509Fingerprint` - Calculates the fingerprint of a x509cert. + * `formatFingerPrint` - Formats a fingerprint. + * `generateNameId` - Generates a `nameID`. + * `getStatus` - Gets Status from a Response. + * `decryptElement` - Decrypts an encrypted element. + * `castKey` - Converts a `XMLSecurityKey` to the correct algorithm. + * `addSign` - Adds signature key and senders certificate to an element + (Message or Assertion). + * `validateSign` - Validates a signature (Message or Assertion). + +##### OneLogin\Saml2\IdPMetadataParser - `IdPMetadataParser.php` ##### + +Auxiliary class that contains several methods to retrieve and process IdP metadata + + * `parseRemoteXML` - Get IdP Metadata Info from URL. + * `parseFileXML` - Get IdP Metadata Info from File. + * `parseXML` - Get IdP Metadata Info from XML. + * `injectIntoSettings` - Inject metadata info into php-saml settings array. + + +For more info, look at the source code; each method is documented and details +about what it does and how to use it are provided. Make sure to also check the doc folder where +HTML documentation about the classes and methods is provided for SAML and +SAML2. + + +Demos included in the toolkit +----------------------------- + +The toolkit includes three demo apps to teach how use the toolkit, take a look on it. + +Demos require that SP and IdP are well configured before test it. + +## Demo1 ## + +### SP setup ### + +The Onelogin's PHP Toolkit allows you to provide the settings info in two ways: + + * Use a `settings.php` file that we should locate at the base folder of the + toolkit. + * Use an array with the setting data. + +In this demo we provide the data in the second way, using a setting array named +`$settingsInfo`. This array users the `settings_example.php` included as a template +to create the `settings.php` settings and store it in the `demo1/` folder. +Configure the SP part and later review the metadata of the IdP and complete the IdP info. + +If you check the code of the index.php file you will see that the `settings.php` +file is loaded in order to get the `$settingsInfo` var to be used in order to initialize +the `Setting` class. + +Notice that in this demo, the `setting.php` file that could be defined at the base +folder of the toolkit is ignored and the libs are loaded using the +`_toolkit_loader.php` located at the base folder of the toolkit. + + +### IdP setup ### + +Once the SP is configured, the metadata of the SP is published at the +`metadata.php` file. Configure the IdP based on that information. + + +### How it works ### + + 1. First time you access to `index.php` view, you can select to login and return + to the same view or login and be redirected to the `attrs.php` view. + + 2. When you click: + + 2.1 in the first link, we access to (`index.php?sso`) an `AuthNRequest` + is sent to the IdP, we authenticate at the IdP and then a Response is sent + through the user's client to the SP, specifically the Assertion Consumer Service view: `index.php?acs`. + Notice that a `RelayState` parameter is set to the url that initiated the + process, the `index.php` view. + + 2.2 in the second link we access to (`attrs.php`) have the same process + described at 2.1 with the difference that as `RelayState` is set the `attrs.php`. + + 3. The SAML Response is processed in the ACS (`index.php?acs`), if the Response + is not valid, the process stops here and a message is shown. Otherwise we + are redirected to the RelayState view. a) `index.php` or b) `attrs.php`. + + 4. We are logged in the app and the user attributes are showed. + At this point, we can test the single log out functionality. + + 5. The single log out functionality could be tested by two ways. + + 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a + Logout Request is sent to the IdP, the session at the IdP is closed and + replies through the client to the SP with a Logout Response (sent to the + Single Logout Service endpoint). The SLS endpoint (`index.php?sls`) of the SP + process the Logout Response and if is valid, close the user session of the + local app. Notice that the SLO Workflow starts and ends at the SP. + + 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP + side, the logout process is initiated at the idP, sends a Logout + Request to the SP (SLS endpoint, `index.php?sls`). The SLS endpoint of the SP + process the Logout Request and if is valid, close the session of the user + at the local app and send a Logout Response to the IdP (to the SLS endpoint + of the IdP). The IdP receives the Logout Response, process it and close the + session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP. + +Notice that all the SAML Requests and Responses are handled by a unique file, +the `index.php` file and how `GET` parameters are used to know the action that +must be done. + + +## Demo2 ## + +### SP setup ### + +The Onelogin's PHP Toolkit allows you to provide the settings info in two ways: + + * Use a `settings.php` file that we should locate at the base folder of the + toolkit. + * Use an array with the setting data. + +The first is the case of the demo2 app. The `setting.php` file and the +`setting_extended.php` file should be defined at the base folder of the toolkit. +Review the `setting_example.php` and the `advanced_settings_example.php` to +learn how to build them. + +In this case as Attribute Consume Service and Single Logout Service we are going to +use the files located in the endpoint folder (`acs.php` and `sls.php`). + + +### IdP setup ### + +Once the SP is configured, the metadata of the SP is published at the +`metadata.php` file. Based on that info, configure the IdP. + + +### How it works ### + +At demo1, we saw how all the SAML Request and Responses were handler at an +unique file, the `index.php` file. This demo1 uses high-level programming. + +At demo2, we have several views: `index.php`, `sso.php`, `slo.php`, `consume.php` +and `metadata.php`. As we said, we will use the endpoints that are defined +in the toolkit (`acs.php`, `sls.php` of the endpoints folder). This demo2 uses +low-level programming. + +Notice that the SSO action can be initiated at `index.php` or `sso.php`. + +The SAML workflow that take place is similar that the workflow defined in the +demo1, only changes the targets. + + 1. When you access `index.php` or `sso.php` for the first time, an `AuthNRequest` is + sent to the IdP automatically, (as `RelayState` is sent the origin url). + We authenticate at the IdP and then a `Response` is sent to the SP, to the + ACS endpoint, in this case `acs.php` of the endpoints folder. + + 2. The SAML Response is processed in the ACS, if the `Response` is not valid, + the process stops here and a message is shown. Otherwise we are redirected + to the `RelayState` view (`sso.php` or `index.php`). The `sso.php` detects if the + user is logged and redirects to `index.php`, so we will be in the + `index.php` at the end. + + 3. We are logged into the app and the user attributes (if any) are shown. + At this point, we can test the single log out functionality. + + 4. The single log out functionality could be tested by two ways. + + 4.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that + we are redirected to the `slo.php` view and there a Logout Request is sent + to the IdP, the session at the IdP is closed and replies to the SP a + Logout Response (sent to the Single Logout Service endpoint). In this case + The SLS endpoint of the SP process the Logout Response and if is + valid, close the user session of the local app. Notice that the SLO + Workflow starts and ends at the SP. + + 4.2 SLO Initiated by IdP. In this case, the action takes place on the IdP + side, the logout process is initiated at the idP, sends a Logout + Request to the SP (SLS endpoint `sls.php` of the endpoint folder). + The SLS endpoint of the SP process the Logout Request and if is valid, + close the session of the user at the local app and sends a Logout Response + to the IdP (to the SLS endpoint of the IdP).The IdP receives the Logout + Response, process it and close the session at of the IdP. Notice that the + SLO Workflow starts and ends at the IdP. + diff --git a/saml/vendor/onelogin/php-saml/_toolkit_loader.php b/saml/vendor/onelogin/php-saml/_toolkit_loader.php new file mode 100644 index 00000000..c4649d76 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/_toolkit_loader.php @@ -0,0 +1,34 @@ + array( + 'requests' => true, + 'responses' => true + ), + + // Security settings + 'security' => array( + + /** signatures and encryptions offered */ + + // Indicates that the nameID of the sent by this SP + // will be encrypted. + 'nameIdEncrypted' => false, + + // Indicates whether the messages sent by this SP + // will be signed. [The Metadata of the SP will offer this info] + 'authnRequestsSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutRequestSigned' => false, + + // Indicates whether the messages sent by this SP + // will be signed. + 'logoutResponseSigned' => false, + + /* Sign the Metadata + False || True (use sp certs) || array ( + 'keyFileName' => 'metadata.key', + 'certFileName' => 'metadata.crt' + ) + || array ( + 'x509cert' => '', + 'privateKey' => '' + ) + */ + 'signMetadata' => false, + + + /** signatures and encryptions required **/ + + // Indicates a requirement for the , and + // elements received by this SP to be signed. + 'wantMessagesSigned' => false, + + // Indicates a requirement for the elements received by + // this SP to be encrypted. + 'wantAssertionsEncrypted' => false, + + // Indicates a requirement for the elements received by + // this SP to be signed. [The Metadata of the SP will offer this info] + 'wantAssertionsSigned' => false, + + // Indicates a requirement for the NameID element on the SAMLResponse received + // by this SP to be present. + 'wantNameId' => true, + + // Indicates a requirement for the NameID received by + // this SP to be encrypted. + 'wantNameIdEncrypted' => false, + + // Authentication context. + // Set to false and no AuthContext will be sent in the AuthNRequest, + // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport' + // Set an array with the possible auth context values: array('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'), + 'requestedAuthnContext' => false, + + // Allows the authn comparison parameter to be set, defaults to 'exact' if + // the setting is not present. + 'requestedAuthnContextComparison' => 'exact', + + // Indicates if the SP will validate all received xmls. + // (In order to validate the xml, 'strict' and 'wantXMLValidation' must be true). + 'wantXMLValidation' => true, + + // If true, SAMLResponses with an empty value at its Destination + // attribute will not be rejected for this fact. + 'relaxDestinationValidation' => false, + + // If true, Destination URL should strictly match to the address to + // which the response has been sent. + // Notice that if 'relaxDestinationValidation' is true an empty Destintation + // will be accepted. + 'destinationStrictlyMatches' => false, + + // If true, the toolkit will not raised an error when the Statement Element + // contain atribute elements with name duplicated + 'allowRepeatAttributeName' => false, + + // If true, SAMLResponses with an InResponseTo value will be rejectd if not + // AuthNRequest ID provided to the validation method. + 'rejectUnsolicitedResponsesWithInResponseTo' => false, + + // Algorithm that the toolkit will use on signing process. Options: + // 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' + // 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' + // 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' + // Notice that rsa-sha1 is a deprecated algorithm and should not be used + 'signatureAlgorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + + // Algorithm that the toolkit will use on digest process. Options: + // 'http://www.w3.org/2000/09/xmldsig#sha1' + // 'http://www.w3.org/2001/04/xmlenc#sha256' + // 'http://www.w3.org/2001/04/xmldsig-more#sha384' + // 'http://www.w3.org/2001/04/xmlenc#sha512' + // Notice that sha1 is a deprecated algorithm and should not be used + 'digestAlgorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256', + + // Algorithm that the toolkit will use for encryption process. Options: + // 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes128-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes192-cbc' + // 'http://www.w3.org/2001/04/xmlenc#aes256-cbc' + // 'http://www.w3.org/2009/xmlenc11#aes128-gcm' + // 'http://www.w3.org/2009/xmlenc11#aes192-gcm' + // 'http://www.w3.org/2009/xmlenc11#aes256-gcm'; + // Notice that aes-cbc are not consider secure anymore so should not be used + 'encryption_algorithm' => 'http://www.w3.org/2009/xmlenc11#aes128-gcm', + + // ADFS URL-Encodes SAML data as lowercase, and the toolkit by default uses + // uppercase. Turn it True for ADFS compatibility on signature verification + 'lowercaseUrlencoding' => false, + ), + + // Contact information template, it is recommended to suply a technical and support contacts + 'contactPerson' => array( + 'technical' => array( + 'givenName' => '', + 'emailAddress' => '' + ), + 'support' => array( + 'givenName' => '', + 'emailAddress' => '' + ), + ), + + // Organization information template, the info in en_US lang is recomended, add more if required + 'organization' => array( + 'en-US' => array( + 'name' => '', + 'displayname' => '', + 'url' => '' + ), + ), +); + + +/* Interoperable SAML 2.0 Web Browser SSO Profile [saml2int] http://saml2int.org/profile/current + + 'authnRequestsSigned' => false, // SP SHOULD NOT sign the , + // MUST NOT assume that the IdP validates the sign + 'wantAssertionsSigned' => true, + 'wantAssertionsEncrypted' => true, // MUST be enabled if SSL/HTTPs is disabled + 'wantNameIdEncrypted' => false, +*/ diff --git a/saml/vendor/onelogin/php-saml/certs/README b/saml/vendor/onelogin/php-saml/certs/README new file mode 100644 index 00000000..1616ebda --- /dev/null +++ b/saml/vendor/onelogin/php-saml/certs/README @@ -0,0 +1,14 @@ +Take care of this folder that could contain private key. Be sure that this folder never is published. + +Onelogin PHP Toolkit expects certs for the SP stored at: + + * sp.key Private Key + * sp.crt Public cert + * sp_new.crt Future Public cert + +Also you can use other cert to sign the metadata of the SP using the: + + * metadata.key + * metadata.crt + +If you are using composer to install the php-saml toolkit, You should move the certs folder to vendor/onelogin/php-saml/certs diff --git a/saml/vendor/onelogin/php-saml/composer.json b/saml/vendor/onelogin/php-saml/composer.json new file mode 100644 index 00000000..42290e8e --- /dev/null +++ b/saml/vendor/onelogin/php-saml/composer.json @@ -0,0 +1,42 @@ +{ + "name": "onelogin/php-saml", + "description": "OneLogin PHP SAML Toolkit", + "license": "MIT", + "homepage": "https://developers.onelogin.com/saml/php", + "keywords": ["saml", "saml2", "onelogin"], + "autoload": { + "psr-4": { + "OneLogin\\": "src/" + } + }, + "support": { + "email": "sixto.garcia@onelogin.com", + "issues": "https://github.com/onelogin/php-saml/issues", + "source": "https://github.com/onelogin/php-saml/" + }, + "require": { + "php": ">=7.3", + "robrichards/xmlseclibs": ">=3.1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "php-coveralls/php-coveralls": "^2.0", + "sebastian/phpcpd": "^4.0 || ^5.0 || ^6.0 ", + "phploc/phploc": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "pdepend/pdepend": "^2.8.0", + "squizlabs/php_codesniffer": "^3.5.8" + }, + "config": { + "platform": { + "php": "7.3.0" + }, + "optimize-autoloader": true, + "sort-packages": true + }, + "suggest": { + "ext-openssl": "Install openssl lib in order to handle with x509 certs (require to support sign and encryption)", + "ext-curl": "Install curl lib to be able to use the IdPMetadataParser for parsing remote XMLs", + "ext-dom": "Install xml lib", + "ext-zlib": "Install zlib" + } +} diff --git a/saml/vendor/onelogin/php-saml/phpunit.xml b/saml/vendor/onelogin/php-saml/phpunit.xml new file mode 100644 index 00000000..600c3bab --- /dev/null +++ b/saml/vendor/onelogin/php-saml/phpunit.xml @@ -0,0 +1,20 @@ + + + + + ./src + + + + + + + + + + + ./tests/src + + + + diff --git a/saml/vendor/onelogin/php-saml/settings_example.php b/saml/vendor/onelogin/php-saml/settings_example.php new file mode 100644 index 00000000..981a21a3 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/settings_example.php @@ -0,0 +1,137 @@ + true, + + // Enable debug mode (to print errors) + 'debug' => false, + + // Set a BaseURL to be used instead of try to guess + // the BaseURL of the view that process the SAML Message. + // Ex. http://sp.example.com/ + // http://example.com/sp/ + 'baseurl' => null, + + // Service Provider Data that we are deploying + 'sp' => array( + // Identifier of the SP entity (must be a URI) + 'entityId' => '', + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'assertionConsumerService' => array( + // URL Location where the from the IdP will be returned + 'url' => '', + // SAML protocol binding to be used when returning the + // message. Onelogin Toolkit supports for this endpoint the + // HTTP-POST binding only + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + ), + // If you need to specify requested attributes, set a + // attributeConsumingService. nameFormat, attributeValue and + // friendlyName can be omitted. Otherwise remove this section. + "attributeConsumingService"=> array( + "serviceName" => "SP test", + "serviceDescription" => "Test Service", + "requestedAttributes" => array( + array( + "name" => "", + "isRequired" => false, + "nameFormat" => "", + "friendlyName" => "", + "attributeValue" => "" + ) + ) + ), + // Specifies info about where and how the message MUST be + // returned to the requester, in this case our SP. + 'singleLogoutService' => array( + // URL Location where the from the IdP will be returned + 'url' => '', + // SAML protocol binding to be used when returning the + // message. Onelogin Toolkit supports for this endpoint the + // HTTP-Redirect binding only + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Specifies constraints on the name identifier to be used to + // represent the requested subject. + // Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + + // Usually x509cert and privateKey of the SP are provided by files placed at + // the certs folder. But we can also provide them with the following parameters + 'x509cert' => '', + 'privateKey' => '', + + /* + * Key rollover + * If you plan to update the SP x509cert and privateKey + * you can define here the new x509cert and it will be + * published on the SP metadata so Identity Providers can + * read them and get ready for rollover. + */ + // 'x509certNew' => '', + ), + + // Identity Provider Data that we want connect with our SP + 'idp' => array( + // Identifier of the IdP entity (must be a URI) + 'entityId' => '', + // SSO endpoint info of the IdP. (Authentication Request protocol) + 'singleSignOnService' => array( + // URL Target of the IdP where the SP will send the Authentication Request Message + 'url' => '', + // SAML protocol binding to be used when returning the + // message. Onelogin Toolkit supports for this endpoint the + // HTTP-Redirect binding only + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // SLO endpoint info of the IdP. + 'singleLogoutService' => array( + // URL Location of the IdP where the SP will send the SLO Request + 'url' => '', + // URL location of the IdP where the SP SLO Response will be sent (ResponseLocation) + // if not set, url for the SLO Request will be used + 'responseUrl' => '', + // SAML protocol binding to be used when returning the + // message. Onelogin Toolkit supports for this endpoint the + // HTTP-Redirect binding only + 'binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + ), + // Public x509 certificate of the IdP + 'x509cert' => '', + /* + * Instead of use the whole x509cert you can use a fingerprint in + * order to validate the SAMLResponse, but we don't recommend to use + * that method on production since is exploitable by a collision + * attack. + * (openssl x509 -noout -fingerprint -in "idp.crt" to generate it, + * or add for example the -sha256 , -sha384 or -sha512 parameter) + * + * If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to + * let the toolkit know which Algorithm was used. Possible values: sha1, sha256, sha384 or sha512 + * 'sha1' is the default value. + */ + // 'certFingerprint' => '', + // 'certFingerprintAlgorithm' => 'sha1', + + /* In some scenarios the IdP uses different certificates for + * signing/encryption, or is under key rollover phase and more + * than one certificate is published on IdP metadata. + * In order to handle that the toolkit offers that parameter. + * (when used, 'x509cert' and 'certFingerprint' values are + * ignored). + */ + // 'x509certMulti' => array( + // 'signing' => array( + // 0 => '', + // ), + // 'encryption' => array( + // 0 => '', + // ) + // ), + ), +); diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Auth.php b/saml/vendor/onelogin/php-saml/src/Saml2/Auth.php new file mode 100644 index 00000000..037b53fb --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Auth.php @@ -0,0 +1,818 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; + +use Exception; + +/** + * Main class of OneLogin's PHP Toolkit + */ +class Auth +{ + /** + * Settings data. + * + * @var Settings + */ + private $_settings; + + /** + * User attributes data. + * + * @var array + */ + private $_attributes = array(); + + /** + * User attributes data with FriendlyName index. + * + * @var array + */ + private $_attributesWithFriendlyName = array(); + + /** + * NameID + * + * @var string + */ + private $_nameid; + + /** + * NameID Format + * + * @var string + */ + private $_nameidFormat; + + /** + * NameID NameQualifier + * + * @var string + */ + private $_nameidNameQualifier; + + /** + * NameID SP NameQualifier + * + * @var string + */ + private $_nameidSPNameQualifier; + + /** + * If user is authenticated. + * + * @var bool + */ + private $_authenticated = false; + + + /** + * SessionIndex. When the user is logged, this stored it + * from the AuthnStatement of the SAML Response + * + * @var string + */ + private $_sessionIndex; + + /** + * SessionNotOnOrAfter. When the user is logged, this stored it + * from the AuthnStatement of the SAML Response + * + * @var int|null + */ + private $_sessionExpiration; + + /** + * The ID of the last message processed + * + * @var string + */ + private $_lastMessageId; + + /** + * The ID of the last assertion processed + * + * @var string + */ + private $_lastAssertionId; + + /** + * The NotOnOrAfter value of the valid SubjectConfirmationData + * node (if any) of the last assertion processed + * + * @var int + */ + private $_lastAssertionNotOnOrAfter; + + /** + * If any error. + * + * @var array + */ + private $_errors = array(); + + /** + * Last error object. + * + * @var Error|null + */ + private $_lastErrorException; + + /** + * Last error. + * + * @var string|null + */ + private $_lastError; + + /** + * Last AuthNRequest ID or LogoutRequest ID generated by this Service Provider + * + * @var string + */ + private $_lastRequestID; + + /** + * The most recently-constructed/processed XML SAML request + * (AuthNRequest, LogoutRequest) + * + * @var string + */ + private $_lastRequest; + + /** + * The most recently-constructed/processed XML SAML response + * (SAMLResponse, LogoutResponse). If the SAMLResponse was + * encrypted, by default tries to return the decrypted XML + * + * @var string|\DomDocument|null + */ + private $_lastResponse; + + /** + * Initializes the SP SAML instance. + * + * @param array|null $settings Setting data + * + * @throws Exception + * @throws Error + */ + public function __construct(array $settings = null) + { + $this->_settings = new Settings($settings); + } + + /** + * Returns the settings info + * + * @return Settings The settings data. + */ + public function getSettings() + { + return $this->_settings; + } + + /** + * Set the strict mode active/disable + * + * @param bool $value Strict parameter + * + * @throws Error + */ + public function setStrict($value) + { + if (!is_bool($value)) { + throw new Error( + 'Invalid value passed to setStrict()', + Error::SETTINGS_INVALID_SYNTAX + ); + } + + $this->_settings->setStrict($value); + } + + /** + * Set schemas path + * + * @param string $path + * @return $this + */ + public function setSchemasPath($path) + { + $this->_paths['schemas'] = $path; + } + + /** + * Process the SAML Response sent by the IdP. + * + * @param string|null $requestId The ID of the AuthNRequest sent by this SP to the IdP + * + * @throws Error + * @throws ValidationError + */ + public function processResponse($requestId = null) + { + $this->_errors = array(); + $this->_lastError = $this->_lastErrorException = null; + if (isset($_POST['SAMLResponse'])) { + // AuthnResponse -- HTTP_POST Binding + $response = new Response($this->_settings, $_POST['SAMLResponse']); + $this->_lastResponse = $response->getXMLDocument(); + + if ($response->isValid($requestId)) { + $this->_attributes = $response->getAttributes(); + $this->_attributesWithFriendlyName = $response->getAttributesWithFriendlyName(); + $this->_nameid = $response->getNameId(); + $this->_nameidFormat = $response->getNameIdFormat(); + $this->_nameidNameQualifier = $response->getNameIdNameQualifier(); + $this->_nameidSPNameQualifier = $response->getNameIdSPNameQualifier(); + $this->_authenticated = true; + $this->_sessionIndex = $response->getSessionIndex(); + $this->_sessionExpiration = $response->getSessionNotOnOrAfter(); + $this->_lastMessageId = $response->getId(); + $this->_lastAssertionId = $response->getAssertionId(); + $this->_lastAssertionNotOnOrAfter = $response->getAssertionNotOnOrAfter(); + } else { + $this->_errors[] = 'invalid_response'; + $this->_lastErrorException = $response->getErrorException(); + $this->_lastError = $response->getError(); + $this->_errors[] = $this->_lastError; + } + } else { + $this->_errors[] = 'invalid_binding'; + throw new Error( + 'SAML Response not found, Only supported HTTP_POST Binding', + Error::SAML_RESPONSE_NOT_FOUND + ); + } + } + + /** + * Process the SAML Logout Response / Logout Request sent by the IdP. + * + * @param bool $keepLocalSession When false will destroy the local session, otherwise will keep it + * @param string|null $requestId The ID of the LogoutRequest sent by this SP to the IdP + * @param bool $retrieveParametersFromServer True if we want to use parameters from $_SERVER to validate the signature + * @param callable $cbDeleteSession Callback to be executed to delete session + * @param bool $stay True if we want to stay (returns the url string) False to redirect + * + * @return string|null + * + * @throws Error + */ + public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false) + { + $this->_errors = array(); + $this->_lastError = $this->_lastErrorException = null; + if (isset($_GET['SAMLResponse'])) { + $logoutResponse = new LogoutResponse($this->_settings, $_GET['SAMLResponse']); + $this->_lastResponse = $logoutResponse->getXML(); + if (!$logoutResponse->isValid($requestId, $retrieveParametersFromServer)) { + $this->_errors[] = 'invalid_logout_response'; + $this->_lastErrorException = $logoutResponse->getErrorException(); + $this->_lastError = $logoutResponse->getError(); + + } else if ($logoutResponse->getStatus() !== Constants::STATUS_SUCCESS) { + $this->_errors[] = 'logout_not_success'; + } else { + $this->_lastMessageId = $logoutResponse->id; + if (!$keepLocalSession) { + if ($cbDeleteSession === null) { + Utils::deleteLocalSession(); + } else { + call_user_func($cbDeleteSession); + } + } + } + } else if (isset($_GET['SAMLRequest'])) { + $logoutRequest = new LogoutRequest($this->_settings, $_GET['SAMLRequest']); + $this->_lastRequest = $logoutRequest->getXML(); + if (!$logoutRequest->isValid($retrieveParametersFromServer)) { + $this->_errors[] = 'invalid_logout_request'; + $this->_lastErrorException = $logoutRequest->getErrorException(); + $this->_lastError = $logoutRequest->getError(); + } else { + if (!$keepLocalSession) { + if ($cbDeleteSession === null) { + Utils::deleteLocalSession(); + } else { + call_user_func($cbDeleteSession); + } + } + $inResponseTo = $logoutRequest->id; + $this->_lastMessageId = $logoutRequest->id; + $responseBuilder = new LogoutResponse($this->_settings); + $responseBuilder->build($inResponseTo); + $this->_lastResponse = $responseBuilder->getXML(); + + $logoutResponse = $responseBuilder->getResponse(); + + $parameters = array('SAMLResponse' => $logoutResponse); + if (isset($_GET['RelayState'])) { + $parameters['RelayState'] = $_GET['RelayState']; + } + + $security = $this->_settings->getSecurityData(); + if (isset($security['logoutResponseSigned']) && $security['logoutResponseSigned']) { + $signature = $this->buildResponseSignature($logoutResponse, isset($parameters['RelayState'])? $parameters['RelayState']: null, $security['signatureAlgorithm']); + $parameters['SigAlg'] = $security['signatureAlgorithm']; + $parameters['Signature'] = $signature; + } + + return $this->redirectTo($this->getSLOResponseUrl(), $parameters, $stay); + } + } else { + $this->_errors[] = 'invalid_binding'; + throw new Error( + 'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding', + Error::SAML_LOGOUTMESSAGE_NOT_FOUND + ); + } + } + + /** + * Redirects the user to the url past by parameter + * or to the url that we defined in our SSO Request. + * + * @param string $url The target URL to redirect the user. + * @param array $parameters Extra parameters to be passed as part of the url + * @param bool $stay True if we want to stay (returns the url string) False to redirect + * + * @return string|null + */ + public function redirectTo($url = '', array $parameters = array(), $stay = false) + { + assert(is_string($url)); + + if (empty($url) && isset($_REQUEST['RelayState'])) { + $url = $_REQUEST['RelayState']; + } + + return Utils::redirect($url, $parameters, $stay); + } + + /** + * Checks if the user is authenticated or not. + * + * @return bool True if the user is authenticated + */ + public function isAuthenticated() + { + return $this->_authenticated; + } + + /** + * Returns the set of SAML attributes. + * + * @return array Attributes of the user. + */ + public function getAttributes() + { + return $this->_attributes; + } + + + /** + * Returns the set of SAML attributes indexed by FriendlyName + * + * @return array Attributes of the user. + */ + public function getAttributesWithFriendlyName() + { + return $this->_attributesWithFriendlyName; + } + + /** + * Returns the nameID + * + * @return string The nameID of the assertion + */ + public function getNameId() + { + return $this->_nameid; + } + + /** + * Returns the nameID Format + * + * @return string The nameID Format of the assertion + */ + public function getNameIdFormat() + { + return $this->_nameidFormat; + } + + /** + * Returns the nameID NameQualifier + * + * @return string The nameID NameQualifier of the assertion + */ + public function getNameIdNameQualifier() + { + return $this->_nameidNameQualifier; + } + + /** + * Returns the nameID SP NameQualifier + * + * @return string The nameID SP NameQualifier of the assertion + */ + public function getNameIdSPNameQualifier() + { + return $this->_nameidSPNameQualifier; + } + + /** + * Returns the SessionIndex + * + * @return string|null The SessionIndex of the assertion + */ + public function getSessionIndex() + { + return $this->_sessionIndex; + } + + /** + * Returns the SessionNotOnOrAfter + * + * @return int|null The SessionNotOnOrAfter of the assertion + */ + public function getSessionExpiration() + { + return $this->_sessionExpiration; + } + + /** + * Returns if there were any error + * + * @return array Errors + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Returns the reason for the last error + * + * @return string|null Error reason + */ + public function getLastErrorReason() + { + return $this->_lastError; + } + + + /** + * Returns the last error + * + * @return Exception|null Error + */ + public function getLastErrorException() + { + return $this->_lastErrorException; + } + + /** + * Returns the requested SAML attribute + * + * @param string $name The requested attribute of the user. + * + * @return array|null Requested SAML attribute ($name). + */ + public function getAttribute($name) + { + assert(is_string($name)); + + $value = null; + if (isset($this->_attributes[$name])) { + return $this->_attributes[$name]; + } + return $value; + } + + /** + * Returns the requested SAML attribute indexed by FriendlyName + * + * @param string $friendlyName The requested attribute of the user. + * + * @return array|null Requested SAML attribute ($friendlyName). + */ + public function getAttributeWithFriendlyName($friendlyName) + { + assert(is_string($friendlyName)); + $value = null; + if (isset($this->_attributesWithFriendlyName[$friendlyName])) { + return $this->_attributesWithFriendlyName[$friendlyName]; + } + return $value; + } + + /** + * Initiates the SSO process. + * + * @param string|null $returnTo The target URL the user should be returned to after login. + * @param array $parameters Extra parameters to be added to the GET + * @param bool $forceAuthn When true the AuthNRequest will set the ForceAuthn='true' + * @param bool $isPassive When true the AuthNRequest will set the Ispassive='true' + * @param bool $stay True if we want to stay (returns the url string) False to redirect + * @param bool $setNameIdPolicy When true the AuthNRequest will set a nameIdPolicy element + * @param string $nameIdValueReq Indicates to the IdP the subject that should be authenticated + * + * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters + * + * @throws Error + */ + public function login($returnTo = null, array $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null) + { + $authnRequest = $this->buildAuthnRequest($this->_settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq); + + $this->_lastRequest = $authnRequest->getXML(); + $this->_lastRequestID = $authnRequest->getId(); + + $samlRequest = $authnRequest->getRequest(); + $parameters['SAMLRequest'] = $samlRequest; + + if (!empty($returnTo)) { + $parameters['RelayState'] = $returnTo; + } else { + $parameters['RelayState'] = Utils::getSelfRoutedURLNoQuery(); + } + + $security = $this->_settings->getSecurityData(); + if (isset($security['authnRequestsSigned']) && $security['authnRequestsSigned']) { + $signature = $this->buildRequestSignature($samlRequest, $parameters['RelayState'], $security['signatureAlgorithm']); + $parameters['SigAlg'] = $security['signatureAlgorithm']; + $parameters['Signature'] = $signature; + } + return $this->redirectTo($this->getSSOurl(), $parameters, $stay); + } + + /** + * Initiates the SLO process. + * + * @param string|null $returnTo The target URL the user should be returned to after logout. + * @param array $parameters Extra parameters to be added to the GET + * @param string|null $nameId The NameID that will be set in the LogoutRequest. + * @param string|null $sessionIndex The SessionIndex (taken from the SAML Response in the SSO process). + * @param bool $stay True if we want to stay (returns the url string) False to redirect + * @param string|null $nameIdFormat The NameID Format will be set in the LogoutRequest. + * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest. + * + * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters + * + * @throws Error + */ + public function logout($returnTo = null, array $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null) + { + $sloUrl = $this->getSLOurl(); + if (empty($sloUrl)) { + throw new Error( + 'The IdP does not support Single Log Out', + Error::SAML_SINGLE_LOGOUT_NOT_SUPPORTED + ); + } + + if (empty($nameId) && !empty($this->_nameid)) { + $nameId = $this->_nameid; + } + if (empty($nameIdFormat) && !empty($this->_nameidFormat)) { + $nameIdFormat = $this->_nameidFormat; + } + + $logoutRequest = new LogoutRequest($this->_settings, null, $nameId, $sessionIndex, $nameIdFormat, $nameIdNameQualifier, $nameIdSPNameQualifier); + + $this->_lastRequest = $logoutRequest->getXML(); + $this->_lastRequestID = $logoutRequest->id; + + $samlRequest = $logoutRequest->getRequest(); + + $parameters['SAMLRequest'] = $samlRequest; + if (!empty($returnTo)) { + $parameters['RelayState'] = $returnTo; + } else { + $parameters['RelayState'] = Utils::getSelfRoutedURLNoQuery(); + } + + $security = $this->_settings->getSecurityData(); + if (isset($security['logoutRequestSigned']) && $security['logoutRequestSigned']) { + $signature = $this->buildRequestSignature($samlRequest, $parameters['RelayState'], $security['signatureAlgorithm']); + $parameters['SigAlg'] = $security['signatureAlgorithm']; + $parameters['Signature'] = $signature; + } + + return $this->redirectTo($sloUrl, $parameters, $stay); + } + + /** + * Gets the IdP SSO url. + * + * @return string The url of the IdP Single Sign On Service + */ + public function getSSOurl() + { + return $this->_settings->getIdPSSOUrl(); + } + + /** + * Gets the IdP SLO url. + * + * @return string|null The url of the IdP Single Logout Service + */ + public function getSLOurl() + { + return $this->_settings->getIdPSLOUrl(); + } + + /** + * Gets the IdP SLO response url. + * + * @return string|null The response url of the IdP Single Logout Service + */ + public function getSLOResponseUrl() + { + return $this->_settings->getIdPSLOResponseUrl(); + } + + + /** + * Gets the ID of the last AuthNRequest or LogoutRequest generated by the Service Provider. + * + * @return string The ID of the Request SAML message. + */ + public function getLastRequestID() + { + return $this->_lastRequestID; + } + + /** + * Creates an AuthnRequest + * + * @param Settings $settings Setting data + * @param bool $forceAuthn When true the AuthNRequest will set the ForceAuthn='true' + * @param bool $isPassive When true the AuthNRequest will set the Ispassive='true' + * @param bool $setNameIdPolicy When true the AuthNRequest will set a nameIdPolicy element + * @param string $nameIdValueReq Indicates to the IdP the subject that should be authenticated + * + * @return AuthnRequest The AuthnRequest object + */ + public function buildAuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq = null) + { + return new AuthnRequest($settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq); + } + + /** + * Generates the Signature for a SAML Request + * + * @param string $samlRequest The SAML Request + * @param string $relayState The RelayState + * @param string $signAlgorithm Signature algorithm method + * + * @return string A base64 encoded signature + * + * @throws Exception + * @throws Error + */ + public function buildRequestSignature($samlRequest, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA256) + { + return $this->buildMessageSignature($samlRequest, $relayState, $signAlgorithm, "SAMLRequest"); + } + + /** + * Generates the Signature for a SAML Response + * + * @param string $samlResponse The SAML Response + * @param string $relayState The RelayState + * @param string $signAlgorithm Signature algorithm method + * + * @return string A base64 encoded signature + * + * @throws Exception + * @throws Error + */ + public function buildResponseSignature($samlResponse, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA256) + { + return $this->buildMessageSignature($samlResponse, $relayState, $signAlgorithm, "SAMLResponse"); + } + + /** + * Generates the Signature for a SAML Message + * + * @param string $samlMessage The SAML Message + * @param string $relayState The RelayState + * @param string $signAlgorithm Signature algorithm method + * @param string $type "SAMLRequest" or "SAMLResponse" + * + * @return string A base64 encoded signature + * + * @throws Exception + * @throws Error + */ + private function buildMessageSignature($samlMessage, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $type = "SAMLRequest") + { + $key = $this->_settings->getSPkey(); + if (empty($key)) { + if ($type == "SAMLRequest") { + $errorMsg = "Trying to sign the SAML Request but can't load the SP private key"; + } else { + $errorMsg = "Trying to sign the SAML Response but can't load the SP private key"; + } + + throw new Error($errorMsg, Error::PRIVATE_KEY_NOT_FOUND); + } + + $objKey = new XMLSecurityKey($signAlgorithm, array('type' => 'private')); + $objKey->loadKey($key, false); + + $security = $this->_settings->getSecurityData(); + if ($security['lowercaseUrlencoding']) { + $msg = $type.'='.rawurlencode($samlMessage); + if (isset($relayState)) { + $msg .= '&RelayState='.rawurlencode($relayState); + } + $msg .= '&SigAlg=' . rawurlencode($signAlgorithm); + } else { + $msg = $type.'='.urlencode($samlMessage); + if (isset($relayState)) { + $msg .= '&RelayState='.urlencode($relayState); + } + $msg .= '&SigAlg=' . urlencode($signAlgorithm); + } + $signature = $objKey->signData($msg); + return base64_encode($signature); + } + + /** + * @return string The ID of the last message processed + */ + public function getLastMessageId() + { + return $this->_lastMessageId; + } + + /** + * @return string The ID of the last assertion processed + */ + public function getLastAssertionId() + { + return $this->_lastAssertionId; + } + + /** + * @return int The NotOnOrAfter value of the valid + * SubjectConfirmationData node (if any) + * of the last assertion processed + */ + public function getLastAssertionNotOnOrAfter() + { + return $this->_lastAssertionNotOnOrAfter; + } + + /** + * Returns the most recently-constructed/processed + * XML SAML request (AuthNRequest, LogoutRequest) + * + * @return string|null The Request XML + */ + public function getLastRequestXML() + { + return $this->_lastRequest; + } + + /** + * Returns the most recently-constructed/processed + * XML SAML response (SAMLResponse, LogoutResponse). + * If the SAMLResponse was encrypted, by default tries + * to return the decrypted XML. + * + * @return string|null The Response XML + */ + public function getLastResponseXML() + { + $response = null; + if (isset($this->_lastResponse)) { + if (is_string($this->_lastResponse)) { + $response = $this->_lastResponse; + } else { + $response = $this->_lastResponse->saveXML(); + } + } + + return $response; + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php b/saml/vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php new file mode 100644 index 00000000..fd9afb53 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/AuthnRequest.php @@ -0,0 +1,214 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +/** + * SAML 2 Authentication Request + */ +class AuthnRequest +{ + /** + * Object that represents the setting info + * + * @var Settings + */ + protected $_settings; + + /** + * SAML AuthNRequest string + * + * @var string + */ + private $_authnRequest; + + /** + * SAML AuthNRequest ID. + * + * @var string + */ + private $_id; + + /** + * Constructs the AuthnRequest object. + * + * @param Settings $settings SAML Toolkit Settings + * @param bool $forceAuthn When true the AuthNReuqest will set the ForceAuthn='true' + * @param bool $isPassive When true the AuthNReuqest will set the Ispassive='true' + * @param bool $setNameIdPolicy When true the AuthNReuqest will set a nameIdPolicy + * @param string $nameIdValueReq Indicates to the IdP the subject that should be authenticated + */ + public function __construct(\OneLogin\Saml2\Settings $settings, $forceAuthn = false, $isPassive = false, $setNameIdPolicy = true, $nameIdValueReq = null) + { + $this->_settings = $settings; + + $spData = $this->_settings->getSPData(); + $security = $this->_settings->getSecurityData(); + + $id = Utils::generateUniqueID(); + $issueInstant = Utils::parseTime2SAML(time()); + + $subjectStr = ""; + if (isset($nameIdValueReq)) { + $subjectStr = << + {$nameIdValueReq} + + +SUBJECT; + } + + $nameIdPolicyStr = ''; + if ($setNameIdPolicy) { + $nameIDPolicyFormat = $spData['NameIDFormat']; + if (isset($security['wantNameIdEncrypted']) && $security['wantNameIdEncrypted']) { + $nameIDPolicyFormat = Constants::NAMEID_ENCRYPTED; + } + + $nameIdPolicyStr = << +NAMEIDPOLICY; + } + + + $providerNameStr = ''; + $organizationData = $settings->getOrganization(); + if (!empty($organizationData)) { + $langs = array_keys($organizationData); + if (in_array('en-US', $langs)) { + $lang = 'en-US'; + } else { + $lang = $langs[0]; + } + if (isset($organizationData[$lang]['displayname']) && !empty($organizationData[$lang]['displayname'])) { + $providerNameStr = << + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + +REQUESTEDAUTHN; + } else { + $requestedAuthnStr .= " \n"; + foreach ($security['requestedAuthnContext'] as $contextValue) { + $requestedAuthnStr .= " ".$contextValue."\n"; + } + $requestedAuthnStr .= ' '; + } + } + + $spEntityId = htmlspecialchars($spData['entityId'], ENT_QUOTES); + $acsUrl = htmlspecialchars($spData['assertionConsumerService']['url'], ENT_QUOTES); + $destination = $this->_settings->getIdPSSOUrl(); + $request = << + {$spEntityId}{$subjectStr}{$nameIdPolicyStr}{$requestedAuthnStr} + +AUTHNREQUEST; + + $this->_id = $id; + $this->_authnRequest = $request; + } + + /** + * Returns deflated, base64 encoded, unsigned AuthnRequest. + * + * @param bool|null $deflate Whether or not we should 'gzdeflate' the request body before we return it. + * + * @return string + */ + public function getRequest($deflate = null) + { + $subject = $this->_authnRequest; + + if (is_null($deflate)) { + $deflate = $this->_settings->shouldCompressRequests(); + } + + if ($deflate) { + $subject = gzdeflate($this->_authnRequest); + } + + $base64Request = base64_encode($subject); + return $base64Request; + } + + /** + * Returns the AuthNRequest ID. + * + * @return string + */ + public function getId() + { + return $this->_id; + } + + /** + * Returns the XML that will be sent as part of the request + * + * @return string + */ + public function getXML() + { + return $this->_authnRequest; + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Constants.php b/saml/vendor/onelogin/php-saml/src/Saml2/Constants.php new file mode 100644 index 00000000..1b467dd6 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Constants.php @@ -0,0 +1,86 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +/** + * Constants of OneLogin PHP Toolkit + * + * Defines all required constants + */ +class Constants +{ + // Value added to the current time in time condition validations + const ALLOWED_CLOCK_DRIFT = 180; // 3 min in seconds + + // NameID Formats + const NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'; + const NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'; + const NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'; + const NAMEID_UNSPECIFIED = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'; + const NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'; + const NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'; + const NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'; + const NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + const NAMEID_ENCRYPTED = 'urn:oasis:names:tc:SAML:2.0:nameid-format:encrypted'; + + // Attribute Name Formats + const ATTRNAME_FORMAT_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified'; + const ATTRNAME_FORMAT_URI = 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri'; + const ATTRNAME_FORMAT_BASIC = 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'; + + // Namespaces + const NS_SAML = 'urn:oasis:names:tc:SAML:2.0:assertion'; + const NS_SAMLP = 'urn:oasis:names:tc:SAML:2.0:protocol'; + const NS_SOAP = 'http://schemas.xmlsoap.org/soap/envelope/'; + const NS_MD = 'urn:oasis:names:tc:SAML:2.0:metadata'; + const NS_XS = 'http://www.w3.org/2001/XMLSchema'; + const NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'; + const NS_XENC = 'http://www.w3.org/2001/04/xmlenc#'; + const NS_DS = 'http://www.w3.org/2000/09/xmldsig#'; + + // Bindings + const BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'; + const BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'; + const BINDING_HTTP_ARTIFACT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'; + const BINDING_SOAP = 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP'; + const BINDING_DEFLATE = 'urn:oasis:names:tc:SAML:2.0:bindings:URL-Encoding:DEFLATE'; + + // Auth Context Class + const AC_UNSPECIFIED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified'; + const AC_PASSWORD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'; + const AC_PASSWORD_PROTECTED = 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'; + const AC_X509 = 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'; + const AC_SMARTCARD = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Smartcard'; + const AC_SMARTCARD_PKI = 'urn:oasis:names:tc:SAML:2.0:ac:classes:SmartcardPKI'; + const AC_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos'; + const AC_WINDOWS = 'urn:federation:authentication:windows'; + const AC_TLS = 'urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient'; + const AC_RSATOKEN = 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken'; + + // Subject Confirmation + const CM_BEARER = 'urn:oasis:names:tc:SAML:2.0:cm:bearer'; + const CM_HOLDER_KEY = 'urn:oasis:names:tc:SAML:2.0:cm:holder-of-key'; + const CM_SENDER_VOUCHES = 'urn:oasis:names:tc:SAML:2.0:cm:sender-vouches'; + + // Status Codes + const STATUS_SUCCESS = 'urn:oasis:names:tc:SAML:2.0:status:Success'; + const STATUS_REQUESTER = 'urn:oasis:names:tc:SAML:2.0:status:Requester'; + const STATUS_RESPONDER = 'urn:oasis:names:tc:SAML:2.0:status:Responder'; + const STATUS_VERSION_MISMATCH = 'urn:oasis:names:tc:SAML:2.0:status:VersionMismatch'; + const STATUS_NO_PASSIVE = 'urn:oasis:names:tc:SAML:2.0:status:NoPassive'; + const STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout'; + const STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded'; +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Error.php b/saml/vendor/onelogin/php-saml/src/Saml2/Error.php new file mode 100644 index 00000000..211acf48 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Error.php @@ -0,0 +1,66 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use Exception; + +/** + * Error class of OneLogin PHP Toolkit + * + * Defines the Error class + */ +class Error extends Exception +{ + // Errors + const SETTINGS_FILE_NOT_FOUND = 0; + const SETTINGS_INVALID_SYNTAX = 1; + const SETTINGS_INVALID = 2; + const METADATA_SP_INVALID = 3; + const SP_CERTS_NOT_FOUND = 4; + // SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead + const CERT_NOT_FOUND = 4; + const REDIRECT_INVALID_URL = 5; + const PUBLIC_CERT_FILE_NOT_FOUND = 6; + const PRIVATE_KEY_FILE_NOT_FOUND = 7; + const SAML_RESPONSE_NOT_FOUND = 8; + const SAML_LOGOUTMESSAGE_NOT_FOUND = 9; + const SAML_LOGOUTREQUEST_INVALID = 10; + const SAML_LOGOUTRESPONSE_INVALID = 11; + const SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12; + const PRIVATE_KEY_NOT_FOUND = 13; + const UNSUPPORTED_SETTINGS_OBJECT = 14; + + /** + * Constructor + * + * @param string $msg Describes the error. + * @param int $code The code error (defined in the error class). + * @param array|null $args Arguments used in the message that describes the error. + */ + public function __construct($msg, $code = 0, $args = array()) + { + assert(is_string($msg)); + assert(is_int($code)); + + if (!isset($args)) { + $args = array(); + } + $params = array_merge(array($msg), $args); + $message = call_user_func_array('sprintf', $params); + + parent::__construct($message, $code); + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/IdPMetadataParser.php b/saml/vendor/onelogin/php-saml/src/Saml2/IdPMetadataParser.php new file mode 100644 index 00000000..947d6548 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/IdPMetadataParser.php @@ -0,0 +1,243 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use DOMDocument; +use Exception; + +/** + * IdP Metadata Parser of OneLogin PHP Toolkit + */ +class IdPMetadataParser +{ + /** + * Get IdP Metadata Info from URL + * + * @param string $url URL where the IdP metadata is published + * @param string $entityId Entity Id of the desired IdP, if no + * entity Id is provided and the XML + * metadata contains more than one + * IDPSSODescriptor, the first is returned + * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat + * @param string $desiredSSOBinding Parse specific binding SSO endpoint + * @param string $desiredSLOBinding Parse specific binding SLO endpoint + * + * @return array metadata info in php-saml settings format + */ + public static function parseRemoteXML($url, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT) + { + $metadataInfo = array(); + + try { + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_FAILONERROR, 1); + + $xml = curl_exec($ch); + if ($xml !== false) { + $metadataInfo = self::parseXML($xml, $entityId, $desiredNameIdFormat, $desiredSSOBinding, $desiredSLOBinding); + } else { + throw new Exception(curl_error($ch), curl_errno($ch)); + } + } catch (Exception $e) { + throw new Exception('Error on parseRemoteXML. '.$e->getMessage()); + } + return $metadataInfo; + } + + /** + * Get IdP Metadata Info from File + * + * @param string $filepath File path + * @param string $entityId Entity Id of the desired IdP, if no + * entity Id is provided and the XML + * metadata contains more than one + * IDPSSODescriptor, the first is returned + * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat + * @param string $desiredSSOBinding Parse specific binding SSO endpoint + * @param string $desiredSLOBinding Parse specific binding SLO endpoint + * + * @return array metadata info in php-saml settings format + */ + public static function parseFileXML($filepath, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT) + { + $metadataInfo = array(); + + try { + if (file_exists($filepath)) { + $data = file_get_contents($filepath); + $metadataInfo = self::parseXML($data, $entityId, $desiredNameIdFormat, $desiredSSOBinding, $desiredSLOBinding); + } + } catch (Exception $e) { + throw new Exception('Error on parseFileXML. '.$e->getMessage()); + } + return $metadataInfo; + } + + /** + * Get IdP Metadata Info from URL + * + * @param string $xml XML that contains IdP metadata + * @param string $entityId Entity Id of the desired IdP, if no + * entity Id is provided and the XML + * metadata contains more than one + * IDPSSODescriptor, the first is returned + * @param string $desiredNameIdFormat If available on IdP metadata, use that nameIdFormat + * @param string $desiredSSOBinding Parse specific binding SSO endpoint + * @param string $desiredSLOBinding Parse specific binding SLO endpoint + * + * @return array metadata info in php-saml settings format + * + * @throws Exception + */ + public static function parseXML($xml, $entityId = null, $desiredNameIdFormat = null, $desiredSSOBinding = Constants::BINDING_HTTP_REDIRECT, $desiredSLOBinding = Constants::BINDING_HTTP_REDIRECT) + { + $metadataInfo = array(); + + $dom = new DOMDocument(); + $dom->preserveWhiteSpace = false; + $dom->formatOutput = true; + try { + $dom = Utils::loadXML($dom, $xml); + if (!$dom) { + throw new Exception('Error parsing metadata'); + } + + $customIdPStr = ''; + if (!empty($entityId)) { + $customIdPStr = '[@entityID="' . $entityId . '"]'; + } + $idpDescryptorXPath = '//md:EntityDescriptor' . $customIdPStr . '/md:IDPSSODescriptor'; + + $idpDescriptorNodes = Utils::query($dom, $idpDescryptorXPath); + + if (isset($idpDescriptorNodes) && $idpDescriptorNodes->length > 0) { + $metadataInfo['idp'] = array(); + + $idpDescriptor = $idpDescriptorNodes->item(0); + + if (empty($entityId) && $idpDescriptor->parentNode->hasAttribute('entityID')) { + $entityId = $idpDescriptor->parentNode->getAttribute('entityID'); + } + + if (!empty($entityId)) { + $metadataInfo['idp']['entityId'] = $entityId; + } + + $ssoNodes = Utils::query($dom, './md:SingleSignOnService[@Binding="'.$desiredSSOBinding.'"]', $idpDescriptor); + if ($ssoNodes->length < 1) { + $ssoNodes = Utils::query($dom, './md:SingleSignOnService', $idpDescriptor); + } + if ($ssoNodes->length > 0) { + $metadataInfo['idp']['singleSignOnService'] = array( + 'url' => $ssoNodes->item(0)->getAttribute('Location'), + 'binding' => $ssoNodes->item(0)->getAttribute('Binding') + ); + } + + $sloNodes = Utils::query($dom, './md:SingleLogoutService[@Binding="'.$desiredSLOBinding.'"]', $idpDescriptor); + if ($sloNodes->length < 1) { + $sloNodes = Utils::query($dom, './md:SingleLogoutService', $idpDescriptor); + } + if ($sloNodes->length > 0) { + $metadataInfo['idp']['singleLogoutService'] = array( + 'url' => $sloNodes->item(0)->getAttribute('Location'), + 'binding' => $sloNodes->item(0)->getAttribute('Binding') + ); + + if ($sloNodes->item(0)->hasAttribute('ResponseLocation')) { + $metadataInfo['idp']['singleLogoutService']['responseUrl'] = $sloNodes->item(0)->getAttribute('ResponseLocation'); + } + } + + $keyDescriptorCertSigningNodes = Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "encryption"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor); + + $keyDescriptorCertEncryptionNodes = Utils::query($dom, './md:KeyDescriptor[not(contains(@use, "signing"))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate', $idpDescriptor); + + if (!empty($keyDescriptorCertSigningNodes) || !empty($keyDescriptorCertEncryptionNodes)) { + $metadataInfo['idp']['x509certMulti'] = array(); + if (!empty($keyDescriptorCertSigningNodes)) { + $idpInfo['x509certMulti']['signing'] = array(); + foreach ($keyDescriptorCertSigningNodes as $keyDescriptorCertSigningNode) { + $metadataInfo['idp']['x509certMulti']['signing'][] = Utils::formatCert($keyDescriptorCertSigningNode->nodeValue, false); + } + } + if (!empty($keyDescriptorCertEncryptionNodes)) { + $idpInfo['x509certMulti']['encryption'] = array(); + foreach ($keyDescriptorCertEncryptionNodes as $keyDescriptorCertEncryptionNode) { + $metadataInfo['idp']['x509certMulti']['encryption'][] = Utils::formatCert($keyDescriptorCertEncryptionNode->nodeValue, false); + } + } + + $idpCertdata = $metadataInfo['idp']['x509certMulti']; + if ((count($idpCertdata) == 1 and + ((isset($idpCertdata['signing']) and count($idpCertdata['signing']) == 1) or (isset($idpCertdata['encryption']) and count($idpCertdata['encryption']) == 1))) or + ((isset($idpCertdata['signing']) && count($idpCertdata['signing']) == 1) && isset($idpCertdata['encryption']) && count($idpCertdata['encryption']) == 1 && strcmp($idpCertdata['signing'][0], $idpCertdata['encryption'][0]) == 0)) { + if (isset($metadataInfo['idp']['x509certMulti']['signing'][0])) { + $metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['signing'][0]; + } else { + $metadataInfo['idp']['x509cert'] = $metadataInfo['idp']['x509certMulti']['encryption'][0]; + } + unset($metadataInfo['idp']['x509certMulti']); + } + } + + $nameIdFormatNodes = Utils::query($dom, './md:NameIDFormat', $idpDescriptor); + if ($nameIdFormatNodes->length > 0) { + $metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNodes->item(0)->nodeValue; + if (!empty($desiredNameIdFormat)) { + foreach ($nameIdFormatNodes as $nameIdFormatNode) { + if (strcmp($nameIdFormatNode->nodeValue, $desiredNameIdFormat) == 0) { + $metadataInfo['sp']['NameIDFormat'] = $nameIdFormatNode->nodeValue; + break; + } + } + } + } + } + } catch (Exception $e) { + throw new Exception('Error parsing metadata. '.$e->getMessage()); + } + + return $metadataInfo; + } + + /** + * Inject metadata info into php-saml settings array + * + * @param array $settings php-saml settings array + * @param array $metadataInfo array metadata info + * + * @return array settings + */ + public static function injectIntoSettings($settings, $metadataInfo) + { + if (isset($metadataInfo['idp']) && isset($settings['idp'])) { + if (isset($metadataInfo['idp']['x509certMulti']) && !empty($metadataInfo['idp']['x509certMulti']) && isset($settings['idp']['x509cert'])) { + unset($settings['idp']['x509cert']); + } + + if (isset($metadataInfo['idp']['x509cert']) && !empty($metadataInfo['idp']['x509cert']) && isset($settings['idp']['x509certMulti'])) { + unset($settings['idp']['x509certMulti']); + } + } + + return array_replace_recursive($settings, $metadataInfo); + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php b/saml/vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php new file mode 100644 index 00000000..108c49be --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/LogoutRequest.php @@ -0,0 +1,494 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; + +use DOMDocument; +use Exception; + +/** + * SAML 2 Logout Request + */ +class LogoutRequest +{ + /** + * Contains the ID of the Logout Request + * + * @var string + */ + public $id; + + /** + * Object that represents the setting info + * + * @var Settings + */ + protected $_settings; + + /** + * SAML Logout Request + * + * @var string + */ + protected $_logoutRequest; + + /** + * After execute a validation process, this var contains the cause + * + * @var Exception + */ + private $_error; + + /** + * Constructs the Logout Request object. + * + * @param Settings $settings Settings + * @param string|null $request A UUEncoded Logout Request. + * @param string|null $nameId The NameID that will be set in the LogoutRequest. + * @param string|null $sessionIndex The SessionIndex (taken from the SAML Response in the SSO process). + * @param string|null $nameIdFormat The NameID Format will be set in the LogoutRequest. + * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest. + * @param string|null $nameIdSPNameQualifier The NameID SP NameQualifier will be set in the LogoutRequest. + */ + public function __construct(\OneLogin\Saml2\Settings $settings, $request = null, $nameId = null, $sessionIndex = null, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null) + { + $this->_settings = $settings; + + $baseURL = $this->_settings->getBaseURL(); + if (!empty($baseURL)) { + Utils::setBaseURL($baseURL); + } + + if (!isset($request) || empty($request)) { + $spData = $this->_settings->getSPData(); + $idpData = $this->_settings->getIdPData(); + $security = $this->_settings->getSecurityData(); + + $id = Utils::generateUniqueID(); + $this->id = $id; + + $issueInstant = Utils::parseTime2SAML(time()); + + $cert = null; + if (isset($security['nameIdEncrypted']) && $security['nameIdEncrypted']) { + $existsMultiX509Enc = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['encryption']) && !empty($idpData['x509certMulti']['encryption']); + + if ($existsMultiX509Enc) { + $cert = $idpData['x509certMulti']['encryption'][0]; + } else { + $cert = $idpData['x509cert']; + } + } + + if (!empty($nameId)) { + if (empty($nameIdFormat) + && $spData['NameIDFormat'] != Constants::NAMEID_UNSPECIFIED) { + $nameIdFormat = $spData['NameIDFormat']; + } + } else { + $nameId = $idpData['entityId']; + $nameIdFormat = Constants::NAMEID_ENTITY; + } + + /* From saml-core-2.0-os 8.3.6, when the entity Format is used: + "The NameQualifier, SPNameQualifier, and SPProvidedID attributes MUST be omitted. + */ + if (!empty($nameIdFormat) && $nameIdFormat == Constants::NAMEID_ENTITY) { + $nameIdNameQualifier = null; + $nameIdSPNameQualifier = null; + } + + // NameID Format UNSPECIFIED omitted + if (!empty($nameIdFormat) && $nameIdFormat == Constants::NAMEID_UNSPECIFIED) { + $nameIdFormat = null; + } + + $nameIdObj = Utils::generateNameId( + $nameId, + $nameIdSPNameQualifier, + $nameIdFormat, + $cert, + $nameIdNameQualifier, + $security['encryption_algorithm'] + ); + + $sessionIndexStr = isset($sessionIndex) ? "{$sessionIndex}" : ""; + + $spEntityId = htmlspecialchars($spData['entityId'], ENT_QUOTES); + $destination = $this->_settings->getIdPSLOUrl(); + $logoutRequest = << + {$spEntityId} + {$nameIdObj} + {$sessionIndexStr} + +LOGOUTREQUEST; + } else { + $decoded = base64_decode($request); + // We try to inflate + $inflated = @gzinflate($decoded); + if ($inflated != false) { + $logoutRequest = $inflated; + } else { + $logoutRequest = $decoded; + } + $this->id = static::getID($logoutRequest); + } + $this->_logoutRequest = $logoutRequest; + } + + /** + * Returns the Logout Request defated, base64encoded, unsigned + * + * @param bool|null $deflate Whether or not we should 'gzdeflate' the request body before we return it. + * + * @return string Deflated base64 encoded Logout Request + */ + public function getRequest($deflate = null) + { + $subject = $this->_logoutRequest; + + if (is_null($deflate)) { + $deflate = $this->_settings->shouldCompressRequests(); + } + + if ($deflate) { + $subject = gzdeflate($this->_logoutRequest); + } + + return base64_encode($subject); + } + + /** + * Returns the ID of the Logout Request. + * + * @param string|DOMDocument $request Logout Request Message + * + * @return string ID + * + * @throws Error + */ + public static function getID($request) + { + if ($request instanceof DOMDocument) { + $dom = $request; + } else { + $dom = new DOMDocument(); + $dom = Utils::loadXML($dom, $request); + } + + + if (false === $dom) { + throw new Error( + "LogoutRequest could not be processed", + Error::SAML_LOGOUTREQUEST_INVALID + ); + } + + $id = $dom->documentElement->getAttribute('ID'); + return $id; + } + + /** + * Gets the NameID Data of the the Logout Request. + * + * @param string|DOMDocument $request Logout Request Message + * @param string|null $key The SP key + * + * @return array Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + * + * @throws Error + * @throws Exception + * @throws ValidationError + */ + public static function getNameIdData($request, $key = null) + { + if ($request instanceof DOMDocument) { + $dom = $request; + } else { + $dom = new DOMDocument(); + $dom = Utils::loadXML($dom, $request); + } + + $encryptedEntries = Utils::query($dom, '/samlp:LogoutRequest/saml:EncryptedID'); + + if ($encryptedEntries->length == 1) { + $encryptedDataNodes = $encryptedEntries->item(0)->getElementsByTagName('EncryptedData'); + $encryptedData = $encryptedDataNodes->item(0); + + if (empty($key)) { + throw new Error( + "Private Key is required in order to decrypt the NameID, check settings", + Error::PRIVATE_KEY_NOT_FOUND + ); + } + + $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private')); + $seckey->loadKey($key); + + $nameId = Utils::decryptElement($encryptedData, $seckey); + + } else { + $entries = Utils::query($dom, '/samlp:LogoutRequest/saml:NameID'); + if ($entries->length == 1) { + $nameId = $entries->item(0); + } + } + + if (!isset($nameId)) { + throw new ValidationError( + "NameID not found in the Logout Request", + ValidationError::NO_NAMEID + ); + } + + $nameIdData = array(); + $nameIdData['Value'] = $nameId->nodeValue; + foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) { + if ($nameId->hasAttribute($attr)) { + $nameIdData[$attr] = $nameId->getAttribute($attr); + } + } + + return $nameIdData; + } + + /** + * Gets the NameID of the Logout Request. + * + * @param string|DOMDocument $request Logout Request Message + * @param string|null $key The SP key + * + * @return string Name ID Value + * + * @throws Error + * @throws Exception + * @throws ValidationError + */ + public static function getNameId($request, $key = null) + { + $nameId = self::getNameIdData($request, $key); + return $nameId['Value']; + } + + /** + * Gets the Issuer of the Logout Request. + * + * @param string|DOMDocument $request Logout Request Message + * + * @return string|null $issuer The Issuer + * + * @throws Exception + */ + public static function getIssuer($request) + { + if ($request instanceof DOMDocument) { + $dom = $request; + } else { + $dom = new DOMDocument(); + $dom = Utils::loadXML($dom, $request); + } + + $issuer = null; + $issuerNodes = Utils::query($dom, '/samlp:LogoutRequest/saml:Issuer'); + if ($issuerNodes->length == 1) { + $issuer = $issuerNodes->item(0)->textContent; + } + return $issuer; + } + + /** + * Gets the SessionIndexes from the Logout Request. + * Notice: Our Constructor only support 1 SessionIndex but this parser + * extracts an array of all the SessionIndex found on a + * Logout Request, that could be many. + * + * @param string|DOMDocument $request Logout Request Message + * + * @return array The SessionIndex value + * + * @throws Exception + */ + public static function getSessionIndexes($request) + { + if ($request instanceof DOMDocument) { + $dom = $request; + } else { + $dom = new DOMDocument(); + $dom = Utils::loadXML($dom, $request); + } + + $sessionIndexes = array(); + $sessionIndexNodes = Utils::query($dom, '/samlp:LogoutRequest/samlp:SessionIndex'); + foreach ($sessionIndexNodes as $sessionIndexNode) { + $sessionIndexes[] = $sessionIndexNode->textContent; + } + return $sessionIndexes; + } + + /** + * Checks if the Logout Request recieved is valid. + * + * @param bool $retrieveParametersFromServer True if we want to use parameters from $_SERVER to validate the signature + * + * @return bool If the Logout Request is or not valid + * + * @throws Exception + * @throws ValidationError + */ + public function isValid($retrieveParametersFromServer = false) + { + $this->_error = null; + try { + $dom = new DOMDocument(); + $dom = Utils::loadXML($dom, $this->_logoutRequest); + + $idpData = $this->_settings->getIdPData(); + $idPEntityId = $idpData['entityId']; + + if ($this->_settings->isStrict()) { + $security = $this->_settings->getSecurityData(); + + if ($security['wantXMLValidation']) { + $res = Utils::validateXML($dom, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); + if (!$res instanceof DOMDocument) { + throw new ValidationError( + "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd", + ValidationError::INVALID_XML_FORMAT + ); + } + } + + $currentURL = Utils::getSelfRoutedURLNoQuery(); + + // Check NotOnOrAfter + if ($dom->documentElement->hasAttribute('NotOnOrAfter')) { + $na = Utils::parseSAML2Time($dom->documentElement->getAttribute('NotOnOrAfter')); + if ($na <= time()) { + throw new ValidationError( + "Could not validate timestamp: expired. Check system clock.", + ValidationError::RESPONSE_EXPIRED + ); + } + } + + // Check destination + if ($dom->documentElement->hasAttribute('Destination')) { + $destination = $dom->documentElement->getAttribute('Destination'); + if (empty($destination)) { + if (!$security['relaxDestinationValidation']) { + throw new ValidationError( + "The LogoutRequest has an empty Destination value", + ValidationError::EMPTY_DESTINATION + ); + } + } else { + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL); + if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) { + $currentURLNoRouted = Utils::getSelfURLNoQuery(); + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted); + if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) { + throw new ValidationError( + "The LogoutRequest was received at $currentURL instead of $destination", + ValidationError::WRONG_DESTINATION + ); + } + } + } + } + + $nameId = static::getNameId($dom, $this->_settings->getSPkey()); + + // Check issuer + $issuer = static::getIssuer($dom); + if (!empty($issuer) && $issuer != $idPEntityId) { + throw new ValidationError( + "Invalid issuer in the Logout Request", + ValidationError::WRONG_ISSUER + ); + } + + if ($security['wantMessagesSigned'] && !isset($_GET['Signature'])) { + throw new ValidationError( + "The Message of the Logout Request is not signed and the SP require it", + ValidationError::NO_SIGNED_MESSAGE + ); + } + } + + if (isset($_GET['Signature'])) { + $signatureValid = Utils::validateBinarySign("SAMLRequest", $_GET, $idpData, $retrieveParametersFromServer); + if (!$signatureValid) { + throw new ValidationError( + "Signature validation failed. Logout Request rejected", + ValidationError::INVALID_SIGNATURE + ); + } + } + + return true; + } catch (Exception $e) { + $this->_error = $e; + $debug = $this->_settings->isDebugActive(); + if ($debug) { + echo htmlentities($this->_error->getMessage()); + } + return false; + } + } + + /** + * After execute a validation process, if fails this method returns the Exception of the cause + * + * @return Exception Cause + */ + public function getErrorException() + { + return $this->_error; + } + + /** + * After execute a validation process, if fails this method returns the cause + * + * @return null|string Error reason + */ + public function getError() + { + $errorMsg = null; + if (isset($this->_error)) { + $errorMsg = htmlentities($this->_error->getMessage()); + } + return $errorMsg; + } + + /** + * Returns the XML that will be sent as part of the request + * or that was received at the SP + * + * @return string + */ + public function getXML() + { + return $this->_logoutRequest; + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/LogoutResponse.php b/saml/vendor/onelogin/php-saml/src/Saml2/LogoutResponse.php new file mode 100644 index 00000000..9c3f020e --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/LogoutResponse.php @@ -0,0 +1,347 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use DOMDocument; +use DOMNodeList; +use Exception; + +/** + * SAML 2 Logout Response + */ +class LogoutResponse +{ + /** + * Contains the ID of the Logout Response + * + * @var string + */ + public $id; + + /** + * Object that represents the setting info + * + * @var Settings + */ + protected $_settings; + + /** + * The decoded, unprocessed XML response provided to the constructor. + * + * @var string|null + */ + protected $_logoutResponse; + + /** + * A DOMDocument class loaded from the SAML LogoutResponse. + * + * @var DOMDocument + */ + public $document; + + /** + * After execute a validation process, if it fails, this var contains the cause + * + * @var Exception|null + */ + private $_error; + + /** + * Constructs a Logout Response object (Initialize params from settings and if provided + * load the Logout Response. + * + * @param Settings $settings Settings. + * @param string|null $response An UUEncoded SAML Logout response from the IdP. + * + * @throws Error + * @throws Exception + */ + public function __construct(\OneLogin\Saml2\Settings $settings, $response = null) + { + $this->_settings = $settings; + + $baseURL = $this->_settings->getBaseURL(); + if (!empty($baseURL)) { + Utils::setBaseURL($baseURL); + } + + if ($response) { + $decoded = base64_decode($response); + $inflated = @gzinflate($decoded); + if ($inflated != false) { + $this->_logoutResponse = $inflated; + } else { + $this->_logoutResponse = $decoded; + } + $this->document = new DOMDocument(); + $this->document = Utils::loadXML($this->document, $this->_logoutResponse); + + if (false === $this->document) { + throw new Error( + "LogoutResponse could not be processed", + Error::SAML_LOGOUTRESPONSE_INVALID + ); + } + + if ($this->document->documentElement->hasAttribute('ID')) { + $this->id = $this->document->documentElement->getAttribute('ID'); + } + } + } + + /** + * Gets the Issuer of the Logout Response. + * + * @return string|null $issuer The Issuer + */ + public function getIssuer() + { + $issuer = null; + $issuerNodes = $this->_query('/samlp:LogoutResponse/saml:Issuer'); + if ($issuerNodes->length == 1) { + $issuer = $issuerNodes->item(0)->textContent; + } + return $issuer; + } + + /** + * Gets the Status of the Logout Response. + * + * @return string|null The Status + */ + public function getStatus() + { + $entries = $this->_query('/samlp:LogoutResponse/samlp:Status/samlp:StatusCode'); + if ($entries->length != 1) { + return null; + } + $status = $entries->item(0)->getAttribute('Value'); + return $status; + } + + /** + * Determines if the SAML LogoutResponse is valid + * + * @param string|null $requestId The ID of the LogoutRequest sent by this SP to the IdP + * @param bool $retrieveParametersFromServer True if we want to use parameters from $_SERVER to validate the signature + * + * @return bool Returns if the SAML LogoutResponse is or not valid + * + * @throws ValidationError + */ + public function isValid($requestId = null, $retrieveParametersFromServer = false) + { + $this->_error = null; + try { + $idpData = $this->_settings->getIdPData(); + $idPEntityId = $idpData['entityId']; + + if ($this->_settings->isStrict()) { + $security = $this->_settings->getSecurityData(); + + if ($security['wantXMLValidation']) { + $res = Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); + if (!$res instanceof DOMDocument) { + throw new ValidationError( + "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd", + ValidationError::INVALID_XML_FORMAT + ); + } + } + + // Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided + if (isset($requestId) && $this->document->documentElement->hasAttribute('InResponseTo')) { + $inResponseTo = $this->document->documentElement->getAttribute('InResponseTo'); + if ($requestId != $inResponseTo) { + throw new ValidationError( + "The InResponseTo of the Logout Response: $inResponseTo, does not match the ID of the Logout request sent by the SP: $requestId", + ValidationError::WRONG_INRESPONSETO + ); + } + } + + // Check issuer + $issuer = $this->getIssuer(); + if (!empty($issuer) && $issuer != $idPEntityId) { + throw new ValidationError( + "Invalid issuer in the Logout Response", + ValidationError::WRONG_ISSUER + ); + } + + $currentURL = Utils::getSelfRoutedURLNoQuery(); + + if ($this->document->documentElement->hasAttribute('Destination')) { + $destination = $this->document->documentElement->getAttribute('Destination'); + if (empty($destination)) { + if (!$security['relaxDestinationValidation']) { + throw new ValidationError( + "The LogoutResponse has an empty Destination value", + ValidationError::EMPTY_DESTINATION + ); + } + } else { + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL); + if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) { + $currentURLNoRouted = Utils::getSelfURLNoQuery(); + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted); + if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) { + throw new ValidationError( + "The LogoutResponse was received at $currentURL instead of $destination", + ValidationError::WRONG_DESTINATION + ); + } + } + } + } + + if ($security['wantMessagesSigned'] && !isset($_GET['Signature'])) { + throw new ValidationError( + "The Message of the Logout Response is not signed and the SP requires it", + ValidationError::NO_SIGNED_MESSAGE + ); + } + } + + if (isset($_GET['Signature'])) { + $signatureValid = Utils::validateBinarySign("SAMLResponse", $_GET, $idpData, $retrieveParametersFromServer); + if (!$signatureValid) { + throw new ValidationError( + "Signature validation failed. Logout Response rejected", + ValidationError::INVALID_SIGNATURE + ); + } + } + return true; + } catch (Exception $e) { + $this->_error = $e; + $debug = $this->_settings->isDebugActive(); + if ($debug) { + echo htmlentities($this->_error->getMessage()); + } + return false; + } + } + + /** + * Extracts a node from the DOMDocument (Logout Response Menssage) + * + * @param string $query Xpath Expression + * + * @return DOMNodeList The queried node + */ + private function _query($query) + { + return Utils::query($this->document, $query); + + } + + /** + * Generates a Logout Response object. + * + * @param string $inResponseTo InResponseTo value for the Logout Response. + */ + public function build($inResponseTo) + { + + $spData = $this->_settings->getSPData(); + + $this->id = Utils::generateUniqueID(); + $issueInstant = Utils::parseTime2SAML(time()); + $spEntityId = htmlspecialchars($spData['entityId'], ENT_QUOTES); + $destination = $this->_settings->getIdPSLOResponseUrl(); + $logoutResponse = << + {$spEntityId} + + + + +LOGOUTRESPONSE; + $this->_logoutResponse = $logoutResponse; + } + + /** + * Returns a Logout Response object. + * + * @param bool|null $deflate Whether or not we should 'gzdeflate' the response body before we return it. + * + * @return string Logout Response deflated and base64 encoded + */ + public function getResponse($deflate = null) + { + $logoutResponse = $this->_logoutResponse; + + if (is_null($deflate)) { + $deflate = $this->_settings->shouldCompressResponses(); + } + + if ($deflate) { + $logoutResponse = gzdeflate($this->_logoutResponse); + } + return base64_encode($logoutResponse); + } + + /** + * After execute a validation process, if fails this method returns the cause. + * + * @return Exception|null Cause + */ + public function getErrorException() + { + return $this->_error; + } + + /** + * After execute a validation process, if fails this method returns the cause + * + * @return null|string Error reason + */ + public function getError() + { + $errorMsg = null; + if (isset($this->_error)) { + $errorMsg = htmlentities($this->_error->getMessage()); + } + return $errorMsg; + } + + /** + * @return string the ID of the Response + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the XML that will be sent as part of the response + * or that was received at the SP + * + * @return string|null + */ + public function getXML() + { + return $this->_logoutResponse; + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Metadata.php b/saml/vendor/onelogin/php-saml/src/Saml2/Metadata.php new file mode 100644 index 00000000..922ad60b --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Metadata.php @@ -0,0 +1,267 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; +use RobRichards\XMLSecLibs\XMLSecurityDSig; + +use DOMDocument; +use Exception; + +/** + * Metadata lib of OneLogin PHP Toolkit + */ +class Metadata +{ + const TIME_VALID = 172800; // 2 days + const TIME_CACHED = 604800; // 1 week + + /** + * Generates the metadata of the SP based on the settings + * + * @param array $sp The SP data + * @param bool|string $authnsign authnRequestsSigned attribute + * @param bool|string $wsign wantAssertionsSigned attribute + * @param int|null $validUntil Metadata's valid time + * @param int|null $cacheDuration Duration of the cache in seconds + * @param array $contacts Contacts info + * @param array $organization Organization ingo + * @param array $attributes + * + * @return string SAML Metadata XML + */ + public static function builder($sp, $authnsign = false, $wsign = false, $validUntil = null, $cacheDuration = null, $contacts = array(), $organization = array(), $attributes = array()) + { + + if (!isset($validUntil)) { + $validUntil = time() + self::TIME_VALID; + } + $validUntilTime = Utils::parseTime2SAML($validUntil); + + if (!isset($cacheDuration)) { + $cacheDuration = self::TIME_CACHED; + } + + $sls = ''; + + if (isset($sp['singleLogoutService'])) { + $slsUrl = htmlspecialchars($sp['singleLogoutService']['url'], ENT_QUOTES); + $sls = << + +SLS_TEMPLATE; + } + + if ($authnsign) { + $strAuthnsign = 'true'; + } else { + $strAuthnsign = 'false'; + } + + if ($wsign) { + $strWsign = 'true'; + } else { + $strWsign = 'false'; + } + + $strOrganization = ''; + + if (!empty($organization)) { + $organizationInfoNames = array(); + $organizationInfoDisplaynames = array(); + $organizationInfoUrls = array(); + foreach ($organization as $lang => $info) { + $organizationInfoNames[] = <<{$info['name']} +ORGANIZATION_NAME; + $organizationInfoDisplaynames[] = <<{$info['displayname']} +ORGANIZATION_DISPLAY; + $organizationInfoUrls[] = <<{$info['url']} +ORGANIZATION_URL; + } + $orgData = implode("\n", $organizationInfoNames)."\n".implode("\n", $organizationInfoDisplaynames)."\n".implode("\n", $organizationInfoUrls); + $strOrganization = << +{$orgData} + +ORGANIZATIONSTR; + } + + $strContacts = ''; + if (!empty($contacts)) { + $contactsInfo = array(); + foreach ($contacts as $type => $info) { + $contactsInfo[] = << + {$info['givenName']} + {$info['emailAddress']} + +CONTACT; + } + $strContacts = "\n".implode("\n", $contactsInfo); + } + + $strAttributeConsumingService = ''; + if (isset($sp['attributeConsumingService'])) { + $attrCsDesc = ''; + if (isset($sp['attributeConsumingService']['serviceDescription'])) { + $attrCsDesc = sprintf( + ' %s' . PHP_EOL, + $sp['attributeConsumingService']['serviceDescription'] + ); + } + if (!isset($sp['attributeConsumingService']['serviceName'])) { + $sp['attributeConsumingService']['serviceName'] = 'Service'; + } + $requestedAttributeData = array(); + foreach ($sp['attributeConsumingService']['requestedAttributes'] as $attribute) { + $requestedAttributeStr = sprintf(' {$attrValue} +ATTRIBUTEVALUE; + } + $reqAttrAuxStr .= "\n "; + } + + $requestedAttributeData[] = $requestedAttributeStr . $reqAttrAuxStr; + } + + $requestedAttributeStr = implode(PHP_EOL, $requestedAttributeData); + $strAttributeConsumingService = << + {$sp['attributeConsumingService']['serviceName']} +{$attrCsDesc}{$requestedAttributeStr} + +METADATA_TEMPLATE; + } + + $spEntityId = htmlspecialchars($sp['entityId'], ENT_QUOTES); + $acsUrl = htmlspecialchars($sp['assertionConsumerService']['url'], ENT_QUOTES); + $metadata = << + + +{$sls} {$sp['NameIDFormat']} + + {$strAttributeConsumingService} + {$strOrganization}{$strContacts} + +METADATA_TEMPLATE; + return $metadata; + } + + /** + * Signs the metadata with the key/cert provided + * + * @param string $metadata SAML Metadata XML + * @param string $key x509 key + * @param string $cert x509 cert + * @param string $signAlgorithm Signature algorithm method + * @param string $digestAlgorithm Digest algorithm method + * + * @return string Signed Metadata + * + * @throws Exception + */ + public static function signMetadata($metadata, $key, $cert, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $digestAlgorithm = XMLSecurityDSig::SHA256) + { + return Utils::addSign($metadata, $key, $cert, $signAlgorithm, $digestAlgorithm); + } + + /** + * Adds the x509 descriptors (sign/encryption) to the metadata + * The same cert will be used for sign/encrypt + * + * @param string $metadata SAML Metadata XML + * @param string $cert x509 cert + * @param bool $wantsEncrypted Whether to include the KeyDescriptor for encryption + * + * @return string Metadata with KeyDescriptors + * + * @throws Exception + */ + public static function addX509KeyDescriptors($metadata, $cert, $wantsEncrypted = true) + { + $xml = new DOMDocument(); + $xml->preserveWhiteSpace = false; + $xml->formatOutput = true; + try { + $xml = Utils::loadXML($xml, $metadata); + if (!$xml) { + throw new Exception('Error parsing metadata'); + } + } catch (Exception $e) { + throw new Exception('Error parsing metadata. '.$e->getMessage()); + } + + $formatedCert = Utils::formatCert($cert, false); + $x509Certificate = $xml->createElementNS(Constants::NS_DS, 'X509Certificate', $formatedCert); + + $keyData = $xml->createElementNS(Constants::NS_DS, 'ds:X509Data'); + $keyData->appendChild($x509Certificate); + + $keyInfo = $xml->createElementNS(Constants::NS_DS, 'ds:KeyInfo'); + $keyInfo->appendChild($keyData); + + $keyDescriptor = $xml->createElementNS(Constants::NS_MD, "md:KeyDescriptor"); + + $SPSSODescriptor = $xml->getElementsByTagName('SPSSODescriptor')->item(0); + $SPSSODescriptor->insertBefore($keyDescriptor->cloneNode(), $SPSSODescriptor->firstChild); + if ($wantsEncrypted === true) { + $SPSSODescriptor->insertBefore($keyDescriptor->cloneNode(), $SPSSODescriptor->firstChild); + } + + $signing = $xml->getElementsByTagName('KeyDescriptor')->item(0); + $signing->setAttribute('use', 'signing'); + $signing->appendChild($keyInfo); + + if ($wantsEncrypted === true) { + $encryption = $xml->getElementsByTagName('KeyDescriptor')->item(1); + $encryption->setAttribute('use', 'encryption'); + + $encryption->appendChild($keyInfo->cloneNode(true)); + } + + return $xml->saveXML(); + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Response.php b/saml/vendor/onelogin/php-saml/src/Saml2/Response.php new file mode 100644 index 00000000..a2f8d6dd --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Response.php @@ -0,0 +1,1237 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; +use RobRichards\XMLSecLibs\XMLSecEnc; + +use DOMDocument; +use DOMNodeList; +use DOMXPath; +use Exception; + +/** + * SAML 2 Authentication Response + */ +class Response +{ + /** + * Settings + * + * @var Settings + */ + protected $_settings; + + /** + * The decoded, unprocessed XML response provided to the constructor. + * + * @var string + */ + public $response; + + /** + * A DOMDocument class loaded from the SAML Response. + * + * @var DOMDocument + */ + public $document; + + /** + * A DOMDocument class loaded from the SAML Response (Decrypted). + * + * @var DOMDocument + */ + public $decryptedDocument; + + /** + * The response contains an encrypted assertion. + * + * @var bool + */ + public $encrypted = false; + + /** + * After validation, if it fail this var has the cause of the problem + * + * @var Exception|null + */ + private $_error; + + /** + * NotOnOrAfter value of a valid SubjectConfirmationData node + * + * @var int + */ + private $_validSCDNotOnOrAfter; + + /** + * Constructs the SAML Response object. + * + * @param Settings $settings Settings. + * @param string $response A UUEncoded SAML response from the IdP. + * + * @throws Exception + * @throws ValidationError + */ + public function __construct(\OneLogin\Saml2\Settings $settings, $response) + { + $this->_settings = $settings; + + $baseURL = $this->_settings->getBaseURL(); + if (!empty($baseURL)) { + Utils::setBaseURL($baseURL); + } + + $this->response = base64_decode($response); + + $this->document = new DOMDocument(); + $this->document = Utils::loadXML($this->document, $this->response); + if (!$this->document) { + throw new ValidationError( + "SAML Response could not be processed", + ValidationError::INVALID_XML_FORMAT + ); + } + + // Quick check for the presence of EncryptedAssertion + $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion'); + if ($encryptedAssertionNodes->length !== 0) { + $this->decryptedDocument = clone $this->document; + $this->encrypted = true; + $this->decryptedDocument = $this->decryptAssertion($this->decryptedDocument); + } + } + + /** + * Determines if the SAML Response is valid using the certificate. + * + * @param string|null $requestId The ID of the AuthNRequest sent by this SP to the IdP + * + * @return bool Validate the document + * + * @throws Exception + * @throws ValidationError + */ + public function isValid($requestId = null) + { + $this->_error = null; + try { + // Check SAML version + if ($this->document->documentElement->getAttribute('Version') != '2.0') { + throw new ValidationError( + "Unsupported SAML version", + ValidationError::UNSUPPORTED_SAML_VERSION + ); + } + + if (!$this->document->documentElement->hasAttribute('ID')) { + throw new ValidationError( + "Missing ID attribute on SAML Response", + ValidationError::MISSING_ID + ); + } + + $this->checkStatus(); + + $singleAssertion = $this->validateNumAssertions(); + if (!$singleAssertion) { + throw new ValidationError( + "SAML Response must contain 1 assertion", + ValidationError::WRONG_NUMBER_OF_ASSERTIONS + ); + } + + $idpData = $this->_settings->getIdPData(); + $idPEntityId = $idpData['entityId']; + $spData = $this->_settings->getSPData(); + $spEntityId = $spData['entityId']; + + $signedElements = $this->processSignedElements(); + + $responseTag = '{'.Constants::NS_SAMLP.'}Response'; + $assertionTag = '{'.Constants::NS_SAML.'}Assertion'; + + $hasSignedResponse = in_array($responseTag, $signedElements); + $hasSignedAssertion = in_array($assertionTag, $signedElements); + + if ($this->_settings->isStrict()) { + $security = $this->_settings->getSecurityData(); + + if ($security['wantXMLValidation']) { + $errorXmlMsg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd"; + $res = Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); + if (!$res instanceof DOMDocument) { + throw new ValidationError( + $errorXmlMsg, + ValidationError::INVALID_XML_FORMAT + ); + } + + // If encrypted, check also the decrypted document + if ($this->encrypted) { + $res = Utils::validateXML($this->decryptedDocument, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive(), $this->_settings->getSchemasPath()); + if (!$res instanceof DOMDocument) { + throw new ValidationError( + $errorXmlMsg, + ValidationError::INVALID_XML_FORMAT + ); + } + } + + } + + $currentURL = Utils::getSelfRoutedURLNoQuery(); + + $responseInResponseTo = null; + if ($this->document->documentElement->hasAttribute('InResponseTo')) { + $responseInResponseTo = $this->document->documentElement->getAttribute('InResponseTo'); + } + + if (!isset($requestId) && isset($responseInResponseTo) && $security['rejectUnsolicitedResponsesWithInResponseTo']) { + throw new ValidationError( + "The Response has an InResponseTo attribute: " . $responseInResponseTo . " while no InResponseTo was expected", + ValidationError::WRONG_INRESPONSETO + ); + } + + // Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided + if (isset($requestId) && $requestId != $responseInResponseTo) { + if ($responseInResponseTo == null) { + throw new ValidationError( + "No InResponseTo at the Response, but it was provided the requestId related to the AuthNRequest sent by the SP: $requestId", + ValidationError::WRONG_INRESPONSETO + ); + } else { + throw new ValidationError( + "The InResponseTo of the Response: $responseInResponseTo, does not match the ID of the AuthNRequest sent by the SP: $requestId", + ValidationError::WRONG_INRESPONSETO + ); + } + } + + if (!$this->encrypted && $security['wantAssertionsEncrypted']) { + throw new ValidationError( + "The assertion of the Response is not encrypted and the SP requires it", + ValidationError::NO_ENCRYPTED_ASSERTION + ); + } + + if ($security['wantNameIdEncrypted']) { + $encryptedIdNodes = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData'); + if ($encryptedIdNodes->length != 1) { + throw new ValidationError( + "The NameID of the Response is not encrypted and the SP requires it", + ValidationError::NO_ENCRYPTED_NAMEID + ); + } + } + + // Validate Conditions element exists + if (!$this->checkOneCondition()) { + throw new ValidationError( + "The Assertion must include a Conditions element", + ValidationError::MISSING_CONDITIONS + ); + } + + // Validate Asserion timestamps + $this->validateTimestamps(); + + // Validate AuthnStatement element exists and is unique + if (!$this->checkOneAuthnStatement()) { + throw new ValidationError( + "The Assertion must include an AuthnStatement element", + ValidationError::WRONG_NUMBER_OF_AUTHSTATEMENTS + ); + } + + // EncryptedAttributes are not supported + $encryptedAttributeNodes = $this->_queryAssertion('/saml:AttributeStatement/saml:EncryptedAttribute'); + if ($encryptedAttributeNodes->length > 0) { + throw new ValidationError( + "There is an EncryptedAttribute in the Response and this SP not support them", + ValidationError::ENCRYPTED_ATTRIBUTES + ); + } + + // Check destination + if ($this->document->documentElement->hasAttribute('Destination')) { + $destination = trim($this->document->documentElement->getAttribute('Destination')); + if (empty($destination)) { + if (!$security['relaxDestinationValidation']) { + throw new ValidationError( + "The response has an empty Destination value", + ValidationError::EMPTY_DESTINATION + ); + } + } else { + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURL); + if (strncmp($destination, $currentURL, $urlComparisonLength) !== 0) { + $currentURLNoRouted = Utils::getSelfURLNoQuery(); + $urlComparisonLength = $security['destinationStrictlyMatches'] ? strlen($destination) : strlen($currentURLNoRouted); + if (strncmp($destination, $currentURLNoRouted, $urlComparisonLength) !== 0) { + throw new ValidationError( + "The response was received at $currentURL instead of $destination", + ValidationError::WRONG_DESTINATION + ); + } + } + } + } + + // Check audience + $validAudiences = $this->getAudiences(); + if (!empty($validAudiences) && !in_array($spEntityId, $validAudiences, true)) { + throw new ValidationError( + sprintf( + "Invalid audience for this Response (expected '%s', got '%s')", + $spEntityId, + implode(',', $validAudiences) + ), + ValidationError::WRONG_AUDIENCE + ); + } + + // Check the issuers + $issuers = $this->getIssuers(); + foreach ($issuers as $issuer) { + $trimmedIssuer = trim($issuer); + if (empty($trimmedIssuer) || $trimmedIssuer !== $idPEntityId) { + throw new ValidationError( + "Invalid issuer in the Assertion/Response (expected '$idPEntityId', got '$trimmedIssuer')", + ValidationError::WRONG_ISSUER + ); + } + } + + // Check the session Expiration + $sessionExpiration = $this->getSessionNotOnOrAfter(); + if (!empty($sessionExpiration) && $sessionExpiration + Constants::ALLOWED_CLOCK_DRIFT <= time()) { + throw new ValidationError( + "The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response", + ValidationError::SESSION_EXPIRED + ); + } + + // Check the SubjectConfirmation, at least one SubjectConfirmation must be valid + $anySubjectConfirmation = false; + $subjectConfirmationNodes = $this->_queryAssertion('/saml:Subject/saml:SubjectConfirmation'); + foreach ($subjectConfirmationNodes as $scn) { + if ($scn->hasAttribute('Method') && $scn->getAttribute('Method') != Constants::CM_BEARER) { + continue; + } + $subjectConfirmationDataNodes = $scn->getElementsByTagName('SubjectConfirmationData'); + if ($subjectConfirmationDataNodes->length == 0) { + continue; + } else { + $scnData = $subjectConfirmationDataNodes->item(0); + if ($scnData->hasAttribute('InResponseTo')) { + $inResponseTo = $scnData->getAttribute('InResponseTo'); + if (isset($responseInResponseTo) && $responseInResponseTo != $inResponseTo) { + continue; + } + } + if ($scnData->hasAttribute('Recipient')) { + $recipient = $scnData->getAttribute('Recipient'); + if (!empty($recipient) && strpos($recipient, $currentURL) === false) { + continue; + } + } + if ($scnData->hasAttribute('NotOnOrAfter')) { + $noa = Utils::parseSAML2Time($scnData->getAttribute('NotOnOrAfter')); + if ($noa + Constants::ALLOWED_CLOCK_DRIFT <= time()) { + continue; + } + } + if ($scnData->hasAttribute('NotBefore')) { + $nb = Utils::parseSAML2Time($scnData->getAttribute('NotBefore')); + if ($nb > time() + Constants::ALLOWED_CLOCK_DRIFT) { + continue; + } + } + + // Save NotOnOrAfter value + if ($scnData->hasAttribute('NotOnOrAfter')) { + $this->_validSCDNotOnOrAfter = $noa; + } + $anySubjectConfirmation = true; + break; + } + } + + if (!$anySubjectConfirmation) { + throw new ValidationError( + "A valid SubjectConfirmation was not found on this Response", + ValidationError::WRONG_SUBJECTCONFIRMATION + ); + } + + if ($security['wantAssertionsSigned'] && !$hasSignedAssertion) { + throw new ValidationError( + "The Assertion of the Response is not signed and the SP requires it", + ValidationError::NO_SIGNED_ASSERTION + ); + } + + if ($security['wantMessagesSigned'] && !$hasSignedResponse) { + throw new ValidationError( + "The Message of the Response is not signed and the SP requires it", + ValidationError::NO_SIGNED_MESSAGE + ); + } + } + + // Detect case not supported + if ($this->encrypted) { + $encryptedIDNodes = Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID'); + if ($encryptedIDNodes->length > 0) { + throw new ValidationError( + 'SAML Response that contains an encrypted Assertion with encrypted nameId is not supported.', + ValidationError::NOT_SUPPORTED + ); + } + } + + if (empty($signedElements) || (!$hasSignedResponse && !$hasSignedAssertion)) { + throw new ValidationError( + 'No Signature found. SAML Response rejected', + ValidationError::NO_SIGNATURE_FOUND + ); + } else { + $cert = $idpData['x509cert']; + $fingerprint = $idpData['certFingerprint']; + $fingerprintalg = $idpData['certFingerprintAlgorithm']; + + $multiCerts = null; + $existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']); + + if ($existsMultiX509Sign) { + $multiCerts = $idpData['x509certMulti']['signing']; + } + + // If find a Signature on the Response, validates it checking the original response + if ($hasSignedResponse && !Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, Utils::RESPONSE_SIGNATURE_XPATH, $multiCerts)) { + throw new ValidationError( + "Signature validation failed. SAML Response rejected", + ValidationError::INVALID_SIGNATURE + ); + } + + // If find a Signature on the Assertion (decrypted assertion if was encrypted) + $documentToCheckAssertion = $this->encrypted ? $this->decryptedDocument : $this->document; + if ($hasSignedAssertion && !Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, Utils::ASSERTION_SIGNATURE_XPATH, $multiCerts)) { + throw new ValidationError( + "Signature validation failed. SAML Response rejected", + ValidationError::INVALID_SIGNATURE + ); + } + } + return true; + } catch (Exception $e) { + $this->_error = $e; + $debug = $this->_settings->isDebugActive(); + if ($debug) { + echo htmlentities($e->getMessage()); + } + return false; + } + } + + /** + * @return string|null the ID of the Response + */ + public function getId() + { + $id = null; + if ($this->document->documentElement->hasAttribute('ID')) { + $id = $this->document->documentElement->getAttribute('ID'); + } + return $id; + } + + /** + * @return string|null the ID of the assertion in the Response + * + * @throws ValidationError + */ + public function getAssertionId() + { + if (!$this->validateNumAssertions()) { + throw new ValidationError("SAML Response must contain 1 Assertion.", ValidationError::WRONG_NUMBER_OF_ASSERTIONS); + } + $assertionNodes = $this->_queryAssertion(""); + $id = null; + if ($assertionNodes->length == 1 && $assertionNodes->item(0)->hasAttribute('ID')) { + $id = $assertionNodes->item(0)->getAttribute('ID'); + } + return $id; + } + + /** + * @return int the NotOnOrAfter value of the valid SubjectConfirmationData + * node if any + */ + public function getAssertionNotOnOrAfter() + { + return $this->_validSCDNotOnOrAfter; + } + + /** + * Checks if the Status is success + * + * @throws ValidationError If status is not success + */ + public function checkStatus() + { + $status = Utils::getStatus($this->document); + + if (isset($status['code']) && $status['code'] !== Constants::STATUS_SUCCESS) { + $explodedCode = explode(':', $status['code']); + $printableCode = array_pop($explodedCode); + + $statusExceptionMsg = 'The status code of the Response was not Success, was '.$printableCode; + if (!empty($status['msg'])) { + $statusExceptionMsg .= ' -> '.$status['msg']; + } + throw new ValidationError( + $statusExceptionMsg, + ValidationError::STATUS_CODE_IS_NOT_SUCCESS + ); + } + } + + /** + * Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique. + * + * @return boolean true if the Conditions element exists and is unique + */ + public function checkOneCondition() + { + $entries = $this->_queryAssertion("/saml:Conditions"); + if ($entries->length == 1) { + return true; + } else { + return false; + } + } + + /** + * Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique. + * + * @return boolean true if the AuthnStatement element exists and is unique + */ + public function checkOneAuthnStatement() + { + $entries = $this->_queryAssertion("/saml:AuthnStatement"); + if ($entries->length == 1) { + return true; + } else { + return false; + } + } + + /** + * Gets the audiences. + * + * @return array @audience The valid audiences of the response + */ + public function getAudiences() + { + $audiences = array(); + + $entries = $this->_queryAssertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience'); + foreach ($entries as $entry) { + $value = trim($entry->textContent); + if (!empty($value)) { + $audiences[] = $value; + } + } + + return array_unique($audiences); + } + + /** + * Gets the Issuers (from Response and Assertion). + * + * @return array @issuers The issuers of the assertion/response + * + * @throws ValidationError + */ + public function getIssuers() + { + $issuers = array(); + + $responseIssuer = Utils::query($this->document, '/samlp:Response/saml:Issuer'); + if ($responseIssuer->length > 0) { + if ($responseIssuer->length == 1) { + $issuers[] = $responseIssuer->item(0)->textContent; + } else { + throw new ValidationError( + "Issuer of the Response is multiple.", + ValidationError::ISSUER_MULTIPLE_IN_RESPONSE + ); + } + } + + $assertionIssuer = $this->_queryAssertion('/saml:Issuer'); + if ($assertionIssuer->length == 1) { + $issuers[] = $assertionIssuer->item(0)->textContent; + } else { + throw new ValidationError( + "Issuer of the Assertion not found or multiple.", + ValidationError::ISSUER_NOT_FOUND_IN_ASSERTION + ); + } + + return array_unique($issuers); + } + + /** + * Gets the NameID Data provided by the SAML response from the IdP. + * + * @return array Name ID Data (Value, Format, NameQualifier, SPNameQualifier) + * + * @throws ValidationError + */ + public function getNameIdData() + { + $encryptedIdDataEntries = $this->_queryAssertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData'); + + if ($encryptedIdDataEntries->length == 1) { + $encryptedData = $encryptedIdDataEntries->item(0); + + $key = $this->_settings->getSPkey(); + $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'private')); + $seckey->loadKey($key); + + $nameId = Utils::decryptElement($encryptedData, $seckey); + + } else { + $entries = $this->_queryAssertion('/saml:Subject/saml:NameID'); + if ($entries->length == 1) { + $nameId = $entries->item(0); + } + } + + $nameIdData = array(); + + if (!isset($nameId)) { + $security = $this->_settings->getSecurityData(); + if ($security['wantNameId']) { + throw new ValidationError( + "NameID not found in the assertion of the Response", + ValidationError::NO_NAMEID + ); + } + } else { + if ($this->_settings->isStrict() && empty($nameId->nodeValue)) { + throw new ValidationError( + "An empty NameID value found", + ValidationError::EMPTY_NAMEID + ); + } + $nameIdData['Value'] = $nameId->nodeValue; + + foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) { + if ($nameId->hasAttribute($attr)) { + if ($this->_settings->isStrict() && $attr == 'SPNameQualifier') { + $spData = $this->_settings->getSPData(); + $spEntityId = $spData['entityId']; + if ($spEntityId != $nameId->getAttribute($attr)) { + throw new ValidationError( + "The SPNameQualifier value mistmatch the SP entityID value.", + ValidationError::SP_NAME_QUALIFIER_NAME_MISMATCH + ); + } + } + $nameIdData[$attr] = $nameId->getAttribute($attr); + } + } + } + + return $nameIdData; + } + + /** + * Gets the NameID provided by the SAML response from the IdP. + * + * @return string|null Name ID Value + * + * @throws ValidationError + */ + public function getNameId() + { + $nameIdvalue = null; + $nameIdData = $this->getNameIdData(); + if (!empty($nameIdData) && isset($nameIdData['Value'])) { + $nameIdvalue = $nameIdData['Value']; + } + return $nameIdvalue; + } + + /** + * Gets the NameID Format provided by the SAML response from the IdP. + * + * @return string|null Name ID Format + * + * @throws ValidationError + */ + public function getNameIdFormat() + { + $nameIdFormat = null; + $nameIdData = $this->getNameIdData(); + if (!empty($nameIdData) && isset($nameIdData['Format'])) { + $nameIdFormat = $nameIdData['Format']; + } + return $nameIdFormat; + } + + /** + * Gets the NameID NameQualifier provided by the SAML response from the IdP. + * + * @return string|null Name ID NameQualifier + * + * @throws ValidationError + */ + public function getNameIdNameQualifier() + { + $nameIdNameQualifier = null; + $nameIdData = $this->getNameIdData(); + if (!empty($nameIdData) && isset($nameIdData['NameQualifier'])) { + $nameIdNameQualifier = $nameIdData['NameQualifier']; + } + return $nameIdNameQualifier; + } + + /** + * Gets the NameID SP NameQualifier provided by the SAML response from the IdP. + * + * @return string|null NameID SP NameQualifier + * + * @throws ValidationError + */ + public function getNameIdSPNameQualifier() + { + $nameIdSPNameQualifier = null; + $nameIdData = $this->getNameIdData(); + if (!empty($nameIdData) && isset($nameIdData['SPNameQualifier'])) { + $nameIdSPNameQualifier = $nameIdData['SPNameQualifier']; + } + return $nameIdSPNameQualifier; + } + + /** + * Gets the SessionNotOnOrAfter from the AuthnStatement. + * Could be used to set the local session expiration + * + * @return int|null The SessionNotOnOrAfter value + * + * @throws Exception + */ + public function getSessionNotOnOrAfter() + { + $notOnOrAfter = null; + $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionNotOnOrAfter]'); + if ($entries->length !== 0) { + $notOnOrAfter = Utils::parseSAML2Time($entries->item(0)->getAttribute('SessionNotOnOrAfter')); + } + return $notOnOrAfter; + } + + /** + * Gets the SessionIndex from the AuthnStatement. + * Could be used to be stored in the local session in order + * to be used in a future Logout Request that the SP could + * send to the SP, to set what specific session must be deleted + * + * @return string|null The SessionIndex value + */ + public function getSessionIndex() + { + $sessionIndex = null; + $entries = $this->_queryAssertion('/saml:AuthnStatement[@SessionIndex]'); + if ($entries->length !== 0) { + $sessionIndex = $entries->item(0)->getAttribute('SessionIndex'); + } + return $sessionIndex; + } + + /** + * Gets the Attributes from the AttributeStatement element. + * + * @return array The attributes of the SAML Assertion + * + * @throws ValidationError + */ + public function getAttributes() + { + return $this->_getAttributesByKeyName('Name'); + } + + /** + * Gets the Attributes from the AttributeStatement element using their FriendlyName. + * + * @return array The attributes of the SAML Assertion + * + * @throws ValidationError + */ + public function getAttributesWithFriendlyName() + { + return $this->_getAttributesByKeyName('FriendlyName'); + } + + /** + * @param string $keyName + * + * @return array + * + * @throws ValidationError + */ + private function _getAttributesByKeyName($keyName = "Name") + { + $attributes = array(); + $entries = $this->_queryAssertion('/saml:AttributeStatement/saml:Attribute'); + + $security = $this->_settings->getSecurityData(); + $allowRepeatAttributeName = $security['allowRepeatAttributeName']; + /** @var $entry DOMNode */ + foreach ($entries as $entry) { + $attributeKeyNode = $entry->attributes->getNamedItem($keyName); + if ($attributeKeyNode === null) { + continue; + } + $attributeKeyName = $attributeKeyNode->nodeValue; + if (in_array($attributeKeyName, array_keys($attributes))) { + if (!$allowRepeatAttributeName) { + throw new ValidationError( + "Found an Attribute element with duplicated ".$keyName, + ValidationError::DUPLICATED_ATTRIBUTE_NAME_FOUND + ); + } + } + $attributeValues = array(); + foreach ($entry->childNodes as $childNode) { + $tagName = ($childNode->prefix ? $childNode->prefix.':' : '') . 'AttributeValue'; + if ($childNode->nodeType == XML_ELEMENT_NODE && $childNode->tagName === $tagName) { + $attributeValues[] = $childNode->nodeValue; + } + } + + if (in_array($attributeKeyName, array_keys($attributes))) { + $attributes[$attributeKeyName] = array_merge($attributes[$attributeKeyName], $attributeValues); + } else { + $attributes[$attributeKeyName] = $attributeValues; + } + } + return $attributes; + } + + /** + * Verifies that the document only contains a single Assertion (encrypted or not). + * + * @return bool TRUE if the document passes. + */ + public function validateNumAssertions() + { + $encryptedAssertionNodes = $this->document->getElementsByTagName('EncryptedAssertion'); + $assertionNodes = $this->document->getElementsByTagName('Assertion'); + + $valid = $assertionNodes->length + $encryptedAssertionNodes->length == 1; + + if ($this->encrypted) { + $assertionNodes = $this->decryptedDocument->getElementsByTagName('Assertion'); + $valid = $valid && $assertionNodes->length == 1; + } + + return $valid; + } + + /** + * Verifies the signature nodes: + * - Checks that are Response or Assertion + * - Check that IDs and reference URI are unique and consistent. + * + * @return array Signed element tags + * + * @throws ValidationError + */ + public function processSignedElements() + { + $signedElements = array(); + $verifiedSeis = array(); + $verifiedIds = array(); + + if ($this->encrypted) { + $signNodes = $this->decryptedDocument->getElementsByTagName('Signature'); + } else { + $signNodes = $this->document->getElementsByTagName('Signature'); + } + foreach ($signNodes as $signNode) { + $responseTag = '{'.Constants::NS_SAMLP.'}Response'; + $assertionTag = '{'.Constants::NS_SAML.'}Assertion'; + + $signedElement = '{'.$signNode->parentNode->namespaceURI.'}'.$signNode->parentNode->localName; + + if ($signedElement != $responseTag && $signedElement != $assertionTag) { + throw new ValidationError( + "Invalid Signature Element $signedElement SAML Response rejected", + ValidationError::WRONG_SIGNED_ELEMENT + ); + } + + // Check that reference URI matches the parent ID and no duplicate References or IDs + $idValue = $signNode->parentNode->getAttribute('ID'); + if (empty($idValue)) { + throw new ValidationError( + 'Signed Element must contain an ID. SAML Response rejected', + ValidationError::ID_NOT_FOUND_IN_SIGNED_ELEMENT + ); + } + + if (in_array($idValue, $verifiedIds)) { + throw new ValidationError( + 'Duplicated ID. SAML Response rejected', + ValidationError::DUPLICATED_ID_IN_SIGNED_ELEMENTS + ); + } + $verifiedIds[] = $idValue; + + $ref = $signNode->getElementsByTagName('Reference'); + if ($ref->length == 1) { + $ref = $ref->item(0); + $sei = $ref->getAttribute('URI'); + if (!empty($sei)) { + $sei = substr($sei, 1); + + if ($sei != $idValue) { + throw new ValidationError( + 'Found an invalid Signed Element. SAML Response rejected', + ValidationError::INVALID_SIGNED_ELEMENT + ); + } + + if (in_array($sei, $verifiedSeis)) { + throw new ValidationError( + 'Duplicated Reference URI. SAML Response rejected', + ValidationError::DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS + ); + } + $verifiedSeis[] = $sei; + } + } else { + throw new ValidationError( + 'Unexpected number of Reference nodes found for signature. SAML Response rejected.', + ValidationError::UNEXPECTED_REFERENCE + ); + } + $signedElements[] = $signedElement; + } + + // Check SignedElements + if (!empty($signedElements) && !$this->validateSignedElements($signedElements)) { + throw new ValidationError( + 'Found an unexpected Signature Element. SAML Response rejected', + ValidationError::UNEXPECTED_SIGNED_ELEMENTS + ); + } + return $signedElements; + } + + /** + * Verifies that the document is still valid according Conditions Element. + * + * @return bool + * + * @throws Exception + * @throws ValidationError + */ + public function validateTimestamps() + { + if ($this->encrypted) { + $document = $this->decryptedDocument; + } else { + $document = $this->document; + } + + $timestampNodes = $document->getElementsByTagName('Conditions'); + for ($i = 0; $i < $timestampNodes->length; $i++) { + $nbAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotBefore"); + $naAttribute = $timestampNodes->item($i)->attributes->getNamedItem("NotOnOrAfter"); + if ($nbAttribute && Utils::parseSAML2Time($nbAttribute->textContent) > time() + Constants::ALLOWED_CLOCK_DRIFT) { + throw new ValidationError( + 'Could not validate timestamp: not yet valid. Check system clock.', + ValidationError::ASSERTION_TOO_EARLY + ); + } + if ($naAttribute && Utils::parseSAML2Time($naAttribute->textContent) + Constants::ALLOWED_CLOCK_DRIFT <= time()) { + throw new ValidationError( + 'Could not validate timestamp: expired. Check system clock.', + ValidationError::ASSERTION_EXPIRED + ); + } + } + return true; + } + + /** + * Verifies that the document has the expected signed nodes. + * + * @param array $signedElements Signed elements + * + * @return bool + * + * @throws ValidationError + */ + public function validateSignedElements($signedElements) + { + if (count($signedElements) > 2) { + return false; + } + + $responseTag = '{'.Constants::NS_SAMLP.'}Response'; + $assertionTag = '{'.Constants::NS_SAML.'}Assertion'; + + $ocurrence = array_count_values($signedElements); + if ((in_array($responseTag, $signedElements) && $ocurrence[$responseTag] > 1) + || (in_array($assertionTag, $signedElements) && $ocurrence[$assertionTag] > 1) + || !in_array($responseTag, $signedElements) && !in_array($assertionTag, $signedElements) + ) { + return false; + } + + // Check that the signed elements found here, are the ones that will be verified + // by Utils->validateSign() + if (in_array($responseTag, $signedElements)) { + $expectedSignatureNodes = Utils::query($this->document, Utils::RESPONSE_SIGNATURE_XPATH); + if ($expectedSignatureNodes->length != 1) { + throw new ValidationError( + "Unexpected number of Response signatures found. SAML Response rejected.", + ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE + ); + } + } + + if (in_array($assertionTag, $signedElements)) { + $expectedSignatureNodes = $this->_query(Utils::ASSERTION_SIGNATURE_XPATH); + if ($expectedSignatureNodes->length != 1) { + throw new ValidationError( + "Unexpected number of Assertion signatures found. SAML Response rejected.", + ValidationError::WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION + ); + } + } + + return true; + } + + /** + * Extracts a node from the DOMDocument (Assertion). + * + * @param string $assertionXpath Xpath Expression + * + * @return DOMNodeList The queried node + */ + protected function _queryAssertion($assertionXpath) + { + if ($this->encrypted) { + $xpath = new DOMXPath($this->decryptedDocument); + } else { + $xpath = new DOMXPath($this->document); + } + + $xpath->registerNamespace('samlp', Constants::NS_SAMLP); + $xpath->registerNamespace('saml', Constants::NS_SAML); + $xpath->registerNamespace('ds', Constants::NS_DS); + $xpath->registerNamespace('xenc', Constants::NS_XENC); + + $assertionNode = '/samlp:Response/saml:Assertion'; + $signatureQuery = $assertionNode . '/ds:Signature/ds:SignedInfo/ds:Reference'; + $assertionReferenceNode = $xpath->query($signatureQuery)->item(0); + if (!$assertionReferenceNode) { + // is the response signed as a whole? + $signatureQuery = '/samlp:Response/ds:Signature/ds:SignedInfo/ds:Reference'; + $responseReferenceNode = $xpath->query($signatureQuery)->item(0); + if ($responseReferenceNode) { + $uri = $responseReferenceNode->attributes->getNamedItem('URI')->nodeValue; + if (empty($uri)) { + $id = $responseReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue; + } else { + $id = substr($responseReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1); + } + $nameQuery = "/samlp:Response[@ID='$id']/saml:Assertion" . $assertionXpath; + } else { + $nameQuery = "/samlp:Response/saml:Assertion" . $assertionXpath; + } + } else { + $uri = $assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue; + if (empty($uri)) { + $id = $assertionReferenceNode->parentNode->parentNode->parentNode->attributes->getNamedItem('ID')->nodeValue; + } else { + $id = substr($assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1); + } + $nameQuery = $assertionNode."[@ID='$id']" . $assertionXpath; + } + + return $xpath->query($nameQuery); + } + + /** + * Extracts nodes that match the query from the DOMDocument (Response Menssage) + * + * @param string $query Xpath Expression + * + * @return DOMNodeList The queried nodes + */ + private function _query($query) + { + if ($this->encrypted) { + return Utils::query($this->decryptedDocument, $query); + } else { + return Utils::query($this->document, $query); + } + } + + /** + * Decrypts the Assertion (DOMDocument) + * + * @param \DomNode $dom DomDocument + * + * @return DOMDocument Decrypted Assertion + * + * @throws Exception + * @throws ValidationError + */ + protected function decryptAssertion(\DomNode $dom) + { + $pem = $this->_settings->getSPkey(); + + if (empty($pem)) { + throw new Error( + "No private key available, check settings", + Error::PRIVATE_KEY_NOT_FOUND + ); + } + + $objenc = new XMLSecEnc(); + $encData = $objenc->locateEncryptedData($dom); + if (!$encData) { + throw new ValidationError( + "Cannot locate encrypted assertion", + ValidationError::MISSING_ENCRYPTED_ELEMENT + ); + } + + $objenc->setNode($encData); + $objenc->type = $encData->getAttribute("Type"); + if (!$objKey = $objenc->locateKey()) { + throw new ValidationError( + "Unknown algorithm", + ValidationError::KEY_ALGORITHM_ERROR + ); + } + + $key = null; + if ($objKeyInfo = $objenc->locateKeyInfo($objKey)) { + if ($objKeyInfo->isEncrypted) { + $objencKey = $objKeyInfo->encryptedCtx; + $objKeyInfo->loadKey($pem, false, false); + $key = $objencKey->decryptKey($objKeyInfo); + } else { + // symmetric encryption key support + $objKeyInfo->loadKey($pem, false, false); + } + } + + if (empty($objKey->key)) { + $objKey->loadKey($key); + } + + $decryptedXML = $objenc->decryptNode($objKey, false); + $decrypted = new DOMDocument(); + $check = Utils::loadXML($decrypted, $decryptedXML); + if ($check === false) { + throw new Exception('Error: string from decrypted assertion could not be loaded into a XML document'); + } + if ($encData->parentNode instanceof DOMDocument) { + return $decrypted; + } else { + $decrypted = $decrypted->documentElement; + $encryptedAssertion = $encData->parentNode; + $container = $encryptedAssertion->parentNode; + + // Fix possible issue with saml namespace + if (!$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml') + && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2') + && !$decrypted->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns') + && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml') + && !$container->hasAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:saml2') + ) { + if (strpos($encryptedAssertion->tagName, 'saml2:') !== false) { + $ns = 'xmlns:saml2'; + } else if (strpos($encryptedAssertion->tagName, 'saml:') !== false) { + $ns = 'xmlns:saml'; + } else { + $ns = 'xmlns'; + } + $decrypted->setAttributeNS('http://www.w3.org/2000/xmlns/', $ns, Constants::NS_SAML); + } + + Utils::treeCopyReplace($encryptedAssertion, $decrypted); + + // Rebuild the DOM will fix issues with namespaces as well + $dom = new DOMDocument(); + return Utils::loadXML($dom, $container->ownerDocument->saveXML()); + } + } + + /** + * After execute a validation process, if fails this method returns the cause + * + * @return Exception|null Cause + */ + public function getErrorException() + { + return $this->_error; + } + + /** + * After execute a validation process, if fails this method returns the cause + * + * @return null|string Error reason + */ + public function getError() + { + $errorMsg = null; + if (isset($this->_error)) { + $errorMsg = htmlentities($this->_error->getMessage()); + } + return $errorMsg; + } + + /** + * Returns the SAML Response document (If contains an encrypted assertion, decrypts it) + * + * @return DomDocument SAML Response + */ + public function getXMLDocument() + { + if ($this->encrypted) { + return $this->decryptedDocument; + } else { + return $this->document; + } + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Settings.php b/saml/vendor/onelogin/php-saml/src/Saml2/Settings.php new file mode 100644 index 00000000..43457bad --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Settings.php @@ -0,0 +1,1166 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; +use RobRichards\XMLSecLibs\XMLSecurityDSig; + +use DOMDocument; +use Exception; + +/** + * Configuration of the OneLogin PHP Toolkit + */ +class Settings +{ + /** + * List of paths. + * + * @var array + */ + private $_paths = array(); + + /** + * @var string + */ + private $_baseurl; + + /** + * Strict. If active, PHP Toolkit will reject unsigned or unencrypted messages + * if it expects them signed or encrypted. If not, the messages will be accepted + * and some security issues will be also relaxed. + * + * @var bool + */ + private $_strict = true; + + /** + * Activate debug mode + * + * @var bool + */ + private $_debug = false; + + /** + * SP data. + * + * @var array + */ + private $_sp = array(); + + /** + * IdP data. + * + * @var array + */ + private $_idp = array(); + + /** + * Compression settings that determine + * whether gzip compression should be used. + * + * @var array + */ + private $_compress = array(); + + /** + * Security Info related to the SP. + * + * @var array + */ + private $_security = array(); + + /** + * Setting contacts. + * + * @var array + */ + private $_contacts = array(); + + /** + * Setting organization. + * + * @var array + */ + private $_organization = array(); + + /** + * Setting errors. + * + * @var array + */ + private $_errors = array(); + + /** + * Valitate SP data only flag + * + * @var bool + */ + private $_spValidationOnly = false; + + /** + * Initializes the settings: + * - Sets the paths of the different folders + * - Loads settings info from settings file or array/object provided + * + * @param array|null $settings SAML Toolkit Settings + * @param bool $spValidationOnly Validate or not the IdP data + * + * @throws Error If any settings parameter is invalid + * @throws Exception If Settings is incorrectly supplied + */ + public function __construct(array $settings = null, $spValidationOnly = false) + { + $this->_spValidationOnly = $spValidationOnly; + $this->_loadPaths(); + + if (!isset($settings)) { + if (!$this->_loadSettingsFromFile()) { + throw new Error( + 'Invalid file settings: %s', + Error::SETTINGS_INVALID, + array(implode(', ', $this->_errors)) + ); + } + $this->_addDefaultValues(); + } else { + if (!$this->_loadSettingsFromArray($settings)) { + throw new Error( + 'Invalid array settings: %s', + Error::SETTINGS_INVALID, + array(implode(', ', $this->_errors)) + ); + } + } + + $this->formatIdPCert(); + $this->formatSPCert(); + $this->formatSPKey(); + $this->formatSPCertNew(); + $this->formatIdPCertMulti(); + } + + /** + * Sets the paths of the different folders + * @suppress PhanUndeclaredConstant + */ + private function _loadPaths() + { + $basePath = dirname(dirname(__DIR__)) . '/'; + $this->_paths = array( + 'base' => $basePath, + 'config' => $basePath, + 'cert' => $basePath.'certs/', + 'lib' => __DIR__ . '/', + ); + + if (defined('ONELOGIN_CUSTOMPATH')) { + $this->_paths['config'] = ONELOGIN_CUSTOMPATH; + $this->_paths['cert'] = ONELOGIN_CUSTOMPATH . 'certs/'; + } + } + + /** + * Returns base path. + * + * @return string The base toolkit folder path + */ + public function getBasePath() + { + return $this->_paths['base']; + } + + /** + * Returns cert path. + * + * @return string The cert folder path + */ + public function getCertPath() + { + return $this->_paths['cert']; + } + + /** + * Returns config path. + * + * @return string The config folder path + */ + public function getConfigPath() + { + return $this->_paths['config']; + } + + /** + * Returns lib path. + * + * @return string The library folder path + */ + public function getLibPath() + { + return $this->_paths['lib']; + } + + /** + * Returns schema path. + * + * @return string The external library folder path + */ + public function getSchemasPath() + { + if (isset($this->_paths['schemas'])) { + return $this->_paths['schemas']; + } + return __DIR__ . '/schemas/'; + } + + /** + * Set schemas path + * + * @param string $path + * @return $this + */ + public function setSchemasPath($path) + { + $this->_paths['schemas'] = $path; + } + + /** + * Loads settings info from a settings Array + * + * @param array $settings SAML Toolkit Settings + * + * @return bool True if the settings info is valid + */ + private function _loadSettingsFromArray(array $settings) + { + if (isset($settings['sp'])) { + $this->_sp = $settings['sp']; + } + if (isset($settings['idp'])) { + $this->_idp = $settings['idp']; + } + + $errors = $this->checkSettings($settings); + if (empty($errors)) { + $this->_errors = array(); + + if (isset($settings['strict'])) { + $this->_strict = $settings['strict']; + } + if (isset($settings['debug'])) { + $this->_debug = $settings['debug']; + } + + if (isset($settings['baseurl'])) { + $this->_baseurl = $settings['baseurl']; + } + + if (isset($settings['compress'])) { + $this->_compress = $settings['compress']; + } + + if (isset($settings['security'])) { + $this->_security = $settings['security']; + } + + if (isset($settings['contactPerson'])) { + $this->_contacts = $settings['contactPerson']; + } + + if (isset($settings['organization'])) { + $this->_organization = $settings['organization']; + } + + $this->_addDefaultValues(); + return true; + } else { + $this->_errors = $errors; + return false; + } + } + + /** + * Loads settings info from the settings file + * + * @return bool True if the settings info is valid + * + * @throws Error + * + * @suppress PhanUndeclaredVariable + */ + private function _loadSettingsFromFile() + { + $filename = $this->getConfigPath().'settings.php'; + + if (!file_exists($filename)) { + throw new Error( + 'Settings file not found: %s', + Error::SETTINGS_FILE_NOT_FOUND, + array($filename) + ); + } + + /** @var array $settings */ + include $filename; + + // Add advance_settings if exists + $advancedFilename = $this->getConfigPath().'advanced_settings.php'; + + if (file_exists($advancedFilename)) { + /** @var array $advancedSettings */ + include $advancedFilename; + $settings = array_merge($settings, $advancedSettings); + } + + + return $this->_loadSettingsFromArray($settings); + } + + /** + * Add default values if the settings info is not complete + */ + private function _addDefaultValues() + { + if (!isset($this->_sp['assertionConsumerService']['binding'])) { + $this->_sp['assertionConsumerService']['binding'] = Constants::BINDING_HTTP_POST; + } + if (isset($this->_sp['singleLogoutService']) && !isset($this->_sp['singleLogoutService']['binding'])) { + $this->_sp['singleLogoutService']['binding'] = Constants::BINDING_HTTP_REDIRECT; + } + + if (!isset($this->_compress['requests'])) { + $this->_compress['requests'] = true; + } + + if (!isset($this->_compress['responses'])) { + $this->_compress['responses'] = true; + } + + // Related to nameID + if (!isset($this->_sp['NameIDFormat'])) { + $this->_sp['NameIDFormat'] = Constants::NAMEID_UNSPECIFIED; + } + if (!isset($this->_security['nameIdEncrypted'])) { + $this->_security['nameIdEncrypted'] = false; + } + if (!isset($this->_security['requestedAuthnContext'])) { + $this->_security['requestedAuthnContext'] = true; + } + + // sign provided + if (!isset($this->_security['authnRequestsSigned'])) { + $this->_security['authnRequestsSigned'] = false; + } + if (!isset($this->_security['logoutRequestSigned'])) { + $this->_security['logoutRequestSigned'] = false; + } + if (!isset($this->_security['logoutResponseSigned'])) { + $this->_security['logoutResponseSigned'] = false; + } + if (!isset($this->_security['signMetadata'])) { + $this->_security['signMetadata'] = false; + } + + // sign expected + if (!isset($this->_security['wantMessagesSigned'])) { + $this->_security['wantMessagesSigned'] = false; + } + if (!isset($this->_security['wantAssertionsSigned'])) { + $this->_security['wantAssertionsSigned'] = false; + } + + // NameID element expected + if (!isset($this->_security['wantNameId'])) { + $this->_security['wantNameId'] = true; + } + + // Relax Destination validation + if (!isset($this->_security['relaxDestinationValidation'])) { + $this->_security['relaxDestinationValidation'] = false; + } + + // Strict Destination match validation + if (!isset($this->_security['destinationStrictlyMatches'])) { + $this->_security['destinationStrictlyMatches'] = false; + } + + // Allow duplicated Attribute Names + if (!isset($this->_security['allowRepeatAttributeName'])) { + $this->_security['allowRepeatAttributeName'] = false; + } + + // InResponseTo + if (!isset($this->_security['rejectUnsolicitedResponsesWithInResponseTo'])) { + $this->_security['rejectUnsolicitedResponsesWithInResponseTo'] = false; + } + + // encrypt expected + if (!isset($this->_security['wantAssertionsEncrypted'])) { + $this->_security['wantAssertionsEncrypted'] = false; + } + if (!isset($this->_security['wantNameIdEncrypted'])) { + $this->_security['wantNameIdEncrypted'] = false; + } + + // XML validation + if (!isset($this->_security['wantXMLValidation'])) { + $this->_security['wantXMLValidation'] = true; + } + + // SignatureAlgorithm + if (!isset($this->_security['signatureAlgorithm'])) { + $this->_security['signatureAlgorithm'] = XMLSecurityKey::RSA_SHA256; + } + + // DigestAlgorithm + if (!isset($this->_security['digestAlgorithm'])) { + $this->_security['digestAlgorithm'] = XMLSecurityDSig::SHA256; + } + + // EncryptionAlgorithm + if (!isset($this->_security['encryption_algorithm'])) { + $this->_security['encryption_algorithm'] = XMLSecurityKey::AES128_CBC; + } + + if (!isset($this->_security['lowercaseUrlencoding'])) { + $this->_security['lowercaseUrlencoding'] = false; + } + + // Certificates / Private key /Fingerprint + if (!isset($this->_idp['x509cert'])) { + $this->_idp['x509cert'] = ''; + } + if (!isset($this->_idp['certFingerprint'])) { + $this->_idp['certFingerprint'] = ''; + } + if (!isset($this->_idp['certFingerprintAlgorithm'])) { + $this->_idp['certFingerprintAlgorithm'] = 'sha1'; + } + + if (!isset($this->_sp['x509cert'])) { + $this->_sp['x509cert'] = ''; + } + if (!isset($this->_sp['privateKey'])) { + $this->_sp['privateKey'] = ''; + } + } + + /** + * Checks the settings info. + * + * @param array $settings Array with settings data + * + * @return array $errors Errors found on the settings data + */ + public function checkSettings(array $settings) + { + if (empty($settings)) { + $errors = array('invalid_syntax'); + } else { + $errors = array(); + if (!$this->_spValidationOnly) { + $idpErrors = $this->checkIdPSettings($settings); + $errors = array_merge($idpErrors, $errors); + } + $spErrors = $this->checkSPSettings($settings); + $errors = array_merge($spErrors, $errors); + + $compressErrors = $this->checkCompressionSettings($settings); + $errors = array_merge($compressErrors, $errors); + } + + return $errors; + } + + /** + * Checks the compression settings info. + * + * @param array $settings Array with settings data + * + * @return array $errors Errors found on the settings data + */ + public function checkCompressionSettings($settings) + { + $errors = array(); + + if (isset($settings['compress'])) { + if (!is_array($settings['compress'])) { + $errors[] = "invalid_syntax"; + } else if (isset($settings['compress']['requests']) + && $settings['compress']['requests'] !== true + && $settings['compress']['requests'] !== false + ) { + $errors[] = "'compress'=>'requests' values must be true or false."; + } else if (isset($settings['compress']['responses']) + && $settings['compress']['responses'] !== true + && $settings['compress']['responses'] !== false + ) { + $errors[] = "'compress'=>'responses' values must be true or false."; + } + } + return $errors; + } + + /** + * Checks the IdP settings info. + * + * @param array $settings Array with settings data + * + * @return array $errors Errors found on the IdP settings data + */ + public function checkIdPSettings(array $settings) + { + if (empty($settings)) { + return array('invalid_syntax'); + } + + $errors = array(); + + if (!isset($settings['idp']) || empty($settings['idp'])) { + $errors[] = 'idp_not_found'; + } else { + $idp = $settings['idp']; + if (!isset($idp['entityId']) || empty($idp['entityId'])) { + $errors[] = 'idp_entityId_not_found'; + } + + if (!isset($idp['singleSignOnService']) + || !isset($idp['singleSignOnService']['url']) + || empty($idp['singleSignOnService']['url']) + ) { + $errors[] = 'idp_sso_not_found'; + } else if (!filter_var($idp['singleSignOnService']['url'], FILTER_VALIDATE_URL)) { + $errors[] = 'idp_sso_url_invalid'; + } + + if (isset($idp['singleLogoutService']) + && isset($idp['singleLogoutService']['url']) + && !empty($idp['singleLogoutService']['url']) + && !filter_var($idp['singleLogoutService']['url'], FILTER_VALIDATE_URL) + ) { + $errors[] = 'idp_slo_url_invalid'; + } + + if (isset($idp['singleLogoutService']) + && isset($idp['singleLogoutService']['responseUrl']) + && !empty($idp['singleLogoutService']['responseUrl']) + && !filter_var($idp['singleLogoutService']['responseUrl'], FILTER_VALIDATE_URL) + ) { + $errors[] = 'idp_slo_response_url_invalid'; + } + + $existsX509 = isset($idp['x509cert']) && !empty($idp['x509cert']); + $existsMultiX509Sign = isset($idp['x509certMulti']) && isset($idp['x509certMulti']['signing']) && !empty($idp['x509certMulti']['signing']); + $existsFingerprint = isset($idp['certFingerprint']) && !empty($idp['certFingerprint']); + if (!($existsX509 || $existsFingerprint || $existsMultiX509Sign) + ) { + $errors[] = 'idp_cert_or_fingerprint_not_found_and_required'; + } + + if (isset($settings['security'])) { + $existsMultiX509Enc = isset($idp['x509certMulti']) && isset($idp['x509certMulti']['encryption']) && !empty($idp['x509certMulti']['encryption']); + + if ((isset($settings['security']['nameIdEncrypted']) && $settings['security']['nameIdEncrypted'] == true) + && !($existsX509 || $existsMultiX509Enc) + ) { + $errors[] = 'idp_cert_not_found_and_required'; + } + } + } + + return $errors; + } + + /** + * Checks the SP settings info. + * + * @param array $settings Array with settings data + * + * @return array $errors Errors found on the SP settings data + */ + public function checkSPSettings(array $settings) + { + if (empty($settings)) { + return array('invalid_syntax'); + } + + $errors = array(); + + if (!isset($settings['sp']) || empty($settings['sp'])) { + $errors[] = 'sp_not_found'; + } else { + $sp = $settings['sp']; + $security = array(); + if (isset($settings['security'])) { + $security = $settings['security']; + } + + if (!isset($sp['entityId']) || empty($sp['entityId'])) { + $errors[] = 'sp_entityId_not_found'; + } + + if (!isset($sp['assertionConsumerService']) + || !isset($sp['assertionConsumerService']['url']) + || empty($sp['assertionConsumerService']['url']) + ) { + $errors[] = 'sp_acs_not_found'; + } else if (!filter_var($sp['assertionConsumerService']['url'], FILTER_VALIDATE_URL)) { + $errors[] = 'sp_acs_url_invalid'; + } + + if (isset($sp['singleLogoutService']) + && isset($sp['singleLogoutService']['url']) + && !filter_var($sp['singleLogoutService']['url'], FILTER_VALIDATE_URL) + ) { + $errors[] = 'sp_sls_url_invalid'; + } + + if (isset($security['signMetadata']) && is_array($security['signMetadata'])) { + if ((!isset($security['signMetadata']['keyFileName']) + || !isset($security['signMetadata']['certFileName'])) && + (!isset($security['signMetadata']['privateKey']) + || !isset($security['signMetadata']['x509cert'])) + ) { + $errors[] = 'sp_signMetadata_invalid'; + } + } + + if (((isset($security['authnRequestsSigned']) && $security['authnRequestsSigned'] == true) + || (isset($security['logoutRequestSigned']) && $security['logoutRequestSigned'] == true) + || (isset($security['logoutResponseSigned']) && $security['logoutResponseSigned'] == true) + || (isset($security['wantAssertionsEncrypted']) && $security['wantAssertionsEncrypted'] == true) + || (isset($security['wantNameIdEncrypted']) && $security['wantNameIdEncrypted'] == true)) + && !$this->checkSPCerts() + ) { + $errors[] = 'sp_certs_not_found_and_required'; + } + } + + if (isset($settings['contactPerson'])) { + $types = array_keys($settings['contactPerson']); + $validTypes = array('technical', 'support', 'administrative', 'billing', 'other'); + foreach ($types as $type) { + if (!in_array($type, $validTypes)) { + $errors[] = 'contact_type_invalid'; + break; + } + } + + foreach ($settings['contactPerson'] as $type => $contact) { + if (!isset($contact['givenName']) || empty($contact['givenName']) + || !isset($contact['emailAddress']) || empty($contact['emailAddress']) + ) { + $errors[] = 'contact_not_enought_data'; + break; + } + } + } + + if (isset($settings['organization'])) { + foreach ($settings['organization'] as $organization) { + if (!isset($organization['name']) || empty($organization['name']) + || !isset($organization['displayname']) || empty($organization['displayname']) + || !isset($organization['url']) || empty($organization['url']) + ) { + $errors[] = 'organization_not_enought_data'; + break; + } + } + } + + return $errors; + } + + /** + * Checks if the x509 certs of the SP exists and are valid. + * + * @return bool + */ + public function checkSPCerts() + { + $key = $this->getSPkey(); + $cert = $this->getSPcert(); + return (!empty($key) && !empty($cert)); + } + + /** + * Returns the x509 private key of the SP. + * + * @return string SP private key + */ + public function getSPkey() + { + $key = null; + if (isset($this->_sp['privateKey']) && !empty($this->_sp['privateKey'])) { + $key = $this->_sp['privateKey']; + } else { + $keyFile = $this->_paths['cert'].'sp.key'; + + if (file_exists($keyFile)) { + $key = file_get_contents($keyFile); + } + } + return $key; + } + + /** + * Returns the x509 public cert of the SP. + * + * @return string SP public cert + */ + public function getSPcert() + { + $cert = null; + + if (isset($this->_sp['x509cert']) && !empty($this->_sp['x509cert'])) { + $cert = $this->_sp['x509cert']; + } else { + $certFile = $this->_paths['cert'].'sp.crt'; + + if (file_exists($certFile)) { + $cert = file_get_contents($certFile); + } + } + return $cert; + } + + /** + * Returns the x509 public of the SP that is + * planed to be used soon instead the other + * public cert + * + * @return string SP public cert New + */ + public function getSPcertNew() + { + $cert = null; + + if (isset($this->_sp['x509certNew']) && !empty($this->_sp['x509certNew'])) { + $cert = $this->_sp['x509certNew']; + } else { + $certFile = $this->_paths['cert'].'sp_new.crt'; + + if (file_exists($certFile)) { + $cert = file_get_contents($certFile); + } + } + return $cert; + } + + /** + * Gets the IdP data. + * + * @return array IdP info + */ + public function getIdPData() + { + return $this->_idp; + } + + /** + * Gets the SP data. + * + * @return array SP info + */ + public function getSPData() + { + return $this->_sp; + } + + /** + * Gets security data. + * + * @return array SP info + */ + public function getSecurityData() + { + return $this->_security; + } + + /** + * Gets contact data. + * + * @return array SP info + */ + public function getContacts() + { + return $this->_contacts; + } + + /** + * Gets organization data. + * + * @return array SP info + */ + public function getOrganization() + { + return $this->_organization; + } + + /** + * Should SAML requests be compressed? + * + * @return bool Yes/No as True/False + */ + public function shouldCompressRequests() + { + return $this->_compress['requests']; + } + + /** + * Should SAML responses be compressed? + * + * @return bool Yes/No as True/False + */ + public function shouldCompressResponses() + { + return $this->_compress['responses']; + } + + /** + * Gets the IdP SSO url. + * + * @return string|null The url of the IdP Single Sign On Service + */ + public function getIdPSSOUrl() + { + $ssoUrl = null; + if (isset($this->_idp['singleSignOnService']) && isset($this->_idp['singleSignOnService']['url'])) { + $ssoUrl = $this->_idp['singleSignOnService']['url']; + } + return $ssoUrl; + } + + /** + * Gets the IdP SLO url. + * + * @return string|null The request url of the IdP Single Logout Service + */ + public function getIdPSLOUrl() + { + $sloUrl = null; + if (isset($this->_idp['singleLogoutService']) && isset($this->_idp['singleLogoutService']['url'])) { + $sloUrl = $this->_idp['singleLogoutService']['url']; + } + return $sloUrl; + } + + /** + * Gets the IdP SLO response url. + * + * @return string|null The response url of the IdP Single Logout Service + */ + public function getIdPSLOResponseUrl() + { + if (isset($this->_idp['singleLogoutService']) && isset($this->_idp['singleLogoutService']['responseUrl'])) { + return $this->_idp['singleLogoutService']['responseUrl']; + } + return $this->getIdPSLOUrl(); + } + + /** + * Gets the SP metadata. The XML representation. + * + * @param bool $alwaysPublishEncryptionCert When 'true', the returned + * metadata will always include an 'encryption' KeyDescriptor. Otherwise, + * the 'encryption' KeyDescriptor will only be included if + * $advancedSettings['security']['wantNameIdEncrypted'] or + * $advancedSettings['security']['wantAssertionsEncrypted'] are enabled. + * @param int|null $validUntil Metadata's valid time + * @param int|null $cacheDuration Duration of the cache in seconds + * + * @return string SP metadata (xml) + * @throws Exception + * @throws Error + */ + public function getSPMetadata($alwaysPublishEncryptionCert = false, $validUntil = null, $cacheDuration = null) + { + $metadata = Metadata::builder($this->_sp, $this->_security['authnRequestsSigned'], $this->_security['wantAssertionsSigned'], $validUntil, $cacheDuration, $this->getContacts(), $this->getOrganization()); + + $certNew = $this->getSPcertNew(); + if (!empty($certNew)) { + $metadata = Metadata::addX509KeyDescriptors( + $metadata, + $certNew, + $alwaysPublishEncryptionCert || $this->_security['wantNameIdEncrypted'] || $this->_security['wantAssertionsEncrypted'] + ); + } + + $cert = $this->getSPcert(); + if (!empty($cert)) { + $metadata = Metadata::addX509KeyDescriptors( + $metadata, + $cert, + $alwaysPublishEncryptionCert || $this->_security['wantNameIdEncrypted'] || $this->_security['wantAssertionsEncrypted'] + ); + } + + //Sign Metadata + if (isset($this->_security['signMetadata']) && $this->_security['signMetadata'] != false) { + if ($this->_security['signMetadata'] === true) { + $keyMetadata = $this->getSPkey(); + $certMetadata = $cert; + + if (!$keyMetadata) { + throw new Error( + 'SP Private key not found.', + Error::PRIVATE_KEY_FILE_NOT_FOUND + ); + } + + if (!$certMetadata) { + throw new Error( + 'SP Public cert not found.', + Error::PUBLIC_CERT_FILE_NOT_FOUND + ); + } + } else if (isset($this->_security['signMetadata']['keyFileName']) && + isset($this->_security['signMetadata']['certFileName'])) { + $keyFileName = $this->_security['signMetadata']['keyFileName']; + $certFileName = $this->_security['signMetadata']['certFileName']; + + $keyMetadataFile = $this->_paths['cert'].$keyFileName; + $certMetadataFile = $this->_paths['cert'].$certFileName; + + if (!file_exists($keyMetadataFile)) { + throw new Error( + 'SP Private key file not found: %s', + Error::PRIVATE_KEY_FILE_NOT_FOUND, + array($keyMetadataFile) + ); + } + + if (!file_exists($certMetadataFile)) { + throw new Error( + 'SP Public cert file not found: %s', + Error::PUBLIC_CERT_FILE_NOT_FOUND, + array($certMetadataFile) + ); + } + $keyMetadata = file_get_contents($keyMetadataFile); + $certMetadata = file_get_contents($certMetadataFile); + } else if (isset($this->_security['signMetadata']['privateKey']) && + isset($this->_security['signMetadata']['x509cert'])) { + $keyMetadata = Utils::formatPrivateKey($this->_security['signMetadata']['privateKey']); + $certMetadata = Utils::formatCert($this->_security['signMetadata']['x509cert']); + if (!$keyMetadata) { + throw new Error( + 'Private key not found.', + Error::PRIVATE_KEY_FILE_NOT_FOUND + ); + } + + if (!$certMetadata) { + throw new Error( + 'Public cert not found.', + Error::PUBLIC_CERT_FILE_NOT_FOUND + ); + } + } else { + throw new Error( + 'Invalid Setting: signMetadata value of the sp is not valid', + Error::SETTINGS_INVALID_SYNTAX + ); + + } + + $signatureAlgorithm = $this->_security['signatureAlgorithm']; + $digestAlgorithm = $this->_security['digestAlgorithm']; + $metadata = Metadata::signMetadata($metadata, $keyMetadata, $certMetadata, $signatureAlgorithm, $digestAlgorithm); + } + return $metadata; + } + + /** + * Validates an XML SP Metadata. + * + * @param string $xml Metadata's XML that will be validate + * + * @return array The list of found errors + * + * @throws Exception + */ + public function validateMetadata($xml) + { + assert(is_string($xml)); + + $errors = array(); + $res = Utils::validateXML($xml, 'saml-schema-metadata-2.0.xsd', $this->_debug, $this->getSchemasPath()); + if (!$res instanceof DOMDocument) { + $errors[] = $res; + } else { + $dom = $res; + $element = $dom->documentElement; + if ($element->tagName !== 'md:EntityDescriptor') { + $errors[] = 'noEntityDescriptor_xml'; + } else { + $validUntil = $cacheDuration = $expireTime = null; + + if ($element->hasAttribute('validUntil')) { + $validUntil = Utils::parseSAML2Time($element->getAttribute('validUntil')); + } + if ($element->hasAttribute('cacheDuration')) { + $cacheDuration = $element->getAttribute('cacheDuration'); + } + + $expireTime = Utils::getExpireTime($cacheDuration, $validUntil); + if (isset($expireTime) && time() > $expireTime) { + $errors[] = 'expired_xml'; + } + } + } + + // TODO: Support Metadata Sign Validation + + return $errors; + } + + /** + * Formats the IdP cert. + */ + public function formatIdPCert() + { + if (isset($this->_idp['x509cert'])) { + $this->_idp['x509cert'] = Utils::formatCert($this->_idp['x509cert']); + } + } + + /** + * Formats the Multple IdP certs. + */ + public function formatIdPCertMulti() + { + if (isset($this->_idp['x509certMulti'])) { + if (isset($this->_idp['x509certMulti']['signing'])) { + foreach ($this->_idp['x509certMulti']['signing'] as $i => $cert) { + $this->_idp['x509certMulti']['signing'][$i] = Utils::formatCert($cert); + } + } + if (isset($this->_idp['x509certMulti']['encryption'])) { + foreach ($this->_idp['x509certMulti']['encryption'] as $i => $cert) { + $this->_idp['x509certMulti']['encryption'][$i] = Utils::formatCert($cert); + } + } + } + } + + /** + * Formats the SP cert. + */ + public function formatSPCert() + { + if (isset($this->_sp['x509cert'])) { + $this->_sp['x509cert'] = Utils::formatCert($this->_sp['x509cert']); + } + } + + /** + * Formats the SP cert. + */ + public function formatSPCertNew() + { + if (isset($this->_sp['x509certNew'])) { + $this->_sp['x509certNew'] = Utils::formatCert($this->_sp['x509certNew']); + } + } + + /** + * Formats the SP private key. + */ + public function formatSPKey() + { + if (isset($this->_sp['privateKey'])) { + $this->_sp['privateKey'] = Utils::formatPrivateKey($this->_sp['privateKey']); + } + } + + /** + * Returns an array with the errors, the array is empty when the settings is ok. + * + * @return array Errors + */ + public function getErrors() + { + return $this->_errors; + } + + /** + * Activates or deactivates the strict mode. + * + * @param bool $value Strict parameter + * + * @throws Exception + */ + public function setStrict($value) + { + if (!is_bool($value)) { + throw new Exception('Invalid value passed to setStrict()'); + } + + $this->_strict = $value; + } + + /** + * Returns if the 'strict' mode is active. + * + * @return bool Strict parameter + */ + public function isStrict() + { + return $this->_strict; + } + + /** + * Returns if the debug is active. + * + * @return bool Debug parameter + */ + public function isDebugActive() + { + return $this->_debug; + } + + /** + * Set a baseurl value. + * + * @param string $baseurl Base URL. + */ + public function setBaseURL($baseurl) + { + $this->_baseurl = $baseurl; + } + + /** + * Returns the baseurl set on the settings if any. + * + * @return null|string The baseurl + */ + public function getBaseURL() + { + return $this->_baseurl; + } + + /** + * Sets the IdP certificate. + * + * @param string $cert IdP certificate + */ + public function setIdPCert($cert) + { + $this->_idp['x509cert'] = $cert; + $this->formatIdPCert(); + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/Utils.php b/saml/vendor/onelogin/php-saml/src/Saml2/Utils.php new file mode 100644 index 00000000..582c117b --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/Utils.php @@ -0,0 +1,1579 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use RobRichards\XMLSecLibs\XMLSecurityKey; +use RobRichards\XMLSecLibs\XMLSecurityDSig; +use RobRichards\XMLSecLibs\XMLSecEnc; + +use DOMDocument; +use DOMElement; +use DOMNodeList; +use DomNode; +use DOMXPath; +use Exception; + +/** + * Utils of OneLogin PHP Toolkit + * + * Defines several often used methods + */ +class Utils +{ + const RESPONSE_SIGNATURE_XPATH = "/samlp:Response/ds:Signature"; + const ASSERTION_SIGNATURE_XPATH = "/samlp:Response/saml:Assertion/ds:Signature"; + + /** + * @var bool Control if the `Forwarded-For-*` headers are used + */ + private static $_proxyVars = false; + + /** + * @var string|null + */ + private static $_host; + + /** + * @var string|null + */ + private static $_protocol; + + /** + * @var string + */ + private static $_protocolRegex = '@^https?://@i'; + + /** + * @var int|null + */ + private static $_port; + + /** + * @var string|null + */ + private static $_baseurlpath; + + /** + * This function load an XML string in a save way. + * Prevent XEE/XXE Attacks + * + * @param DOMDocument $dom The document where load the xml. + * @param string $xml The XML string to be loaded. + * + * @return DOMDocument|false $dom The result of load the XML at the DOMDocument + * + * @throws Exception + */ + public static function loadXML(DOMDocument $dom, $xml) + { + assert($dom instanceof DOMDocument); + assert(is_string($xml)); + + $oldEntityLoader = null; + if (PHP_VERSION_ID < 80000) { + $oldEntityLoader = libxml_disable_entity_loader(true); + } + + $res = $dom->loadXML($xml); + + if (PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($oldEntityLoader); + } + + foreach ($dom->childNodes as $child) { + if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) { + throw new Exception( + 'Detected use of DOCTYPE/ENTITY in XML, disabled to prevent XXE/XEE attacks' + ); + } + } + + if (!$res) { + return false; + } else { + return $dom; + } + } + + /** + * This function attempts to validate an XML string against the specified schema. + * + * It will parse the string into a DOMDocument and validate this document against the schema. + * + * @param string|DOMDocument $xml The XML string or document which should be validated. + * @param string $schema The schema filename which should be used. + * @param bool $debug To disable/enable the debug mode + * @param string $schemaPath Change schema path + * + * @return string|DOMDocument $dom string that explains the problem or the DOMDocument + * + * @throws Exception + */ + public static function validateXML($xml, $schema, $debug = false, $schemaPath = null) + { + assert(is_string($xml) || $xml instanceof DOMDocument); + assert(is_string($schema)); + + libxml_clear_errors(); + libxml_use_internal_errors(true); + + if ($xml instanceof DOMDocument) { + $dom = $xml; + } else { + $dom = new DOMDocument; + $dom = self::loadXML($dom, $xml); + if (!$dom) { + return 'unloaded_xml'; + } + } + + if (isset($schemaPath)) { + $schemaFile = $schemaPath . $schema; + } else { + $schemaFile = __DIR__ . '/schemas/' . $schema; + } + + $oldEntityLoader = null; + if (PHP_VERSION_ID < 80000) { + $oldEntityLoader = libxml_disable_entity_loader(false); + } + $res = $dom->schemaValidate($schemaFile); + if (PHP_VERSION_ID < 80000) { + libxml_disable_entity_loader($oldEntityLoader); + } + if (!$res) { + $xmlErrors = libxml_get_errors(); + syslog(LOG_INFO, 'Error validating the metadata: '.var_export($xmlErrors, true)); + + if ($debug) { + foreach ($xmlErrors as $error) { + echo htmlentities($error->message)."\n"; + } + } + return 'invalid_xml'; + } + + return $dom; + } + + /** + * Import a node tree into a target document + * Copy it before a reference node as a sibling + * and at the end of the copy remove + * the reference node in the target document + * As it were 'replacing' it + * Leaving nested default namespaces alone + * (Standard importNode with deep copy + * mangles nested default namespaces) + * + * The reference node must not be a DomDocument + * It CAN be the top element of a document + * Returns the copied node in the target document + * + * @param DomNode $targetNode + * @param DomNode $sourceNode + * @param bool $recurse + * @return DOMNode + * @throws Exception + */ + public static function treeCopyReplace(DomNode $targetNode, DomNode $sourceNode, $recurse = false) + { + if ($targetNode->parentNode === null) { + throw new Exception('Illegal argument targetNode. It has no parentNode.'); + } + $clonedNode = $targetNode->ownerDocument->importNode($sourceNode, false); + if ($recurse) { + $resultNode = $targetNode->appendChild($clonedNode); + } else { + $resultNode = $targetNode->parentNode->insertBefore($clonedNode, $targetNode); + } + if ($sourceNode->childNodes !== null) { + foreach ($sourceNode->childNodes as $child) { + self::treeCopyReplace($resultNode, $child, true); + } + } + if (!$recurse) { + $targetNode->parentNode->removeChild($targetNode); + } + return $resultNode; + } + + /** + * Returns a x509 cert (adding header & footer if required). + * + * @param string $cert A x509 unformated cert + * @param bool $heads True if we want to include head and footer + * + * @return string $x509 Formatted cert + */ + public static function formatCert($cert, $heads = true) + { + $x509cert = str_replace(array("\x0D", "\r", "\n"), "", $cert); + if (!empty($x509cert)) { + $x509cert = str_replace('-----BEGIN CERTIFICATE-----', "", $x509cert); + $x509cert = str_replace('-----END CERTIFICATE-----', "", $x509cert); + $x509cert = str_replace(' ', '', $x509cert); + + if ($heads) { + $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n"; + } + + } + return $x509cert; + } + + /** + * Returns a private key (adding header & footer if required). + * + * @param string $key A private key + * @param bool $heads True if we want to include head and footer + * + * @return string $rsaKey Formatted private key + */ + public static function formatPrivateKey($key, $heads = true) + { + $key = str_replace(array("\x0D", "\r", "\n"), "", $key); + if (!empty($key)) { + if (strpos($key, '-----BEGIN PRIVATE KEY-----') !== false) { + $key = Utils::getStringBetween($key, '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----'); + $key = str_replace(' ', '', $key); + + if ($heads) { + $key = "-----BEGIN PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END PRIVATE KEY-----\n"; + } + } else if (strpos($key, '-----BEGIN RSA PRIVATE KEY-----') !== false) { + $key = Utils::getStringBetween($key, '-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----'); + $key = str_replace(' ', '', $key); + + if ($heads) { + $key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END RSA PRIVATE KEY-----\n"; + } + } else { + $key = str_replace(' ', '', $key); + + if ($heads) { + $key = "-----BEGIN RSA PRIVATE KEY-----\n".chunk_split($key, 64, "\n")."-----END RSA PRIVATE KEY-----\n"; + } + } + } + return $key; + } + + /** + * Extracts a substring between 2 marks + * + * @param string $str The target string + * @param string $start The initial mark + * @param string $end The end mark + * + * @return string A substring or an empty string if is not able to find the marks + * or if there is no string between the marks + */ + public static function getStringBetween($str, $start, $end) + { + $str = ' ' . $str; + $ini = strpos($str, $start); + + if ($ini == 0) { + return ''; + } + + $ini += strlen($start); + $len = strpos($str, $end, $ini) - $ini; + return substr($str, $ini, $len); + } + + /** + * Executes a redirection to the provided url (or return the target url). + * + * @param string $url The target url + * @param array $parameters Extra parameters to be passed as part of the url + * @param bool $stay True if we want to stay (returns the url string) False to redirect + * + * @return string|null $url + * + * @throws Error + */ + public static function redirect($url, array $parameters = array(), $stay = false) + { + assert(is_string($url)); + + if (substr($url, 0, 1) === '/') { + $url = self::getSelfURLhost() . $url; + } + + /** + * Verify that the URL matches the regex for the protocol. + * By default this will check for http and https + */ + $wrongProtocol = !preg_match(self::$_protocolRegex, $url); + $url = filter_var($url, FILTER_VALIDATE_URL); + if ($wrongProtocol || empty($url)) { + throw new Error( + 'Redirect to invalid URL: ' . $url, + Error::REDIRECT_INVALID_URL + ); + } + + /* Add encoded parameters */ + if (strpos($url, '?') === false) { + $paramPrefix = '?'; + } else { + $paramPrefix = '&'; + } + + foreach ($parameters as $name => $value) { + if ($value === null) { + $param = urlencode($name); + } else if (is_array($value)) { + $param = ""; + foreach ($value as $val) { + $param .= urlencode($name) . "[]=" . urlencode($val). '&'; + } + if (!empty($param)) { + $param = substr($param, 0, -1); + } + } else { + $param = urlencode($name) . '=' . urlencode($value); + } + + if (!empty($param)) { + $url .= $paramPrefix . $param; + $paramPrefix = '&'; + } + } + + if ($stay) { + return $url; + } + + header('Pragma: no-cache'); + header('Cache-Control: no-cache, must-revalidate'); + header('Location: ' . $url); + exit(); + } + + /** + * @param $protocolRegex string + */ + public static function setProtocolRegex($protocolRegex) + { + if (!empty($protocolRegex)) { + self::$_protocolRegex = $protocolRegex; + } + } + + /** + * Set the Base URL value. + * + * @param string $baseurl The base url to be used when constructing URLs + */ + public static function setBaseURL($baseurl) + { + if (!empty($baseurl)) { + $baseurlpath = '/'; + $matches = array(); + if (preg_match('#^https?://([^/]*)/?(.*)#i', $baseurl, $matches)) { + if (strpos($baseurl, 'https://') === false) { + self::setSelfProtocol('http'); + $port = '80'; + } else { + self::setSelfProtocol('https'); + $port = '443'; + } + + $currentHost = $matches[1]; + if (false !== strpos($currentHost, ':')) { + list($currentHost, $possiblePort) = explode(':', $matches[1], 2); + if (is_numeric($possiblePort)) { + $port = $possiblePort; + } + } + + if (isset($matches[2]) && !empty($matches[2])) { + $baseurlpath = $matches[2]; + } + + self::setSelfHost($currentHost); + self::setSelfPort($port); + self::setBaseURLPath($baseurlpath); + } + } else { + self::$_host = null; + self::$_protocol = null; + self::$_port = null; + self::$_baseurlpath = null; + } + } + + /** + * @param bool $proxyVars Whether to use `X-Forwarded-*` headers to determine port/domain/protocol + */ + public static function setProxyVars($proxyVars) + { + self::$_proxyVars = (bool)$proxyVars; + } + + /** + * @return bool + */ + public static function getProxyVars() + { + return self::$_proxyVars; + } + + /** + * Returns the protocol + the current host + the port (if different than + * common ports). + * + * @return string The URL + */ + public static function getSelfURLhost() + { + $currenthost = self::getSelfHost(); + + $port = ''; + + if (self::isHTTPS()) { + $protocol = 'https'; + } else { + $protocol = 'http'; + } + + $portnumber = self::getSelfPort(); + + if (isset($portnumber) && ($portnumber != '80') && ($portnumber != '443')) { + $port = ':' . $portnumber; + } + + return $protocol."://" . $currenthost . $port; + } + + /** + * @param string $host The host to use when constructing URLs + */ + public static function setSelfHost($host) + { + self::$_host = $host; + } + + /** + * @param string $baseurlpath The baseurl path to use when constructing URLs + */ + public static function setBaseURLPath($baseurlpath) + { + if (empty($baseurlpath)) { + self::$_baseurlpath = null; + } else if ($baseurlpath == '/') { + self::$_baseurlpath = '/'; + } else { + self::$_baseurlpath = '/' . trim($baseurlpath, '/') . '/'; + } + } + + /** + * @return string The baseurlpath to be used when constructing URLs + */ + public static function getBaseURLPath() + { + return self::$_baseurlpath; + } + + /** + * @return string The raw host name + */ + protected static function getRawHost() + { + if (self::$_host) { + $currentHost = self::$_host; + } elseif (self::getProxyVars() && array_key_exists('HTTP_X_FORWARDED_HOST', $_SERVER)) { + $currentHost = $_SERVER['HTTP_X_FORWARDED_HOST']; + } elseif (array_key_exists('HTTP_HOST', $_SERVER)) { + $currentHost = $_SERVER['HTTP_HOST']; + } elseif (array_key_exists('SERVER_NAME', $_SERVER)) { + $currentHost = $_SERVER['SERVER_NAME']; + } else { + if (function_exists('gethostname')) { + $currentHost = gethostname(); + } else { + $currentHost = php_uname("n"); + } + } + return $currentHost; + } + + /** + * @param int $port The port number to use when constructing URLs + */ + public static function setSelfPort($port) + { + self::$_port = $port; + } + + /** + * @param string $protocol The protocol to identify as using, usually http or https + */ + public static function setSelfProtocol($protocol) + { + self::$_protocol = $protocol; + } + + /** + * @return string http|https + */ + public static function getSelfProtocol() + { + $protocol = 'http'; + if (self::$_protocol) { + $protocol = self::$_protocol; + } elseif (self::getSelfPort() == 443) { + $protocol = 'https'; + } elseif (self::getProxyVars() && isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + $protocol = $_SERVER['HTTP_X_FORWARDED_PROTO']; + } elseif (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { + $protocol = 'https'; + } + return $protocol; + } + + /** + * Returns the current host. + * + * @return string $currentHost The current host + */ + public static function getSelfHost() + { + $currentHost = self::getRawHost(); + + // strip the port + if (false !== strpos($currentHost, ':')) { + list($currentHost, $port) = explode(':', $currentHost, 2); + } + + return $currentHost; + } + + /** + * @return null|string The port number used for the request + */ + public static function getSelfPort() + { + $portnumber = null; + if (self::$_port) { + $portnumber = self::$_port; + } else if (self::getProxyVars() && isset($_SERVER["HTTP_X_FORWARDED_PORT"])) { + $portnumber = $_SERVER["HTTP_X_FORWARDED_PORT"]; + } else if (isset($_SERVER["SERVER_PORT"])) { + $portnumber = $_SERVER["SERVER_PORT"]; + } else { + $currentHost = self::getRawHost(); + + // strip the port + if (false !== strpos($currentHost, ':')) { + list($currentHost, $port) = explode(':', $currentHost, 2); + if (is_numeric($port)) { + $portnumber = $port; + } + } + } + return $portnumber; + } + + /** + * Checks if https or http. + * + * @return bool $isHttps False if https is not active + */ + public static function isHTTPS() + { + return self::getSelfProtocol() == 'https'; + } + + /** + * Returns the URL of the current host + current view. + * + * @return string + */ + public static function getSelfURLNoQuery() + { + $selfURLNoQuery = self::getSelfURLhost(); + + $infoWithBaseURLPath = self::buildWithBaseURLPath($_SERVER['SCRIPT_NAME']); + if (!empty($infoWithBaseURLPath)) { + $selfURLNoQuery .= $infoWithBaseURLPath; + } else { + $selfURLNoQuery .= $_SERVER['SCRIPT_NAME']; + } + + if (isset($_SERVER['PATH_INFO'])) { + $selfURLNoQuery .= $_SERVER['PATH_INFO']; + } + + return $selfURLNoQuery; + } + + /** + * Returns the routed URL of the current host + current view. + * + * @return string + */ + public static function getSelfRoutedURLNoQuery() + { + $selfURLhost = self::getSelfURLhost(); + $route = ''; + + if (!empty($_SERVER['REQUEST_URI'])) { + $route = $_SERVER['REQUEST_URI']; + if (!empty($_SERVER['QUERY_STRING'])) { + $route = self::strLreplace($_SERVER['QUERY_STRING'], '', $route); + if (substr($route, -1) == '?') { + $route = substr($route, 0, -1); + } + } + } + + $infoWithBaseURLPath = self::buildWithBaseURLPath($route); + if (!empty($infoWithBaseURLPath)) { + $route = $infoWithBaseURLPath; + } + + $selfRoutedURLNoQuery = $selfURLhost . $route; + + $pos = strpos($selfRoutedURLNoQuery, "?"); + if ($pos !== false) { + $selfRoutedURLNoQuery = substr($selfRoutedURLNoQuery, 0, $pos); + } + + return $selfRoutedURLNoQuery; + } + + public static function strLreplace($search, $replace, $subject) + { + $pos = strrpos($subject, $search); + + if ($pos !== false) { + $subject = substr_replace($subject, $replace, $pos, strlen($search)); + } + + return $subject; + } + + /** + * Returns the URL of the current host + current view + query. + * + * @return string + */ + public static function getSelfURL() + { + $selfURLhost = self::getSelfURLhost(); + + $requestURI = ''; + if (!empty($_SERVER['REQUEST_URI'])) { + $requestURI = $_SERVER['REQUEST_URI']; + $matches = array(); + if ($requestURI[0] !== '/' && preg_match('#^https?://[^/]*(/.*)#i', $requestURI, $matches)) { + $requestURI = $matches[1]; + } + } + + $infoWithBaseURLPath = self::buildWithBaseURLPath($requestURI); + if (!empty($infoWithBaseURLPath)) { + $requestURI = $infoWithBaseURLPath; + } + + return $selfURLhost . $requestURI; + } + + /** + * Returns the part of the URL with the BaseURLPath. + * + * @param string $info Contains path info + * + * @return string + */ + protected static function buildWithBaseURLPath($info) + { + $result = ''; + $baseURLPath = self::getBaseURLPath(); + if (!empty($baseURLPath)) { + $result = $baseURLPath; + if (!empty($info)) { + $path = explode('/', $info); + $extractedInfo = array_pop($path); + if (!empty($extractedInfo)) { + $result .= $extractedInfo; + } + } + } + return $result; + } + + /** + * Extract a query param - as it was sent - from $_SERVER[QUERY_STRING] + * + * @param string $name The param to-be extracted + * + * @return string + */ + public static function extractOriginalQueryParam($name) + { + $index = strpos($_SERVER['QUERY_STRING'], $name.'='); + $substring = substr($_SERVER['QUERY_STRING'], $index + strlen($name) + 1); + $end = strpos($substring, '&'); + return $end ? substr($substring, 0, strpos($substring, '&')) : $substring; + } + + /** + * Generates an unique string (used for example as ID for assertions). + * + * @return string A unique string + */ + public static function generateUniqueID() + { + return 'ONELOGIN_' . sha1(uniqid((string)mt_rand(), true)); + } + + /** + * Converts a UNIX timestamp to SAML2 timestamp on the form + * yyyy-mm-ddThh:mm:ss(\.s+)?Z. + * + * @param string|int $time The time we should convert (DateTime). + * + * @return string $timestamp SAML2 timestamp. + */ + public static function parseTime2SAML($time) + { + $date = new \DateTime("@$time", new \DateTimeZone('UTC')); + $timestamp = $date->format("Y-m-d\TH:i:s\Z"); + return $timestamp; + } + + /** + * Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z + * to a UNIX timestamp. The sub-second part is ignored. + * + * @param string $time The time we should convert (SAML Timestamp). + * + * @return int $timestamp Converted to a unix timestamp. + * + * @throws Exception + */ + public static function parseSAML2Time($time) + { + $matches = array(); + + /* We use a very strict regex to parse the timestamp. */ + $exp1 = '/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)'; + $exp2 = 'T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D'; + if (preg_match($exp1 . $exp2, $time, $matches) == 0) { + throw new Exception( + 'Invalid SAML2 timestamp passed to' . + ' parseSAML2Time: ' . $time + ); + } + + /* Extract the different components of the time from the + * matches in the regex. int cast will ignore leading zeroes + * in the string. + */ + $year = (int) $matches[1]; + $month = (int) $matches[2]; + $day = (int) $matches[3]; + $hour = (int) $matches[4]; + $minute = (int) $matches[5]; + $second = (int) $matches[6]; + + /* We use gmmktime because the timestamp will always be given + * in UTC. + */ + $ts = gmmktime($hour, $minute, $second, $month, $day, $year); + + return $ts; + } + + + /** + * Interprets a ISO8601 duration value relative to a given timestamp. + * + * @param string $duration The duration, as a string. + * @param int|null $timestamp The unix timestamp we should apply the + * duration to. Optional, default to the + * current time. + * + * @return int The new timestamp, after the duration is applied. + * + * @throws Exception + */ + public static function parseDuration($duration, $timestamp = null) + { + assert(is_string($duration)); + assert(is_null($timestamp) || is_int($timestamp)); + + $matches = array(); + + /* Parse the duration. We use a very strict pattern. */ + $durationRegEx = '#^(-?)P(?:(?:(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?)|(?:(\\d+)W))$#D'; + if (!preg_match($durationRegEx, $duration, $matches)) { + throw new Exception('Invalid ISO 8601 duration: ' . $duration); + } + + $durYears = (empty($matches[2]) ? 0 : (int)$matches[2]); + $durMonths = (empty($matches[3]) ? 0 : (int)$matches[3]); + $durDays = (empty($matches[4]) ? 0 : (int)$matches[4]); + $durHours = (empty($matches[5]) ? 0 : (int)$matches[5]); + $durMinutes = (empty($matches[6]) ? 0 : (int)$matches[6]); + $durSeconds = (empty($matches[7]) ? 0 : (int)$matches[7]); + $durWeeks = (empty($matches[8]) ? 0 : (int)$matches[8]); + + if (!empty($matches[1])) { + /* Negative */ + $durYears = -$durYears; + $durMonths = -$durMonths; + $durDays = -$durDays; + $durHours = -$durHours; + $durMinutes = -$durMinutes; + $durSeconds = -$durSeconds; + $durWeeks = -$durWeeks; + } + + if ($timestamp === null) { + $timestamp = time(); + } + + if ($durYears !== 0 || $durMonths !== 0) { + /* Special handling of months and years, since they aren't a specific interval, but + * instead depend on the current time. + */ + + /* We need the year and month from the timestamp. Unfortunately, PHP doesn't have the + * gmtime function. Instead we use the gmdate function, and split the result. + */ + $yearmonth = explode(':', gmdate('Y:n', $timestamp)); + $year = (int)$yearmonth[0]; + $month = (int)$yearmonth[1]; + + /* Remove the year and month from the timestamp. */ + $timestamp -= gmmktime(0, 0, 0, $month, 1, $year); + + /* Add years and months, and normalize the numbers afterwards. */ + $year += $durYears; + $month += $durMonths; + while ($month > 12) { + $year += 1; + $month -= 12; + } + while ($month < 1) { + $year -= 1; + $month += 12; + } + + /* Add year and month back into timestamp. */ + $timestamp += gmmktime(0, 0, 0, $month, 1, $year); + } + + /* Add the other elements. */ + $timestamp += $durWeeks * 7 * 24 * 60 * 60; + $timestamp += $durDays * 24 * 60 * 60; + $timestamp += $durHours * 60 * 60; + $timestamp += $durMinutes * 60; + $timestamp += $durSeconds; + + return $timestamp; + } + + /** + * Compares 2 dates and returns the earliest. + * + * @param string|null $cacheDuration The duration, as a string. + * @param string|int|null $validUntil The valid until date, as a string or as a timestamp + * + * @return int|null $expireTime The expiration time. + * + * @throws Exception + */ + public static function getExpireTime($cacheDuration = null, $validUntil = null) + { + $expireTime = null; + + if ($cacheDuration !== null) { + $expireTime = self::parseDuration($cacheDuration, time()); + } + + if ($validUntil !== null) { + if (is_int($validUntil)) { + $validUntilTime = $validUntil; + } else { + $validUntilTime = self::parseSAML2Time($validUntil); + } + if ($expireTime === null || $expireTime > $validUntilTime) { + $expireTime = $validUntilTime; + } + } + + return $expireTime; + } + + + /** + * Extracts nodes from the DOMDocument. + * + * @param DOMDocument $dom The DOMDocument + * @param string $query \Xpath Expression + * @param DOMElement|null $context Context Node (DOMElement) + * + * @return DOMNodeList The queried nodes + */ + public static function query(DOMDocument $dom, $query, DOMElement $context = null) + { + $xpath = new DOMXPath($dom); + $xpath->registerNamespace('samlp', Constants::NS_SAMLP); + $xpath->registerNamespace('saml', Constants::NS_SAML); + $xpath->registerNamespace('ds', Constants::NS_DS); + $xpath->registerNamespace('xenc', Constants::NS_XENC); + $xpath->registerNamespace('xsi', Constants::NS_XSI); + $xpath->registerNamespace('xs', Constants::NS_XS); + $xpath->registerNamespace('md', Constants::NS_MD); + + if (isset($context)) { + $res = $xpath->query($query, $context); + } else { + $res = $xpath->query($query); + } + return $res; + } + + /** + * Checks if the session is started or not. + * + * @return bool true if the sessíon is started + */ + public static function isSessionStarted() + { + if (PHP_VERSION_ID >= 50400) { + return session_status() === PHP_SESSION_ACTIVE ? true : false; + } else { + return session_id() === '' ? false : true; + } + } + + /** + * Deletes the local session. + */ + public static function deleteLocalSession() + { + if (Utils::isSessionStarted()) { + session_unset(); + session_destroy(); + } else { + $_SESSION = array(); + } + } + + /** + * Calculates the fingerprint of a x509cert. + * + * @param string $x509cert x509 cert formatted + * @param string $alg Algorithm to be used in order to calculate the fingerprint + * + * @return null|string Formatted fingerprint + */ + public static function calculateX509Fingerprint($x509cert, $alg = 'sha1') + { + assert(is_string($x509cert)); + + $arCert = explode("\n", $x509cert); + $data = ''; + $inData = false; + + foreach ($arCert as $curData) { + if (! $inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = true; + } elseif ((strncmp($curData, '-----BEGIN PUBLIC KEY', 21) == 0) || (strncmp($curData, '-----BEGIN RSA PRIVATE KEY', 26) == 0)) { + /* This isn't an X509 certificate. */ + return null; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + break; + } + $data .= trim($curData); + } + } + + if (empty($data)) { + return null; + } + + $decodedData = base64_decode($data); + + switch ($alg) { + case 'sha512': + case 'sha384': + case 'sha256': + $fingerprint = hash($alg, $decodedData, false); + break; + case 'sha1': + default: + $fingerprint = strtolower(sha1($decodedData)); + break; + } + return $fingerprint; + } + + /** + * Formates a fingerprint. + * + * @param string $fingerprint fingerprint + * + * @return string Formatted fingerprint + */ + public static function formatFingerPrint($fingerprint) + { + $formatedFingerprint = str_replace(':', '', $fingerprint); + $formatedFingerprint = strtolower($formatedFingerprint); + return $formatedFingerprint; + } + + /** + * Generates a nameID. + * + * @param string $value fingerprint + * @param string $spnq SP Name Qualifier + * @param string|null $format SP Format + * @param string|null $cert IdP Public cert to encrypt the nameID + * @param string|null $nq IdP Name Qualifier + * @param string|null $encAlg Encryption algorithm + * + * @return string $nameIDElement DOMElement | XMLSec nameID + * + * @throws Exception + */ + public static function generateNameId($value, $spnq, $format = null, $cert = null, $nq = null, $encAlg = XMLSecurityKey::AES128_CBC) + { + + $doc = new DOMDocument(); + + $nameId = $doc->createElement('saml:NameID'); + if (isset($spnq)) { + $nameId->setAttribute('SPNameQualifier', $spnq); + } + if (isset($nq)) { + $nameId->setAttribute('NameQualifier', $nq); + } + if (isset($format)) { + $nameId->setAttribute('Format', $format); + } + $nameId->appendChild($doc->createTextNode($value)); + + $doc->appendChild($nameId); + + if (!empty($cert)) { + if ($encAlg == XMLSecurityKey::AES128_CBC) { + $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, array('type'=>'public')); + } else { + $seckey = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, array('type'=>'public')); + } + $seckey->loadKey($cert); + + $enc = new XMLSecEnc(); + $enc->setNode($nameId); + $enc->type = XMLSecEnc::Element; + + $symmetricKey = new XMLSecurityKey($encAlg); + $symmetricKey->generateSessionKey(); + $enc->encryptKey($seckey, $symmetricKey); + + $encryptedData = $enc->encryptNode($symmetricKey); + + $newdoc = new DOMDocument(); + + $encryptedID = $newdoc->createElement('saml:EncryptedID'); + + $newdoc->appendChild($encryptedID); + + $encryptedID->appendChild($encryptedID->ownerDocument->importNode($encryptedData, true)); + + return $newdoc->saveXML($encryptedID); + } else { + return $doc->saveXML($nameId); + } + } + + + /** + * Gets Status from a Response. + * + * @param DOMDocument $dom The Response as XML + * + * @return array $status The Status, an array with the code and a message. + * + * @throws ValidationError + */ + public static function getStatus(DOMDocument $dom) + { + $status = array(); + + $statusEntry = self::query($dom, '/samlp:Response/samlp:Status'); + if ($statusEntry->length != 1) { + throw new ValidationError( + "Missing Status on response", + ValidationError::MISSING_STATUS + ); + } + + $codeEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusCode', $statusEntry->item(0)); + if ($codeEntry->length != 1) { + throw new ValidationError( + "Missing Status Code on response", + ValidationError::MISSING_STATUS_CODE + ); + } + $code = $codeEntry->item(0)->getAttribute('Value'); + $status['code'] = $code; + + $status['msg'] = ''; + $messageEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', $statusEntry->item(0)); + if ($messageEntry->length == 0) { + $subCodeEntry = self::query($dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', $statusEntry->item(0)); + if ($subCodeEntry->length == 1) { + $status['msg'] = $subCodeEntry->item(0)->getAttribute('Value'); + } + } else if ($messageEntry->length == 1) { + $msg = $messageEntry->item(0)->textContent; + $status['msg'] = $msg; + } + + return $status; + } + + /** + * Decrypts an encrypted element. + * + * @param DOMElement $encryptedData The encrypted data. + * @param XMLSecurityKey $inputKey The decryption key. + * @param bool $formatOutput Format or not the output. + * + * @return DOMElement The decrypted element. + * + * @throws ValidationError + */ + public static function decryptElement(DOMElement $encryptedData, XMLSecurityKey $inputKey, $formatOutput = true) + { + + $enc = new XMLSecEnc(); + + $enc->setNode($encryptedData); + $enc->type = $encryptedData->getAttribute("Type"); + + $symmetricKey = $enc->locateKey($encryptedData); + if (!$symmetricKey) { + throw new ValidationError( + 'Could not locate key algorithm in encrypted data.', + ValidationError::KEY_ALGORITHM_ERROR + ); + } + + $symmetricKeyInfo = $enc->locateKeyInfo($symmetricKey); + if (!$symmetricKeyInfo) { + throw new ValidationError( + "Could not locate for the encrypted key.", + ValidationError::KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA + ); + } + + $inputKeyAlgo = $inputKey->getAlgorithm(); + if ($symmetricKeyInfo->isEncrypted) { + $symKeyInfoAlgo = $symmetricKeyInfo->getAlgorithm(); + + if ($symKeyInfoAlgo === XMLSecurityKey::RSA_OAEP_MGF1P && $inputKeyAlgo === XMLSecurityKey::RSA_1_5) { + $inputKeyAlgo = XMLSecurityKey::RSA_OAEP_MGF1P; + } + + if ($inputKeyAlgo !== $symKeyInfoAlgo) { + throw new ValidationError( + 'Algorithm mismatch between input key and key used to encrypt ' . + ' the symmetric key for the message. Key was: ' . + var_export($inputKeyAlgo, true) . '; message was: ' . + var_export($symKeyInfoAlgo, true), + ValidationError::KEY_ALGORITHM_ERROR + ); + } + + $encKey = $symmetricKeyInfo->encryptedCtx; + $symmetricKeyInfo->key = $inputKey->key; + $keySize = $symmetricKey->getSymmetricKeySize(); + if ($keySize === null) { + // To protect against "key oracle" attacks + throw new ValidationError( + 'Unknown key size for encryption algorithm: ' . var_export($symmetricKey->type, true), + ValidationError::KEY_ALGORITHM_ERROR + ); + } + + $key = $encKey->decryptKey($symmetricKeyInfo); + if (strlen($key) != $keySize) { + $encryptedKey = $encKey->getCipherValue(); + $pkey = openssl_pkey_get_details($symmetricKeyInfo->key); + $pkey = sha1(serialize($pkey), true); + $key = sha1($encryptedKey . $pkey, true); + + /* Make sure that the key has the correct length. */ + if (strlen($key) > $keySize) { + $key = substr($key, 0, $keySize); + } elseif (strlen($key) < $keySize) { + $key = str_pad($key, $keySize); + } + } + $symmetricKey->loadKey($key); + } else { + $symKeyAlgo = $symmetricKey->getAlgorithm(); + if ($inputKeyAlgo !== $symKeyAlgo) { + throw new ValidationError( + 'Algorithm mismatch between input key and key in message. ' . + 'Key was: ' . var_export($inputKeyAlgo, true) . '; message was: ' . + var_export($symKeyAlgo, true), + ValidationError::KEY_ALGORITHM_ERROR + ); + } + $symmetricKey = $inputKey; + } + + $decrypted = $enc->decryptNode($symmetricKey, false); + + $xml = ''.$decrypted.''; + $newDoc = new DOMDocument(); + if ($formatOutput) { + $newDoc->preserveWhiteSpace = false; + $newDoc->formatOutput = true; + } + $newDoc = self::loadXML($newDoc, $xml); + if (!$newDoc) { + throw new ValidationError( + 'Failed to parse decrypted XML.', + ValidationError::INVALID_XML_FORMAT + ); + } + + $decryptedElement = $newDoc->firstChild->firstChild; + if ($decryptedElement === null) { + throw new ValidationError( + 'Missing encrypted element.', + ValidationError::MISSING_ENCRYPTED_ELEMENT + ); + } + + return $decryptedElement; + } + + /** + * Converts a XMLSecurityKey to the correct algorithm. + * + * @param XMLSecurityKey $key The key. + * @param string $algorithm The desired algorithm. + * @param string $type Public or private key, defaults to public. + * + * @return XMLSecurityKey The new key. + * + * @throws Exception + */ + public static function castKey(XMLSecurityKey $key, $algorithm, $type = 'public') + { + assert(is_string($algorithm)); + assert($type === 'public' || $type === 'private'); + + // do nothing if algorithm is already the type of the key + if ($key->type === $algorithm) { + return $key; + } + + if (!Utils::isSupportedSigningAlgorithm($algorithm)) { + throw new Exception('Unsupported signing algorithm.'); + } + + $keyInfo = openssl_pkey_get_details($key->key); + if ($keyInfo === false) { + throw new Exception('Unable to get key details from XMLSecurityKey.'); + } + if (!isset($keyInfo['key'])) { + throw new Exception('Missing key in public key details.'); + } + $newKey = new XMLSecurityKey($algorithm, array('type'=>$type)); + $newKey->loadKey($keyInfo['key']); + return $newKey; + } + + /** + * @param $algorithm + * + * @return bool + */ + public static function isSupportedSigningAlgorithm($algorithm) + { + return in_array( + $algorithm, + array( + XMLSecurityKey::RSA_1_5, + XMLSecurityKey::RSA_SHA1, + XMLSecurityKey::RSA_SHA256, + XMLSecurityKey::RSA_SHA384, + XMLSecurityKey::RSA_SHA512 + ) + ); + } + + /** + * Adds signature key and senders certificate to an element (Message or Assertion). + * + * @param string|DOMDocument $xml The element we should sign + * @param string $key The private key + * @param string $cert The public + * @param string $signAlgorithm Signature algorithm method + * @param string $digestAlgorithm Digest algorithm method + * + * @return string + * + * @throws Exception + */ + public static function addSign($xml, $key, $cert, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $digestAlgorithm = XMLSecurityDSig::SHA256) + { + if ($xml instanceof DOMDocument) { + $dom = $xml; + } else { + $dom = new DOMDocument(); + $dom = self::loadXML($dom, $xml); + if (!$dom) { + throw new Exception('Error parsing xml string'); + } + } + + /* Load the private key. */ + $objKey = new XMLSecurityKey($signAlgorithm, array('type' => 'private')); + $objKey->loadKey($key, false); + + /* Get the EntityDescriptor node we should sign. */ + $rootNode = $dom->firstChild; + + /* Sign the metadata with our private key. */ + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + + $objXMLSecDSig->addReferenceList( + array($rootNode), + $digestAlgorithm, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), + array('id_name' => 'ID') + ); + + $objXMLSecDSig->sign($objKey); + + /* Add the certificate to the signature. */ + $objXMLSecDSig->add509Cert($cert, true); + + $insertBefore = $rootNode->firstChild; + $messageTypes = array('AuthnRequest', 'Response', 'LogoutRequest','LogoutResponse'); + if (in_array($rootNode->localName, $messageTypes)) { + $issuerNodes = self::query($dom, '/'.$rootNode->tagName.'/saml:Issuer'); + if ($issuerNodes->length == 1) { + $insertBefore = $issuerNodes->item(0)->nextSibling; + } + } + + /* Add the signature. */ + $objXMLSecDSig->insertSignature($rootNode, $insertBefore); + + /* Return the DOM tree as a string. */ + $signedxml = $dom->saveXML(); + + return $signedxml; + } + + /** + * Validates a signature (Message or Assertion). + * + * @param string|\DomNode $xml The element we should validate + * @param string|null $cert The public cert + * @param string|null $fingerprint The fingerprint of the public cert + * @param string|null $fingerprintalg The algorithm used to get the fingerprint + * @param string|null $xpath The xpath of the signed element + * @param array|null $multiCerts Multiple public certs + * + * @return bool + * + * @throws Exception + */ + public static function validateSign($xml, $cert = null, $fingerprint = null, $fingerprintalg = 'sha1', $xpath = null, $multiCerts = null) + { + if ($xml instanceof DOMDocument) { + $dom = clone $xml; + } else if ($xml instanceof DOMElement) { + $dom = clone $xml->ownerDocument; + } else { + $dom = new DOMDocument(); + $dom = self::loadXML($dom, $xml); + } + + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->idKeys = array('ID'); + + if ($xpath) { + $nodeset = Utils::query($dom, $xpath); + $objDSig = $nodeset->item(0); + $objXMLSecDSig->sigNode = $objDSig; + } else { + $objDSig = $objXMLSecDSig->locateSignature($dom); + } + + if (!$objDSig) { + throw new Exception('Cannot locate Signature Node'); + } + + $objKey = $objXMLSecDSig->locateKey(); + if (!$objKey) { + throw new Exception('We have no idea about the key'); + } + + if (!Utils::isSupportedSigningAlgorithm($objKey->type)) { + throw new Exception('Unsupported signing algorithm.'); + } + + $objXMLSecDSig->canonicalizeSignedInfo(); + + try { + $retVal = $objXMLSecDSig->validateReference(); + } catch (Exception $e) { + throw $e; + } + + XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig); + + if (!empty($multiCerts)) { + // If multiple certs are provided, I may ignore $cert and + // $fingerprint provided by the method and just check the + // certs on the array + $fingerprint = null; + } else { + // else I add the cert to the array in order to check + // validate signatures with it and the with it and the + // $fingerprint value + $multiCerts = array($cert); + } + + $valid = false; + foreach ($multiCerts as $cert) { + if (!empty($cert)) { + $objKey->loadKey($cert, false, true); + if ($objXMLSecDSig->verify($objKey) === 1) { + $valid = true; + break; + } + } else { + if (!empty($fingerprint)) { + $domCert = $objKey->getX509Certificate(); + $domCertFingerprint = Utils::calculateX509Fingerprint($domCert, $fingerprintalg); + if (Utils::formatFingerPrint($fingerprint) == $domCertFingerprint) { + $objKey->loadKey($domCert, false, true); + if ($objXMLSecDSig->verify($objKey) === 1) { + $valid = true; + break; + } + } + } + } + } + return $valid; + } + + /** + * Validates a binary signature + * + * @param string $messageType Type of SAML Message + * @param array $getData HTTP GET array + * @param array $idpData IdP setting data + * @param bool $retrieveParametersFromServer Indicates where to get the values in order to validate the Sign, from getData or from $_SERVER + * + * @return bool + * + * @throws Exception + */ + public static function validateBinarySign($messageType, $getData, $idpData, $retrieveParametersFromServer = false) + { + if (!isset($getData['SigAlg'])) { + $signAlg = XMLSecurityKey::RSA_SHA1; + } else { + $signAlg = $getData['SigAlg']; + } + + if ($retrieveParametersFromServer) { + $signedQuery = $messageType.'='.Utils::extractOriginalQueryParam($messageType); + if (isset($getData['RelayState'])) { + $signedQuery .= '&RelayState='.Utils::extractOriginalQueryParam('RelayState'); + } + $signedQuery .= '&SigAlg='.Utils::extractOriginalQueryParam('SigAlg'); + } else { + $signedQuery = $messageType.'='.urlencode($getData[$messageType]); + if (isset($getData['RelayState'])) { + $signedQuery .= '&RelayState='.urlencode($getData['RelayState']); + } + $signedQuery .= '&SigAlg='.urlencode($signAlg); + } + + if ($messageType == "SAMLRequest") { + $strMessageType = "Logout Request"; + } else { + $strMessageType = "Logout Response"; + } + $existsMultiX509Sign = isset($idpData['x509certMulti']) && isset($idpData['x509certMulti']['signing']) && !empty($idpData['x509certMulti']['signing']); + if ((!isset($idpData['x509cert']) || empty($idpData['x509cert'])) && !$existsMultiX509Sign) { + throw new Error( + "In order to validate the sign on the ".$strMessageType.", the x509cert of the IdP is required", + Error::CERT_NOT_FOUND + ); + } + + if ($existsMultiX509Sign) { + $multiCerts = $idpData['x509certMulti']['signing']; + } else { + $multiCerts = array($idpData['x509cert']); + } + + $signatureValid = false; + foreach ($multiCerts as $cert) { + $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type' => 'public')); + $objKey->loadKey($cert, false, true); + + if ($signAlg != XMLSecurityKey::RSA_SHA1) { + try { + $objKey = Utils::castKey($objKey, $signAlg, 'public'); + } catch (Exception $e) { + $ex = new ValidationError( + "Invalid signAlg in the recieved ".$strMessageType, + ValidationError::INVALID_SIGNATURE + ); + if (count($multiCerts) == 1) { + throw $ex; + } + } + } + + if ($objKey->verifySignature($signedQuery, base64_decode($getData['Signature'])) === 1) { + $signatureValid = true; + break; + } + } + return $signatureValid; + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/ValidationError.php b/saml/vendor/onelogin/php-saml/src/Saml2/ValidationError.php new file mode 100644 index 00000000..889f531c --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/ValidationError.php @@ -0,0 +1,100 @@ + + * @license MIT https://github.com/onelogin/php-saml/blob/master/LICENSE + * @link https://github.com/onelogin/php-saml + */ + +namespace OneLogin\Saml2; + +use Exception; + +/** + * ValidationError class of OneLogin PHP Toolkit + * + * This class implements another custom Exception handler, + * related to exceptions that happens during validation process. + */ +class ValidationError extends Exception +{ + // Validation Errors + const UNSUPPORTED_SAML_VERSION = 0; + const MISSING_ID = 1; + const WRONG_NUMBER_OF_ASSERTIONS = 2; + const MISSING_STATUS = 3; + const MISSING_STATUS_CODE = 4; + const STATUS_CODE_IS_NOT_SUCCESS = 5; + const WRONG_SIGNED_ELEMENT = 6; + const ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7; + const DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8; + const INVALID_SIGNED_ELEMENT = 9; + const DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10; + const UNEXPECTED_SIGNED_ELEMENTS = 11; + const WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12; + const WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13; + const INVALID_XML_FORMAT = 14; + const WRONG_INRESPONSETO = 15; + const NO_ENCRYPTED_ASSERTION = 16; + const NO_ENCRYPTED_NAMEID = 17; + const MISSING_CONDITIONS = 18; + const ASSERTION_TOO_EARLY = 19; + const ASSERTION_EXPIRED = 20; + const WRONG_NUMBER_OF_AUTHSTATEMENTS = 21; + const NO_ATTRIBUTESTATEMENT = 22; + const ENCRYPTED_ATTRIBUTES = 23; + const WRONG_DESTINATION = 24; + const EMPTY_DESTINATION = 25; + const WRONG_AUDIENCE = 26; + const ISSUER_MULTIPLE_IN_RESPONSE = 27; + const ISSUER_NOT_FOUND_IN_ASSERTION = 28; + const WRONG_ISSUER = 29; + const SESSION_EXPIRED = 30; + const WRONG_SUBJECTCONFIRMATION = 31; + const NO_SIGNED_MESSAGE = 32; + const NO_SIGNED_ASSERTION = 33; + const NO_SIGNATURE_FOUND = 34; + const KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35; + const CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36; + const UNSUPPORTED_RETRIEVAL_METHOD = 37; + const NO_NAMEID = 38; + const EMPTY_NAMEID = 39; + const SP_NAME_QUALIFIER_NAME_MISMATCH = 40; + const DUPLICATED_ATTRIBUTE_NAME_FOUND = 41; + const INVALID_SIGNATURE = 42; + const WRONG_NUMBER_OF_SIGNATURES = 43; + const RESPONSE_EXPIRED = 44; + const UNEXPECTED_REFERENCE = 45; + const NOT_SUPPORTED = 46; + const KEY_ALGORITHM_ERROR = 47; + const MISSING_ENCRYPTED_ELEMENT = 48; + + + /** + * Constructor + * + * @param string $msg Describes the error. + * @param int $code The code error (defined in the error class). + * @param array|null $args Arguments used in the message that describes the error. + */ + public function __construct($msg, $code = 0, $args = array()) + { + assert(is_string($msg)); + assert(is_int($code)); + + if (!isset($args)) { + $args = array(); + } + $params = array_merge(array($msg), $args); + $message = call_user_func_array('sprintf', $params); + + parent::__construct($message, $code); + } +} diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-assertion-2.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-assertion-2.0.xsd new file mode 100644 index 00000000..2b2f7b80 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-assertion-2.0.xsd @@ -0,0 +1,283 @@ + + + + + + + Document identifier: saml-schema-assertion-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New assertion schema for SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-2.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-2.0.xsd new file mode 100644 index 00000000..e4754faf --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-2.0.xsd @@ -0,0 +1,23 @@ + + + + + + Document identifier: saml-schema-authn-context-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + New core authentication context schema for SAML V2.0. + This is just an include of all types from the schema + referred to in the include statement below. + + + + + + \ No newline at end of file diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-types-2.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-types-2.0.xsd new file mode 100644 index 00000000..8513959a --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-authn-context-types-2.0.xsd @@ -0,0 +1,821 @@ + + + + + + Document identifier: saml-schema-authn-context-types-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + New core authentication context schema types for SAML V2.0. + + + + + + + A particular assertion on an identity + provider's part with respect to the authentication + context associated with an authentication assertion. + + + + + + + + Refers to those characteristics that describe the + processes and mechanisms + the Authentication Authority uses to initially create + an association between a Principal + and the identity (or name) by which the Principal will + be known + + + + + + + + This element indicates that identification has been + performed in a physical + face-to-face meeting with the principal and not in an + online manner. + + + + + + + + + + + + + + + + + + + + Refers to those characterstics that describe how the + 'secret' (the knowledge or possession + of which allows the Principal to authenticate to the + Authentication Authority) is kept secure + + + + + + + + This element indicates the types and strengths of + facilities + of a UA used to protect a shared secret key from + unauthorized access and/or use. + + + + + + + + This element indicates the types and strengths of + facilities + of a UA used to protect a private key from + unauthorized access and/or use. + + + + + + + The actions that must be performed + before the private key can be used. + + + + + + Whether or not the private key is shared + with the certificate authority. + + + + + + + In which medium is the key stored. + memory - the key is stored in memory. + smartcard - the key is stored in a smartcard. + token - the key is stored in a hardware token. + MobileDevice - the key is stored in a mobile device. + MobileAuthCard - the key is stored in a mobile + authentication card. + + + + + + + + + + + This element indicates that a password (or passphrase) + has been used to + authenticate the Principal to a remote system. + + + + + + + + This element indicates that a Pin (Personal + Identification Number) has been used to authenticate the Principal to + some local system in order to activate a key. + + + + + + + + This element indicates that a hardware or software + token is used + as a method of identifying the Principal. + + + + + + + + This element indicates that a time synchronization + token is used to identify the Principal. hardware - + the time synchonization + token has been implemented in hardware. software - the + time synchronization + token has been implemented in software. SeedLength - + the length, in bits, of the + random seed used in the time synchronization token. + + + + + + + + This element indicates that a smartcard is used to + identity the Principal. + + + + + + + + This element indicates the minimum and/or maximum + ASCII length of the password which is enforced (by the UA or the + IdP). In other words, this is the minimum and/or maximum number of + ASCII characters required to represent a valid password. + min - the minimum number of ASCII characters required + in a valid password, as enforced by the UA or the IdP. + max - the maximum number of ASCII characters required + in a valid password, as enforced by the UA or the IdP. + + + + + + + + This element indicates the length of time for which an + PIN-based authentication is valid. + + + + + + + + Indicates whether the password was chosen by the + Principal or auto-supplied by the Authentication Authority. + principalchosen - the Principal is allowed to choose + the value of the password. This is true even if + the initial password is chosen at random by the UA or + the IdP and the Principal is then free to change + the password. + automatic - the password is chosen by the UA or the + IdP to be cryptographically strong in some sense, + or to satisfy certain password rules, and that the + Principal is not free to change it or to choose a new password. + + + + + + + + + + + + + + + + + + + Refers to those characteristics that define the + mechanisms by which the Principal authenticates to the Authentication + Authority. + + + + + + + + The method that a Principal employs to perform + authentication to local system components. + + + + + + + + The method applied to validate a principal's + authentication across a network + + + + + + + + Supports Authenticators with nested combinations of + additional complexity. + + + + + + + + Indicates that the Principal has been strongly + authenticated in a previous session during which the IdP has set a + cookie in the UA. During the present session the Principal has only + been authenticated by the UA returning the cookie to the IdP. + + + + + + + + Rather like PreviousSession but using stronger + security. A secret that was established in a previous session with + the Authentication Authority has been cached by the local system and + is now re-used (e.g. a Master Secret is used to derive new session + keys in TLS, SSL, WTLS). + + + + + + + + This element indicates that the Principal has been + authenticated by a zero knowledge technique as specified in ISO/IEC + 9798-5. + + + + + + + + + + This element indicates that the Principal has been + authenticated by a challenge-response protocol utilizing shared secret + keys and symmetric cryptography. + + + + + + + + + + + + This element indicates that the Principal has been + authenticated by a mechanism which involves the Principal computing a + digital signature over at least challenge data provided by the IdP. + + + + + + + + The local system has a private key but it is used + in decryption mode, rather than signature mode. For example, the + Authentication Authority generates a secret and encrypts it using the + local system's public key: the local system then proves it has + decrypted the secret. + + + + + + + + The local system has a private key and uses it for + shared secret key agreement with the Authentication Authority (e.g. + via Diffie Helman). + + + + + + + + + + + + + + + This element indicates that the Principal has been + authenticated through connection from a particular IP address. + + + + + + + + The local system and Authentication Authority + share a secret key. The local system uses this to encrypt a + randomised string to pass to the Authentication Authority. + + + + + + + + The protocol across which Authenticator information is + transferred to an Authentication Authority verifier. + + + + + + + + This element indicates that the Authenticator has been + transmitted using bare HTTP utilizing no additional security + protocols. + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechanism protected by an IPSEC session. + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechanism protected by a WTLS session. + + + + + + + + This element indicates that the Authenticator has been + transmitted solely across a mobile network using no additional + security mechanism. + + + + + + + + + + + This element indicates that the Authenticator has been + transmitted using a transport mechnanism protected by an SSL or TLS + session. + + + + + + + + + + + + Refers to those characteristics that describe + procedural security controls employed by the Authentication Authority. + + + + + + + + + + + + Provides a mechanism for linking to external (likely + human readable) documents in which additional business agreements, + (e.g. liability constraints, obligations, etc) can be placed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This attribute indicates whether or not the + Identification mechanisms allow the actions of the Principal to be + linked to an actual end user. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This element indicates that the Key Activation Limit is + defined as a specific duration of time. + + + + + + + + This element indicates that the Key Activation Limit is + defined as a number of usages. + + + + + + + + This element indicates that the Key Activation Limit is + the session. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-metadata-2.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-metadata-2.0.xsd new file mode 100644 index 00000000..86e58f9b --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-metadata-2.0.xsd @@ -0,0 +1,336 @@ + + + + + + + + + Document identifier: saml-schema-metadata-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V2.0 (March, 2005): + Schema for SAML metadata, first published in SAML 2.0. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-protocol-2.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-protocol-2.0.xsd new file mode 100644 index 00000000..7fa6f489 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/saml-schema-protocol-2.0.xsd @@ -0,0 +1,302 @@ + + + + + + + Document identifier: saml-schema-protocol-2.0 + Location: http://docs.oasis-open.org/security/saml/v2.0/ + Revision history: + V1.0 (November, 2002): + Initial Standard Schema. + V1.1 (September, 2003): + Updates within the same V1.0 namespace. + V2.0 (March, 2005): + New protocol schema based in a SAML V2.0 namespace. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-metadata-attr.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-metadata-attr.xsd new file mode 100644 index 00000000..f23e462a --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-metadata-attr.xsd @@ -0,0 +1,35 @@ + + + + + + Document title: SAML V2.0 Metadata Extention for Entity Attributes Schema + Document identifier: sstc-metadata-attr.xsd + Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security + Revision history: + V1.0 (November 2008): + Initial version. + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-attribute-ext.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-attribute-ext.xsd new file mode 100644 index 00000000..ad309c14 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-attribute-ext.xsd @@ -0,0 +1,25 @@ + + + + + + Document title: SAML V2.0 Attribute Extension Schema + Document identifier: sstc-saml-attribute-ext.xsd + Location: http://www.oasis-open.org/committees/documents.php?wg_abbrev=security + Revision history: + V1.0 (October 2008): + Initial version. + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd new file mode 100644 index 00000000..3236ffcd --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-algsupport-v1.0.xsd @@ -0,0 +1,41 @@ + + + + + + Document title: Metadata Extension Schema for SAML V2.0 Metadata Profile for Algorithm Support Version 1.0 + Document identifier: sstc-saml-metadata-algsupport.xsd + Location: http://docs.oasis-open.org/security/saml/Post2.0/ + Revision history: + V1.0 (June 2010): + Initial version. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd new file mode 100644 index 00000000..de0b754a --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/sstc-saml-metadata-ui-v1.0.xsd @@ -0,0 +1,89 @@ + + + + + + Document title: Metadata Extension Schema for SAML V2.0 Metadata Extensions for Login and Discovery User Interface Version 1.0 + Document identifier: sstc-saml-metadata-ui-v1.0.xsd + Location: http://docs.oasis-open.org/security/saml/Post2.0/ + Revision history: + 16 November 2010: + Added Keywords element/type. + 01 November 2010 + Changed filename. + September 2010: + Initial version. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xenc-schema.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xenc-schema.xsd new file mode 100644 index 00000000..d6d79103 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xenc-schema.xsd @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xml.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xml.xsd new file mode 100644 index 00000000..aea7d0db --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     
+

+ or +

+
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xmldsig-core-schema.xsd b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xmldsig-core-schema.xsd new file mode 100644 index 00000000..6f5acc75 --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/schemas/xmldsig-core-schema.xsd @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/saml/vendor/onelogin/php-saml/src/Saml2/version.json b/saml/vendor/onelogin/php-saml/src/Saml2/version.json new file mode 100644 index 00000000..220f250a --- /dev/null +++ b/saml/vendor/onelogin/php-saml/src/Saml2/version.json @@ -0,0 +1,7 @@ +{ + "php-saml": { + "version": "4.0.0", + "released": "02/03/2021" + } +} + diff --git a/saml/vendor/php-saml b/saml/vendor/php-saml new file mode 160000 index 00000000..f30f5062 --- /dev/null +++ b/saml/vendor/php-saml @@ -0,0 +1 @@ +Subproject commit f30f5062f3653c4d2082892d207f4dc3e577d979 diff --git a/saml/vendor/robrichards/xmlseclibs/CHANGELOG.txt b/saml/vendor/robrichards/xmlseclibs/CHANGELOG.txt new file mode 100644 index 00000000..351b1042 --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/CHANGELOG.txt @@ -0,0 +1,228 @@ +xmlseclibs.php +||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +05, Sep 2020, 3.1.1 +Features: +- Support OAEP (iggyvolz) + +Bug Fixes: +- Fix AES128 (iggyvolz) + +Improvements: +- Fix tests for older PHP + +22, Apr 2020, 3.1.0 +Features: +- Support AES-GCM. Requires PHP 7.1. (François Kooman) + +Improvements: +- Fix Travis tests for older PHP versions. +- Use DOMElement interface to fix some IDEs reporting documentation errors + +Bug Fixes: +- FIX missing InclusiveNamespaces PrefixList from Java + Apache WSS4J. (njake) + +06, Nov 2019, 3.0.4 +Security Improvements: +- Insure only a single SignedInfo element exists within a signature during + verification. Refs CVE-2019-3465. +Bug Fixes: +- Fix variable casing. + +15, Nov 2018, 3.0.3 +Bug Fixes: +- Fix casing of class name. (Willem Stuursma-Ruwen) +- Fix Xpath casing. (Tim van Dijen) + +Improvements: +- Make PCRE2 compliant. (Stefan Winter) +- Add PHP 7.3 support. (Stefan Winter) + +27, Sep 2018, 3.0.2 +Security Improvements: +- OpenSSL is now a requirement rather than suggestion. (Slaven Bacelic) +- Filter input to avoid XPath injection. (Jaime Pérez) + +Bug Fixes: +- Fix missing parentheses (Tim van Dijen) + +Improvements: +- Use strict comparison operator to compare digest values. (Jaime Pérez) +- Remove call to file_get_contents that doesn't even work. (Jaime Pérez) +- Document potentially dangerous return value behaviour. (Thijs Kinkhorst) + +31, Aug 2017, 3.0.1 +Bug Fixes: +- Fixed missing () in function call. (Dennis Væversted) + +Improvements: +- Add OneLogin to supported software. +- Add .gitattributes to remove unneeded files. (Filippo Tessarotto) +- Fix bug in example code. (Dan Church) +- Travis: add PHP 7.1, move hhvm to allowed failures. (Thijs Kinkhorst) +- Drop failing extract-win-cert test (Thijs Kinkhorst). (Thijs Kinkhorst) +- Add comments to warn about return values of verify(). (Thijs Kinkhorst) +- Fix tests to properly check return code of verify(). (Thijs Kinkhorst) +- Restore support for PHP >= 5.4. (Jaime Pérez) + +25, May 2017, 3.0.0 +Improvements: +- Remove use of mcrypt (skymeyer) + +08, Sep 2016, 2.0.1 +Bug Fixes: +- Strip whitespace characters when parsing X509Certificate. fixes #84 + (klemen.bratec) +- Certificate 'subject' values can be arrays. fixes #80 (Andreas Stangl) +- HHVM signing node with ID attribute w/out namespace regenerates ID value. + fixes #88 (Milos Tomic) + +Improvements: +- Fix typos and add some PHPDoc Blocks. (gfaust-qb) +- Update lightSAML link. (Milos Tomic) +- Update copyright dates. + +31, Jul 2015, 2.0.0 +Features: +- Namespace support. Classes now in the RobRichards\XMLSecLibs\ namespace. + +Improvements: +- Dropped support for PHP 5.2 + +31, Jul 2015, 1.4.1 +Bug Fixes: +- Allow for large digest values that may have line breaks. fixes #62 + +Features: +- Support for locating specific signature when multiple exist in + document. (griga3k) + +Improvements: +- Add optional argument to XMLSecurityDSig to define the prefix to be used, + also allowing for null to use no prefix, for the dsig namespace. fixes #13 +- Code cleanup +- Depreciated XMLSecurityDSig::generate_GUID for XMLSecurityDSig::generateGUID + +23, Jun 2015, 1.4.0 +Features: +- Support for PSR-0 standard. +- Support for X509SubjectName. (Milos Tomic) +- Add HMAC-SHA1 support. + +Improvements: +- Add how to install to README. (Bernardo Vieira da Silva) +- Code cleanup. (Jaime Pérez) +- Normalilze tests. (Hidde Wieringa) +- Add basic usage to README. (Hidde Wieringa) + +21, May 2015, 1.3.2 +Bug Fixes: +- Fix Undefined variable notice. (dpieper85) +- Fix typo when setting MimeType attribute. (Eugene OZ) +- Fix validateReference() with enveloping signatures + +Features: +- canonicalizeData performance optimization. (Jaime Pérez) +- Add composer support (Maks3w) + +19, Jun 2013, 1.3.1 +Features: +- return encrypted node from XMLSecEnc::encryptNode() when replace is set to + false. (Olav) +- Add support for RSA SHA384 and RSA_SHA512 and SHA384 digest. (Jaime PŽrez) +- Add options parameter to the add cert methods. +- Add optional issuerSerial creation with cert + +Bug Fixes: +- Fix persisted Id when namespaced. (Koen Thomeer) + +Improvements: +- Add LICENSE file +- Convert CHANGELOG.txt to UTF-8 + +26, Sep 2011, 1.3.0 +Features: +- Add param to append sig to node when signing. Fixes a problem when using + inclusive canonicalization to append a signature within a namespaced subtree. + ex. $objDSig->sign($objKey, $appendToNode); +- Add ability to encrypt by reference +- Add support for refences within an encrypted key +- Add thumbprint generation capability (XMLSecurityKey->getX509Thumbprint() and + XMLSecurityKey::getRawThumbprint($cert)) +- Return signature element node from XMLSecurityDSig::insertSignature() and + XMLSecurityDSig::appendSignature() methods +- Support for with simple URI Id reference. +- Add XMLSecurityKey::getSymmetricKeySize() method (Olav) +- Add XMLSecEnc::getCipherValue() method (Olav) +- Improve XMLSecurityKey:generateSessionKey() logic (Olav) + +Bug Fixes: +- Change split() to explode() as split is now depreciated +- ds:References using empty or simple URI Id reference should never include + comments in canonicalized data. +- Make sure that the elements in EncryptedData are emitted in the correct + sequence. + +11 Jan 2010, 1.2.2 +Features: +- Add support XPath support when creating signature. Provides support for + working with EBXML documents. +- Add reference option to force creation of URI attribute. For use + when adding a DOM Document where by default no URI attribute is added. +- Add support for RSA-SHA256 + +Bug Fixes: +- fix bug #5: createDOMDocumentFragment() in decryptNode when data is node + content (patch by Francois Wang) + + +08 Jul 2008, 1.2.1 +Features: +- Attempt to use mhash when hash extension is not present. (Alfredo Cubitos). +- Add fallback to built-in sha1 if both hash and mhash are not available and + throw error for other for other missing hashes. (patch by Olav Morken). +- Add getX509Certificate method to retrieve the x509 cert used for Key. + (patch by Olav Morken). +- Add getValidatedNodes method to retrieve the elements signed by the + signature. (patch by Olav Morken). +- Add insertSignature method for precision signature insertion. Merge + functionality from appendSignature in the process. (Olav Morken, Rob). +- Finally add some tests + +Bug Fixes: +- Fix canonicalization for Document node when using PHP < 5.2. +- Add padding for RSA_SHA1. (patch by Olav Morken). + + +27 Nov 2007, 1.2.0 +Features: +- New addReference/List option (overwrite). Boolean flag indicating if URI + value should be overwritten if already existing within document. + Default is TRUE to maintain BC. + +18 Nov 2007, 1.1.2 +Bug Fixes: +- Remove closing PHP tag to fix extra whitespace characters from being output + +11 Nov 2007, 1.1.1 +Features: +- Add getRefNodeID() and getRefIDs() methods missed in previous release. + Provide functionality to find URIs of existing reference nodes. + Required by simpleSAMLphp project + +Bug Fixes: +- Remove erroneous whitespace causing issues under certain circumastances. + +18 Oct 2007, 1.1.0 +Features: +- Enable creation of enveloping signature. This allows the creation of + managed information cards. +- Add addObject method for enveloping signatures. +- Add staticGet509XCerts method. Chained certificates within a PEM file can + now be added within the X509Data node. +- Add xpath support within transformations +- Add InclusiveNamespaces prefix list support within exclusive transformations. + +Bug Fixes: +- Initialize random number generator for mcrypt_create_iv. (Joan Cornadó). +- Fix an interoperability issue with .NET when encrypting data in CBC mode. + (Joan Cornadó). diff --git a/saml/vendor/robrichards/xmlseclibs/LICENSE b/saml/vendor/robrichards/xmlseclibs/LICENSE new file mode 100644 index 00000000..4fe5e5ff --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2007-2019, Robert Richards . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Robert Richards nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/saml/vendor/robrichards/xmlseclibs/README.md b/saml/vendor/robrichards/xmlseclibs/README.md new file mode 100644 index 00000000..a576080a --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/README.md @@ -0,0 +1,85 @@ +#xmlseclibs + +xmlseclibs is a library written in PHP for working with XML Encryption and Signatures. + +The author of xmlseclibs is Rob Richards. + +# Branches +Master is currently the only actively maintained branch. +* master/3.1: Added AES-GCM support requiring 7.1+ +* 3.0: Removes mcrypt usage requiring 5.4+ (5.6.24+ recommended for security reasons) +* 2.0: Contains namespace support requiring 5.3+ +* 1.4: Contains auto-loader support while also maintaining backwards compatiblity with the older 1.3 version using the xmlseclibs.php file. Supports PHP 5.2+ + +# Requirements + +xmlseclibs requires PHP version 5.4 or greater. **5.6.24+ recommended for security reasons** + + +## How to Install + +Install with [`composer.phar`](http://getcomposer.org). + +```sh +php composer.phar require "robrichards/xmlseclibs" +``` + + +## Use cases + +xmlseclibs is being used in many different software. + +* [SimpleSAMLPHP](https://github.com/simplesamlphp/simplesamlphp) +* [LightSAML](https://github.com/lightsaml/lightsaml) +* [OneLogin](https://github.com/onelogin/php-saml) + +## Basic usage + +The example below shows basic usage of xmlseclibs, with a SHA-256 signature. + +```php +use RobRichards\XMLSecLibs\XMLSecurityDSig; +use RobRichards\XMLSecLibs\XMLSecurityKey; + +// Load the XML to be signed +$doc = new DOMDocument(); +$doc->load('./path/to/file/tobesigned.xml'); + +// Create a new Security object +$objDSig = new XMLSecurityDSig(); +// Use the c14n exclusive canonicalization +$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); +// Sign using SHA-256 +$objDSig->addReference( + $doc, + XMLSecurityDSig::SHA256, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature') +); + +// Create a new (private) Security key +$objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type'=>'private')); +/* +If key has a passphrase, set it using +$objKey->passphrase = ''; +*/ +// Load the private key +$objKey->loadKey('./path/to/privatekey.pem', TRUE); + +// Sign the XML file +$objDSig->sign($objKey); + +// Add the associated public key to the signature +$objDSig->add509Cert(file_get_contents('./path/to/file/mycert.pem')); + +// Append the signature to the XML +$objDSig->appendSignature($doc->documentElement); +// Save the signed XML +$doc->save('./path/to/signed.xml'); +``` + +## How to Contribute + +* [Open Issues](https://github.com/robrichards/xmlseclibs/issues) +* [Open Pull Requests](https://github.com/robrichards/xmlseclibs/pulls) + +Mailing List: https://groups.google.com/forum/#!forum/xmlseclibs diff --git a/saml/vendor/robrichards/xmlseclibs/composer.json b/saml/vendor/robrichards/xmlseclibs/composer.json new file mode 100644 index 00000000..22ce7a3e --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/composer.json @@ -0,0 +1,21 @@ +{ + "name": "robrichards/xmlseclibs", + "description": "A PHP library for XML Security", + "license": "BSD-3-Clause", + "keywords": [ + "xml", + "xmldsig", + "signature", + "security" + ], + "homepage": "https://github.com/robrichards/xmlseclibs", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "require": { + "php": ">= 5.4", + "ext-openssl": "*" + } +} diff --git a/saml/vendor/robrichards/xmlseclibs/src/Utils/XPath.php b/saml/vendor/robrichards/xmlseclibs/src/Utils/XPath.php new file mode 100644 index 00000000..8cdc48e1 --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/src/Utils/XPath.php @@ -0,0 +1,44 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards + * @copyright 2007-2020 Robert Richards + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +class XMLSecEnc +{ + const template = " + + + +"; + + const Element = 'http://www.w3.org/2001/04/xmlenc#Element'; + const Content = 'http://www.w3.org/2001/04/xmlenc#Content'; + const URI = 3; + const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#'; + + /** @var null|DOMDocument */ + private $encdoc = null; + + /** @var null|DOMNode */ + private $rawNode = null; + + /** @var null|string */ + public $type = null; + + /** @var null|DOMElement */ + public $encKey = null; + + /** @var array */ + private $references = array(); + + public function __construct() + { + $this->_resetTemplate(); + } + + private function _resetTemplate() + { + $this->encdoc = new DOMDocument(); + $this->encdoc->loadXML(self::template); + } + + /** + * @param string $name + * @param DOMNode $node + * @param string $type + * @throws Exception + */ + public function addReference($name, $node, $type) + { + if (! $node instanceOf DOMNode) { + throw new Exception('$node is not of type DOMNode'); + } + $curencdoc = $this->encdoc; + $this->_resetTemplate(); + $encdoc = $this->encdoc; + $this->encdoc = $curencdoc; + $refuri = XMLSecurityDSig::generateGUID(); + $element = $encdoc->documentElement; + $element->setAttribute("Id", $refuri); + $this->references[$name] = array("node" => $node, "type" => $type, "encnode" => $encdoc, "refuri" => $refuri); + } + + /** + * @param DOMNode $node + */ + public function setNode($node) + { + $this->rawNode = $node; + } + + /** + * Encrypt the selected node with the given key. + * + * @param XMLSecurityKey $objKey The encryption key and algorithm. + * @param bool $replace Whether the encrypted node should be replaced in the original tree. Default is true. + * @throws Exception + * + * @return DOMElement The -element. + */ + public function encryptNode($objKey, $replace = true) + { + $data = ''; + if (empty($this->rawNode)) { + throw new Exception('Node to encrypt has not been set'); + } + if (! $objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($this->encdoc); + $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue'); + $cipherValue = $objList->item(0); + if ($cipherValue == null) { + throw new Exception('Error locating CipherValue element within template'); + } + switch ($this->type) { + case (self::Element): + $data = $doc->saveXML($this->rawNode); + $this->encdoc->documentElement->setAttribute('Type', self::Element); + break; + case (self::Content): + $children = $this->rawNode->childNodes; + foreach ($children AS $child) { + $data .= $doc->saveXML($child); + } + $this->encdoc->documentElement->setAttribute('Type', self::Content); + break; + default: + throw new Exception('Type is currently not supported'); + } + + $encMethod = $this->encdoc->documentElement->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod')); + $encMethod->setAttribute('Algorithm', $objKey->getAlgorithm()); + $cipherValue->parentNode->parentNode->insertBefore($encMethod, $cipherValue->parentNode->parentNode->firstChild); + + $strEncrypt = base64_encode($objKey->encryptData($data)); + $value = $this->encdoc->createTextNode($strEncrypt); + $cipherValue->appendChild($value); + + if ($replace) { + switch ($this->type) { + case (self::Element): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $this->encdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + case (self::Content): + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); + while ($this->rawNode->firstChild) { + $this->rawNode->removeChild($this->rawNode->firstChild); + } + $this->rawNode->appendChild($importEnc); + return $importEnc; + } + } else { + return $this->encdoc->documentElement; + } + } + + /** + * @param XMLSecurityKey $objKey + * @throws Exception + */ + public function encryptReferences($objKey) + { + $curRawNode = $this->rawNode; + $curType = $this->type; + foreach ($this->references AS $name => $reference) { + $this->encdoc = $reference["encnode"]; + $this->rawNode = $reference["node"]; + $this->type = $reference["type"]; + try { + $encNode = $this->encryptNode($objKey); + $this->references[$name]["encnode"] = $encNode; + } catch (Exception $e) { + $this->rawNode = $curRawNode; + $this->type = $curType; + throw $e; + } + } + $this->rawNode = $curRawNode; + $this->type = $curType; + } + + /** + * Retrieve the CipherValue text from this encrypted node. + * + * @throws Exception + * @return string|null The Ciphervalue text, or null if no CipherValue is found. + */ + public function getCipherValue() + { + if (empty($this->rawNode)) { + throw new Exception('Node to decrypt has not been set'); + } + + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($doc); + $xPath->registerNamespace('xmlencr', self::XMLENCNS); + /* Only handles embedded content right now and not a reference */ + $query = "./xmlencr:CipherData/xmlencr:CipherValue"; + $nodeset = $xPath->query($query, $this->rawNode); + $node = $nodeset->item(0); + + if (!$node) { + return null; + } + + return base64_decode($node->nodeValue); + } + + /** + * Decrypt this encrypted node. + * + * The behaviour of this function depends on the value of $replace. + * If $replace is false, we will return the decrypted data as a string. + * If $replace is true, we will insert the decrypted element(s) into the + * document, and return the decrypted element(s). + * + * @param XMLSecurityKey $objKey The decryption key that should be used when decrypting the node. + * @param boolean $replace Whether we should replace the encrypted node in the XML document with the decrypted data. The default is true. + * + * @return string|DOMElement The decrypted data. + */ + public function decryptNode($objKey, $replace=true) + { + if (! $objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + + $encryptedData = $this->getCipherValue(); + if ($encryptedData) { + $decrypted = $objKey->decryptData($encryptedData); + if ($replace) { + switch ($this->type) { + case (self::Element): + $newdoc = new DOMDocument(); + $newdoc->loadXML($decrypted); + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $newdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, true); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + case (self::Content): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + $doc = $this->rawNode; + } else { + $doc = $this->rawNode->ownerDocument; + } + $newFrag = $doc->createDocumentFragment(); + $newFrag->appendXML($decrypted); + $parent = $this->rawNode->parentNode; + $parent->replaceChild($newFrag, $this->rawNode); + return $parent; + default: + return $decrypted; + } + } else { + return $decrypted; + } + } else { + throw new Exception("Cannot locate encrypted data"); + } + } + + /** + * Encrypt the XMLSecurityKey + * + * @param XMLSecurityKey $srcKey + * @param XMLSecurityKey $rawKey + * @param bool $append + * @throws Exception + */ + public function encryptKey($srcKey, $rawKey, $append=true) + { + if ((! $srcKey instanceof XMLSecurityKey) || (! $rawKey instanceof XMLSecurityKey)) { + throw new Exception('Invalid Key'); + } + $strEncKey = base64_encode($srcKey->encryptData($rawKey->key)); + $root = $this->encdoc->documentElement; + $encKey = $this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptedKey'); + if ($append) { + $keyInfo = $root->insertBefore($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'), $root->firstChild); + $keyInfo->appendChild($encKey); + } else { + $this->encKey = $encKey; + } + $encMethod = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:EncryptionMethod')); + $encMethod->setAttribute('Algorithm', $srcKey->getAlgorith()); + if (! empty($srcKey->name)) { + $keyInfo = $encKey->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo')); + $keyInfo->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name)); + } + $cipherData = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherData')); + $cipherData->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:CipherValue', $strEncKey)); + if (is_array($this->references) && count($this->references) > 0) { + $refList = $encKey->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:ReferenceList')); + foreach ($this->references AS $name => $reference) { + $refuri = $reference["refuri"]; + $dataRef = $refList->appendChild($this->encdoc->createElementNS(self::XMLENCNS, 'xenc:DataReference')); + $dataRef->setAttribute("URI", '#' . $refuri); + } + } + return; + } + + /** + * @param XMLSecurityKey $encKey + * @return DOMElement|string + * @throws Exception + */ + public function decryptKey($encKey) + { + if (! $encKey->isEncrypted) { + throw new Exception("Key is not Encrypted"); + } + if (empty($encKey->key)) { + throw new Exception("Key is missing data to perform the decryption"); + } + return $this->decryptNode($encKey, false); + } + + /** + * @param DOMDocument $element + * @return DOMNode|null + */ + public function locateEncryptedData($element) + { + if ($element instanceof DOMDocument) { + $doc = $element; + } else { + $doc = $element->ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $query = "//*[local-name()='EncryptedData' and namespace-uri()='".self::XMLENCNS."']"; + $nodeset = $xpath->query($query); + return $nodeset->item(0); + } + return null; + } + + /** + * Returns the key from the DOM + * @param null|DOMNode $node + * @return null|XMLSecurityKey + */ + public function locateKey($node=null) + { + if (empty($node)) { + $node = $this->rawNode; + } + if (! $node instanceof DOMNode) { + return null; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', self::XMLENCNS); + $query = ".//xmlsecenc:EncryptionMethod"; + $nodeset = $xpath->query($query, $node); + if ($encmeth = $nodeset->item(0)) { + $attrAlgorithm = $encmeth->getAttribute("Algorithm"); + try { + $objKey = new XMLSecurityKey($attrAlgorithm, array('type' => 'private')); + } catch (Exception $e) { + return null; + } + return $objKey; + } + } + return null; + } + + /** + * @param null|XMLSecurityKey $objBaseKey + * @param null|DOMNode $node + * @return null|XMLSecurityKey + * @throws Exception + */ + public static function staticLocateKeyInfo($objBaseKey=null, $node=null) + { + if (empty($node) || (! $node instanceof DOMNode)) { + return null; + } + $doc = $node->ownerDocument; + if (!$doc) { + return null; + } + + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', self::XMLENCNS); + $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "./xmlsecdsig:KeyInfo"; + $nodeset = $xpath->query($query, $node); + $encmeth = $nodeset->item(0); + if (!$encmeth) { + /* No KeyInfo in EncryptedData / EncryptedKey. */ + return $objBaseKey; + } + + foreach ($encmeth->childNodes AS $child) { + switch ($child->localName) { + case 'KeyName': + if (! empty($objBaseKey)) { + $objBaseKey->name = $child->nodeValue; + } + break; + case 'KeyValue': + foreach ($child->childNodes AS $keyval) { + switch ($keyval->localName) { + case 'DSAKeyValue': + throw new Exception("DSAKeyValue currently not supported"); + case 'RSAKeyValue': + $modulus = null; + $exponent = null; + if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) { + $modulus = base64_decode($modulusNode->nodeValue); + } + if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) { + $exponent = base64_decode($exponentNode->nodeValue); + } + if (empty($modulus) || empty($exponent)) { + throw new Exception("Missing Modulus or Exponent"); + } + $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent); + $objBaseKey->loadKey($publicKey); + break; + } + } + break; + case 'RetrievalMethod': + $type = $child->getAttribute('Type'); + if ($type !== 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') { + /* Unsupported key type. */ + break; + } + $uri = $child->getAttribute('URI'); + if ($uri[0] !== '#') { + /* URI not a reference - unsupported. */ + break; + } + $id = substr($uri, 1); + + $query = '//xmlsecenc:EncryptedKey[@Id="'.XPath::filterAttrValue($id, XPath::DOUBLE_QUOTE).'"]'; + $keyElement = $xpath->query($query)->item(0); + if (!$keyElement) { + throw new Exception("Unable to locate EncryptedKey with @Id='$id'."); + } + + return XMLSecurityKey::fromEncryptedKeyElement($keyElement); + case 'EncryptedKey': + return XMLSecurityKey::fromEncryptedKeyElement($child); + case 'X509Data': + if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) { + if ($x509certNodes->length > 0) { + $x509cert = $x509certNodes->item(0)->textContent; + $x509cert = str_replace(array("\r", "\n", " "), "", $x509cert); + $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n"; + $objBaseKey->loadKey($x509cert, false, true); + } + } + break; + } + } + return $objBaseKey; + } + + /** + * @param null|XMLSecurityKey $objBaseKey + * @param null|DOMNode $node + * @return null|XMLSecurityKey + */ + public function locateKeyInfo($objBaseKey=null, $node=null) + { + if (empty($node)) { + $node = $this->rawNode; + } + return self::staticLocateKeyInfo($objBaseKey, $node); + } +} diff --git a/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityDSig.php b/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityDSig.php new file mode 100644 index 00000000..9986123e --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityDSig.php @@ -0,0 +1,1162 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards + * @copyright 2007-2020 Robert Richards + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +class XMLSecurityDSig +{ + const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; + const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; + const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; + const SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'; + const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'; + const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160'; + + const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'; + const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'; + + const template = ' + + + +'; + + const BASE_TEMPLATE = ' + + + +'; + + /** @var DOMElement|null */ + public $sigNode = null; + + /** @var array */ + public $idKeys = array(); + + /** @var array */ + public $idNS = array(); + + /** @var string|null */ + private $signedInfo = null; + + /** @var DomXPath|null */ + private $xPathCtx = null; + + /** @var string|null */ + private $canonicalMethod = null; + + /** @var string */ + private $prefix = ''; + + /** @var string */ + private $searchpfx = 'secdsig'; + + /** + * This variable contains an associative array of validated nodes. + * @var array|null + */ + private $validatedNodes = null; + + /** + * @param string $prefix + */ + public function __construct($prefix='ds') + { + $template = self::BASE_TEMPLATE; + if (! empty($prefix)) { + $this->prefix = $prefix.':'; + $search = array("ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + $query = ".//secdsig:Signature"; + $nodeset = $xpath->query($query, $objDoc); + $this->sigNode = $nodeset->item($pos); + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length > 1) { + throw new Exception("Invalid structure - Too many SignedInfo elements found"); + } + return $this->sigNode; + } + return null; + } + + /** + * @param string $name + * @param null|string $value + * @return DOMElement + */ + public function createNewSignNode($name, $value=null) + { + $doc = $this->sigNode->ownerDocument; + if (! is_null($value)) { + $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name, $value); + } else { + $node = $doc->createElementNS(self::XMLDSIGNS, $this->prefix.$name); + } + return $node; + } + + /** + * @param string $method + * @throws Exception + */ + public function setCanonicalMethod($method) + { + switch ($method) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $this->canonicalMethod = $method; + break; + default: + throw new Exception('Invalid Canonical Method'); + } + if ($xpath = $this->getXPathObj()) { + $query = './'.$this->searchpfx.':SignedInfo'; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sinfo = $nodeset->item(0)) { + $query = './'.$this->searchpfx.'CanonicalizationMethod'; + $nodeset = $xpath->query($query, $sinfo); + if (! ($canonNode = $nodeset->item(0))) { + $canonNode = $this->createNewSignNode('CanonicalizationMethod'); + $sinfo->insertBefore($canonNode, $sinfo->firstChild); + } + $canonNode->setAttribute('Algorithm', $this->canonicalMethod); + } + } + } + + /** + * @param DOMNode $node + * @param string $canonicalmethod + * @param null|array $arXPath + * @param null|array $prefixList + * @return string + */ + private function canonicalizeData($node, $canonicalmethod, $arXPath=null, $prefixList=null) + { + $exclusive = false; + $withComments = false; + switch ($canonicalmethod) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + $exclusive = false; + $withComments = false; + break; + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + $withComments = true; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + $exclusive = true; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $exclusive = true; + $withComments = true; + break; + } + + if (is_null($arXPath) && ($node instanceof DOMNode) && ($node->ownerDocument !== null) && $node->isSameNode($node->ownerDocument->documentElement)) { + /* Check for any PI or comments as they would have been excluded */ + $element = $node; + while ($refnode = $element->previousSibling) { + if ($refnode->nodeType == XML_PI_NODE || (($refnode->nodeType == XML_COMMENT_NODE) && $withComments)) { + break; + } + $element = $refnode; + } + if ($refnode == null) { + $node = $node->ownerDocument; + } + } + + return $node->C14N($exclusive, $withComments, $arXPath, $prefixList); + } + + /** + * @return null|string + */ + public function canonicalizeSignedInfo() + { + + $doc = $this->sigNode->ownerDocument; + $canonicalmethod = null; + if ($doc) { + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length > 1) { + throw new Exception("Invalid structure - Too many SignedInfo elements found"); + } + if ($signInfoNode = $nodeset->item(0)) { + $query = "./secdsig:CanonicalizationMethod"; + $nodeset = $xpath->query($query, $signInfoNode); + $prefixList = null; + if ($canonNode = $nodeset->item(0)) { + $canonicalmethod = $canonNode->getAttribute('Algorithm'); + foreach ($canonNode->childNodes as $node) + { + if ($node->localName == 'InclusiveNamespaces') { + if ($pfx = $node->getAttribute('PrefixList')) { + $arpfx = array_filter(explode(' ', $pfx)); + if (count($arpfx) > 0) { + $prefixList = array_merge($prefixList ? $prefixList : array(), $arpfx); + } + } + } + } + } + $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod, null, $prefixList); + return $this->signedInfo; + } + } + return null; + } + + /** + * @param string $digestAlgorithm + * @param string $data + * @param bool $encode + * @return string + * @throws Exception + */ + public function calculateDigest($digestAlgorithm, $data, $encode = true) + { + switch ($digestAlgorithm) { + case self::SHA1: + $alg = 'sha1'; + break; + case self::SHA256: + $alg = 'sha256'; + break; + case self::SHA384: + $alg = 'sha384'; + break; + case self::SHA512: + $alg = 'sha512'; + break; + case self::RIPEMD160: + $alg = 'ripemd160'; + break; + default: + throw new Exception("Cannot validate digest: Unsupported Algorithm <$digestAlgorithm>"); + } + + $digest = hash($alg, $data, true); + if ($encode) { + $digest = base64_encode($digest); + } + return $digest; + + } + + /** + * @param $refNode + * @param string $data + * @return bool + */ + public function validateDigest($refNode, $data) + { + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + $query = 'string(./secdsig:DigestMethod/@Algorithm)'; + $digestAlgorithm = $xpath->evaluate($query, $refNode); + $digValue = $this->calculateDigest($digestAlgorithm, $data, false); + $query = 'string(./secdsig:DigestValue)'; + $digestValue = $xpath->evaluate($query, $refNode); + return ($digValue === base64_decode($digestValue)); + } + + /** + * @param $refNode + * @param DOMNode $objData + * @param bool $includeCommentNodes + * @return string + */ + public function processTransforms($refNode, $objData, $includeCommentNodes = true) + { + $data = $objData; + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + $query = './secdsig:Transforms/secdsig:Transform'; + $nodelist = $xpath->query($query, $refNode); + $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + $arXPath = null; + $prefixList = null; + foreach ($nodelist AS $transform) { + $algorithm = $transform->getAttribute("Algorithm"); + switch ($algorithm) { + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + + if (!$includeCommentNodes) { + /* We remove comment nodes by forcing it to use a canonicalization + * without comments. + */ + $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + } else { + $canonicalMethod = $algorithm; + } + + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'InclusiveNamespaces') { + if ($pfx = $node->getAttribute('PrefixList')) { + $arpfx = array(); + $pfxlist = explode(" ", $pfx); + foreach ($pfxlist AS $pfx) { + $val = trim($pfx); + if (! empty($val)) { + $arpfx[] = $val; + } + } + if (count($arpfx) > 0) { + $prefixList = $arpfx; + } + } + break; + } + $node = $node->nextSibling; + } + break; + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + if (!$includeCommentNodes) { + /* We remove comment nodes by forcing it to use a canonicalization + * without comments. + */ + $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + } else { + $canonicalMethod = $algorithm; + } + + break; + case 'http://www.w3.org/TR/1999/REC-xpath-19991116': + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'XPath') { + $arXPath = array(); + $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']'; + $arXPath['namespaces'] = array(); + $nslist = $xpath->query('./namespace::*', $node); + foreach ($nslist AS $nsnode) { + if ($nsnode->localName != "xml") { + $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue; + } + } + break; + } + $node = $node->nextSibling; + } + break; + } + } + if ($data instanceof DOMNode) { + $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList); + } + return $data; + } + + /** + * @param DOMNode $refNode + * @return bool + */ + public function processRefNode($refNode) + { + $dataObject = null; + + /* + * Depending on the URI, we may not want to include comments in the result + * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel + */ + $includeCommentNodes = true; + + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + + /* This reference identifies a node with the given id by using + * a URI on the form "#identifier". This should not include comments. + */ + $includeCommentNodes = false; + + $xPath = new DOMXPath($refNode->ownerDocument); + if ($this->idNS && is_array($this->idNS)) { + foreach ($this->idNS as $nspf => $ns) { + $xPath->registerNamespace($nspf, $ns); + } + } + $iDlist = '@Id="'.XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"'; + if (is_array($this->idKeys)) { + foreach ($this->idKeys as $idKey) { + $iDlist .= " or @".XPath::filterAttrName($idKey).'="'. + XPath::filterAttrValue($identifier, XPath::DOUBLE_QUOTE).'"'; + } + } + $query = '//*['.$iDlist.']'; + $dataObject = $xPath->query($query)->item(0); + } else { + $dataObject = $refNode->ownerDocument; + } + } + } else { + /* This reference identifies the root node with an empty URI. This should + * not include comments. + */ + $includeCommentNodes = false; + + $dataObject = $refNode->ownerDocument; + } + $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes); + if (!$this->validateDigest($refNode, $data)) { + return false; + } + + if ($dataObject instanceof DOMNode) { + /* Add this node to the list of validated nodes. */ + if (! empty($identifier)) { + $this->validatedNodes[$identifier] = $dataObject; + } else { + $this->validatedNodes[] = $dataObject; + } + } + + return true; + } + + /** + * @param DOMNode $refNode + * @return null + */ + public function getRefNodeID($refNode) + { + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + return $identifier; + } + } + } + return null; + } + + /** + * @return array + * @throws Exception + */ + public function getRefIDs() + { + $refids = array(); + + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + foreach ($nodeset AS $refNode) { + $refids[] = $this->getRefNodeID($refNode); + } + return $refids; + } + + /** + * @return bool + * @throws Exception + */ + public function validateReference() + { + $docElem = $this->sigNode->ownerDocument->documentElement; + if (! $docElem->isSameNode($this->sigNode)) { + if ($this->sigNode->parentNode != null) { + $this->sigNode->parentNode->removeChild($this->sigNode); + } + } + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo[1]/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + + /* Initialize/reset the list of validated nodes. */ + $this->validatedNodes = array(); + + foreach ($nodeset AS $refNode) { + if (! $this->processRefNode($refNode)) { + /* Clear the list of validated nodes. */ + $this->validatedNodes = null; + throw new Exception("Reference validation failed"); + } + } + return true; + } + + /** + * @param DOMNode $sinfoNode + * @param DOMDocument $node + * @param string $algorithm + * @param null|array $arTransforms + * @param null|array $options + */ + private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=null, $options=null) + { + $prefix = null; + $prefix_ns = null; + $id_name = 'Id'; + $overwrite_id = true; + $force_uri = false; + + if (is_array($options)) { + $prefix = empty($options['prefix']) ? null : $options['prefix']; + $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns']; + $id_name = empty($options['id_name']) ? 'Id' : $options['id_name']; + $overwrite_id = !isset($options['overwrite']) ? true : (bool) $options['overwrite']; + $force_uri = !isset($options['force_uri']) ? false : (bool) $options['force_uri']; + } + + $attname = $id_name; + if (! empty($prefix)) { + $attname = $prefix.':'.$attname; + } + + $refNode = $this->createNewSignNode('Reference'); + $sinfoNode->appendChild($refNode); + + if (! $node instanceof DOMDocument) { + $uri = null; + if (! $overwrite_id) { + $uri = $prefix_ns ? $node->getAttributeNS($prefix_ns, $id_name) : $node->getAttribute($id_name); + } + if (empty($uri)) { + $uri = self::generateGUID(); + $node->setAttributeNS($prefix_ns, $attname, $uri); + } + $refNode->setAttribute("URI", '#'.$uri); + } elseif ($force_uri) { + $refNode->setAttribute("URI", ''); + } + + $transNodes = $this->createNewSignNode('Transforms'); + $refNode->appendChild($transNodes); + + if (is_array($arTransforms)) { + foreach ($arTransforms AS $transform) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + if (is_array($transform) && + (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) && + (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']))) { + $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116'); + $XPathNode = $this->createNewSignNode('XPath', $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query']); + $transNode->appendChild($XPathNode); + if (! empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) { + foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) { + $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace); + } + } + } else { + $transNode->setAttribute('Algorithm', $transform); + } + } + } elseif (! empty($this->canonicalMethod)) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + $transNode->setAttribute('Algorithm', $this->canonicalMethod); + } + + $canonicalData = $this->processTransforms($refNode, $node); + $digValue = $this->calculateDigest($algorithm, $canonicalData); + + $digestMethod = $this->createNewSignNode('DigestMethod'); + $refNode->appendChild($digestMethod); + $digestMethod->setAttribute('Algorithm', $algorithm); + + $digestValue = $this->createNewSignNode('DigestValue', $digValue); + $refNode->appendChild($digestValue); + } + + /** + * @param DOMDocument $node + * @param string $algorithm + * @param null|array $arTransforms + * @param null|array $options + */ + public function addReference($node, $algorithm, $arTransforms=null, $options=null) + { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + + /** + * @param array $arNodes + * @param string $algorithm + * @param null|array $arTransforms + * @param null|array $options + */ + public function addReferenceList($arNodes, $algorithm, $arTransforms=null, $options=null) + { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + foreach ($arNodes AS $node) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + } + + /** + * @param DOMElement|string $data + * @param null|string $mimetype + * @param null|string $encoding + * @return DOMElement + */ + public function addObject($data, $mimetype=null, $encoding=null) + { + $objNode = $this->createNewSignNode('Object'); + $this->sigNode->appendChild($objNode); + if (! empty($mimetype)) { + $objNode->setAttribute('MimeType', $mimetype); + } + if (! empty($encoding)) { + $objNode->setAttribute('Encoding', $encoding); + } + + if ($data instanceof DOMElement) { + $newData = $this->sigNode->ownerDocument->importNode($data, true); + } else { + $newData = $this->sigNode->ownerDocument->createTextNode($data); + } + $objNode->appendChild($newData); + + return $objNode; + } + + /** + * @param null|DOMNode $node + * @return null|XMLSecurityKey + */ + public function locateKey($node=null) + { + if (empty($node)) { + $node = $this->sigNode; + } + if (! $node instanceof DOMNode) { + return null; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)"; + $algorithm = $xpath->evaluate($query, $node); + if ($algorithm) { + try { + $objKey = new XMLSecurityKey($algorithm, array('type' => 'public')); + } catch (Exception $e) { + return null; + } + return $objKey; + } + } + return null; + } + + /** + * Returns: + * Bool when verifying HMAC_SHA1; + * Int otherwise, with following meanings: + * 1 on succesful signature verification, + * 0 when signature verification failed, + * -1 if an error occurred during processing. + * + * NOTE: be very careful when checking the int return value, because in + * PHP, -1 will be cast to True when in boolean context. Always check the + * return value in a strictly typed way, e.g. "$obj->verify(...) === 1". + * + * @param XMLSecurityKey $objKey + * @return bool|int + * @throws Exception + */ + public function verify($objKey) + { + $doc = $this->sigNode->ownerDocument; + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + $query = "string(./secdsig:SignatureValue)"; + $sigValue = $xpath->evaluate($query, $this->sigNode); + if (empty($sigValue)) { + throw new Exception("Unable to locate SignatureValue"); + } + return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue)); + } + + /** + * @param XMLSecurityKey $objKey + * @param string $data + * @return mixed|string + */ + public function signData($objKey, $data) + { + return $objKey->signData($data); + } + + /** + * @param XMLSecurityKey $objKey + * @param null|DOMNode $appendToNode + */ + public function sign($objKey, $appendToNode = null) + { + // If we have a parent node append it now so C14N properly works + if ($appendToNode != null) { + $this->resetXPathObj(); + $this->appendSignature($appendToNode); + $this->sigNode = $appendToNode->lastChild; + } + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $query = "./secdsig:SignatureMethod"; + $nodeset = $xpath->query($query, $sInfo); + $sMethod = $nodeset->item(0); + $sMethod->setAttribute('Algorithm', $objKey->type); + $data = $this->canonicalizeData($sInfo, $this->canonicalMethod); + $sigValue = base64_encode($this->signData($objKey, $data)); + $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue); + if ($infoSibling = $sInfo->nextSibling) { + $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling); + } else { + $this->sigNode->appendChild($sigValueNode); + } + } + } + } + + public function appendCert() + { + + } + + /** + * @param XMLSecurityKey $objKey + * @param null|DOMNode $parent + */ + public function appendKey($objKey, $parent=null) + { + $objKey->serializeKey($parent); + } + + + /** + * This function inserts the signature element. + * + * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode + * is specified, the signature element will be inserted as the last element before $beforeNode. + * + * @param DOMNode $node The node the signature element should be inserted into. + * @param DOMNode $beforeNode The node the signature element should be located before. + * + * @return DOMNode The signature element node + */ + public function insertSignature($node, $beforeNode = null) + { + + $document = $node->ownerDocument; + $signatureElement = $document->importNode($this->sigNode, true); + + if ($beforeNode == null) { + return $node->insertBefore($signatureElement); + } else { + return $node->insertBefore($signatureElement, $beforeNode); + } + } + + /** + * @param DOMNode $parentNode + * @param bool $insertBefore + * @return DOMNode + */ + public function appendSignature($parentNode, $insertBefore = false) + { + $beforeNode = $insertBefore ? $parentNode->firstChild : null; + return $this->insertSignature($parentNode, $beforeNode); + } + + /** + * @param string $cert + * @param bool $isPEMFormat + * @return string + */ + public static function get509XCert($cert, $isPEMFormat=true) + { + $certs = self::staticGet509XCerts($cert, $isPEMFormat); + if (! empty($certs)) { + return $certs[0]; + } + return ''; + } + + /** + * @param string $certs + * @param bool $isPEMFormat + * @return array + */ + public static function staticGet509XCerts($certs, $isPEMFormat=true) + { + if ($isPEMFormat) { + $data = ''; + $certlist = array(); + $arCert = explode("\n", $certs); + $inData = false; + foreach ($arCert AS $curData) { + if (! $inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = true; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + $inData = false; + $certlist[] = $data; + $data = ''; + continue; + } + $data .= trim($curData); + } + } + return $certlist; + } else { + return array($certs); + } + } + + /** + * @param DOMElement $parentRef + * @param string $cert + * @param bool $isPEMFormat + * @param bool $isURL + * @param null|DOMXPath $xpath + * @param null|array $options + * @throws Exception + */ + public static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=true, $isURL=false, $xpath=null, $options=null) + { + if ($isURL) { + $cert = file_get_contents($cert); + } + if (! $parentRef instanceof DOMElement) { + throw new Exception('Invalid parent Node parameter'); + } + $baseDoc = $parentRef->ownerDocument; + + if (empty($xpath)) { + $xpath = new DOMXPath($parentRef->ownerDocument); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + } + + $query = "./secdsig:KeyInfo"; + $nodeset = $xpath->query($query, $parentRef); + $keyInfo = $nodeset->item(0); + $dsig_pfx = ''; + if (! $keyInfo) { + $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS); + if (! empty($pfx)) { + $dsig_pfx = $pfx.":"; + } + $inserted = false; + $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo'); + + $query = "./secdsig:Object"; + $nodeset = $xpath->query($query, $parentRef); + if ($sObject = $nodeset->item(0)) { + $sObject->parentNode->insertBefore($keyInfo, $sObject); + $inserted = true; + } + + if (! $inserted) { + $parentRef->appendChild($keyInfo); + } + } else { + $pfx = $keyInfo->lookupPrefix(self::XMLDSIGNS); + if (! empty($pfx)) { + $dsig_pfx = $pfx.":"; + } + } + + // Add all certs if there are more than one + $certs = self::staticGet509XCerts($cert, $isPEMFormat); + + // Attach X509 data node + $x509DataNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Data'); + $keyInfo->appendChild($x509DataNode); + + $issuerSerial = false; + $subjectName = false; + if (is_array($options)) { + if (! empty($options['issuerSerial'])) { + $issuerSerial = true; + } + if (! empty($options['subjectName'])) { + $subjectName = true; + } + } + + // Attach all certificate nodes and any additional data + foreach ($certs as $X509Cert) { + if ($issuerSerial || $subjectName) { + if ($certData = openssl_x509_parse("-----BEGIN CERTIFICATE-----\n".chunk_split($X509Cert, 64, "\n")."-----END CERTIFICATE-----\n")) { + if ($subjectName && ! empty($certData['subject'])) { + if (is_array($certData['subject'])) { + $parts = array(); + foreach ($certData['subject'] AS $key => $value) { + if (is_array($value)) { + foreach ($value as $valueElement) { + array_unshift($parts, "$key=$valueElement"); + } + } else { + array_unshift($parts, "$key=$value"); + } + } + $subjectNameValue = implode(',', $parts); + } else { + $subjectNameValue = $certData['issuer']; + } + $x509SubjectNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SubjectName', $subjectNameValue); + $x509DataNode->appendChild($x509SubjectNode); + } + if ($issuerSerial && ! empty($certData['issuer']) && ! empty($certData['serialNumber'])) { + if (is_array($certData['issuer'])) { + $parts = array(); + foreach ($certData['issuer'] AS $key => $value) { + array_unshift($parts, "$key=$value"); + } + $issuerName = implode(',', $parts); + } else { + $issuerName = $certData['issuer']; + } + + $x509IssuerNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerSerial'); + $x509DataNode->appendChild($x509IssuerNode); + + $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509IssuerName', $issuerName); + $x509IssuerNode->appendChild($x509Node); + $x509Node = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509SerialNumber', $certData['serialNumber']); + $x509IssuerNode->appendChild($x509Node); + } + } + + } + $x509CertNode = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'X509Certificate', $X509Cert); + $x509DataNode->appendChild($x509CertNode); + } + } + + /** + * @param string $cert + * @param bool $isPEMFormat + * @param bool $isURL + * @param null|array $options + */ + public function add509Cert($cert, $isPEMFormat=true, $isURL=false, $options=null) + { + if ($xpath = $this->getXPathObj()) { + self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath, $options); + } + } + + /** + * This function appends a node to the KeyInfo. + * + * The KeyInfo element will be created if one does not exist in the document. + * + * @param DOMNode $node The node to append to the KeyInfo. + * + * @return DOMNode The KeyInfo element node + */ + public function appendToKeyInfo($node) + { + $parentRef = $this->sigNode; + $baseDoc = $parentRef->ownerDocument; + + $xpath = $this->getXPathObj(); + if (empty($xpath)) { + $xpath = new DOMXPath($parentRef->ownerDocument); + $xpath->registerNamespace('secdsig', self::XMLDSIGNS); + } + + $query = "./secdsig:KeyInfo"; + $nodeset = $xpath->query($query, $parentRef); + $keyInfo = $nodeset->item(0); + if (! $keyInfo) { + $dsig_pfx = ''; + $pfx = $parentRef->lookupPrefix(self::XMLDSIGNS); + if (! empty($pfx)) { + $dsig_pfx = $pfx.":"; + } + $inserted = false; + $keyInfo = $baseDoc->createElementNS(self::XMLDSIGNS, $dsig_pfx.'KeyInfo'); + + $query = "./secdsig:Object"; + $nodeset = $xpath->query($query, $parentRef); + if ($sObject = $nodeset->item(0)) { + $sObject->parentNode->insertBefore($keyInfo, $sObject); + $inserted = true; + } + + if (! $inserted) { + $parentRef->appendChild($keyInfo); + } + } + + $keyInfo->appendChild($node); + + return $keyInfo; + } + + /** + * This function retrieves an associative array of the validated nodes. + * + * The array will contain the id of the referenced node as the key and the node itself + * as the value. + * + * Returns: + * An associative array of validated nodes or null if no nodes have been validated. + * + * @return array Associative array of validated nodes + */ + public function getValidatedNodes() + { + return $this->validatedNodes; + } +} diff --git a/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityKey.php b/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityKey.php new file mode 100644 index 00000000..7eed04d2 --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/src/XMLSecurityKey.php @@ -0,0 +1,813 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards + * @copyright 2007-2020 Robert Richards + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + */ + +class XMLSecurityKey +{ + const TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + const AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + const AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + const AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + const AES128_GCM = 'http://www.w3.org/2009/xmlenc11#aes128-gcm'; + const AES192_GCM = 'http://www.w3.org/2009/xmlenc11#aes192-gcm'; + const AES256_GCM = 'http://www.w3.org/2009/xmlenc11#aes256-gcm'; + const RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + const RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + const RSA_OAEP = 'http://www.w3.org/2009/xmlenc11#rsa-oaep'; + const DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'; + const RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + const RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + const RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'; + const RSA_SHA512 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; + const HMAC_SHA1 = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1'; + const AUTHTAG_LENGTH = 16; + + /** @var array */ + private $cryptParams = array(); + + /** @var int|string */ + public $type = 0; + + /** @var mixed|null */ + public $key = null; + + /** @var string */ + public $passphrase = ""; + + /** @var string|null */ + public $iv = null; + + /** @var string|null */ + public $name = null; + + /** @var mixed|null */ + public $keyChain = null; + + /** @var bool */ + public $isEncrypted = false; + + /** @var XMLSecEnc|null */ + public $encryptedCtx = null; + + /** @var mixed|null */ + public $guid = null; + + /** + * This variable contains the certificate as a string if this key represents an X509-certificate. + * If this key doesn't represent a certificate, this will be null. + * @var string|null + */ + private $x509Certificate = null; + + /** + * This variable contains the certificate thumbprint if we have loaded an X509-certificate. + * @var string|null + */ + private $X509Thumbprint = null; + + /** + * @param string $type + * @param null|array $params + * @throws Exception + */ + public function __construct($type, $params=null) + { + switch ($type) { + case (self::TRIPLEDES_CBC): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'des-ede3-cbc'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + $this->cryptParams['keysize'] = 24; + $this->cryptParams['blocksize'] = 8; + break; + case (self::AES128_CBC): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-128-cbc'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + $this->cryptParams['keysize'] = 16; + $this->cryptParams['blocksize'] = 16; + break; + case (self::AES192_CBC): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-192-cbc'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + $this->cryptParams['keysize'] = 24; + $this->cryptParams['blocksize'] = 16; + break; + case (self::AES256_CBC): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-256-cbc'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + $this->cryptParams['keysize'] = 32; + $this->cryptParams['blocksize'] = 16; + break; + case (self::AES128_GCM): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-128-gcm'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2009/xmlenc11#aes128-gcm'; + $this->cryptParams['keysize'] = 16; + $this->cryptParams['blocksize'] = 16; + break; + case (self::AES192_GCM): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-192-gcm'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2009/xmlenc11#aes192-gcm'; + $this->cryptParams['keysize'] = 24; + $this->cryptParams['blocksize'] = 16; + break; + case (self::AES256_GCM): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['cipher'] = 'aes-256-gcm'; + $this->cryptParams['type'] = 'symmetric'; + $this->cryptParams['method'] = 'http://www.w3.org/2009/xmlenc11#aes256-gcm'; + $this->cryptParams['keysize'] = 32; + $this->cryptParams['blocksize'] = 16; + break; + case (self::RSA_1_5): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_OAEP_MGF1P): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + $this->cryptParams['hash'] = null; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_OAEP): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2009/xmlenc11#rsa-oaep'; + $this->cryptParams['hash'] = 'http://www.w3.org/2009/xmlenc11#mgf1sha1'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_SHA1): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_SHA256): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['digest'] = 'SHA256'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_SHA384): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['digest'] = 'SHA384'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::RSA_SHA512): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['digest'] = 'SHA512'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + case (self::HMAC_SHA1): + $this->cryptParams['library'] = $type; + $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#hmac-sha1'; + break; + default: + throw new Exception('Invalid Key Type'); + } + $this->type = $type; + } + + /** + * Retrieve the key size for the symmetric encryption algorithm.. + * + * If the key size is unknown, or this isn't a symmetric encryption algorithm, + * null is returned. + * + * @return int|null The number of bytes in the key. + */ + public function getSymmetricKeySize() + { + if (! isset($this->cryptParams['keysize'])) { + return null; + } + return $this->cryptParams['keysize']; + } + + /** + * Generates a session key using the openssl-extension. + * In case of using DES3-CBC the key is checked for a proper parity bits set. + * @return string + * @throws Exception + */ + public function generateSessionKey() + { + if (!isset($this->cryptParams['keysize'])) { + throw new Exception('Unknown key size for type "' . $this->type . '".'); + } + $keysize = $this->cryptParams['keysize']; + + $key = openssl_random_pseudo_bytes($keysize); + + if ($this->type === self::TRIPLEDES_CBC) { + /* Make sure that the generated key has the proper parity bits set. + * Mcrypt doesn't care about the parity bits, but others may care. + */ + for ($i = 0; $i < strlen($key); $i++) { + $byte = ord($key[$i]) & 0xfe; + $parity = 1; + for ($j = 1; $j < 8; $j++) { + $parity ^= ($byte >> $j) & 1; + } + $byte |= $parity; + $key[$i] = chr($byte); + } + } + + $this->key = $key; + return $key; + } + + /** + * Get the raw thumbprint of a certificate + * + * @param string $cert + * @return null|string + */ + public static function getRawThumbprint($cert) + { + + $arCert = explode("\n", $cert); + $data = ''; + $inData = false; + + foreach ($arCert AS $curData) { + if (! $inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = true; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + break; + } + $data .= trim($curData); + } + } + + if (! empty($data)) { + return strtolower(sha1(base64_decode($data))); + } + + return null; + } + + /** + * Loads the given key, or - with isFile set true - the key from the keyfile. + * + * @param string $key + * @param bool $isFile + * @param bool $isCert + * @throws Exception + */ + public function loadKey($key, $isFile=false, $isCert = false) + { + if ($isFile) { + $this->key = file_get_contents($key); + } else { + $this->key = $key; + } + if ($isCert) { + $this->key = openssl_x509_read($this->key); + openssl_x509_export($this->key, $str_cert); + $this->x509Certificate = $str_cert; + $this->key = $str_cert; + } else { + $this->x509Certificate = null; + } + if ($this->cryptParams['library'] == 'openssl') { + switch ($this->cryptParams['type']) { + case 'public': + if ($isCert) { + /* Load the thumbprint if this is an X509 certificate. */ + $this->X509Thumbprint = self::getRawThumbprint($this->key); + } + $this->key = openssl_get_publickey($this->key); + if (! $this->key) { + throw new Exception('Unable to extract public key'); + } + break; + + case 'private': + $this->key = openssl_get_privatekey($this->key, $this->passphrase); + break; + + case'symmetric': + if (strlen($this->key) < $this->cryptParams['keysize']) { + throw new Exception('Key must contain at least '.$this->cryptParams['keysize'].' characters for this cipher, contains '.strlen($this->key)); + } + break; + + default: + throw new Exception('Unknown type'); + } + } + } + + /** + * ISO 10126 Padding + * + * @param string $data + * @param integer $blockSize + * @throws Exception + * @return string + */ + private function padISO10126($data, $blockSize) + { + if ($blockSize > 256) { + throw new Exception('Block size higher than 256 not allowed'); + } + $padChr = $blockSize - (strlen($data) % $blockSize); + $pattern = chr($padChr); + return $data . str_repeat($pattern, $padChr); + } + + /** + * Remove ISO 10126 Padding + * + * @param string $data + * @return string + */ + private function unpadISO10126($data) + { + $padChr = substr($data, -1); + $padLen = ord($padChr); + return substr($data, 0, -$padLen); + } + + /** + * Encrypts the given data (string) using the openssl-extension + * + * @param string $data + * @return string + */ + private function encryptSymmetric($data) + { + $this->iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length($this->cryptParams['cipher'])); + $authTag = null; + if(in_array($this->cryptParams['cipher'], ['aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'])) { + if (version_compare(PHP_VERSION, '7.1.0') < 0) { + throw new Exception('PHP 7.1.0 is required to use AES GCM algorithms'); + } + $authTag = openssl_random_pseudo_bytes(self::AUTHTAG_LENGTH); + $encrypted = openssl_encrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA, $this->iv, $authTag); + } else { + $data = $this->padISO10126($data, $this->cryptParams['blocksize']); + $encrypted = openssl_encrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv); + } + + if (false === $encrypted) { + throw new Exception('Failure encrypting Data (openssl symmetric) - ' . openssl_error_string()); + } + return $this->iv . $encrypted . $authTag; + } + + /** + * Decrypts the given data (string) using the openssl-extension + * + * @param string $data + * @return string + */ + private function decryptSymmetric($data) + { + $iv_length = openssl_cipher_iv_length($this->cryptParams['cipher']); + $this->iv = substr($data, 0, $iv_length); + $data = substr($data, $iv_length); + $authTag = null; + if(in_array($this->cryptParams['cipher'], ['aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm'])) { + if (version_compare(PHP_VERSION, '7.1.0') < 0) { + throw new Exception('PHP 7.1.0 is required to use AES GCM algorithms'); + } + // obtain and remove the authentication tag + $offset = 0 - self::AUTHTAG_LENGTH; + $authTag = substr($data, $offset); + $data = substr($data, 0, $offset); + $decrypted = openssl_decrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA, $this->iv, $authTag); + } else { + $decrypted = openssl_decrypt($data, $this->cryptParams['cipher'], $this->key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $this->iv); + } + + if (false === $decrypted) { + throw new Exception('Failure decrypting Data (openssl symmetric) - ' . openssl_error_string()); + } + return null !== $authTag ? $decrypted : $this->unpadISO10126($decrypted); + } + + /** + * Encrypts the given public data (string) using the openssl-extension + * + * @param string $data + * @return string + * @throws Exception + */ + private function encryptPublic($data) + { + if (! openssl_public_encrypt($data, $encrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data (openssl public) - ' . openssl_error_string()); + } + return $encrypted; + } + + /** + * Decrypts the given public data (string) using the openssl-extension + * + * @param string $data + * @return string + * @throws Exception + */ + private function decryptPublic($data) + { + if (! openssl_public_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data (openssl public) - ' . openssl_error_string()); + } + return $decrypted; + } + + /** + * Encrypts the given private data (string) using the openssl-extension + * + * @param string $data + * @return string + * @throws Exception + */ + private function encryptPrivate($data) + { + if (! openssl_private_encrypt($data, $encrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data (openssl private) - ' . openssl_error_string()); + } + return $encrypted; + } + + /** + * Decrypts the given private data (string) using the openssl-extension + * + * @param string $data + * @return string + * @throws Exception + */ + private function decryptPrivate($data) + { + if (! openssl_private_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data (openssl private) - ' . openssl_error_string()); + } + return $decrypted; + } + + /** + * Signs the given data (string) using the openssl-extension + * + * @param string $data + * @return string + * @throws Exception + */ + private function signOpenSSL($data) + { + $algo = OPENSSL_ALGO_SHA1; + if (! empty($this->cryptParams['digest'])) { + $algo = $this->cryptParams['digest']; + } + if (! openssl_sign($data, $signature, $this->key, $algo)) { + throw new Exception('Failure Signing Data: ' . openssl_error_string() . ' - ' . $algo); + } + return $signature; + } + + /** + * Verifies the given data (string) belonging to the given signature using the openssl-extension + * + * Returns: + * 1 on succesful signature verification, + * 0 when signature verification failed, + * -1 if an error occurred during processing. + * + * NOTE: be very careful when checking the return value, because in PHP, + * -1 will be cast to True when in boolean context. So always check the + * return value in a strictly typed way, e.g. "$obj->verify(...) === 1". + * + * @param string $data + * @param string $signature + * @return int + */ + private function verifyOpenSSL($data, $signature) + { + $algo = OPENSSL_ALGO_SHA1; + if (! empty($this->cryptParams['digest'])) { + $algo = $this->cryptParams['digest']; + } + return openssl_verify($data, $signature, $this->key, $algo); + } + + /** + * Encrypts the given data (string) using the regarding php-extension, depending on the library assigned to algorithm in the contructor. + * + * @param string $data + * @return mixed|string + */ + public function encryptData($data) + { + if ($this->cryptParams['library'] === 'openssl') { + switch ($this->cryptParams['type']) { + case 'symmetric': + return $this->encryptSymmetric($data); + case 'public': + return $this->encryptPublic($data); + case 'private': + return $this->encryptPrivate($data); + } + } + } + + /** + * Decrypts the given data (string) using the regarding php-extension, depending on the library assigned to algorithm in the contructor. + * + * @param string $data + * @return mixed|string + */ + public function decryptData($data) + { + if ($this->cryptParams['library'] === 'openssl') { + switch ($this->cryptParams['type']) { + case 'symmetric': + return $this->decryptSymmetric($data); + case 'public': + return $this->decryptPublic($data); + case 'private': + return $this->decryptPrivate($data); + } + } + } + + /** + * Signs the data (string) using the extension assigned to the type in the constructor. + * + * @param string $data + * @return mixed|string + */ + public function signData($data) + { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->signOpenSSL($data); + case (self::HMAC_SHA1): + return hash_hmac("sha1", $data, $this->key, true); + } + } + + /** + * Verifies the data (string) against the given signature using the extension assigned to the type in the constructor. + * + * Returns in case of openSSL: + * 1 on succesful signature verification, + * 0 when signature verification failed, + * -1 if an error occurred during processing. + * + * NOTE: be very careful when checking the return value, because in PHP, + * -1 will be cast to True when in boolean context. So always check the + * return value in a strictly typed way, e.g. "$obj->verify(...) === 1". + * + * @param string $data + * @param string $signature + * @return bool|int + */ + public function verifySignature($data, $signature) + { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->verifyOpenSSL($data, $signature); + case (self::HMAC_SHA1): + $expectedSignature = hash_hmac("sha1", $data, $this->key, true); + return strcmp($signature, $expectedSignature) == 0; + } + } + + /** + * @deprecated + * @see getAlgorithm() + * @return mixed + */ + public function getAlgorith() + { + return $this->getAlgorithm(); + } + + /** + * @return mixed + */ + public function getAlgorithm() + { + return $this->cryptParams['method']; + } + + /** + * + * @param int $type + * @param string $string + * @return null|string + */ + public static function makeAsnSegment($type, $string) + { + switch ($type) { + case 0x02: + if (ord($string) > 0x7f) + $string = chr(0).$string; + break; + case 0x03: + $string = chr(0).$string; + break; + } + + $length = strlen($string); + + if ($length < 128) { + $output = sprintf("%c%c%s", $type, $length, $string); + } else if ($length < 0x0100) { + $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string); + } else if ($length < 0x010000) { + $output = sprintf("%c%c%c%c%s", $type, 0x82, $length / 0x0100, $length % 0x0100, $string); + } else { + $output = null; + } + return $output; + } + + /** + * + * Hint: Modulus and Exponent must already be base64 decoded + * @param string $modulus + * @param string $exponent + * @return string + */ + public static function convertRSA($modulus, $exponent) + { + /* make an ASN publicKeyInfo */ + $exponentEncoding = self::makeAsnSegment(0x02, $exponent); + $modulusEncoding = self::makeAsnSegment(0x02, $modulus); + $sequenceEncoding = self::makeAsnSegment(0x30, $modulusEncoding.$exponentEncoding); + $bitstringEncoding = self::makeAsnSegment(0x03, $sequenceEncoding); + $rsaAlgorithmIdentifier = pack("H*", "300D06092A864886F70D0101010500"); + $publicKeyInfo = self::makeAsnSegment(0x30, $rsaAlgorithmIdentifier.$bitstringEncoding); + + /* encode the publicKeyInfo in base64 and add PEM brackets */ + $publicKeyInfoBase64 = base64_encode($publicKeyInfo); + $encoding = "-----BEGIN PUBLIC KEY-----\n"; + $offset = 0; + while ($segment = substr($publicKeyInfoBase64, $offset, 64)) { + $encoding = $encoding.$segment."\n"; + $offset += 64; + } + return $encoding."-----END PUBLIC KEY-----\n"; + } + + /** + * @param mixed $parent + */ + public function serializeKey($parent) + { + + } + + /** + * Retrieve the X509 certificate this key represents. + * + * Will return the X509 certificate in PEM-format if this key represents + * an X509 certificate. + * + * @return string The X509 certificate or null if this key doesn't represent an X509-certificate. + */ + public function getX509Certificate() + { + return $this->x509Certificate; + } + + /** + * Get the thumbprint of this X509 certificate. + * + * Returns: + * The thumbprint as a lowercase 40-character hexadecimal number, or null + * if this isn't a X509 certificate. + * + * @return string Lowercase 40-character hexadecimal number of thumbprint + */ + public function getX509Thumbprint() + { + return $this->X509Thumbprint; + } + + + /** + * Create key from an EncryptedKey-element. + * + * @param DOMElement $element The EncryptedKey-element. + * @throws Exception + * + * @return XMLSecurityKey The new key. + */ + public static function fromEncryptedKeyElement(DOMElement $element) + { + + $objenc = new XMLSecEnc(); + $objenc->setNode($element); + if (! $objKey = $objenc->locateKey()) { + throw new Exception("Unable to locate algorithm for this Encrypted Key"); + } + $objKey->isEncrypted = true; + $objKey->encryptedCtx = $objenc; + XMLSecEnc::staticLocateKeyInfo($objKey, $element); + return $objKey; + } + +} diff --git a/saml/vendor/robrichards/xmlseclibs/xmlseclibs.php b/saml/vendor/robrichards/xmlseclibs/xmlseclibs.php new file mode 100644 index 00000000..1c10acc7 --- /dev/null +++ b/saml/vendor/robrichards/xmlseclibs/xmlseclibs.php @@ -0,0 +1,47 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards + * @copyright 2007-2020 Robert Richards + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 3.1.1 + */ + +$xmlseclibs_srcdir = dirname(__FILE__) . '/src/'; +require $xmlseclibs_srcdir . '/XMLSecurityKey.php'; +require $xmlseclibs_srcdir . '/XMLSecurityDSig.php'; +require $xmlseclibs_srcdir . '/XMLSecEnc.php'; +require $xmlseclibs_srcdir . '/Utils/XPath.php';