SAML 令牌提供程序

此示例演示如何实现自定义客户端 SAML 令牌提供程序。 Windows Communication Foundation(WCF)中的令牌提供程序用于向安全基础结构提供凭据。 令牌提供程序通常检查目标并颁发适当的凭据,以便安全基础结构可以保护消息。 WCF 随附有默认凭据管理器令牌提供程序。 WCF 还附带 CardSpace 令牌提供程序。 自定义令牌提供程序在以下情况下非常有用:

  • 存在不能由这些令牌提供程序操作的凭据存储。

  • 想要提供自己的自定义传输机制,以便从用户提供详细信息这一刻起到 WCF 客户端框架使用凭据时转换凭据。

  • 要生成一个自定义令牌。

此示例演示如何生成一个自定义令牌提供程序,该提供程序允许使用从 WCF 客户端框架外部获取的 SAML 令牌。

概括而言,此示例演示了以下内容:

  • 如何使用自定义令牌提供程序对客户端进行配置。

  • 如何将 SAML 令牌传递给自定义客户端凭据。

  • 如何将 SAML 令牌提供给 WCF 客户端框架。

  • 客户端如何通过服务器的 X.509 证书对其进行身份验证。

该服务公开两个终结点,用于与服务通信,使用配置文件 App.config定义。每个终结点由地址、绑定和协定组成。 绑定由使用消息安全的标准 wsFederationHttpBinding 进行配置。 一个终结点要求客户端使用使用对称证明密钥的 SAML 令牌进行身份验证,而另一个终结点要求客户端使用非对称证明密钥的 SAML 令牌进行身份验证。 该服务还使用 serviceCredentials 行为配置服务证书。 此 serviceCredentials 行为允许你配置服务证书。 客户端使用服务证书对服务进行身份验证并提供消息保护。 以下配置引用在示例安装过程中安装的“localhost”证书,如本主题末尾的设置说明中所述。 该 serviceCredentials 行为还允许配置受信任的证书来对 SAML 令牌进行签名。 以下配置引用示例期间安装的“Alice”证书。

<system.serviceModel>
 <services>
  <service
          name="Microsoft.ServiceModel.Samples.CalculatorService"
          behaviorConfiguration="CalculatorServiceBehavior">
   <host>
    <baseAddresses>
     <!-- configure base address provided by host -->
     <add
  baseAddress="http://localhost:8000/servicemodelsamples/service/" />
    </baseAddresses>
   </host>
   <!-- use base address provided by host -->
   <!-- Endpoint that expect SAML tokens with Symmetric proof keys -->
   <endpoint address="calc/symm"
             binding="wsFederationHttpBinding"
             bindingConfiguration="Binding1"
             contract="Microsoft.ServiceModel.Samples.ICalculator" />
   <!-- Endpoint that expect SAML tokens with Asymmetric proof keys -->
   <endpoint address="calc/asymm"
             binding="wsFederationHttpBinding"
             bindingConfiguration="Binding2"
             contract="Microsoft.ServiceModel.Samples.ICalculator" />
  </service>
 </services>

 <bindings>
  <wsFederationHttpBinding>
   <!-- Binding that expect SAML tokens with Symmetric proof keys -->
   <binding name="Binding1">
    <security mode="Message">
     <message negotiateServiceCredential ="false"
              issuedKeyType="SymmetricKey"
              issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1"  />
    </security>
   </binding>
   <!-- Binding that expect SAML tokens with Asymmetric proof keys -->
   <binding name="Binding2">
    <security mode="Message">
     <message negotiateServiceCredential ="false"
              issuedKeyType="AsymmetricKey"
              issuedTokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1"  />
    </security>
   </binding>
  </wsFederationHttpBinding>
 </bindings>

 <behaviors>
  <serviceBehaviors>
   <behavior name="CalculatorServiceBehavior">
    <!--
    The serviceCredentials behavior allows one to define a service certificate.
    A service certificate is used by a client to authenticate the service and provide message protection.
    This configuration references the "localhost" certificate installed during the setup instructions.
    -->
    <serviceCredentials>
     <!-- Set allowUntrustedRsaIssuers to true to allow self-signed, asymmetric key based SAML tokens -->
     <issuedTokenAuthentication allowUntrustedRsaIssuers ="true" >
      <!-- Add Alice to the list of certs trusted to issue SAML tokens -->
      <knownCertificates>
       <add storeLocation="LocalMachine"
            storeName="TrustedPeople"
            x509FindType="FindBySubjectName"
            findValue="Alice"/>
      </knownCertificates>
     </issuedTokenAuthentication>
     <serviceCertificate storeLocation="LocalMachine"
                         storeName="My"
                         x509FindType="FindBySubjectName"
                         findValue="localhost"  />
    </serviceCredentials>
   </behavior>
  </serviceBehaviors>
 </behaviors>

</system.serviceModel>

以下步骤演示如何开发自定义 SAML 令牌提供程序并将其与 WCF 集成:安全框架:

  1. 编写自定义 SAML 令牌提供程序。

    此示例实现了一个自定义的 SAML 令牌提供程序,该提供程序会基于构造时提供的 SAML 断言来返回安全令牌。

    为了执行此任务,从 SecurityTokenProvider 类派生了自定义令牌提供程序并重写了 GetTokenCore 方法。 此方法创建并返回一个新的 SecurityToken

    protected override SecurityToken GetTokenCore(TimeSpan timeout)
    {
     // Create a SamlSecurityToken from the provided assertion
     SamlSecurityToken samlToken = new SamlSecurityToken(assertion);
    
     // Create a SecurityTokenSerializer that will be used to
     // serialize the SamlSecurityToken
     WSSecurityTokenSerializer ser = new WSSecurityTokenSerializer();
     // Create a memory stream to write the serialized token into
     // Use an initial size of 64Kb
     MemoryStream s = new MemoryStream(UInt16.MaxValue);
    
     // Create an XmlWriter over the stream
     XmlWriter xw = XmlWriter.Create(s);
    
     // Write the SamlSecurityToken into the stream
     ser.WriteToken(xw, samlToken);
    
     // Seek back to the beginning of the stream
     s.Seek(0, SeekOrigin.Begin);
    
     // Load the serialized token into a DOM
     XmlDocument dom = new XmlDocument();
     dom.Load(s);
    
     // Create a KeyIdentifierClause for the SamlSecurityToken
     SamlAssertionKeyIdentifierClause samlKeyIdentifierClause = samlToken.CreateKeyIdentifierClause<SamlAssertionKeyIdentifierClause>();
    
    // Return a GenericXmlToken from the XML for the
    // SamlSecurityToken, the proof token, the valid from and valid
    // until times from the assertion and the key identifier clause
    // created above
    return new GenericXmlSecurityToken(dom.DocumentElement, proofToken, assertion.Conditions.NotBefore, assertion.Conditions.NotOnOrAfter, samlKeyIdentifierClause, samlKeyIdentifierClause, null);
    }
    
  2. 编写自定义安全令牌管理器。

    SecurityTokenManager 类用于为在 SecurityTokenProvider 方法中传入的特定 SecurityTokenRequirement 创建 CreateSecurityTokenProvider。 安全令牌管理器还用于创建令牌验证器和令牌序列化程序,但此示例未涵盖这些令牌。 在本示例中,自定义安全令牌管理器继承自 ClientCredentialsSecurityTokenManager 类并重写 CreateSecurityTokenProvider 方法,以便当传递的令牌需求指示请求 SAML 令牌时返回自定义 SAML 令牌提供程序。 如果客户端凭据类(请参阅步骤 3)未指定断言,则安全令牌管理器将创建适当的实例。

    public class SamlSecurityTokenManager : ClientCredentialsSecurityTokenManager
    {
     SamlClientCredentials samlClientCredentials;
    
     public SamlSecurityTokenManager ( SamlClientCredentials samlClientCredentials)
      : base(samlClientCredentials)
     {
      // Store the creating client credentials
      this.samlClientCredentials = samlClientCredentials;
     }
    
     public override SecurityTokenProvider CreateSecurityTokenProvider ( SecurityTokenRequirement tokenRequirement )
     {
      // If token requirement matches SAML token return the
      // custom SAML token provider
      if (tokenRequirement.TokenType == SecurityTokenTypes.Saml ||
          tokenRequirement.TokenType == "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1")
      {
       // Retrieve the SAML assertion and proof token from the
       // client credentials
       SamlAssertion assertion = this.samlClientCredentials.Assertion;
       SecurityToken prooftoken = this.samlClientCredentials.ProofToken;
    
       // If either the assertion of proof token is null...
       if (assertion == null || prooftoken == null)
       {
        // ...get the SecurityBindingElement and then the
        // specified algorithm suite
        SecurityBindingElement sbe = null;
        SecurityAlgorithmSuite sas = null;
    
        if ( tokenRequirement.TryGetProperty<SecurityBindingElement> ( "http://schemas.microsoft.com/ws/2006/05/servicemodel/securitytokenrequirement/SecurityBindingElement", out sbe))
        {
         sas = sbe.DefaultAlgorithmSuite;
        }
    
        // If the token requirement is for a SymmetricKey based token..
        if (tokenRequirement.KeyType == SecurityKeyType.SymmetricKey)
        {
         // Create a symmetric proof token
         prooftoken = SamlUtilities.CreateSymmetricProofToken ( tokenRequirement.KeySize );
         // and a corresponding assertion based on the claims specified in the client credentials
         assertion = SamlUtilities.CreateSymmetricKeyBasedAssertion ( this.samlClientCredentials.Claims, new X509SecurityToken ( samlClientCredentials.ClientCertificate.Certificate ), new X509SecurityToken ( samlClientCredentials.ServiceCertificate.DefaultCertificate ), (BinarySecretSecurityToken)prooftoken, sas);
        }
        // otherwise...
        else
        {
         // Create an asymmetric proof token
         prooftoken = SamlUtilities.CreateAsymmetricProofToken();
         // and a corresponding assertion based on the claims
         // specified in the client credentials
         assertion = SamlUtilities.CreateAsymmetricKeyBasedAssertion ( this.samlClientCredentials.Claims, prooftoken, sas );
        }
       }
    
       // Create a SamlSecurityTokenProvider based on the assertion and proof token
       return new SamlSecurityTokenProvider(assertion, prooftoken);
      }
      // otherwise use base implementation
      else
      {
       return base.CreateSecurityTokenProvider(tokenRequirement);
      }
    }
    
  3. 编写自定义客户端凭据。

    客户端凭据类用于表示为客户端代理配置的凭据,并创建一个安全令牌管理器,用于获取令牌验证器、令牌提供程序和令牌序列化程序。

    public class SamlClientCredentials : ClientCredentials
    {
     ClaimSet claims;
     SamlAssertion assertion;
     SecurityToken proofToken;
    
     public SamlClientCredentials() : base()
     {
      // Set SupportInteractive to false to suppress Cardspace UI
      base.SupportInteractive = false;
     }
    
     protected SamlClientCredentials(SamlClientCredentials other) : base ( other )
     {
      // Just do reference copy given sample nature
      this.assertion = other.assertion;
      this.claims = other.claims;
      this.proofToken = other.proofToken;
     }
    
     public SamlAssertion Assertion { get { return assertion; } set { assertion = value; } }
    
     public SecurityToken ProofToken { get { return proofToken; } set { proofToken = value; } }
     public ClaimSet Claims { get { return claims; } set { claims = value; } }
    
     protected override ClientCredentials CloneCore()
     {
      return new SamlClientCredentials(this);
     }
    
     public override SecurityTokenManager CreateSecurityTokenManager()
     {
      // return custom security token manager
      return new SamlSecurityTokenManager(this);
     }
    }
    
  4. 将客户端配置为使用自定义客户端凭据。

    该示例删除默认客户端凭据类并提供新的客户端凭据类,以便客户端可以使用自定义客户端凭据。

    // Create new credentials class
    SamlClientCredentials samlCC = new SamlClientCredentials();
    
    // Set the client certificate. This is the cert that will be used to sign the SAML token in the symmetric proof key case
    samlCC.ClientCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindBySubjectName, "Alice");
    
    // Set the service certificate. This is the cert that will be used to encrypt the proof key in the symmetric proof key case
    samlCC.ServiceCertificate.SetDefaultCertificate(StoreLocation.CurrentUser, StoreName.TrustedPeople, X509FindType.FindBySubjectName, "localhost");
    
    // Create some claims to put in the SAML assertion
    IList<Claim> claims = new List<Claim>();
    claims.Add(Claim.CreateNameClaim(samlCC.ClientCertificate.Certificate.Subject));
    ClaimSet claimset = new DefaultClaimSet(claims);
    samlCC.Claims = claimset;
    
    // set new credentials
    client.ChannelFactory.Endpoint.Behaviors.Remove(typeof(ClientCredentials));
    client.ChannelFactory.Endpoint.Behaviors.Add(samlCC);
    

在服务上,将显示与调用方关联的声明。 运行示例时,操作请求和响应将显示在客户端控制台窗口中。 在客户端窗口中按 Enter 关闭客户端。

设置批处理文件

此示例中包含的 Setup.bat 批处理文件允许使用相关证书配置服务器,以运行需要基于服务器证书的安全的自承载应用程序。 必须修改此批处理文件,以便跨计算机或在非承载情况下工作。

下面提供了批处理文件不同节的简要概述,以便可以修改批处理文件从而在相应的配置中运行。

  • 创建服务器证书:

    Setup.bat 批处理文件中的以下行创建要使用的服务器证书。 %SERVER_NAME%变量指定服务器名称。 更改此变量可以指定您自己的服务器名称。 此批处理文件中的默认值为 localhost。

    证书存储在 LocalMachine 存储位置下的 My(个人)存储区中。

    echo ************
    echo Server cert setup starting
    echo %SERVER_NAME%
    echo ************
    echo making server cert
    echo ************
    makecert.exe -sr LocalMachine -ss My -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
    
  • 将服务器证书安装到客户端的受信任证书存储中:

    Setup.bat 批处理文件中的以下行将服务器证书复制到客户端的受信任的人的存储区中。 此步骤是必需的,因为 Makecert.exe 生成的证书不受客户端系统隐式信任。 如果已有一个证书,该证书已植根于客户端受信任的根证书(例如Microsoft颁发的证书),则不需要使用服务器证书填充客户端证书存储区。

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r LocalMachine -s TrustedPeople
    
  • 创建发行者证书。

    Setup.bat 批处理文件中的以下行创建要使用的颁发者证书。 变量 %USER_NAME% 指定颁发者名称。 更改此变量以指定自己的颁发者名称。 此批处理文件中的默认值为 Alice。

    证书存储在 CurrentUser 存储位置下的 My(个人)存储区中。

    echo ************
    echo Server cert setup starting
    echo %SERVER_NAME%
    echo ************
    echo making server cert
    echo ************
    makecert.exe -sr CurrentUser -ss My -a sha1 -n CN=%USER_NAME% -sky exchange -pe
    
  • 将颁发者证书安装到服务器的受信任证书存储中。

    Setup.bat 批处理文件中的以下行将服务器证书复制到客户端的受信任的人的存储区中。 此步骤是必需的,因为 Makecert.exe 生成的证书不受客户端系统隐式信任。 如果已有一个证书,该证书已植根于客户端受信任的根证书(例如Microsoft颁发的证书),则不需要使用颁发者证书填充服务器证书存储区。

    certmgr.exe -add -r CurrentUser -s My -c -n %USER_NAME% -r LocalMachine -s TrustedPeople
    

设置和生成示例

  1. 确保已为 Windows Communication Foundation 示例 执行One-Time 安装过程。

  2. 要生成解决方案,请按照生成 Windows Communication Foundation 示例中的说明进行操作。

注释

如果使用 Svcutil.exe 重新生成此示例的配置,请确保修改客户端配置中的终结点名称以匹配客户端代码。

在同一计算机上运行示例

  1. 使用管理员权限从 Visual Studio 命令提示符内的示例安装文件夹运行 Setup.bat。 这会安装运行示例所需的所有证书。

    注释

    Setup.bat 批处理文件设计为通过 Visual Studio 命令提示符运行。 在 Visual Studio 命令提示中设置的 PATH 环境变量指向包含 Setup.bat 脚本所需的可执行文件的目录。

  2. 从 service\bin 启动 Service.exe。

  3. 从 \client\bin 启动 Client.exe。 客户端活动显示在客户端控制台应用程序中。

  4. 如果客户端和服务无法通信,请参阅 WCF 示例 故障排除提示。

跨计算机运行示例

  1. 在服务计算机上为服务二进制文件创建目录。

  2. 将服务程序文件复制到服务计算机上的服务目录。 此外,将 Setup.bat 和 Cleanup.bat 文件复制到服务计算机。

  3. 必须具有一个其主题名称中包含计算机的完全限定域名的服务器证书。 必须更新 Service.exe.config 文件以反映此新证书名称。 可以通过修改 Setup.bat 批处理文件来创建服务器证书。 请注意,setup.bat 文件必须在使用管理员权限打开的 Visual Studio 窗口的开发人员命令提示符下运行。 必须将变量 %SERVER_NAME% 设置为用于托管服务的计算机的完全限定主机名。

  4. 将服务器证书复制到客户端 CurrentUser-TrustedPeople 存储中。 当服务器证书由客户端受信任的颁发者颁发时,不需要执行此步骤。

  5. 在服务计算机的 Service.exe.config 文件中,更改基址的值以指定一个完全限定的计算机名称,而不是 localhost。

  6. 在服务计算机上,从命令提示符运行 Service.exe。

  7. 将 \client\bin\ 文件夹中的客户端程序文件(在特定于语言的文件夹下)复制到客户端计算机。

  8. 在客户端计算机上的 Client.exe.config 文件中,更改终结点的地址值以匹配服务的新地址。

  9. 在客户端计算机上,从命令提示符窗口启动 Client.exe

  10. 如果客户端和服务无法通信,请参阅 WCF 示例 故障排除提示。

运行示例后进行清理

  1. 运行完示例后,在示例文件夹中运行 Cleanup.bat。