次の方法で共有


OData and OAuth – protecting an OData Service using OAuth 2.0

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:

  • Delegation: In a delegation scenario a third party (generally an application) is granted access to a user’s resources without the user disclosing their credentials (username and password) to the third party.
  • Federation: In a federation scenario a user’s credentials on one domain (perhaps their corporate network) implies access to resources on a resource domain (say a data provider). They key though is that the credentials used (if any) on the resource domain are not disclosed to the end users and the user never discloses their credentials to the resource domain either.

 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.

The Scenario

We’re going to create an OData service based on this Entity Framework model for managing a user’s Favorite Uris:

image

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.

Business Rules

We need our Data Service to:

  • Automatically create a new user whenever someone with an unknown email-address hits the system.
  • Allow only administrators to query, create, update or delete users.
  • Allow Administrators to see all favorites.
  • Allow Administrators to update and delete all favorites.
  • Allow Users to see public favorites and their private favorites.
  • Allow Users to create new favorites. But the OwnerId, CreateDate and ‘Public’ values should be set for them, i.e. what they user sends on the wire will be ignored.
  • Allow Users to edit and delete only their favorites.
  • Allow un-authenticated requests to query only public favorites.

Implementation

Prerequisites

  • Windows Server 2008 R2 or Windows 7
  • Visual Studio 2010
  • Internet Information Services (IIS) enabled with IIS Metabase and IIS6 Configuration Compatibility
  • Windows Identity Foundation (WIF) (https://go.microsoft.com/fwlink/?LinkId=204657)
  • An existing Data Service Project that you want to protect.

Creating our Data Service

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;
   }

Authentication

Configuring ACS

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: 

  1. Sign in with your LiveId (or create a new one). Once you’ve logged on you’ll see something like this:

    image

  2. Click the ‘create a project’ link and choose a name for your project.

    image

  3. Click on your new project:

    image

  4. Click ‘Add Service Namespace’ and choose a service namespace that is available:

    image

    Then you will see this:

    image

  5. You’ll have to wait about 20 seconds for Azure to provision your namespace. Once it is active click on the ‘Access Control’ the link:

    image

  6. Click on ‘Identity Providers’ which will allow you to configure ACS to accept identities from Google, Yahoo and Live, by clicking ‘Add Identity Provider’ on the screen below:

    image

  7. Once you’ve added Google and Yahoo click on ‘Return to Access Control Service’

  8. Click on ‘Relying Party Applications’:

    image

    NOTE: As you can see there is already a ‘Relying Party Application’ called AccessControlManagement. That is the application we are currently using that manages our ACS instance. It trusts our ACS to make claims about the current user’s identity.

    As you can see this management application thinks I am an administrator (top right corner). This is because I logged on to AppFabric using LiveId as odatademo@hotmail.com who is the owner of this Service Namespace.

    Now we can create a relying party – i.e. something to represent our OData favorites service – which will ‘rely’ on ACS to make claims about who is making the request, to do this:

  9. Click on ‘Add Relying Party Application’.

    image
    image

  10. Fill in the form like this and then click ‘Save’

    Name: Choose a name that represents your Application
    Realm: Choose the ‘domain’ that you intend to host your application at. This will work even if you are testing on localhost first so long as web.config settings that control your OAuth security module match.
    Return URL: Choose some url relative to your domain, like in the above sample. Note this is not need by the server, this is only needed when we write a client – which we will do in the next blog post. You *will* need to change this value as you move from testing to live deployment, because your clients will actually follow this link.
    Error URL: Leave this blank
    Token format: Choose SWT (i.e. a Simple Web Token which can be embedded in request headers).
    Token lifetime (secs): Leave at the default.
    Identity providers: Leave the default.
    Rule groups: Leave the default.
    Token signing key: Click ‘Generate’ to produce a key or paste an existing key in.
    Effective date: Leave the default.
    Expiration date: Leave the default.

  11. Click on ‘Return to Access Control Service’.

  12. Click on ‘Rule Groups’

  13. Click on ‘Default Rule Group for [your relying party]’

    image

  14. Click on ‘Generate Rules’

  15. Leave all Identity Providers checked and click the ‘Generate’ button.

    You should see these rules get generated automatically:

    image

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.

Server Building Blocks

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.

Configuration

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:

image

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="https://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 == "https://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;
}

Real World Note:

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. 

Authorization

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.

Summary

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 James
Program Manager
Microsoft

Comments

  • Anonymous
    February 02, 2011
    Excellent sample.Could you please publish the sample code for other benefit?

  • Anonymous
    February 07, 2011
    Excellent post.How long does it extend the trial period?thanks

  • Anonymous
    February 10, 2011
    I've got everything building, and the HTTP module is being called, but the requests don't have an OAuth header and never go through the authorization routine...  How do I get IIS to challenge for the OAuth header?

  • Anonymous
    August 20, 2012
    Great post. Explains everything so clearly.

  • Anonymous
    June 13, 2013
    inappropriate example The topic is: OData and OAuth – protecting an OData Service using OAuth 2.0 not OData and OAuth – protecting an OData Service using OAuth 2.0 using AppFabric