ACS SAML / ADFS v2 Sample

The November 2009 CTP of ACS integrates with Active Directory Federation Server v2. ACS can act as a bridge between enterprise identity and REST web services.

The runtime flow is pretty simple (shown below).

image

  1. At runtime, the client app requests a SAML bearer token from AD FS v2. The easiest way to do this is with Windows Identity Foundation (WIF).
  2. The client app POSTs the SAML token to ACS over SSL. ACS uses configurable rules to calculate the claims in a Simple Web Token (SWT), creates a SWT, signs it, and returns it to the client app. The protocol for this exchange is OAuth WRAP.
  3. Next, the client packages the SWT in the HTTP Authorization header and sends it to the REST web service along with whatever payload the REST web service requires.
  4. Once the REST web service receives the token & payload, it validates the token and checks the claims in the token. The REST web services allows or denies access based on the outcome.

Viola. You have a REST web service that integrates with AD FS v2 via OAuth WRAP and SWT.

Mini AD FS setup (for this scenario only)

There is some setup required to enable this scenario (other than acquiring an ACS Service Namespace). For starters, you’ll need an AD FS v2 server. Since this requires a domain, I’ve provided a service that replicates the basic token issuing behavior of AD FS (at the bottom of this post).  The only relying party trusted by this service is ACS.

To setup the service, you’ll need to update the App.config file. Update the “signingCertName” to a cert in your LocalMachine / Personal cert store. Also update the “serviceNamespace” to your ACS service namespace.

 <?xml version="1.0" encoding="utf-8" ?> 
<configuration> 
  <appSettings> 
    <add key="signingCertName" value="CN=localhost"/> 
    <add key="stsBaseAddress" value="localhost/miniadfs"/> 
    <add key="stsPath" value="Trust/13/Windows"/> 
    <add key="serviceNamespace" value="justinpdcdemo"/> 
    <add key="acsHostname" value="accesscontrol.windows.net"/> 
  </appSettings> 
</configuration>

You’ll also have to setup SSL for your IIS install (https://learn.iis.net/page.aspx/144/how-to-setup-ssl-on-iis-70/)

You’ll also need to install the WIF RC. Available here: https://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=defd2019-a61f-4327-9332-6a4b6103527a#tm

From there, you should be able to run the service.

Fed Metadata Setup with ACS

After you have the mini ADFS service running, you’ll want to use the Fed Metadata it publishes to create an issuer in ACS. Also in the sample below is some code that shows you how to programmatically do that.

If you’d rather use a tool, you can use the Management Browser (https://code.msdn.microsoft.com/acmbrowser).

Simply create a new Issuer, select FedMetadata from the Algorithm drop down, and set the URL of the fed metadata server. In the miniADFS server, that URL is https://localhost/LocalADFSv2/FederationMetadata/2007-06/FederationMetadata.xml

image

Creating a Scope & Rule for the new Issuer

Next, you’ll want to create a scope and a rule that refers to that issuer. The sample at the bottom of this post uses a scope with an applies_to URI of https://localhost/samltest. You can use the Management Browser to create one.

With the scope in place, we can create a rule. All rules require the name of the Issuer and a claim type in the antecedent. When you create an Issuer using Fed Metadata, the Issuer name is fixed in the Fed Metadata. My MiniADFS server uses an issuer name of https://localhost/miniadfs/Trust/13/Windows. It also spits out claims of type https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name.

With that data, you can create a Passthrough rule. Passthrough rules basically countersign the input claims. In this case, a passthrough rule would countersign any https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name claim issued by the issuer https://localhost/miniadfs/Trust/13/Windows. The consequent of the rule can be of any type you choose. To keep the token compact, I’ll use a claim type of “name”.

You can set all this up using the management browser, as shown below.

image

Acquiring A SAML Token

With the Issuer, Scope, and Rule setup, let’s get a SAML token using WIF (the RC). The code for doing this is in the SAMLClient project from the code sample in this post. The WIF code is pretty straightforward:

 private static string GetSAMLToken()
{
    WSTrustChannelFactory trustChannelFactory =
        new WSTrustChannelFactory(new WindowsWSTrustBinding(SecurityMode.TransportWithMessageCredential),
            new EndpointAddress(new Uri(samlUrl)));

    trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;

    try
    {
        RequestSecurityToken rst =
            new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue, WSTrust13Constants.KeyTypes.Bearer);
        rst.AppliesTo = new EndpointAddress(acsUrl);
        rst.TokenType = Microsoft.IdentityModel.Tokens.SecurityTokenTypes.Saml2TokenProfile11;

        WSTrustChannel channel = (WSTrustChannel)trustChannelFactory.CreateChannel();
        GenericXmlSecurityToken token = channel.Issue(rst) as GenericXmlSecurityToken;
        string tokenString = token.TokenXml.OuterXml;
        return tokenString;
    }
    finally
    {
        trustChannelFactory.Close();
    }
}

The only trick is to ensure you are using the Bearer key type (Yes, you can use WIF to request a Bearer token).

Using the SAML token to get a SWT

Next, you can use the SAML token to request a SWT from ACS:

 private static string SendSAMLTokenToACS(string samlToken)
{
    try
    {
        WebClient client = new WebClient();
        client.BaseAddress = acsUrl;

        NameValueCollection parameters = new NameValueCollection();
        // ensure the applies_to URI is created in your ACS
        // service namespace
        parameters.Add("applies_to", "https://localhost/samltest");
        parameters.Add("wrap_SAML", samlToken);

        byte[] responseBytes = client.UploadValues("", parameters);
        string response = Encoding.UTF8.GetString(responseBytes);

        return response
            .Split('&')
            .Single(value => value.StartsWith("wrap_token=", StringComparison.OrdinalIgnoreCase))
            .Split('=')[1];
    }
    catch (WebException wex)
    {
        string value = new StreamReader(wex.Response.GetResponseStream()).ReadToEnd();
        throw;
    }
}

Viola! That’s all there is.

Here’s the full code sample – Let me know any feedback you have…