Pass SAML token into web api call

13,498

Solution 1

When you want to access a resource which is protected via SSO (like ADFS, I assume), I found it easiest to use following approach: Show a WebBrowser element, let user enter credentials, then grab global cookies, and pass them into new HttpClient which performs the actual HTTP operation.

Here a complete code sample which downloads all build statuses from a SAML-protected Jenkins server:

private void Start()
{
    var t = new Thread(ThreadProc);
    t.SetApartmentState(ApartmentState.STA);
    t.Start();
    t.Join();
}

public async void ThreadProc()
{
    try
    {
        var urlBase = "https://JENKINS/";
        var url = urlBase + "job/JOBNAME/api/json?depth=1&tree=lastBuild[timestamp],builds[number,result,timestamp,url,actions[lastBuiltRevision[SHA1,branch[name]],totalCount,failCount,skipCount],building,duration]";

        var form = new Form();
        var browser = new System.Windows.Forms.WebBrowser();

        browser.SetBounds(0, 0, 400, 400);
        form.Size = new System.Drawing.Size(400, 400);
        form.Controls.AddRange(new Control[] { browser });
        form.FormBorderStyle = FormBorderStyle.FixedDialog;
        form.StartPosition = FormStartPosition.CenterScreen;
        form.MinimizeBox = false;
        form.MaximizeBox = false;

        // Navigate to base URL. It should internally forward to login form. After logging in, close browser window.
        browser.Navigate(urlBase);
        form.ShowDialog();

        var cookieString = GetGlobalCookies(urlBase);
        var cookieContainer = new System.Net.CookieContainer();
        using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
        using (var client = new HttpClient(handler) { BaseAddress = new Uri(urlBase, UriKind.Absolute) })
        {
            cookieContainer.SetCookies(client.BaseAddress, cookieString);
            var response = await client.GetAsync(url);
            if (response.IsSuccessStatusCode)
            {
                var responseStream = await response.Content.ReadAsStreamAsync();
                using (var reader = new System.IO.StreamReader(responseStream))
                {
                    var responseString = await reader.ReadToEndAsync();
                }
            }
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex);
    }
}

[System.Runtime.InteropServices.DllImport("wininet.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
private static extern bool InternetGetCookieEx(string pchURL, string pchCookieName,
    System.Text.StringBuilder pchCookieData, ref uint pcchCookieData, int dwFlags, IntPtr lpReserved);

private const int INTERNET_COOKIE_HTTPONLY = 0x00002000;

public string GetGlobalCookies(string uri)
{
    uint uiDataSize = 2048;
    var sbCookieData = new System.Text.StringBuilder((int)uiDataSize);
    if (InternetGetCookieEx(uri, null, sbCookieData, ref uiDataSize,
            INTERNET_COOKIE_HTTPONLY, IntPtr.Zero)
        &&
        sbCookieData.Length > 0)
    {
        return sbCookieData.ToString().Replace(";", ",");
    }
    return null;
}

Solution 2

(SET)

            string samlString = "blah blah blah";

            byte[] bytes = Encoding.UTF8.GetBytes(samlString);

            string base64SamlString = Convert.ToBase64String(bytes);

            myHttpClient.DefaultRequestHeaders.Add("X-My-Custom-Header", base64SamlString);

(GET)

        IEnumerable<string> headerValues = request.Headers.GetValues("X-My-Custom-Header");

        if (null != headerValues)

        {

            var encoding = Encoding.GetEncoding("iso-8859-1");

            string samlToken = encoding.GetString(Convert.FromBase64String(headerValues.FirstOrDefault()));

        }
Share:
13,498
Glen Hughes
Author by

Glen Hughes

Updated on August 02, 2022

Comments

  • Glen Hughes
    Glen Hughes almost 2 years

    I have a web application and web api services that authenticate through ADFS. They are contained in the same IIS application, and the web app makes calls back to the web api services without a problem.

    I'm now trying to call the same services from a different application, but am having trouble passing the token. I am able to authenticate and retrieve a SAML token with the following code:

    var stsEndpoint = "https://MyAdfsServer/adfs/services/trust/13/UsernameMixed";
    var reliantPartyUri = "https://MyDomain/AppRoot/";
    
    var factory = new Microsoft.IdentityModel.Protocols.WSTrust.WSTrustChannelFactory(
                new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
                new EndpointAddress(stsEndpoint));
    
    factory.TrustVersion = System.ServiceModel.Security.TrustVersion.WSTrust13;
    
    // Username and Password here...
    factory.Credentials.UserName.UserName = @"Domain\UserName";
    factory.Credentials.UserName.Password = "Password";
    
    var rst = new RequestSecurityToken
        {
            RequestType = RequestTypes.Issue,
            AppliesTo = new EndpointAddress(reliantPartyUri),
            KeyType = KeyTypes.Bearer,
        };
    
    var channel = factory.CreateChannel();
    var token = channel.Issue(rst) as GenericXmlSecurityToken;
    
    var saml = token.TokenXml.OuterXml;
    

    However, I'm not sure how to pass the saml in to the web api call. I've tried this:

    using (var handler = new HttpClientHandler() 
        { 
            ClientCertificateOptions = ClientCertificateOption.Automatic,
            AllowAutoRedirect = false
        })
    {
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://MyDomain/AppRoot/api/");
    
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("SAML", saml);
    
            HttpResponseMessage response = client.GetAsync("MyService/Get/").Result;
    
            // Get the results...
            var result = response.Content.ReadAsStringAsync().Result;
            var status = response.StatusCode;
        }
    }
    

    This is returning a status code of 302 and trying to redirect me to the ADFS server for authentication. Is there another way to pass the SAML token to the web api service?

  • dario_ramos
    dario_ramos over 8 years
    This approach gave me an error 411: Length required when I call encoding.GetString
  • granadaCoder
    granadaCoder over 8 years
    What is your string-length of "headerValues.FirstOrDefault()" ?
  • dario_ramos
    dario_ramos over 8 years
    Sorry, I ended up encoding the data as POST data (i.e. not as a header) and lost track of that code
  • Igor
    Igor about 3 years
    Actually worked for me, thank you so much it's what I was searching for, because I wanted to let the user do the login on SSO if not already done