Using WCF and WIF to perform WS-Trust active claim requests

The last week I have been working on a claim-based identity scenario that involves two separate instances of a Active Directory Federation Services (ADFS) 2.0 as Security Token Service (STS). The first STS is an identity provider STS (IP-STS) and the second a resource STS (R-STS) . The R-STS and IP-STS have a trust relationship in that the IP-STS is a trusted identity provider to the R-STS. Typically this setup is used with the passive profile for federation from browser clients. The web application (SharePoint 2010 for our scenario) would require authentication, redirect to the R-STS, which in turn would redirected (after IP selection) to the IP-STS. Here the end-user will authenticate, gets a identity claims from the IP-STS and is redirected back to the R-STS. The resource STS can augment claims from information stored in AD Lightweight Directory Services (AD-LDS) and issue the final claim token to the passively federated web application.

image

In our case we have a SharePoint 2007 application, which does not have its own STS to initiate the entire process. Also, business requirements demand that

  • the user interface is completely controlled
  • no redirects to other pages during authentication
  • authentication takes place from within a certain screen flow

These requirements made us investigate the active profile flow for the setup. In an active profile flow you will use the web services of ADFS 2.0 to communicate directly to the STS instances. You can use either the WS-Trust or WS-Federation protocol depending on your requirements. WS-Federation allows you to call the R-STS directly with the retrieval of the IP-STS claim performed under the covers. WS-Trust is used for the individuals steps. You can also leverage WS-Trust to take full control of the individual steps. To get acquainted with the entire active scenario we chose to the latter approach. Windows Communication Foundation (WCF) and Windows Identity Foundation (WIF) provide ready-to-use channels for the WS-Trust protocol and WS-Federation protocol. The scenario will take several steps:

  1. Acquire an identity claim from the IP-STS by using username/password authentication to authenticate.
  2. The IP-STS will issue a claim token regarding our identity, sign and encrypt it and send it back to us.
  3. Use the claim token from the IP-STS to authenticate against the R-STS and request a new claim token with additional claims from AD-LDS.
  4. The R-STS will also issue the (augmented) claim, sign and (optionally) encrypt it and send it.
  5. We need to decrypt the token, check its signature and issuer and convert the claim token to a principal object.

Note that we made some design choices, such as using username/password to authenticate against the IP-STS. You could make similar or different choices depending on your particular scenario.

Step 1: Acquiring an identity claims from an active STS

The first part of the process is the acquisition of a claim token from the IP-STS. We need one of the specialized WCF binding for a WS-Trust channel. Because we are using username and password to authenticate, this comes down to UserNameWSTrustBinding. We want to use transport security (an HTTPS connection) with credentials inside of the message (because Message security would mean that we need encryption and certificate exchange for this part). The endpoint at ADFS is https://identityadfs.company.com/services/trust/2005/usernamemixed to reflect our choices. The 2005 endpoint will use the newer WS-Trust 1.3 specification (as opposed to the February 2005 one).

image

var binding = new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential);
var factory = new WSTrustChannelFactory(binding, "https://identityadfs.company.com/adfs/services/trust/13/usernamemixed")
  {
    TrustVersion = TrustVersion.WSTrustFeb2005,
  };
 
factory.Credentials.UserName.UserName = @"youraccount";
factory.Credentials.UserName.Password = @"abc123!";
factory.ConfigureChannelFactory();

We now have a configured channel factory to create an actual WS-Trust channel. Across that channel we want to send a Request Security Token (RST) message.

var rst = new RequestSecurityToken
  {
    RequestType = WSTrust13Constants.RequestTypes.Issue,
    AppliesTo = new EndpointAddress(http://resourceadfs.company.com/adfs/services/trust),
    KeyType = WSTrust13Constants.KeyTypes.Symmetric
  };
var channel = factory.CreateChannel();
return channel.Issue(rst);

As you can see this RST message indicates an issue request for the audience “http://sourceadfs.company.com/adfs/services/trust/active”, which is the identifier of the relying party (the IP-STS) administration in the R-STS . In other words: a identity claim token intended to be shown to the R-STS.

We also ask the IP-STS issuer to encrypt the claim token with a symmetric key. You will see why this is in a moment. In other situations you could have asked for a Bearer (unecnrypted) key type token, which would have allowed you to inspect the token.

Step 2: Issue token from IP-STS

The IP-STS will accept and validate our credentials against the Active Directory Domain Services identity store. When successfully authenticated, ADFS will use its claim rules to extract information from ADDS and build a claim token with the indicated claims. Next it will encrypt this claim token using a certificate with a public key. This public key belongs to a public/private key pair that the R-STS holds. The IP-STS can encrypt the token, where only the R-STS can decrypt it using its private key.

image

Step 3: Use claim token to authenticate and request augmented claim token

Now, we can use the issued identity claim token to authenticate against the R-STS. We will again use a WS-Trust channel, but now with issued token authentication.

image

var binding = new IssuedTokenWSTrustBinding();
binding.SecurityMode = SecurityMode.TransportWithMessageCredential;
binding.KeyType = SecurityKeyType.SymmetricKey;
 
var factory = new WSTrustChannelFactory(binding, 
"https://resourceadfs.company.com/adfs/services/trust/13/issuedtokenmixedsymmetricbasic256"
); factory.TrustVersion = TrustVersion.WSTrust13; factory.Credentials.SupportInteractive = false;
factory.ConfigureChannelFactory();

The configuration of the channel factory is similar to the first step. The key differences are that we now have an IssuedTokenWSTrustBinding and a symmetric key for the issued token. The reason for choosing the symmetric encrypted request in the first step is that the R-STS does not accept Bearer key tokens for authentication. They have to be (a)symmetrically signed for the R-STS to use them as an authentication means. Also, we suppress the Interactive support (meaning InfoCard c.q. CardSpace UI), as we are initiating a service request (hence, no user is available).

Next, we create another RST message and send it out to the R-STS.

var rst = new RequestSecurityToken
  {
    RequestType = RequestTypes.Issue,
    AppliesTo = new EndpointAddress("http://sp2007webapp.company.com"),
    KeyType = KeyTypes.Bearer
  };   var channel = factory.CreateChannelWithIssuedToken(token);
return channel.Issue(rst);

The RST now indicates an audience for the SharePoint 2007 application. Its key type can be Bearer or (A)Symmetric, depending on how you want the token to be transferred. Should you choose the Symmetric or Asymmetric key type, you will need to install a server certificate on the web application’s IIS server, extract the public key and install it on the R-STS server as the encryption certificate for the relying party (i.e. the SharePoint app) trust.

Finally, using an extension method from WIF we create a channel from the factory that can convey the token as an authentication means. Lastly, we send out the RST message and return the token to the caller after step 4 has completed.

Step 4: Issuing an augmented claim

Once the issued token arrives at the R-STS, ADFS will decrypt the token using its public/private keypair. If it is a valid token, the R-STS will believe its trusted IP-STS that the credentials originally provided where correct. According to the configured claim rules the R-STS will create new and/or additional claims (called claim augmentation) to the new claim token. It will include the audience, sign it, encrypt it if you indicated so in the RST, and send out the RST response (RSTR).

image

Step 5: Check claim token and create principal

The last step is back on the SharePoint application.

GenericXmlSecurityToken augmentedToken = AugmentClaims(issuedToken);
var tokenReader = new StringReader(augmentedToken.TokenXml.OuterXml);
var reader = XmlReader.Create(tokenReader);

The issuedToken variable contains the token received from step 1 and 2. The AugmentClaims method is the implementation shown in step 3. When the R-STS returns the augmented claim, it will surface as a GenericXmlSecurityToken. We set up an XmlReader to start reading the token.

Remember that to read the token we need to decrypt it, check its signature, audience and issuer. This checking is taken care of by a set of token handlers

SecurityTokenHandlerCollection handlers = 
SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();

The default security handlers are some eight handlers for various types of tokens. They all use the configuration defined at the SecurityTokenHandlerCollection. First, we will add our public/private keypair as the certificate that the token resolver should use for decryption. The SecurityTokenResolver will use the thumbprint as a reference to the certificate

X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificates = store.Certificates;
X509Certificate2 certificate = certificates.Find(
X509FindType.FindByThumbprint,
"ThumbprintForWebAppPubPrivCertificateAllCapitals", true)[0];
List<SecurityToken> serviceTokens = new List<SecurityToken>(); serviceTokens.Add(new X509SecurityToken(certificate)); SecurityTokenResolver serviceResolver =
SecurityTokenResolver.CreateDefaultSecurityTokenResolver(
serviceTokens.AsReadOnly(), false); handlers.Configuration.ServiceTokenResolver = serviceResolver;

You will only need the code fragment for the token resolver above if you indicated an encrypted token. We did not, so these 8 lines of code are not necessary.

Next, we will add a certificate store for the issuer resolution. The token handlers will look up the issuer’s certificate thumbprint in the Personal certificates store of Local machine (not a user’s store).

SecurityTokenHandlerCollection handlers = 
SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
X509CertificateStoreTokenResolver certificateStoreIssuerResolver =
new X509CertificateStoreTokenResolver(StoreName.My, StoreLocation.LocalMachine);
handlers.Configuration.IssuerTokenResolver = certificateStoreIssuerResolver;
var registry = new ConfigurationBasedIssuerNameRegistry();
registry.AddTrustedIssuer("ThumbPrintForResourceADFSPubSigningAllCapitals", 
http://resourceadfs.company.com/adfs/services/trust);
handlers.Configuration.IssuerNameRegistry = registry;
handlers.Configuration.AudienceRestriction.AllowedAudienceUris.
Add(new Uri(http://sp2007webapp.company.com));

With all this setup, all that remains it to actually read (decrypt) the token and to check its signature, issuer and audience. The set of identities that is included will contain 1 IClaimsIdentity derived object containing our identity with corresponding claims.

var samlToken = handlers.ReadToken(reader);

IClaimsIdentity identity = handlers.ValidateToken(samlToken)[0];

All done

At this point we have a ClaimsIdentity that we can use to construct a claim principal and start reading claims for authorization inside our application. You might want to have the claims principal set to the current thread Thread.CurrentPrincipal and the HTTP context via HttpContext.Current.User. Inside the identity you can see the initial authentication type (username/password in our case) and for each claim the issuer (R-STS) and the original issuer (IP-STS, only when you passed through the original IP-STS claim as a R-STS claim).

All steps assume a happy flow, but you will need to include the usual exception handling for Timeout-, Communication-, ObjectDisposedException and other exceptions that may occur.

Hope this helps.

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s