Thinktecture.IdentityModel: WIF Support for WCF REST Services and OData
- by Your DisplayName here!
The latest drop of Thinktecture.IdentityModel includes
plumbing and support for WIF, claims and tokens for WCF REST services and Data Services
(aka OData).
Cibrax has an alternative implementation that
uses the WCF Rest Starter Kit. His recent post reminded me that I should finally “document”
that part of our library.
Features include:
generic plumbing for all WebServiceHost derived WCF services
support for SAML and SWT tokens
support for ClaimsAuthenticationManager and ClaimsAuthorizationManager
based solely on native WCF extensibility points (and WIF)
This post walks you through the setup of an OData / WCF DataServices endpoint with
token authentication and claims support. This sample is also included in the codeplex
download along a similar sample for plain WCF REST services.
Setting up the Data Service
To prove the point I have created a simple WCF Data Service that renders
the claims of the current client as an OData set.
public class ClaimsData
{
public IQueryable<ViewClaim>
Claims
{
get { return GetClaims().AsQueryable();
}
}
private List<ViewClaim>
GetClaims()
{
var claims
= new List<ViewClaim>();
var identity
= Thread.CurrentPrincipal.Identity as IClaimsIdentity;
int id
= 0;
identity.Claims.ToList().ForEach(claim
=>
{
claims.Add(new ViewClaim
{
Id
= ++id,
ClaimType
= claim.ClaimType,
Value
= claim.Value,
Issuer
= claim.Issuer
});
});
return claims;
}
}
…and hooked that up with a read only data service:
public class ClaimsDataService : DataService<ClaimsData>
{
public static void InitializeService(IDataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
}
}
Enabling WIF
Before you enable WIF, you should generate your client proxies. Afterwards
the service will only accept requests with an access token – and svcutil does not
support that.
All the WIF magic is done in a special service authorization manager called the FederatedWebServiceAuthorizationManager.
This code checks incoming calls to see if the Authorization HTTP header (or X-Authorization
for environments where you are not allowed to set the authorization header) contains
a token. This header must either start with SAML access_token= or WRAP
access_token= (for SAML or SWT tokens respectively).
For SAML validation, the plumbing uses the normal WIF configuration. For SWT you can
either pass in a SimpleWebTokenRequirement or the SwtIssuer, SwtAudience and SwtSigningKey app
settings are checked.If the token can be successfully validated, ClaimsAuthenticationManager and ClaimsAuthorizationManager are
invoked and the IClaimsPrincipal gets established.
The service authorization manager gets wired up by the FederatedWebServiceHostFactory:
public class FederatedWebServiceHostFactory : WebServiceHostFactory
{
protected override ServiceHost CreateServiceHost(
Type serviceType, Uri[]
baseAddresses)
{
var host
= base.CreateServiceHost(serviceType, baseAddresses);
host.Authorization.ServiceAuthorizationManager
=
new FederatedWebServiceAuthorizationManager();
host.Authorization.PrincipalPermissionMode
= PrincipalPermissionMode.Custom;
return host;
}
}
The last step is to set up the .svc file to use the service host factory (see the
sample download).
Calling the Service
To call the service you need to somehow get a token. This is up to you. You
can either use WSTrustChannelFactory (for the full CLR), WSTrustClient (Silverlight)
or some other way to obtain a token. The sample also includes code to generate SWT
tokens for testing – but the whole WRAP/SWT support will be subject of a separate
post.
I created some extensions methods for the most common web clients (WebClient, HttpWebRequest, DataServiceContext)
that allow easy setting of the token, e.g.:
public static void SetAccessToken(this DataServiceContext context,
string token, string type, string headerName)
{
context.SendingRequest
+= (s, e) =>
{
e.RequestHeaders[headerName]
= GetHeader(token, type);
};
}
Making a query against the Data Service could look like this:
static void CallService(string token, string type)
{
var data
= new ClaimsData(new Uri("https://server/odata.svc/"));
data.SetAccessToken(token,
type);
data.Claims.ToList().ForEach(c
=>
Console.WriteLine("{0}\n
{1}\n ({2})\n", c.ClaimType, c.Value, c.Issuer));
}
HTH