Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ADFS STS authentication with console application

Tags:

c#

saml

adfs

I have a website and API secured with our corporate ADFS-backed token service. I need to hit an endpoint on the API with a C# console application. I am finding a lack of resources for using C# code to access STS secured websites. It uses ADFS 3.0.

When I use an HttpClient (or similar) to hit an endpoint I receive an HTML form in return.

My code:

Uri baseAddress = new Uri("http://localhost:64022");

using (HttpClient client = new HttpClient() { BaseAddress = baseAddress })
{
    HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "#");
    HttpResponseMessage response = client.SendAsync(request).Result;

    var encoding = ASCIIEncoding.ASCII;
    using (var reader = new System.IO.StreamReader(response.Content.ReadAsStreamAsync().Result, encoding))
    {
        string responseText = reader.ReadToEnd();
    }
}

The settings I have in my web.config file for my application are:

<system.identityModel.services>
    <federationConfiguration>
        <cookieHandler requireSsl="false" persistentSessionLifetime="1.0:0:0" />
        <wsFederation persistentCookiesOnPassiveRedirects="true" passiveRedirectEnabled="true" issuer="https://sts.company.com/adfs/ls/" realm="http://myapp.company.com/" requireHttps="false" />
    </federationConfiguration>
</system.identityModel.services>
<system.identityModel>
    <identityConfiguration>
        <audienceUris>
            <add value="http://myapp.company.com/" />
        </audienceUris>
        <issuerNameRegistry>
            <trustedIssuers>
                <add thumbprint="0000000000000000000000000000000000000000" name="https://sts.company.com/adfs/services/trust" />
            </trustedIssuers>
        </issuerNameRegistry>
    </identityConfiguration>
</system.identityModel>

I am not sure what the various terms will be. What will my remote address be? My client id? What is the thumbprint?

like image 369
hsimah Avatar asked Sep 01 '16 01:09

hsimah


2 Answers

I figured out how to accomplish this. I can't say for certain if this is the best implementation possible, but it works for me.

Class ADFS Token Provider

public class ADFSUsernameMixedTokenProvider
{
    private readonly Uri adfsUserNameMixedEndpoint;

    /// <summary>
    /// Initializes a new instance of the <see cref="ADFSUsernameMixedTokenProvider"/> class
    /// </summary>
    /// <param name="adfsUserNameMixedEndpoint">i.e. https://adfs.mycompany.com/adfs/services/trust/13/usernamemixed </param>
    public ADFSUsernameMixedTokenProvider(Uri adfsUserNameMixedEndpoint)
    {
        this.adfsUserNameMixedEndpoint = adfsUserNameMixedEndpoint;
    }

    /// <summary>
    /// Requests a security token from the ADFS server
    /// </summary>
    /// <param name="username">The username</param>
    /// <param name="password">The password</param>
    /// <param name="endpoint">The ADFS endpoint</param>
    /// <returns></returns>
    public GenericXmlSecurityToken RequestToken(string username, SecureString password, string endpoint)
    {
        WSTrustChannelFactory factory = new WSTrustChannelFactory(
                new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential),
                 new EndpointAddress(adfsUserNameMixedEndpoint));

        factory.TrustVersion = TrustVersion.WSTrust13;

        factory.Credentials.UserName.UserName = username;
        factory.Credentials.UserName.Password = new System.Net.NetworkCredential(string.Empty, password).Password;

        RequestSecurityToken token = new RequestSecurityToken
        {
            RequestType = RequestTypes.Issue,
            AppliesTo = new EndpointReference(endpoint),
            KeyType = KeyTypes.Bearer
        };

        IWSTrustChannelContract channel = factory.CreateChannel();

        return channel.Issue(token) as GenericXmlSecurityToken;
    }
}

Class Authentication

public class Authentication
{
    private GenericXmlSecurityToken token;
    private string site = "https://my.site.com"
    private string appliesTo = "http://my.site.com"
    private string authUsernameEndpoint = "https://sts-prod.site.com/adfs/services/trust/13/usernamemixed";

    public Authentication(PSCredential credential)
    {
        ADFSUsernameMixedTokenProvider tokenProvider = new ADFSUsernameMixedTokenProvider(new Uri(authUsernameEndpoint));
        token = tokenProvider.RequestToken(credential.UserName, credential.Password, appliesTo);
    }

    public CookieContainer GetFedAuthCookies()
    {
        string prepareToken = WrapInSoapMessage(token, appliesTo);
        string samlServer = site.EndsWith("/") ? site : site + "/";
        string stringData = $"wa=wsignin1.0&wresult={HttpUtility.UrlEncode(prepareToken)}&wctx={HttpUtility.UrlEncode("rm=1&id=passive&ru=%2f")}";

        CookieContainer cookies = new CookieContainer();
        HttpWebRequest request = WebRequest.Create(samlServer) as HttpWebRequest;
        request.Method = "POST";
        request.ContentType = "application/x-www-form-urlencoded";
        request.CookieContainer = cookies;
        request.AllowAutoRedirect = false;
        byte[] data = Encoding.UTF8.GetBytes(stringData);
        request.ContentLength = data.Length;

        using (Stream stream = request.GetRequestStream())
        {
            stream.Write(data, 0, data.Length);
        }

        using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
        {
            using (Stream stream = response.GetResponseStream())
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    string responseFromServer = reader.ReadToEnd();
                }
            }
        }

        return cookies;
    }

    private string WrapInSoapMessage(GenericXmlSecurityToken token, string site)
    {
        string validFrom = token.ValidFrom.ToString("o");
        string validTo = token.ValidTo.ToString("o");
        string securityToken = token.TokenXml.OuterXml;
        string soapTemplate = @"<t:RequestSecurityTokenResponse xmlns:t=""http://schemas.xmlsoap.org/ws/2005/02/trust""><t:Lifetime><wsu:Created xmlns:wsu=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"">{0}</wsu:Created><wsu:Expires xmlns:wsu=""http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"">{1}</wsu:Expires></t:Lifetime><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>{2}</wsa:Address></wsa:EndpointReference></wsp:AppliesTo><t:RequestedSecurityToken>{3}</t:RequestedSecurityToken><t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType></t:RequestSecurityTokenResponse>";

        return string.Format(soapTemplate, validFrom, validTo, site, securityToken);
    }
}

Usage

Authentication auth = new Authentication(credential);
CookieContainer container = auth.GetFedAuthCookies();
HttpWebRequest request = WebRequest.Create("https://api.my.site.com/") as HttpWebRequest;

request.Method = method;
request.ContentType = "application/json";
request.CookieContainer = cookieContainer;
request.AllowAutoRedirect = false;

using (WebResponse response = request.GetResponse())
{
    using (Stream dataStream = response.GetResponseStream())
    {
        using (StreamReader reader = new StreamReader(dataStream))
        {
            return JsonConvert.DeserializeObject<dynamic>(reader.ReadToEnd());
        }
    }
}

I use this with a PowerShell cmdlet, which is where the PSCredential object comes from. I hope this helps someone wanting to authenticate with ADFS 3.0 from a C# console application - it took me longer than I would like to admit.

like image 190
hsimah Avatar answered Nov 15 '22 00:11

hsimah


What version of ADFS are you dealing with? Based on version these are the best choices for Web API support

  • ADFS 2.0: In this case, the best pattern for web API is to use WS-Trust and WS-* for the interaction with the API over SOAP.
  • ADFS 2012R2 (or 3.0): You can use OAuth for this, probably your best bet. We have limited support for building mobile apps using the authorization grant profile. See https://msdn.microsoft.com/en-us/library/dn633593.aspx for additional information with a sample.
  • ADFS 2016 (or 4.0): You have the full gamut of OAuth/OpenID Connect supports web API, web app, multi-tier, single page app development patterns. See https://technet.microsoft.com/en-us/windows-server-docs/identity/ad-fs/ad-fs-development for the patterns.

Hope this helps.

Thanks //Sam

(Twitter: @MrADFS)

like image 23
SamuelD MSFT Avatar answered Nov 14 '22 22:11

SamuelD MSFT