Using Windows CardSpace for securing a WPF smartclient, in WCF & token caching sauce
Hello everybody.
It’s some time that I have this sample in the buffer: I am publishing now in a rush, since this sundaly I leave for 2 weeks in EU for some nice CardSpace briefings here and there. I won’t be on mail very much, so I hope you will hold most questions for when I will be back in Redmond 🙂
I would really love to speak at lenght about this, but I really don’t have much time (I have also to mention all the ones that helped, and the list is long!:)). For the time being I am including an exerpt of the sample documentation: later I’ll go deeper on it.
Enjoy 🙂
Vittorio
———————————————
Windows CardSpace, WCF and Token Caching
Windows CardSpace provides a consistent experience across web and rich client scenarios. Windows Communication Foundation (WCF) supports CardSpace out of the box, supplying a powerful means of handling authentication in web service based applications: users enjoy an easy experience that shields them from the complexities of WS-Policy, while WCF receives a token for securing the messages.
The WCF programming model stores credentials on a per-channel basis: hence, in normal conditions the user would be prompted to choose a card as many times as a channel is created and used. WCF extensibility model, however, offers an easy way of modifying this behavior.
The sample presented here demonstrates how a simple WPF application can leverage CardSpace for securing the access to two different WCF web services, prompting the user only once.
Contents of this sample
This sample contains the following components:
A sample certificate
Installation scripts
The Simple WPF Smartclient solution
A sample selfissued card
This document
Certificate Setup
This example does not require any website setup. The example requires, however, the installation of an X509 certificate. The configuration is done using the installation batch file provided in the sample folder:
Install.bat
If you have troubles on Windows Vista, please install those certificates manually under the Local Machine store or refer to the documentation here.
For more information and troubleshooting tips, consult the document installation-instructions.doc located in the documents folder. Please note that this document provides help for a wide range of CardSpace examples, hence it sometimes refer to things (like websites setup) that do not apply to the sample here described.
The Application
The application is a WPF client which aggregates weather and traffic info on a simple map. It can be accessed by opening the solution MeteoAndTraffic.sln in Visual Studio. Before examining the code, let’s verify the intended behavior of the application: once you loaded the solution, start it by pressing F5. The screenshot below shows the application as it appears once it starts.
The user can click on the “Get Traffic Info” button for retrieving traffic
information from a web service: the data will be displayed in the form of different colors on the road segments. The button “Get Weather Info” will contact a different web service, which will reply with weather information on the area. Both web services are offered by the same organization, nonetheless they are exposed on different endpoints.
Both web services are secured by CardSpace, and configured for accepting the self issued card provided with the sample. The self issued card can be imported by double clicking on the file SampleCardsPrimaCard.crds: the password is “88888888” without the quotes.
Once the card has been imported, click on any of the two buttons mentioned above: the order is not important.
The system will open the identity selector, prompting for a self issued card.
Select the sample card “Prima Card” and hit Send: the identity selector will close and the application will perform the web service call.
Assuming that the button clicked was “Get Weather Info”, the application will show a scene similar to the following:
At this point, all subsequent calls will not prompt the user again regardless of the service invoked. Pushing the “Get Traffic Info” button will update the visualization with traffic data right away, without showing the identity selector.
If you want to be prompted again, it is enough to empty the token cache by pressing the “Flush Token” button: the next invocation will not find a suitable token in the cache, hence the user will be asked to select a card and restart the cycle.
Let’s take a closer look at the code that made this behavior possible.
The client code
The client application is a simple Windows Presentation Foundation application: the visual components were created with Microsoft Expression Interactive Designer, then the project has been imported into Visual Studio 2005. The details of the WPF design are out of the scope of this document and won’t be discussed here (refer to Vittorio’s blog for any question about it).
You can find the client project in the MeteoAndTraffic solution, under the MeteoAndTraffic project name. It contains the following files:
Application.xaml
Application.xaml.cs
AssemblyInfo.cs
App.config
MeteoServiceContract.cs
TrafficServiceContract.cs
Map.xaml
Map.xaml.cs
We can safely ignore Application.xaml , Application.xaml.cs and AssemblyInfo.cs.
The file App.config contains the typical WCF configuration which is necessary for invoking two endpoints with wsHttpBinding and CardSpace. If you are not familiar with the use of WCF and CardSpace, please refer to the SDK documentation or consider going through a virtual lab.
The MeteoServiceContract.cs and TrafficServiceContract.cs files contain the contract definitions of the weather and traffic web services, respectively. Again, please consult the documentation suggested above if those concepts are not clear.
Map.xaml contains WPF code defining the appearance of the main UI; Map.xaml.cs contains the initialization code and the actual web service invocation code. Here there is an excerpt of Map.xaml.cs:
…
public MapScene()
{
this.InitializeComponent();
}
void BtnTrafficClicked(object sender, RoutedEventArgs e)
{
try
{
ChannelFactory<TrafficService.ITraffic> cnFactory = new
ChannelFactory<TrafficService.ITraffic>(“TrafficClient”);
TrafficService.ITraffic chn = cnFactory.CreateChannel();
List<TrafficInfo> ti = chn.GetTrafficInfo();
cnFactory.Close();
UpdateTrafficUI(ti);
}
catch (Exception ee)
{
MessageBox.Show(“Something went wrong: nn” + ee.ToString());
}
}
…
That code fragment looks exactly like a usual WCF sample. Let’s assume for a moment that the associated config file contains the usual WCF+CardSpace configuration, as described here.
In BtnTrafficClicked, the event handler associated to the “Get Traffic Info” button, we:
1. Create a ChannelFactory connected to the contract type (TrafficService.ITraffic) and endpoint associated to the service we want to invoke
2. Use the ChannelFactory for instantiating a proxy to the service
3. Use the proxy for invoking the service and obtaining the data we need (in this case, a List<TrafficInfo>)
4. Close the channel (via ChannelFactory.Close()) and use the data obtained for updating the UI
From the description in the former section, we know that at a certain point during the execution of this code (namely in point 3 in the list above) the user has been prompted by the CardSpace UI. When the user selects one card, the system will obtain the corresponding security token (from the internal STS if a self issued card is selected, from an external identity provider in case of a managed card): this token will be stored in the channel and used to secure the call.
If I would reuse the same channel instance for subsequent calls, WCF would use the token already stored in the channel cache and would not ask the user to re-select the card. Since we close the channel, however, the token cache is lost and subsequent clicks on the “Get Traffic Info” button would cause the CardSpace UI to show again. Unfortunately, just avoiding destroying the channel does not solve the problem. If the user would press the “Get Weather Info” we would execute BtnMeteoClicked: the code would be perfectly analogous to what we have described for BtnTrafficClicked. The problem is that the channel instance would be necessarily different from the one we created in BtnTrafficClicked, so WCF would have to populate the token cache of this new channel and in so doing it would again prompt the user for selecting a card.
Fortunately, the extensibility model of WCF allows modifying the above described behavior; furthermore, it is possible to implement our custom caching behavior without imposing any modification to the C# code.
If you are interested in the details, please refer to the credential types explanation in the SDK documentation; in this context, it is enough to say that the token caching logic is managed by the ClientCredentials behavior. We can modify the way in which a channel handles credentials by replacing ClientCredentials from its behaviors list with our own implementation. For doing that we need to
1. Create our implementation of ClientCredentials
2. Change the configuration of the client so that it will take advantage of our class, as opposed to the ClientCredentials implementation supplied by the framework
In the remainder of this section we will explore 2., while in the next section we will get a closer look to the implementation details of our custom behavior.
Below you can see the first lines of the config file associated to our client:
<?xml version=”1.0″ encoding=”utf-8″ ?>
<configuration>
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name=”SimpleCardSpaceTokenClientCredentials” type=”SimpleCardSpaceTokenClientCredentialsSample.SimpleCardSpaceTokenClientCredentialsConfigHandler, SimpleCardSpaceTokenClientCredentials, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null” />
</behaviorExtensions>
</extensions>
In those lines we declare a config handler for our custom ClientCredentials class. This basically tells to the system that:
· There is a new behavior available, represented by the tag SimpleCardSpaceTokenClientCredentials
· The code for reading the configuration values is in the class SimpleCardSpaceTokenClientCredentialsSample.SimpleCardSpaceTokenClientCredentialsConfigHandler, provided by the assembly SimpleCardSpaceTokenClientCredentials.
The next lines represent the endpoints recognized by the client application.
<client>
<endpoint
name=”MeteoClient”
address=”http://localhost:4127/MeteoService/MeteoEndpoint”
contract=”MeteoService.IMeteo”
binding=”wsHttpBinding”
bindingConfiguration=”myBinding”
behaviorConfiguration=”MyClientBehavior”>
<identity>
<certificateReference
findValue=’www.fabrikam.com’
storeLocation=’LocalMachine’
storeName=’My’
x509FindType=’FindBySubjectName’ />
</identity>
</endpoint>
<endpoint
name=”TrafficClient”
address=”http://localhost:4128/TrafficService/TrafficEndpoint”
contract=”TrafficService.ITraffic”
binding=”wsHttpBinding”
bindingConfiguration=”myBinding”
behaviorConfiguration=”MyClientBehavior”>
<identity>
<certificateReference
findValue=’www.fabrikam.com’
storeLocation=’LocalMachine’
storeName=’My’
x509FindType=’FindBySubjectName’ />
</identity>
</endpoint>
</client>
<bindings>
<wsHttpBinding>
<binding name=”myBinding”>
<security mode=”Message”>
<message clientCredentialType=”IssuedToken”/>
</security>
</binding>
</wsHttpBinding>
</bindings>
Here it is all business as usual: there is no significant difference from what is described here. We define an endpoint for getting weather information and one for getting traffic data; they both share the same binding, wsHttpBinding, and the same behavior configuration.
The final lines of the configuration file are the one where we actually apply our custom behavior.
<behaviors>
<endpointBehaviors>
<behavior name=’MyClientBehavior’>
<!–<ClientCredentials>
<serviceCertificate>
<authentication
trustedStoreLocation=’LocalMachine’
revocationMode=’NoCheck’/>
<defaultCertificate
findValue=’www.fabrikam.com’
storeLocation=’LocalMachine’
storeName=’My’
x509FindType=’FindBySubjectName’ />
</serviceCertificate>
</ClientCredentials>–>
<SimpleCardSpaceTokenClientCredentials>
<serviceCertificate>
<authentication
trustedStoreLocation=’LocalMachine’
revocationMode=’NoCheck’/>
<defaultCertificate
findValue=’www.fabrikam.com’
storeLocation=’LocalMachine’
storeName=’My’
x509FindType=’FindBySubjectName’ />
</serviceCertificate>
</SimpleCardSpaceTokenClientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
In the commented element you will probably recognize the usual behavior, as described here. We substituted it with our custom behavior, SimpleCardSpaceTokenClientCredentials; note that the content of the element is exactly the same of the commented version. The semantic of every subelement does not change: the effect of every setting is preserved in the new implementation, too. The only significant difference when we use our custom behavior is that tokens will survive across channel instances, and will be shared by all the endpoints that make use of our behavior in the current process.
In the next section we will get a closer look at the SimpleCardSpaceTokenClientCredentials code, to see how we obtained this result.
[[[[[[[
This is exactly what we do in the highlighted lines:
· We add to the scene class a member of type SimpleCardSpaceTokenClientCredentials, our custom ClientCredentials behavior (more on the internals of SimpleCardSpaceTokenClientCredentials in the next section) and we initialize it in the constructor
· When we create the channel, we substitute the default ClientCredentials with our instance
The SimpleCardSpaceTokenClientCredentials class
The SimpleCardSpaceTokenClientCredentials class is contained in the project SimpleCardSpaceTokenClientCredentials, namely in the file SimpleCardSpaceTokenClientCredentials.cs. It derives from ClientCredentials. It will be instanced automatically whenever the system will find it in the behaviors list in an endpoint configuration, as we have shown in the former section.
The tokenCache static member will survive for the entire lifetime of the UI, and will be accessible by every SimpleCardSpaceTokenClientCredentials instance: this will allow us to share its content across all the channel instances that we will use for invoking services, for as long as the user will work with the application.
Simply saving the first token obtained from CardSpace and sharing it indiscriminately, however, is not a correct solution. Different services may be bonded to different policies: in that case, the token previously cached is simply not suitable for calling the new service and the CardSpace UI MUST appear. Furthermore, tokens can expire: it is then necessary to prompt the user again in order to obtain a fresh version of the credentials.
Let’s focus on the code that deals with the caching function. ClientCredentials has a method, GetInfoCardSecurityToken, that is called every time there is the need of obtaining a token from the CardSpace UI. This is the hook where we can implement our caching strategy: our custom class will override GetInfoCardSecurityToken, saving the new tokens obtained from the UI in a store and serving them back without invoking the UI if a suitable token was already cached. Below is a snippet of the relevant code.
/// <summary>
/// Custom ClientCredentials behavior, contianing caching logic
/// </summary>
public class SimpleCardSpaceTokenClientCredentials : ClientCredentials
{
/// <summary>
/// The static token cache, indexed by policy
/// </summary>
static Dictionary<PolicyChainKey,SecurityToken> tokenCache;
protected override SecurityToken GetInfoCardSecurityToken(bool requiresInfoCard, CardSpacePolicyElement[] chain, SecurityTokenSerializer tokenSerializer)
{
PolicyChainKey pck = new PolicyChainKey(chain);
TimeZone localZone = TimeZone.CurrentTimeZone;
if ((tokenCache.ContainsKey(pck)) && (localZone.ToLocalTime(tokenCache[pck].ValidFrom) < DateTime.Now) && (localZone.ToLocalTime(tokenCache[pck].ValidTo) > DateTime.Now))
{
return tokenCache[pck];
}
lock (tokenCache)
{
tokenCache.Add(pck, base.GetInfoCardSecurityToken(requiresInfoCard, chain, tokenSerializer));
}
return tokenCache[pck];
}
The highlighted member represents the true token cache. It is a dictionary of SecurityToken: every item is associated with a key which represents the policy conditions under which the token was obtained. We will explain this point in much greater detail later in the section.
The override of GetInfoCardSecurityToken is really straightforward:
1. The code derives a dictionary key from the policy requirements associated with the current token request (more on this later)
2. If the token cache
a. Contains a token compatible with the current policy requirements
b. Such a token is still valid
then the method returns such a token, without invoking the UI. Otherwise: a new token is obtained from the UI (via base.GetInfoCardSecurityToken), it gets cached and returned.
This is really easy. Of course, we need to take steps for ensuring that the mechanism will not be abused. For this reason, we cache tokens according to the policy that was enforced when every single token was originally obtained.
Namely, we concentrate on three elements:
1. The certificate associated with the web service we are calling (our target)
2. The URI of the issuer
3. The URI of the target
Before deciding to reuse a token we have in the cache, we have to verify if the current policy requirement is “equivalent” to the ones associated with the cached token.
The certificate HAS to be the same. If a token was obtained for a web service with a certain identity, in the form of an X509 certificate, the results of many cryptographic operations will be bonded to that certificate. Even the value of the PPID claim will depend on that.
The URI of the STS, from where we obtained the cached token, needs to be the same as well. We don’t want to repurpose any token against the explicit requirements of the policy which is mandating a well determined STS.
Where we have some degree of discretion is in the URI of the target. We can’t mandate that the two are exactly the same, of course: that would partially defy the very reasons for which we are building a cache. Two different services will have two different URIs for their endpoints, yet they may use the same token. This is the case of our example, where two web services are offered by the same entity: both services share the same x509 certificate, and the same base address. In this example we arbitrarily decided that two URIs are equivalent if the server name corresponds.
Such requirements have been expressed in the implementation of the PolicyChainKey class. The PolicyChainKey class contains three members which contain information about the target URI, the issuer URI and the certificate hash string associated with a token. PolicyChainKey overrides the Equals method in a way that expresses the equivalence concepts listed above.
Thanks to the Equals override, checking a hashtable for a certain PolicyChainKey key value corresponds to verifying if the cache contains a token that satisfies the policy requirements in the sense defined above.
The code below is the source of the PolicyChainKey class.
public class PolicyChainKey
{
string targetHost;
string issuer;
string certificate;
public PolicyChainKey(CardSpacePolicyElement[] chain)
{
if (chain.Length != 0)
{
EndpointAddress ea = EndpointAddress.ReadFrom(XmlDictionaryReader.CreateDictionaryReader(new XmlTextReader(new System.IO.StringReader(chain[0].Target.OuterXml))));
X509CertificateEndpointIdentity xcei = ea.Identity as X509CertificateEndpointIdentity;
certificate = xcei.Certificates[0].GetCertHashString();
targetHost = ea.Uri.Host;
if (chain[0].Issuer != null)
{
ea = EndpointAddress.ReadFrom(XmlDictionaryReader.CreateDictionaryReader(new XmlTextReader(new System.IO.StringReader(chain[0].Issuer.OuterXml))));
issuer = ea.Uri.ToString();
}
}
}
public override bool Equals(Object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
PolicyChainKey p = (PolicyChainKey)obj;
return (targetHost == p.targetHost) && (certificate == p.certificate) && (issuer == p.issuer);
}
public override int GetHashCode()
{
return (issuer == null) ? targetHost.GetHashCode() ^ certificate.GetHashCode() :
targetHost.GetHashCode() ^ certificate.GetHashCode() ^ issuer.GetHashCode();
}
}
The last class defined in SimpleCardSpaceTokenClientCredentials.cs is straightforward.
SimpleCardSpaceTokenClientCredentialsConfigHandler defines how to read the configuration tag, and it basically uses directly the implementation of the base class.
/// <summary>
/// Configuration element for the SimpleCardSpaceTokenClientCredentials behavior
/// </summary>
public class SimpleCardSpaceTokenClientCredentialsConfigHandler : ClientCredentialsElement
{
public override Type BehaviorType
{
get { return typeof(SimpleCardSpaceTokenClientCredentials); }
}
protected override object CreateBehavior()
{
SimpleCardSpaceTokenClientCredentials scstcc = new SimpleCardSpaceTokenClientCredentials();
base.ApplyConfiguration(scstcc);
return scstcc;
}
}
The services
The services do not need to be aware of what the client is doing: they play no part in implementing the caching behavior featured by the sample.
The two service projects, MeteoService and TrafficService, do not really contain anything different from the usual samples.
The only code we added simulates a very rough form of authorization control: the body of the method checks if the token used for securing the call contains the PPID of the sample self issued card we provided. You can easily modify that hardcoded value with a PPID of your own by changing the value of the AppSettings key “PPIDToCheck”, or even better substitute the entire check with a more proper authorization mechanism. Note that if you don’t provide any value for “PPIDToCheck” in the service configuration the authorization step will be skipped.
The snipped below is taken from the file Program.cs of the TrafficService project; the project MeteoService is perfectly equivalent.
class Traffic : ITraffic
{
private bool AuthorizeByPPID()
{
string ppid = System.Configuration.ConfigurationManager.AppSettings[“PPIDToCheck”];
if (ppid == null)
return true;
AuthorizationContext ac = ServiceSecurityContext.Current.AuthorizationContext;
foreach (ClaimSet ct in ac.ClaimSets)
foreach (Claim c in ct)
{
if (c.ClaimType == “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier”)
return (c.Resource.ToString() == ppid);
}
return false;
}
#region ITraffic Members
public List<TrafficInfo> GetTrafficInfo()
{
if (AuthorizeByPPID())
{…
Conclusion
Windows CardSpace provides a consistent mean of authenticating users, both on web applications and web services.
In this sample we have shown how you can obtain a smoother experience when developing smartclient applications, by reducing the number of unnecessary prompts with an adequate caching strategy.
If you have any question, please refer to the Windows CardSpace forums or to Vittorio’s blog.
7 Comments