在不使用 WIFI 的情况下在 WCF 服务调用中包含 SAML 2.0 令牌

Posted

技术标签:

【中文标题】在不使用 WIFI 的情况下在 WCF 服务调用中包含 SAML 2.0 令牌【英文标题】:Including SAML2.0 token in WCF service call without using WIF 【发布时间】:2014-02-13 02:28:19 【问题描述】:

我正在尝试设置受ADFS 保护的WCF 服务。我目前能够请求令牌并使用 WIFThinktecture IdentityModel 4.5 使用以下代码将其与请求一起发送:

static SecurityToken GetToken()

    var factory = new WSTrustChannelFactory(
          new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
          "https://fs2.server2012.local/adfs/services/trust/13/usernamemixed") 
    
        TrustVersion = TrustVersion.WSTrust13 
    ;


    if (factory.Credentials != null)
    
        factory.Credentials.UserName.UserName = @"username";
        factory.Credentials.UserName.Password = "password";
    

    var rst = new RequestSecurityToken
    
        RequestType = RequestTypes.Issue,
        KeyType = KeyTypes.Symmetric,
        AppliesTo = new EndpointReference(
            "https://wcfservicecertificate/wcfservice/Service.svc/wstrust"),
    ;

    var channel = factory.CreateChannel();
    RequestSecurityTokenResponse rstr;
    return channel.Issue(rst, out rstr);

有了这个我可以使用ChannelFactory.CreateChannelWithIssuedToken调用WCF服务:

var factory = new ChannelFactory<IService>(binding, 
    new EndpointAddress("https://wcfservicecertificate/wcfservice/Service.svc/wstrust"));
if (factory.Credentials != null)

    factory.Credentials.SupportInteractive = false;
    factory.Credentials.UseIdentityConfiguration = true;


var proxy = factory.CreateChannelWithIssuedToken(GetToken());
var result= proxy.GetData(2);

这可以按预期工作,但只能在(移动)Windows 平台上使用。我也希望能够在 iosandroid 上使用相同的原理。使用this article,我能够使用以下代码从 ADFS 请求安全令牌:

const string soapMessage =
@"<s:Envelope xmlns:s=""http://www.w3.org/2003/05/soap-envelope""
    xmlns:a=""http://www.w3.org/2005/08/addressing""
    xmlns:u=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"">
    <s:Header>
        <a:Action s:mustUnderstand=""1"">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
        <a:To s:mustUnderstand=""1"">https://fs2.server2012.local/adfs/services/trust/13/UsernameMixed</a:To>
        <o:Security s:mustUnderstand=""1"" xmlns:o=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"">
            <o:UsernameToken u:Id=""uuid-6a13a244-dac6-42c1-84c5-cbb345b0c4c4-1"">
            <o:Username>username</o:Username>
            <o:Password Type=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"">password</o:Password>
            </o:UsernameToken>
        </o:Security>
    </s:Header>
    <s:Body>
        <trust:RequestSecurityToken xmlns:trust=""http://docs.oasis-open.org/ws-sx/ws-trust/200512"">
            <wsp:AppliesTo xmlns:wsp=""http://schemas.xmlsoap.org/ws/2004/09/policy"">
            <a:EndpointReference>
                <a:Address>https://wcfservicecertificate/wcfservice/Service.svc/wstrust</a:Address>
            </a:EndpointReference>
            </wsp:AppliesTo>
            <trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>                        
            <trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
            <trust:TokenType>urn:oasis:names:tc:SAML:2.0:assertion</trust:TokenType>
        </trust:RequestSecurityToken>
    </s:Body>
</s:Envelope>";


var webClient = new WebClient();

webClient.Headers.Add("Content-Type", "application/soap+xml; charset=utf-8");

var result = webClient.UploadString(
        address: "https://fs2.server2012.local/adfs/services/trust/13/UsernameMixed",
        method: "POST",
        data: soapMessage);

这会产生一个 SAML2.0 令牌,我想向我们的 WCF 服务发送请求以进行身份​​验证。有各种来源(包括前面提到的文章)表明这应该是可能的,但我还没有找到解决方案。

任何帮助将不胜感激。

【问题讨论】:

我不知道这个问题的答案,但我很快就要自己解决这些问题,所以把它当作一个学习练习。根据我的研究,似乎更准确的问题陈述是“WCF 服务期望令牌在消息中的什么位置?”。我还不能让谷歌就这个问题给我一个直接的答案。祝你好运! 只是一个想法,但如果您使用 WebClient 获取 SAML 令牌,我会假设您将使用 WebClient 或其他 http 客户端向 WCF 端点发出请求。如果是这样,您可以使用 Fiddler 之类的工具检查您的工作 http 请求(*** c# 代码),然后使用 WebClient 复制它。 我建议您考虑使用 OAuth 2 和 JWT 令牌而不是 WS-Trust 和 SAML。 【参考方案1】:

您可以使用将 SAML 与 OAuth 或其他授权技术结合使用的混合解决方案之一。这对网络钓鱼技术更安全。对于仅 SAML 的方法,您可以参考以下链接:How to pass security tokenfrom one wcf service to another wcf service。据说需要在webconfig上开启saveBootstrapTokens属性。

这个链接也很有用:Availability of Bootstrap Tokens

【讨论】:

【参考方案2】:

无需使用 WIF 即可轻松完成。让我们完全避免 WIF 和 .Net 框架,并在 Java 中进行,以进行说明。首先像您所做的那样使用模板方法调用安全令牌服务。然后,您需要从响应中提取 SAML,对其进行 Base64 编码并将其填充到对受保护 WCF 服务的后续请求的 Autorization 标头中。如果您正在为不可否认性编码,您可能还需要对 ProofKey 执行相同的操作。此外,为了简洁起见,我只显示使用用户名/密码的身份验证,因为证书身份验证涉及更多工作 - 您必须对消息的一部分(SHA1)进行哈希处理,然后使用证书的私钥加密哈希,然后将其添加为 xml元素到原始消息等...

这里是java帮助代码:

import java.io.*;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Base64;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.HttpsURLConnection;

public class SecurityService 

private String _username;
private String _password;
private String _stsUrl;
private String _samlAssertion;
private String _samlEncoded;
private String _binarySecret;
private String _workingDirectory;
private String _platformUrl;
private String _soapBody;
private Integer _responseCode;
private Integer _plaformResponseCode;
private String _response;
private String _platformResponse;
private String _xproofSignature;
private Map<String, String> _headerDictionary;

public void setUsername(String username) 
    this._username = username;


public void setPassword(String password) 
    this._password = password;


public void setStsUrl(String stsUrl) 
    this._stsUrl = stsUrl;


public String getStsUrl() 
    return _stsUrl;


public void setplatformUrl(String platformUrl) 
    this._platformUrl = platformUrl;


public String getSamlAssertion() 
    return _samlAssertion;


public String getSamlEncoded() 
    return _samlEncoded;


public String getSoapBody() 
    return _soapBody;


public Integer getResponseCode() 
    return _responseCode;


public Integer getPlatformResponseCode() 
    return _plaformResponseCode;


public String getResponse() 
    return _response;


public String getPlatformResponse() 
    return _platformResponse;


public String getXProofSignature() 
    return _xproofSignature;


public String getBinarySecret() 
    return _binarySecret;


public String gePlatFormUrl() 
    return _platformUrl;


public void setHeaderDictionary(Map<String, String> headerDictionary)
   this._headerDictionary = headerDictionary;


public Map<String, String> getHeaderDictionary()
   return _headerDictionary;


public SecurityService() throws Exception 


public SecurityService(Boolean useConfig) throws Exception 

    if (useConfig) 
        this._workingDirectory = System.getProperty("user.dir") + "\\app.config";
        this.getProperties();
    
    

public void sendAuthenticatedGet() throws Exception 

    URL obj = new URL(_platformUrl);
    HttpURLConnection con = (HttpURLConnection) obj.openConnection();

    // optional default is GET
    con.setRequestMethod("GET");

    // Add request header        
    con.setRequestProperty("Authorization", "Saml " + _samlEncoded);
    con.setRequestProperty("X-ProofSignature", _xproofSignature);

    _plaformResponseCode = con.getResponseCode();       

    BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String inputLine;
    StringBuffer response = new StringBuffer();

    while ((inputLine = in.readLine()) != null) 
        response.append(inputLine);
    
    in.close(); 

    _platformResponse = response.toString();



public void sendAuthenticatedPost(String body) throws Exception 

    URL obj = new URL(_platformUrl);
    HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();

    //add request header
    con.setRequestMethod("POST");
    con.setRequestProperty("Content-Type", "application/json");

    // Add request header
    con.setRequestProperty("Authorization", "Saml " + _samlEncoded);
    con.setRequestProperty("X-ProofSignature", _xproofSignature);

    // Add Azure Subscription Key using generic Add Headers method
    if (_headerDictionary != null) 
        for (String key : _headerDictionary.keySet()) 
            con.setRequestProperty(key, _headerDictionary.get(key));
        
    

    _soapBody = body;

    // Send post request
    con.setDoOutput(true);
    DataOutputStream wr = new DataOutputStream(con.getOutputStream());
    //wr.writeBytes(urlParameters);
    wr.writeBytes(_soapBody);
    wr.flush();
    wr.close();
    _responseCode = con.getResponseCode();

    BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String inputLine;
    StringBuffer response = new StringBuffer();

    while ((inputLine = in.readLine()) != null) 
        response.append(inputLine);
    
    in.close();

    _response = response.toString();



// HTTP POST request
public void sendPostToSts() throws Exception 

    URL obj = new URL(_stsUrl);
    HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();

    //add request header
    con.setRequestMethod("POST");
    con.setRequestProperty("Content-Type", "application/soap+xml");

    String body = getTemplateCertificate();

    _soapBody = (((body.replace("[Created]", Instant.now().toString())).replace("[Expires]", Instant.now()
            .plusSeconds(300).toString())).replace("[username]", _username)).replace("[password]", _password).replace("[stsUrl]",                _stsUrl);

    // Send post request
    con.setDoOutput(true);
    DataOutputStream wr = new DataOutputStream(con.getOutputStream());
    //wr.writeBytes(urlParameters);
    wr.writeBytes(_soapBody);
    wr.flush();
    wr.close();
    _responseCode = con.getResponseCode();

    BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
    String inputLine;
    StringBuffer response = new StringBuffer();

    while ((inputLine = in.readLine()) != null) 
        response.append(inputLine);
    
    in.close();

    _response = response.toString();
    // Get Binary Secret
    // <trust:BinarySecret></trust:BinarySecret>

    final Pattern patternBinarySecret = Pattern.compile("<trust:BinarySecret>(.+?)</trust:BinarySecret>");
    final Matcher matcherBinarySecret = patternBinarySecret.matcher(response.toString());
    matcherBinarySecret.find();

    _binarySecret = matcherBinarySecret.group(1);

    // Get the SAML Assertion
    final Pattern patternEncryptedAssertion = Pattern.compile("<trust:RequestedSecurityToken>(.+?)</trust:RequestedSecurityToken>");
    final Matcher matcherEncryptedAssertion = patternEncryptedAssertion.matcher(response.toString());
    matcherEncryptedAssertion.find();
    _samlAssertion = matcherEncryptedAssertion.group(1);        


    byte[] proofKeyBytes = _binarySecret.getBytes("UTF-8");
    String encoded = Base64.getEncoder().encodeToString(proofKeyBytes);
    byte[] decoded = Base64.getDecoder().decode(encoded);

    // SAML Stuff - Works beautifully
    byte[] samlBytes = _samlAssertion.getBytes("UTF-8");
    _samlEncoded = Base64.getEncoder().encodeToString(samlBytes);       

    _xproofSignature = this.encode(_samlAssertion, _binarySecret);


private static String readFile( String file ) throws IOException 
    BufferedReader reader = new BufferedReader( new FileReader(file));
    String line = null;
    StringBuilder stringBuilder = new StringBuilder();
    String ls = System.getProperty("line.separator");

    try 
        while( ( line = reader.readLine() ) != null ) 
            stringBuilder.append( line );
            stringBuilder.append( ls );
        

        return stringBuilder.toString();
     finally 
        reader.close();
    


// Embedded WS-Trust template for username/password RST
private static String getTemplate () 
    return "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:a=\"http://www.w3.org/2005/08/addressing\" xmlns:u=               \"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\"><s:Header><a:Action s:mustUnderstand=               \"1\">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action><a:MessageID>urn:uuid:cfea5555-248c-46c3-9b4d-              54936b7f815c</a:MessageID><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To                s:mustUnderstand=\"1\">[stsUrl]</a:To><o:Security s:mustUnderstand=\"1\" xmlns:o=\"http://docs.oasis-               open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\"><u:Timestamp u:Id=\"_0\"><u:Created>[Created]              </u:Created><u:Expires>[Expires]</u:Expires></u:Timestamp><o:UsernameToken u:Id=\"uuid-e273c018-1da7-466e-8671-86f6bfe7ce3c-              17\"><o:Username>[username]</o:Username><o:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-              token-profile-1.0#PasswordText\">[password]              </o:Password></o:UsernameToken></o:Security></s:Header><s:Body><trust:RequestSecurityToken xmlns:trust=\"http://docs.oasis-               open.org/ws-sx/ws-trust/200512\"><wsp:AppliesTo xmlns:wsp=\"http://schemas.xmlsoap.org/ws/2004/09/policy               \"><wsa:EndpointReference xmlns:wsa=\"http://www.w3.org/2005/08/addressing               \"><wsa:Address>https://mbplatform/</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><trust:RequestType>http://docs.oasis-               open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType><trust:TokenType>http://docs.oasis-open.org/wss/oasis-wss-saml-token-              profile-1.1#SAMLV2.0</trust:TokenType></trust:RequestSecurityToken></s:Body></s:Envelope>";
    

private String encode(String key, String data) throws Exception 
    Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
    SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
    sha256_HMAC.init(secret_key);
    return Base64.getEncoder().encodeToString(sha256_HMAC.doFinal(data.getBytes("UTF-8")));


private void getProperties() throws Exception 
    Properties prop = new Properties();
    String fileName = _workingDirectory;
    InputStream is = new FileInputStream(fileName);
    prop.load(is);
    _username = prop.getProperty("app.username");
    _password = prop.getProperty("app.password");
    _platformUrl = prop.getProperty("app.platformUrl");
    _stsUrl = prop.getProperty("app.stsUrl");

这里是示例用法:

SecurityService mbss = new SecurityService(true);

    mbss.sendPostToSts();

    System.out.println("CONTACTING AZURE SECURITY TOKEN SERVICE");
    System.out.println("\nSending 'POST' request to URL : " + mbss.getStsUrl());
    System.out.println("\nPost parameters : \n" + mbss.getSoapBody());
    System.out.println("\nResponse Code : " + mbss.getResponseCode());
    System.out.println("\nHERE IS THE SAML RESPONSE\n");
    System.out.println(mbss.getResponse());
    System.out.println("\nHERE IS THE BINARY SECRET\n");
    System.out.println(mbss.getBinarySecret());
    System.out.println("\nHERE IS THE SAML ASSERTION\n");
    System.out.println(mbss.getSamlAssertion());
    System.out.println("\nHERE IS THE ENCODED SAML ASSERTION\n");
    System.out.println(mbss.getSamlEncoded());
    System.out.println("\nHERE IS THE X-PROOF SIGNATURE\n");
    System.out.println(mbss.getXProofSignature());
    System.out.println("\nNOW CONTACTING WCF SERVICES WITH SECURITY HEADER\n");

    mbss.sendAuthenticatedGet();


    System.out.println("\nSending 'GET' request to URL : " + mbss.gePlatFormUrl());
    System.out.println("Response Code : " + mbss.getPlatformResponseCode());
    System.out.println("\nHERE ARE THE RESULTS FOLKS...ENJOY\n");
    System.out.println(mbss.getPlatformResponse());

【讨论】:

以上是关于在不使用 WIFI 的情况下在 WCF 服务调用中包含 SAML 2.0 令牌的主要内容,如果未能解决你的问题,请参考以下文章

在不提示的情况下在 Powershell 中获取当前用户的凭据对象

如何在不通过 Web 调用的情况下在浏览器中查找地理位置

如何在不脱离控制台的情况下在 Windows 中的 Python 中执行 os.execv()?

如何在不为每条记录调用发送方法的情况下在 Kafka Avro 生产者中发送对象的 ArrayList? [复制]

我可以在不使用 prisma 的情况下在我的 graphQL 服务器中使用 MongoDB 吗?

是否可以在不使用继承的情况下在viewDidAppear上调用某些代码