In this post you will learn how to create an OData service that is protected using OAuth 2.0, which is the OData team’s official recommendation in these scenarios:
So if your scenarios is one of the above or some slight variation we recommend that you use OAuth 2.0 to protect your service, it provides the utmost flexibility and power.
To explore this scenario we are going to walkthrough a real-world scenario, from end to end.
We’re going to create an OData service based on this Entity Framework model for managing a user’s Favorite Uris:
As you can see this is a pretty simple model with just Users and Favorites.
Our service should not require its own username and password, which is a sure way to annoy users today. Instead it will rely on well-known third parties like Google and Yahoo, to provide the users identity. We’ll use AppFabric Access Control Services (aka ACS) because it provides an easy way to bridge these third parties claims and rewrite them as a signed OAuth 2.0 Simple Web Token or SWT.
The idea is that we will trust email-address claims issued by our ACS service via a SWT in the Authorization header of the request. We’ll then use a HttpModule to convert that SWT into a WIF ClaimsPrincipal.
Then our service’s job will be to map the EmailAddress in the incoming claim to a User entity in the database via the User’s EmailAddress property, and use that to enforce Business Rules.
We need our Data Service to:
First we add a DataService that exposes our Entity Framework model like this:
public class Favorites : DataService<FavoritesModelContainer>{ // This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.All); config.SetEntitySetPageSize("*", 100); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } }
You can use https://portal.appfabriclabs.com/ to create an AppFabric project, which will allow you to trial Access Control Services (or ACS) for free. The steps involved are:
This set of rules will take claims from Google, Yahoo and Windows Live Id, and pass them through untouched by sign then with the Token Signing Key we generated earlier.
Notice that LiveId claims don’t include an ‘emailaddress’ or ‘name’, so if we want to support LiveId our OAuth module on the server will need to figure out a way to convert a ‘nameidentifier’ claim into a ‘name’ and ‘emailaddress’ which is beyond the scope of this blog post.
At this point we’ve finished configuring ACS, and we can configure our OData Service to trust it.
We will rely on a sample the WIF team recently released that includes a lot of useful OAuth 2.0 helper code. This code builds on WIF adding some very useful extensions.
The most useful code for our purposes is a class called OAuthProtectionModule. This is a HttpModule that converts claims made via a Simple Web Token (SWT) in the incoming request’s Authorization header into a ClaimsPrincipal which it then assigns to HttpContext.Current.User.
If you’ve been following the OData and Authentication series, this general approach will be familiar to you. It means that by the time calls get to your OData service the HttpContext.Current.User has the current user (if any) and can be used to make decisions about whether to authorize the request.
There is a lot of code in the WIF sample that we don’t need. All you really need is the OAuthProtectionModule, so my suggestion is you pull that out into a separate project and grab classes from the sample as required. When I did that I moved things around a little and ended up with something that looked like this:
You might want to simplify the SamplesConfiguration class too, to remove unnecessary configuration information. I also decided to move the actual configuration into the web.config. When you make those changes you should end up with something like this:
public static class SamplesConfiguration{ public static string ServiceNamespace { get { return ConfigurationManager.AppSettings["ServiceNamespace"]; } }
public static string RelyingPartyRealm { get { return ConfigurationManager.AppSettings["RelyingPartyRealm"]; } } public static string RelyingPartySigningKey { get { return ConfigurationManager.AppSettings["RelyingPartySigningKey"]; } }
public static string AcsHostUrl { get { return ConfigurationManager.AppSettings["AcsHostUrl"]; } } }
Then you need to add your configuration information to your web.config:
<!-- this is the Relying Party signing key we generated earlier, i.e. the key ACS will use to sign the SWT – that our module can verify by signing and compariing --><add key="RelyingPartySigningKey" value="cx3SesVUdDE0yGYD+86BLzyffu0xPBRGUYR4wKPpklc="/><!-- the dns name of the SWT issuer --> <add key="AcsHostUrl" value="accesscontrol.appfabriclabs.com"/><!-- this is the your ACS ServiceNamespace of your OData service --><add key="ServiceNamespace" value="odatafavorites"/><!-- this is the intented url of your service (you don’t need to use a local address during development it isn’t verified --><add key="RelyingPartyRealm" value="http://favorites.odata.org/"/>
With these values in place the next step is to enable the OAuthProtectionModule too.
<system.webServer> <validation validateIntegratedModeConfiguration="false" /> <modules runAllManagedModulesForAllRequests="true"> <add name="OAuthProtectionModule" preCondition="managedHandler"type="OnlineFavoritesSite.OAuthProtectionModule"/> </modules></system.webServer>
With this in place any requests that include a correctly signed SWT in the Authorization header will have the HttpContext.Current.User set by the time you get into Data Services code.
Now we just need a function to pull back a User (from the Database) based on the EmailAddress claim contained in the HttpContext.Current.User by calling GetOrCreateUserFromPrinciple(..).
Per our business requirements this function automatically creates a new non-administrator user whenever a new EmailAddress is encountered. It talks to the database using the current ObjectContext which it accesses via DataService.CurrentDataSource.
public User GetOrCreateUserFromPrincipal(IPrincipal principal){ var emailAddress = GetEmailAddressFromPrincipal(principal); return GetOrCreateUserForEmail(emailAddress);}
private string GetEmailAddressFromPrincipal(IPrincipal principal){ if (principal == null) return null; else if ((principal is GenericPrincipal)) return principal.Identity.Name; else if ((principal is IClaimsPrincipal)) return GetEmailAddressFromClaim(principal as IClaimsPrincipal); else throw new InvalidOperationException("Unexpected Principal type");}
private string GetEmailAddressFromClaim(IClaimsPrincipal principal){ if (principal == null) throw new InvalidOperationException("Need a claims principal to extract EmailAddress claim"); var emailAddress = principal.Identities[0].Claims .Where(c => c.ClaimType == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") .Select(c => c.Value) .SingleOrDefault();
return emailAddress;}
private User GetOrCreateUserForEmail(string emailAddress){ if (emailAddress == null) throw new InvalidOperationException("Need an emailaddress");
var ctx = CurrentDataSource as FavoritesModelContainer; var user = ctx.Users.WhereDbAndMemory(u => u.EmailAddress == emailAddress).SingleOrDefault(); if (user == null) { user = new User { Id = Guid.NewGuid(), EmailAddress = emailAddress, CreatedDate = DateTime.Now, Administrator = false }; ctx.Users.AddObject(user); } return user;}
One thing that is interesting about this code is the call to WhereDbAndMemory(..) in GetOrCreateUserForEmail(..). Initially it was just a normal Where(..) call.
But that introduced a pretty sinister bug.
It turned out that often my query interceptors / change interceptors where being called multiple times in a single request and because this method creates a new user without saving it to the database every time it is called, it was creating more than one user for the same emailAddress. Which later failed the SingleOrDefault() test.
The solution is to look for any unsaved Users in the ObjectContext, before creating another User. To do this I wrote a little extension method that allows you to query both the Database and unsaved changes in one go:
public static IEnumerable<T> WhereDbAndMemory<T>( this ObjectQuery<T> sequence, Expression<Func<T, bool>> filter) where T: class{ var sequence1 = sequence.Where(filter).ToArray(); var state = EntityState.Added | EntityState.Modified | EntityState.Unchanged; var entries = sequence.Context.ObjectStateManager.GetObjectStateEntries(state); var merged = sequence1.Concat( entries.Select(e => e.Entity).OfType<T>().Where(filter.Compile()) ).Distinct(); return merged;}
By using this function we can be sure to only ever create one User for a particular emailAddress.
To implement our required business rules we need to create a series of Query and Change Interceptors that allow different users to do different things.
Our first interceptor controls who can query users:
[QueryInterceptor("Users")]public Expression<Func<User, bool>> FilterUsers(){ if (!HttpContext.Current.Request.IsAuthenticated) throw new DataServiceException(401, "Permission Denied"); User user = GetOrCreateUserFromPrincipal(HttpContext.Current.User); if (user.Administrator) return (u) => true; else throw new DataServiceException(401, "Permission Denied");}
Per our requirement this only allows authenticated Administrators to query Users.
Next we need an interceptor that only allows administrators to modify a user:
[ChangeInterceptor("Users")]public void ChangeUser(User updated, UpdateOperations operations){ if (!HttpContext.Current.Request.IsAuthenticated) throw new DataServiceException(401, "Permission Denied"); var user = GetOrCreateUserFromPrincipal(HttpContext.Current.User); if (!user.Administrator) throw new DataServiceException(401, "Permission Denied");}
And now we restrict access to Favorites:
[QueryInterceptor("Favorites")]public Expression<Func<Favorite, bool>> FilterFavorites(){ if (!HttpContext.Current.Request.IsAuthenticated) return (f) => f.Public == true; var user = GetOrCreateUserFromPrincipal(HttpContext.Current.User); var emailAddress = user.EmailAddress; if (user.Administrator) return (f) => true; else return (f) => f.Public == true || f.User.EmailAddress == emailAddress;}
As you can see administrators see everything, users see their favorites and everything public, and non-authenticated requests get to see just public favorites.
Finally we control who can create, edit and delete favorites:
[ChangeInterceptor("Favorites")]public void ChangeFavorite(Favorite updated, UpdateOperations operations){ if (!HttpContext.Current.Request.IsAuthenticated) throw new DataServiceException(401, "Permission Denied"); // Get the current USER or create the current user... var user = GetOrCreateUserFromPrincipal(HttpContext.Current.User); // Handle Inserts... if ((operations & UpdateOperations.Add) == UpdateOperations.Add) { // fill in the OwnerId, CreatedDate and Public properties updated.OwnerId = user.Id; updated.CreatedDate = DateTime.Now; updated.Public = false; } else if ((operations & UpdateOperations.Change) == UpdateOperations.Change) { // Administrators can do whatever they want. if (user.Administrator) return; // We don't trust the OwnerId on the wire (updated.OwnerId) because // we should never do security checks based on something that the client // can modify!!! var original = GetOriginal(updated); if (original.OwnerId == user.Id) { // non-administrators can't modify these values. updated.OwnerId = user.Id; updated.CreatedDate = original.CreatedDate; updated.Public = original.Public; return; }
// if we got here... they aren't allowed to do anything! throw new DataServiceException(401, "Permission Denied"); } else if ((operations & UpdateOperations.Delete) == UpdateOperations.Delete) { // in a delete operation you can’t update the OwnerId – it is impossible // in the protocol, so it is safe to just check that. if (updated.OwnerId != user.Id && !user.Administrator) throw new DataServiceException(401, "Permission Denied"); }}
Unauthenticated change requests are not allowed.
For additions we always set the ‘OwnedId’, ‘CreatedDate’ and ‘Public’ properties overriding whatever was sent on the wire.
For updates we allow administrators to make any changes, whereas owners can just edit their favorites, and they can’t change the ‘OwnerId’, ‘CreatedData’ or ‘Public’ properties.
It is also very important to understand that we have to get the original values before we check to see if someone is the owner of a particular favorite. We do this using this function that leverages some low level Entity Framework code:
private Favorite GetOriginal(Favorite updated){ // For MERGE based updates (which is the default) 'updated' will be in the // ObjectContext.ObjectStateManager. // For PUT based updates 'updated' will NOT be in the // ObjectContext.ObjectStateManager, but it will contain a copy // of the same entity. // So to normalize we should find the ObjectStateEntry in the ObjectStateManager // by EntityKey not by Entity. var entityKey = new EntityKey("FavoritesModelContainer.Favorites","Id", updated.Id); var entry = CurrentDataSource.ObjectStateManager.GetObjectStateEntry(entityKey); // Now we have the entity lets construct a copy with the original values. var original = new Favorite { Id = entry.OriginalValues.GetGuid(entry.OriginalValues.GetOrdinal("Id")), CreatedDate = entry.OriginalValues.GetDateTime(entry.OriginalValues.GetOrdinal("CreateDate")), Description = entry.OriginalValues.GetString(entry.OriginalValues.GetOrdinal("Description")), Name = entry.OriginalValues.GetString(entry.OriginalValues.GetOrdinal("Name")), OwnerId = entry.OriginalValues.GetGuid(entry.OriginalValues.GetOrdinal("OwnerId")), Public = entry.OriginalValues.GetBoolean(entry.OriginalValues.GetOrdinal("Public")), Uri = entry.OriginalValues.GetString(entry.OriginalValues.GetOrdinal("Uri")), };
return original;}
This constructs a copy of the unmodified entity setting all the properties from the original values in the ObjectStateEntry. While we don’t actually need all the original values, I personally hate creating a function that only does half a job; it is a bug waiting to happen.
Finally administrators can delete any favorites but users can only delete their own.
We’ve gone from zero to hero in this example, all our business rules are implemented, our OData Service is protected using OAuth 2.0 and everything is working great. The only problem is we don’t have a working client.
So in the next post we’ll create a Windows Phone 7 application for our OData service that knows how to authenticate.
Alex JamesProgram Manager Microsoft