Wouter Roos
Wouter Roos

Reputation: 201

Including SAML2.0 token in WCF service call without using WIF

I'm trying to set up a WCF service protected by ADFS. I'm currently able to request a token and send it with the request using WIF and Thinktecture IdentityModel 4.5 with the following code:

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);
}

With this I can call the WCF service by using ChannelFactory.CreateChannelWithIssuedToken:

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

This works as expected but can only be used on (mobile) windows platforms. I would also like to be able to use the same principle on iOS and Android. Using this article I was able to request a security token from ADFS using the following code:

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

This results in a SAML2.0 token which I would like to send in a request to our WCF service in order to authenticate. There are various sources (including the article mentioned earlier) which state that this should be possible but I've yet to find a solution.

Any help would be appreciated.

Upvotes: 14

Views: 3948

Answers (2)

malfus
malfus

Reputation: 391

This can easily be done without using WIF. Lets completely avoid WIF and the .Net framework and do it in Java for illustration purposes. First make a call to the Security Token Service using the template approach like you have done. You then need to extract the SAML from the response, Base64 encode it and stuff it in the Autorization header of the subsequent request to your protected WCF service. You may also need to do the same with a ProofKey if you are coding for Non-Repudiation. Also I'm only showing authentication using username/password for brevity as Certificate Authentication involves much more work - you have to hash (SHA1 )part of the message then encrypt the hash with the private key of the cert and then add this as a xml element to the original message etc...

Here is the java helper code:

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");
}

}

and here is example usage:

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

Upvotes: 0

Kuzgun
Kuzgun

Reputation: 4747

You can use one of hybrid solutions which use SAML with OAuth or other authorization technologies. This is more secure against phising techniques. For SAML only approach, you can refer to following link: How to pass security tokenfrom one wcf service to another wcf service. It is said that you need to enable saveBootstrapTokens property on webconfig.

This link can be useful too: Availability of Bootstrap Tokens

Upvotes: 1

Related Questions