patrickallaert/php-saml

PHP SAML 工具包,基于 onelogin/php-saml 分支

1.0.3 2021-07-15 16:41 UTC

This package is auto-updated.

Last update: 2024-09-16 00:11:10 UTC


README

Build Status License

使用此库将 SAML 支持添加到您的 PHP 软件。

警告

此版本与 PHP 7.1+ 兼容,不包括 xmlseclibs(您需要通过 composer 安装,依赖关系在 composer.json 中描述)

为什么要在我的软件中添加 SAML 支持?

SAML 是一种基于 XML 的用于网页浏览器单点登录的标准,由 OASIS 安全服务技术委员会定义。该标准自 2002 年以来一直存在,但由于其优势,近年来越来越受欢迎。

  • 易用性 - 从门户或内部网络一键访问、深度链接、消除密码和自动续期会话使用户的生活更加轻松。
  • 安全性 - 基于强大的数字签名进行身份验证和完整性验证,SAML 是一个安全的单点登录协议,全球最大和最注重安全的企业的首选。
  • 速度 - SAML 很快。只需一个浏览器重定向就可以安全地将用户签到应用程序中。
  • 钓鱼预防 - 如果您没有应用程序的密码,您就不能被骗到在假登录页面上输入它。
  • IT 友好性 - SAML 简化了 IT 的工作,因为它集中了身份验证,提供了更大的可见性,并简化了目录集成。
  • 机会 - B2B 云供应商应支持 SAML 以促进其产品的集成。

一般描述

SAML PHP 工具包允许您在 PHP 应用程序上构建 SP(服务提供者)并将其连接到任何 IdP(身份提供者)。

支持

  • SSO 和 SLO(SP 启动和 IdP 启动)。
  • 断言和 nameId 加密。
  • 断言签名。
  • 消息签名:AuthNRequest、LogoutRequest、LogoutResponses。
  • 启用断言消费者服务端点。
  • 启用单点登出服务端点。
  • 发布 SP 元数据(可以进行签名)。

主要功能

  • saml2int - 实现 SAML 2.0 Web 浏览器 SSO 配置文件。
  • 无会话 - 忘记那些常见的 SP 和最终应用程序之间的冲突,工具包将委托最终应用程序中的会话。
  • 易于使用 - 开发人员将允许进行高级和低级编程,提供 2 个易于使用的 API。
  • 经过测试 - 经过彻底测试。
  • 流行 - 许多 PHP SAML 插件使用它。

安装

依赖项

  • php >= 7.1 以及一些核心扩展,如 php-xmlphp-datephp-zlib
  • openssl。安装 openssl 库。它处理 x509 证书。
  • gettext。安装该库及其 php 驱动。它处理翻译。
  • curl。如果您计划使用 IdP 元数据解析器,请安装该库及其 php 驱动。

代码

选项 1. 从 GitHub 下载

工具包托管在 GitHub 上。您可以从以下网址下载它:

选项 2. Composer

工具包支持 composer。您可以在以下网址找到 patrickallaert/php-saml 软件包:https://packagist.org.cn/packages/patrickallaert/php-saml

为了将SAML工具包导入到当前的PHP项目中,请执行以下操作:

composer require patrickallaert/php-saml

重要 在此选项中,x509证书必须存储在 vendor/patrickallaert/php-saml/certs,设置文件存储在 vendor/patrickallaert/php-saml

使用 composer update 或类似命令更新包时,您的设置可能会被删除。因此,强烈建议您不要使用设置文件,而是直接将设置作为数组传递给构造函数(本文件后面将解释)。如果您不采用此方法,那么在更新包时,您的设置可能会被删除。

安全警告

在生产环境中,必须将 strict 参数设置为 "true",并且在 security 下的 signatureAlgorithmdigestAlgorithm 必须设置为 SHA1 以外的值(见 https://shattered.io/)。否则,您的环境将不安全,容易受到攻击。

在生产环境中,我们还强烈建议在设置中注册IdP证书而不是使用指纹方法。指纹是一个散列,所以最终可能会受到碰撞攻击,这可能会导致签名验证绕过。其他SAML工具包已弃用该机制,我们保持它以实现兼容性,并且也可以在测试环境中使用。

入门指南

了解工具包

SAML工具包包含不同的文件夹(certsendpointslibdemo等)和一些文件。

让我们先描述一下这些文件夹

certs/

SAML需要x509证书来签名和加密如NameIDMessageAssertionMetadata等元素。

如果我们的环境需要签名或加密支持,则此文件夹可能包含SP将使用的x509证书和私钥

  • sp.crt - SP的公钥证书
  • sp.key - SP的私钥

或者我们也可以在设置文件中提供这些数据,在 $settings['sp']['x509cert']$settings['sp']['privateKey']

有时我们可能需要在SP发布的元数据上签名,在这种情况下,我们可以使用前面提到的x509证书或使用新的x.509证书:metadata.crtmetadata.key

如果您正在进行密钥滚动过程并希望在服务提供商的元数据中发布该x509证书,请使用 sp_new.crt

src/

此文件夹包含工具包的核心,即库

  • Saml2 文件夹包含在后面章节中描述的新版本的类和方法。

doc/

此文件夹包含工具包的API文档。

endpoints/

工具包有三个端点

  • metadata.php - SP的元数据发布位置。
  • acs.php - 断言消费者服务。处理SAML响应。
  • sls.php - 单一登出服务。处理登出请求和登出响应。

您可以使用工具包提供的文件或创建自己的端点文件来添加SAML支持到您的应用程序。请注意,这些端点文件使用工具包基本文件夹的设置文件。

locale/

Locale文件夹包含一些翻译:en_USes_ES 作为概念验证。目前没有翻译,但我们最终会本地化消息并支持多种语言。

其他重要文件

  • settings_example.php - 创建包含工具包基本配置信息的 settings.php 文件的模板。
  • advanced_settings_example.php - 一个模板,用于创建包含与安全、联系人以及与SP关联的组织的额外配置信息的 advanced_settings.php 文件。

杂项

  • tests/ - 包含工具包的单元测试。
  • demo1/ - 包含一个带有SAML支持的简单PHP应用示例。阅读其中的 Readme.txt 以获取更多信息。
  • demo2/ - 包含另一个示例。

工作原理

设置

首先,我们需要配置工具包。需要配置SP的信息、IdP的信息,在某些情况下,还需要配置高级安全问题,如签名和加密。

有两种方式提供设置信息

  • 使用位于工具包基本目录下的 settings.php 文件。
  • 使用设置数据的数组,并将其直接提供给类的构造函数。

有一个模板文件,settings_example.php,您可以将此文件复制一份,重命名并编辑它。

<?php

$settings = 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.
    'strict' => 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 <AuthnResponse> message MUST be
        // returned to the requester, in this case our SP.
        'assertionConsumerService' => array(
            // URL Location where the <Response> from the IdP will be returned
            'url' => '',
            // SAML protocol binding to be used when returning the <Response>
            // message. 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 <Logout Response> message MUST be
        // returned to the requester, in this case our SP.
        'singleLogoutService' => array(
            // URL Location where the <Response> from the IdP will be returned
            'url' => '',
            // SAML protocol binding to be used when returning the <Response>
            // message. 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 <Response>
            // message. 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' => '',
            // SAML protocol binding to be used when returning the <Response>
            // message. 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 => '<cert1-string>',
        //      ),
        //      'encryption' => array(
        //          0 => '<cert2-string>',
        //      )
        // ),
    ),
);

除了必需的设置数据(IdP,SP)外,还可以定义其他信息。与存在基本信息的模板类似,工具包基本目录中还有一个名为 advanced_settings_example.php 的模板,您可以复制并重命名它为 advanced_settings.php

<?php

$advancedSettings = array(

    // Compression settings
    'compress' => array(
        'requests' => true,
        'responses' => true
    ),
    // Security settings
    'security' => array(

        /** signatures and encryptions offered */

        // Indicates that the nameID of the <samlp:logoutRequest> sent by this SP
        // will be encrypted.
        'nameIdEncrypted' => false,

        // Indicates whether the <samlp:AuthnRequest> messages sent by this SP
        // will be signed.  [Metadata of the SP will offer this info]
        'authnRequestsSigned' => false,

        // Indicates whether the <samlp:logoutRequest> messages sent by this SP
        // will be signed.
        'logoutRequestSigned' => false,

        // Indicates whether the <samlp:logoutResponse> 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'
                                                )
        */
        'signMetadata' => false,

        /** signatures and encryptions required **/

        // Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest>
        // and <samlp:LogoutResponse> elements received by this SP to be signed.
        'wantMessagesSigned' => false,

        // Indicates a requirement for the <saml:Assertion> elements received by
        // this SP to be encrypted.
        'wantAssertionsEncrypted' => false,

        // Indicates a requirement for the <saml:Assertion> 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' => true,

        // 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,

        // 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',

        // 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' => ''
        ),
    ),
);

压缩设置允许您指示IdP是否可以接受使用gzip 压缩的数据(请求和响应)。但如果我们在 getRequestgetResponse 方法中提供 $deflate 布尔参数,它将具有比压缩设置更高的优先级。

在安全部分,您可以设置SP将如何处理消息和断言。联系IdP的管理员,询问IdP期望什么,并决定SP将处理哪些验证以及SP将有哪些要求,并将这些要求传达给IdP的管理员。

知道了可以配置哪种类型的数据后,让我们来谈谈在工具包中处理设置的方式。

描述的设置文件(settings.phpadvanced_settings.php)在工具包构造函数中没有提供其他设置信息数组时将被加载。让我们看看一些例子。

// Initializes toolkit with settings.php & advanced_settings files.
$auth = new Saml2\Auth();
//or
$settings = new Saml2\Settings();

// Initializes toolkit with the array provided.
$auth = new Saml2\Auth($settingsInfo);
//or
$settings = new Saml2\Settings($settingsInfo);

您可以在包含构造函数执行文件中声明 $settingsInfo,或者将它们放在任何文件中,然后加载该文件以获取如以下示例所示的可用数组。

<?php

require_once 'custom_settings.php';  // The custom_settings.php contains a
                                     // $settingsInfo array.

$auth = new Saml2\Auth($settingsInfo);

如何加载库

如果您使用composer安装了此库,请使用composer的 vendor/autoload.php 脚本。

否则,使用一个 PSR-4 兼容的加载机制,了解命名空间 Saml2\ 应与 src/Saml2 文件夹匹配。

启动SSO

为了向IdP发送 AuthNRequest

<?php

$auth = new Saml2\Auth(); // Constructor of the SP, loads settings.php
                                   // and advanced_settings.php
$auth->login();   // Method that sent the AuthNRequest

AuthNRequest 将根据 advanced_settings.php 的安全信息('authnRequestsSigned')进行签名或未签名。

然后,IdP将返回SAML响应到用户的客户端。然后,客户端将被转发到SP的属性消费者服务,并带有此信息。如果我们没有在登录方法中设置 'url' 参数,并且我们正在使用工具包提供的默认ACS(endpoints/acs.php),那么ACS端点将重定向用户到启动SSO请求的文件。

我们可以设置一个 'returnTo' url 来更改工作流程并将用户重定向到另一个PHP文件。

$newTargetUrl = 'http://example.com/consume2.php';
$auth = new Saml2\Auth();
$auth->login($newTargetUrl);

登录方法可以接收其他五个可选参数

  • $parameters - 一个参数数组,它将被添加到HTTP-Redirect的GET中。
  • $forceAuthn - 当为true时,AuthNRequest 将设置 ForceAuthn='true'
  • $isPassive - 当为true时,AuthNRequest 将设置 Ispassive='true'
  • $strict - 如果我们想保持(返回URL字符串)则设置为true,如果我们要重定向则设置为false
  • $setNameIdPolicy - 当为true时,AuthNRequest将设置一个nameIdPolicy元素。

如果需要匹配未来的SAMLResponse ID和要发送的AuthNRequest ID,则必须提取并保存该AuthNRequest ID。

$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();

SP端点

与SP相关有三个重要视图:元数据视图、ACS视图和SLS视图。工具包在端点目录中提供了这些视图的示例。

SP元数据 endpoints/metadata.php

此代码将根据我们在设置文件中提供的信息提供我们的SP的XML元数据文件。

<?php

try {
    $auth = new Saml2\Auth();
    $settings = $auth->getSettings();
    $metadata = $settings->getSPMetadata();
    $errors = $settings->validateMetadata($metadata);
    if (empty($errors)) {
        header('Content-Type: text/xml');
        echo $metadata;
    } else {
        throw new Saml2\Error(
            'Invalid SP metadata: '.implode(', ', $errors),
            Saml2\Error::METADATA_SP_INVALID
        );
    }
} catch (Exception $e) {
    echo $e->getMessage();
}

getSPMetadata将根据advanced_settings.php'signMetadata')中的安全信息返回签名的或未签名的元数据。

在暴露XML元数据之前,会进行检查以确保提供的信息有效。

除了使用Auth对象外,您还可以直接使用

$settings = new Saml2\Settings($settingsInfo, true);

来获取设置对象,并且通过true参数我们将避免IdP设置验证。

属性消费者服务(ACS) endpoints/acs.php

此代码处理IdP通过用户的客户端转发给SP的SAML响应。

<?php

session_start();  // IMPORTANT: This is required in order to be able
                  // to store the user data in the session.

$auth = new Saml2\Auth();

$auth->processResponse($_SESSION['AuthNRequestID'] ?? null);
unset($_SESSION['AuthNRequestID']);

$errors = $auth->getErrors();

if (!empty($errors)) {
    echo '<p>' . implode(', ', $errors) . '</p>';
    exit();
}

if (!$auth->isAuthenticated()) {
    echo "<p>Not authenticated</p>";
    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']) && Saml2\Utils::getSelfURL() != $_POST['RelayState']) {
    $auth->redirectTo($_POST['RelayState']);
}

$attributes = $_SESSION['samlUserdata'];
$nameId = $_SESSION['samlNameId'];

echo '<h1>Identified user: '. htmlentities($nameId) .'</h1>';

if (!empty($attributes)) {
    echo '<h2>' . _('User attributes:') . '</h2>';
    echo '<table><thead><th>' . _('Name') . '</th><th>' . _('Values') . '</th></thead><tbody>';
    foreach ($attributes as $attributeName => $attributeValues) {
        echo '<tr><td>' . htmlentities($attributeName) . '</td><td><ul>';
        foreach ($attributeValues as $attributeValue) {
            echo '<li>' . htmlentities($attributeValue) . '</li>';
        }
        echo '</ul></td></tr>';
    }
    echo '</tbody></table>';
} else {
    echo _('No attributes found.');
}

处理SAML响应并检查是否存在错误。它还验证用户是否已认证并将userdata存储在会话中。

此时有两种可能的替代方案

  1. 如果没有提供RelayState,我们可以在此视图中显示用户数据或我们想要的方式。

  2. 如果提供了RelayState,则发生重定向。

注意,我们在重定向之前将用户数据保存在会话中,以便在RelayState视图中可用用户数据。

getAttributes方法

为了检索属性,我们可以使用

$attributes = $auth->getAttributes();

使用此方法,我们可以获取由IdP在SAML响应的断言中提供的所有用户数据。

如果我们执行print_r($attributes),我们可以得到

Array
(
    [cn] => Array
        (
            [0] => John
        )
    [sn] => Array
        (
            [0] => Doe
        )
    [mail] => Array
        (
            [0] => john.doe@example.com
        )
    [groups] => Array
        (
            [0] => users
            [1] => members
        )
)

每个属性名称都可以用作$attributes的索引来获取值。每个属性值都是一个数组 - 单值属性是一个包含单个元素的数组。

以下代码是等效的

$attributes = $auth->getAttributes();
print_r($attributes['cn']);
print_r($auth->getAttribute('cn'));

在尝试获取属性之前,检查用户是否已认证。如果用户未认证或SAML断言中没有属性,则返回空数组。例如,如果我们调用getAttributes$auth->processResponse之前,则getAttributes()将返回空数组。

单一注销服务(SLS) endpoints/sls.php

此代码处理注销请求和注销响应。

<?php

session_start();  // IMPORTANT: This is required in order to be able
                  // to close the user session.

$auth = new Saml2\Auth();

$auth->processSLO(false, $_SESSION['LogoutRequestID'] ?? null);

$errors = $auth->getErrors();

if (empty($errors)) {
    echo 'Sucessfully logged out';
} else {
    echo implode(', ', $errors);
}

如果SLS端点收到注销响应,则验证响应并可以关闭会话

// part of the processSLO method

$logoutResponse = new Saml2\LogoutResponse($this->_settings, $_GET['SAMLResponse']);
if (!$logoutResponse->isValid($requestId)) {
    $this->_errors[] = 'invalid_logout_response';
} else if ($logoutResponse->getStatus() !== Saml2\Constants::STATUS_SUCCESS) {
    $this->_errors[] = 'logout_not_success';
} else {
    if (!$keepLocalSession) {
        Saml2\Utils::deleteLocalSession();
    }
}

如果SLS端点收到注销请求,则验证请求,关闭会话并向IdP的SLS端点发送注销响应。

// part of the processSLO method

$request = gzinflate(base64_decode($_GET['SAMLRequest']));
if (!Saml2\LogoutRequest::isValid($this->_settings, $request)) {
    $this->_errors[] = 'invalid_logout_request';
} else {
    if (!$keepLocalSession) {
        Saml2\Utils::deleteLocalSession();
    }

    $inResponseTo = $request->id;
    $responseBuilder = new 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);
}

如果您没有使用默认的PHP会话,或者需要手动销毁会话,可以将回调方法传递给processSLO方法作为第四个参数

$keepLocalSession = False;
$callback = function () {
    // Destroy user session
};

$auth->processSLO($keepLocalSession, null, false, $callback);

如果我们不希望processSLO销毁会话,则将true参数传递给processSLO方法

$keepLocalSession = True;
$auth->processSLO($keepLocalSession);

启动SLO

为了向IdP发送注销请求

<?php

$auth = new Saml2\Auth();

$auth->logout();   // Method that sent the Logout Request.

还有八个可选参数可以设置

  • $returnTo - 用户注销后应返回的目标URL。
  • $parameters - 要添加到GET的额外参数。
  • $name_id - 将用于构建LogoutRequest。如果未设置name_id参数并且认证对象处理了包含NameId的SAML响应,则将使用此NameId
  • $session_index - 用于标识用户会话的会话索引。
  • $stay - 如果我们想要停留(返回URL字符串)则为True,False表示重定向。
  • $nameIdFormat - NameID格式将在LogoutRequest中设置。
  • $nameIdNameQualifier - NameID NameQualifier将在LogoutRequest中设置。
  • $nameIdSPNameQualifier - NameID SP NameQualifier将在LogoutRequest中设置。

根据advanced_settings.php'logoutRequestSigned')中的安全信息,Logout Request将被签名或未签名发送。

身份提供者将通过用户的客户端将Logout Response发送到SP的单个注销服务。如果我们未在注销方法中设置'url'参数,并使用工具包提供的默认SLS(endpoints/sls.php),则SLS端点将重定向用户到启动SLO请求的文件。

我们可以设置一个'returnTo' URL来改变工作流程并将用户重定向到其他PHP文件。

$newTargetUrl = 'http://example.com/loggedOut.php';
$auth = new Saml2\Auth();
$auth->logout($newTargetUrl);

具有所有参数的更复杂的注销

$auth = new Saml2\Auth();

$auth->logout(
    null,
    [],
    $_SESSION['samlNameId'] ?? null,
    $_SESSION['samlSessionIndex'] ?? null,
    false,
    $_SESSION['samlNameIdFormat'] ?? null,
    $_SESSION['samlNameIdNameQualifier'] ?? null,
    $_SESSION['samlNameIdSPNameQualifier'] ?? null
);

如果需要匹配未来的LogoutResponse ID和要发送的LogoutRequest ID,则必须提取并存储该LogoutRequest ID。

$sloBuiltUrl = $auth->logout(null, [], $nameId, $sessionIndex, true);
$_SESSION['LogoutRequestID'] = $auth->getLastRequestID();
header('Pragma: no-cache');
header('Cache-Control: no-cache, must-revalidate');
header('Location: ' . $sloBuiltUrl);
exit();

一个示例视图,该视图启动SSO请求并处理响应(是acs目标)

我们可以编写一个独特的文件来启动SSO流程,处理响应,获取属性,启动SLO并处理注销响应。

注意:请查看包含该用例的demo1文件夹;在后面的部分中,我们将更详细地解释demo1用例。

<?php

session_start();    // Initialize the session, we do that because
                    // Note that processResponse and processSLO
                    // methods could manipulate/close that session

require_once __DIR__ . "/../vendor/autoload.php";
require_once 'settings.php';    // Load the setting info as an Array

$auth = new Saml2\Auth($settingsInfo);  // Initialize the SP SAML instance

if (isset($_GET['sso'])) {    // SSO action.  Will send an AuthNRequest to the IdP
    $auth->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 '<p>' . implode(', ', $errors) . '</p>';
    }
                                          // This check if the response was
    if (!$auth->isAuthenticated()) {      // sucessfully validated and the user
        echo '<p>Not authenticated</p>';  // data retrieved or not
        exit();
    }

    $_SESSION['samlUserdata'] = $auth->getAttributes(); // Retrieves user data
    if (isset($_POST['RelayState']) && 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 '<p>Sucessfully logged out</p>';
    } else {
        echo '<p>' . implode(', ', $errors) . '</p>';
    }
}

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:<br>';
        echo '<table><thead><th>Name</th><th>Values</th></thead><tbody>';
        foreach ($attributes as $attributeName => $attributeValues) {
            echo '<tr><td>' . htmlentities($attributeName) . '</td><td><ul>';
            foreach ($attributeValues as $attributeValue) {
                echo '<li>' . htmlentities($attributeValue) . '</li>';
            }
            echo '</ul></td></tr>';
        }
        echo '</tbody></table>';
    } else {                             // If there is not user data, we notify
        echo "<p>You don't have any attribute</p>";
    }

    echo '<p><a href="?slo" >Logout</a></p>'; // Print some links with possible
} else {                                      // actions
    echo '<p><a href="?sso" >Login</a></p>';
    echo '<p><a href="?sso2" >Login and access to attrs.php page</a></p>';
}

URL猜测方法

php-saml工具包使用Saml2\Utils中的一系列方法来猜测处理SAML消息的URL。

  • getSelfHost 返回当前主机。
  • getSelfPort 返回请求使用的端口号。
  • isHTTPS 检查协议是https还是http。
  • getSelfURLhost 返回协议 + 当前主机 + 端口(如果不同于常用端口)。
  • getSelfURL 返回当前主机 + 当前视图 + 查询的URL。
  • getSelfURLNoQuery 返回当前主机 + 当前视图的URL。
  • getSelfRoutedURLNoQuery 返回当前主机 + 当前视图的路由URL。

getSelfURLNoQuery和getSelfRoutedURLNoQuery用于计算currentURL以验证SAML元素如Destination或Recipient。

当PHP应用程序位于代理或负载均衡器后面时,我们可以执行setProxyVars(true)setSelfPort,此时isHTTPS将负责处理$_SERVER["HTTP_X_FORWARDED_PORT"]$_SERVER['HTTP_X_FORWARDED_PROTO']变量(否则它们将被忽略)。

此外,开发人员可以使用setSelfProtocolsetSelfHostsetSelfPortgetBaseURLPath来定义isHTTPSgetSelfHostgetSelfPortgetBaseURLPath返回的特定值。并定义一个setBasePath以用于getSelfURLgetSelfRoutedURLNoQuery,以替换从$_SERVER["REQUEST_URI"]中提取的数据。

在设置中,开发人员将能够设置一个'baseurl'参数,该参数将自动使用setBaseURL来为setSelfProtocolsetSelfHostsetSelfPortsetBaseURLPath设置值。

位于负载均衡器后面工作

当在SSL卸载的负载均衡器后面工作时,断言请求URL和SAML响应的Destination属性可能会失败。

您应该能够通过配置服务器使其知道代理并在请求时返回原始URL来解决这个问题。

或者使用前一部分中描述的方法。

SP密钥轮换

如果您计划更新SP的x509cert和privateKey,可以将新的x509cert定义为$settings['sp']['x509certNew'],并将其发布在SP元数据上,以便身份提供者可以读取它们并准备轮换。

具有多个证书的IdP

在一些场景中,身份提供者(IdP)使用不同的证书进行签名/加密,或者处于密钥轮换阶段,在IdP元数据上发布了多个证书。

为了处理这种情况,工具包提供了$settings['idp']['x509certMulti']参数。

当使用该参数时,工具包会忽略'x509cert''certFingerprint'值。

x509certMulti是一个包含2个键的数组

  • signing。一个证书数组,用于验证IdP签名
  • encryption 一个包含一个唯一证书的数组,用于加密发送给IdP的数据

重放攻击

为了避免重放攻击,您可以存储已处理的SAML消息的ID,以避免重复处理。由于消息会过期并且会因该原因失效,您不需要存储这些ID超过您当前接受的时间范围。

使用Auth对象的getLastMessageId/getLastAssertionId方法获取最后一个已处理消息/声明的ID。

主要类和方法

下面描述了可以调用的主要类和方法。

Saml2库

现在让我们描述SAML2库的类和方法。

Saml2\Auth - Auth.php

PHP工具包的主要类

  • Auth - 初始化SP SAML实例
  • login - 启动SSO过程。
  • logout - 启动SLO过程。
  • processResponse - 处理由IdP发送的SAML响应。
  • processSLO - 处理由IdP发送的SAML注销响应/注销请求。
  • redirectTo - 将用户重定向到通过参数传入的url或我们定义在SSO请求中的url。
  • isAuthenticated - 检查用户是否已认证。
  • getAttributes - 返回SAML属性的集合。
  • getAttribute - 返回请求的SAML属性
  • getNameId - 返回nameID
  • getNameIdFormat - 获取IdP提供的NameID格式
  • getNameIdNameQualifier - 获取SAML响应字符串中提供的NameID NameQualifier
  • getNameIdNameSPQualifier - 获取SAML响应字符串中提供的NameID SP NameQualifier
  • getSessionIndex - 从AuthnStatement获取SessionIndex
  • getErrors - 返回是否存在错误
  • getLastRequestID - 最后生成的SAML请求消息的ID
  • buildRequestSignature - 为SAML请求生成签名
  • buildResponseSignature - 为SAML响应生成签名
  • getSettings - 返回设置信息
  • setStrict - 设置严格模式激活/禁用
  • getLastRequestID - 获取由服务提供商生成的最后一个AuthNRequest或LogoutRequest的ID。
Saml2\AuthnRequest - AuthnRequest.php

SAML 2身份验证请求类

  • AuthnRequest - 构建AuthnRequest对象。
  • getRequest - 返回压缩的、base64编码的、未签名的AuthnRequest
  • getId - 返回AuthNRequest ID。
Saml2\Response - Response.php

SAML 2身份验证响应类

  • Response - 构建SAML响应对象。
  • isValid - 使用证书确定SAML响应是否有效。
  • checkStatus - 检查状态是否成功。
  • getAudiences - 获取受众。
  • getIssuers - 获取颁发者(从响应和断言中获取)
  • getNameIdData - 获取IdP提供的NameID数据
  • getNameId - 获取IdP提供的NameID
  • getNameIdFormat - 获取IdP提供的NameID格式
  • getNameIdNameQualifier - 获取SAML响应字符串中提供的NameID NameQualifier
  • getNameIdNameSPQualifier - 获取SAML响应字符串中提供的NameID SP NameQualifier
  • getSessionNotOnOrAfter - 从AuthnStatement获取SessionNotOnOrAfter
  • getSessionIndex - 从AuthnStatement获取SessionIndex
  • getAttributes - 从AttributeStatement元素获取属性
  • validateNumAssertions - 验证文档只包含单个断言(加密或未加密)。
  • validateTimestamps - 验证文档是否根据条件元素仍然有效。
  • getErrorException - 执行验证过程后,如果失败,此方法返回触发的异常。
Saml2\LogoutRequest - LogoutRequest.php

SAML 2 注销请求类

  • LogoutRequest - 构造注销请求对象。
  • getRequest - 返回注销请求的压缩、base64编码、未签名版本。
  • getID - 返回注销请求的ID。 (如果您有对象,可以访问id属性)
  • getNameIdData - 获取注销请求的NameID数据。
  • getNameId - 获取注销请求的NameID。
  • getIssuer - 获取注销请求的发行者。
  • getSessionIndexes - 获取注销请求中的SessionIndexes。
  • isValid - 检查接收到的注销请求是否有效。
  • getErrorException - 执行验证过程后,如果失败,此方法返回触发的异常。
  • getXML - 返回作为请求的一部分发送的XML或接收到的SP处的XML。
Saml2\LogoutResponse - LogoutResponse.php

SAML 2 注销响应类

  • LogoutResponse - 构造注销响应对象(从设置中初始化参数,如果提供则加载注销响应)
  • getIssuer - 获取注销响应的发行者。
  • getStatus - 获取注销响应的状态。
  • isValid - 确定SAML注销响应是否有效
  • build - 生成注销响应对象。
  • getResponse - 返回注销响应对象。
  • getErrorException - 执行验证过程后,如果失败,此方法返回触发的异常。
Saml2\Settings - Settings.php

SAML PHP工具包的配置

  • Settings - 初始化设置:设置不同文件夹的路径并从设置文件或提供的数组/对象中加载设置信息
  • checkSettings - 检查设置信息。
  • getExtLibPath - 返回外部库路径。
  • checkSPCerts - 检查SP的x509证书是否存在且有效。
  • getSPkey - 返回SP的x509私钥。
  • getSPcert - 返回SP的x509公钥。
  • getSPcertNew - 返回SP未来的x509公钥。
  • getIdPData - 获取IdP数据。
  • getSPData 获取SP数据。
  • getSecurityData - 获取安全数据。
  • getContacts - 获取联系数据。
  • getOrganization - 获取组织数据。
  • getSPMetadata - 获取SP元数据。XML表示形式。
  • validateMetadata - 验证XML SP元数据。
  • getErrors - 返回包含错误的一个数组,当设置正常时数组为空。
  • getLastErrorException - 返回与最后一个错误相关的异常
  • getBaseURL - 如果有设置,返回设置的baseurl。
  • setStrict - 激活或停用严格模式。
  • isStrict - 返回是否激活了'严格'模式。
Saml2\Metadata - Metadata.php

一个包含与SP元数据相关功能的类

  • builder - 根据设置生成SP的元数据。
  • signmetadata - 使用提供的密钥/证书对元数据进行签名。
  • addX509KeyDescriptors - 将x509描述符(签名/加密)添加到元数据中
Saml2\Utils - Utils.php

一个包含多个方法的辅助类

  • validateXML - 此函数尝试将XML字符串与指定的模式进行验证。
  • formatCert - 返回x509证书(如果需要添加头部和尾部)。
  • formatPrivateKey - 返回RSA私钥(如果需要添加头部和尾部)。
  • redirect - 执行到提供的url的重定向(或返回目标url)。
  • isHTTPS - 检查是否是https或http。
  • getSelfHost - 返回当前主机。
  • getSelfURLhost - 返回协议 + 当前主机 + 端口(如果与常用端口不同)。
  • getSelfURLNoQuery - 返回当前主机 + 当前视图的 URL。
  • getSelfURL - 返回当前主机 + 当前视图 + 查询的 URL。
  • generateUniqueID - 生成一个唯一的字符串(例如用作断言的 ID)。
  • parseTime2SAML - 将 UNIX 时间戳转换为 SAML2 时间戳,格式为 yyyy-mm-ddThh:mm:ss(.s+)?Z
  • parseSAML2Time - 将格式为 yyyy-mm-ddThh:mm:ss(.s+)?Z 的 SAML2 时间戳转换为 UNIX 时间戳。忽略子秒部分。
  • parseDuration - 解释与给定时间戳相关的 ISO8601 持续时间值。
  • getExpireTime - 比较两个日期并返回最早的一个。
  • query - 从 DOMDocument 中提取节点。
  • isSessionStarted - 检查会话是否已启动。
  • deleteLocalSession - 删除本地会话。
  • calculateX509Fingerprint - 计算 x509cert 的指纹。
  • formatFingerPrint - 格式化指纹。
  • generateNameId - 生成 nameID
  • getStatus - 从响应中获取状态。
  • decryptElement - 解密加密的元素。
  • castKey - 将 XMLSecurityKey 转换为正确的算法。
  • addSign - 将签名密钥和发送者证书添加到元素(消息或断言)中。
  • validateSign - 验证签名(消息或断言)。
Saml2\IdPMetadataParser - IdPMetadataParser.php

辅助类,包含多个用于检索和处理 IdP 元数据的方法

  • parseRemoteXML - 从 URL 获取 IdP 元数据信息。
  • parseFileXML - 从文件获取 IdP 元数据信息。
  • parseXML - 从 XML 获取 IdP 元数据信息。
  • injectIntoSettings - 将元数据信息注入 php-saml 设置数组。

有关更多信息,请参阅源代码;每个方法都有文档,并提供有关其做什么以及如何使用的详细信息。请确保也查看 doc 文件夹,其中提供了有关 SAML 和 SAML2 类和方法的 HTML 文档。

工具包中的演示

工具包包括三个演示应用程序,用于说明如何使用工具包,请查看它。

演示需要在使用前正确配置 SP 和 IdP。

Demo1

SP 设置

SAML PHP 工具包允许您以两种方式提供设置信息

  • 使用位于工具包基本目录下的 settings.php 文件。
  • 使用设置数据的数组。

在这个演示中,我们以第二种方式提供数据,使用名为 $settingsInfo 的设置数组。此数组使用包含的 settings_example.php 作为模板来创建 settings.php 设置,并将其存储在 demo1/ 文件夹中。配置 SP 部分,然后检查 IdP 的元数据并完成 IdP 信息。

如果您检查 index.php 文件的代码,您将看到 settings.php 文件被加载以获取 $settingsInfo 变量,以便用于初始化 Setting 类。

请注意,在这个演示中,可以定义在工具包基本文件夹中的 setting.php 文件被忽略。

IdP 设置

一旦配置了 SP,SP 的元数据就会发布在 metadata.php 文件中。根据该信息配置 IdP。

工作原理

  1. 第一次访问 index.php 视图时,您可以选择登录并返回到同一视图或登录并重定向到 attrs.php 视图。

  2. 当您点击

    第一个链接中的 2.1 时,我们访问到 (index.php?sso),向 IdP 发送 AuthNRequest,在 IdP 进行身份验证,然后通过用户的客户端将响应发送到 SP,特别是断言消费者服务视图:index.php?acs。请注意,已设置 RelayState 参数为启动过程的 URL,即 index.php 视图。

    2.2 在第二个链接中,我们访问到(attrs.php)的流程与2.1中描述的相同,不同之处在于将RelayState设置为attrs.php

  3. SAML响应在ACS(index.php?acs)中处理,如果响应无效,则在此处停止流程并显示消息。否则,我们将被重定向到RelayState视图。a) index.php或b) attrs.php

  4. 我们在应用中登录,并显示用户属性。在这个点上,我们可以测试单个注销功能。

  5. 单个注销功能可以通过两种方式进行测试。

    5.1 由SP触发的SLO。在SP上单击“注销”链接,之后向IdP发送注销请求,关闭IdP上的会话,并通过客户端向SP回复注销响应(发送到单点注销服务端点)。SP的SLS端点(index.php?sls)处理注销响应,如果有效,则关闭本地应用的会话。请注意,SLO工作流从SP开始并结束。

    5.2 由IdP触发的SLO。在这种情况下,操作发生在IdP端,注销流程在IdP处启动,向SP发送注销请求(SLS端点,index.php?sls)。SP的SLS端点处理注销请求,如果有效,则关闭本地应用的会话,并向IdP发送注销响应(发送到IdP的SLS端点)。IdP接收注销响应,处理它并关闭IdP的会话。请注意,SLO工作流从IdP开始并结束。

请注意,所有SAML请求和响应都由一个唯一的文件处理,即index.php文件,以及如何使用GET参数来知道必须执行的操作。

演示2

SP 设置

SAML PHP 工具包允许您以两种方式提供设置信息

  • 使用位于工具包基本目录下的 settings.php 文件。
  • 使用设置数据的数组。

首先是演示2应用的情况。应在工具包的基础文件夹中定义setting.php文件和setting_extended.php文件。查看setting_example.phpadvanced_settings_example.php>以了解如何构建它们。

在这种情况下,我们将使用端点文件夹中定位的文件(acs.phpsls.php)作为属性消耗服务(Attribute Consume Service)和单点注销服务(Single Logout Service)。

IdP 设置

一旦配置了SP,SP的元数据将被发布在metadata.php文件中。根据这些信息,配置IdP。

工作原理

在演示1中,我们看到所有SAML请求和响应都在一个文件中处理,即index.php文件。这个演示1使用高级编程。

在演示2中,我们有多个视图:index.phpsso.phpslo.phpconsume.phpmetadata.php。正如我们所说,我们将使用工具包中定义的端点(端点文件夹中的acs.phpsls.php)。这个演示2使用低级编程。

请注意,SSO操作可以从index.phpsso.php发起。

发生的SAML工作流与演示1中定义的工作流类似,只是更改了目标。

  1. 第一次访问index.phpsso.php时,会自动向IdP发送AuthNRequest(因为发送了原始url作为RelayState)。我们在IdP处进行认证,然后向SP发送响应,到ACS端点,在这种情况下是端点文件夹中的acs.php

  2. SAML响应在ACS中处理,如果响应无效,则在此处停止流程并显示消息。否则,我们将被重定向到RelayState视图(sso.phpindex.php)。sso.php检测用户是否已登录,并将其重定向到index.php,因此我们最终将在index.php中。

  3. 我们已登录到应用,并显示用户属性(如果有)。在这个点上,我们可以测试单个注销功能。

  4. 单个注销功能可以通过两种方式进行测试。

    4.1 由SP发起的SLO。在SP上点击“注销”链接后,我们将被重定向到slo.php视图,在那里向IdP发送注销请求,关闭IdP上的会话,并向SP发送注销响应(发送到单点注销服务端点)。在这种情况下,SP的SLS端点处理注销响应,如果有效,将关闭本地应用程序的用户会话。请注意,SLO工作流程在SP上开始和结束。

    4.2 由IdP发起的SLO。在这种情况下,操作发生在IdP端,注销过程在idP端启动,向SP(端点文件夹中的SLS端点sls.php)发送注销请求。SP的SLS端点处理注销请求,如果有效,将关闭本地应用程序中用户的会话,并向IdP(向IdP的SLS端点)发送注销响应。IdP接收注销响应,处理它并关闭IdP的会话。请注意,SLO工作流程在IdP上开始和结束。