Udostępnij za pośrednictwem


Storing per user data in Azure Mobile Services

Following up on a comment made in one of my posts, this one will talk about some ways to ensure that the data stored / retrieved in an Azure Mobile Services table is limited to the user which is using the application. That’s an interesting issue, and since I’ve done it wrong in the past, I think it’s worth discussing them here.

Before we move on, if you haven’t done so, I strongly recommend you to read the tutorial about Getting started with authentication in Mobile Services, since this post assumes that you already know how to add authentication to your app.

Client or server (or both)?

Like many other things you may want to implement in a connected mobile application, there are often two things which can be accomplished both in the client application, or as a server-side component. For example, in my last post I showed two ways of implementing a 1:n relationship between two model types both on the client and on the server side. The first app I was writing that I wanted to store per-user data in the Azure Mobile Service database, I started with a simple approach: add a new property to my model class (UserId), which I got from the MobileServiceUser type returned from the LoginAsync method. In my example (app to store movie ratings), my model data used to be (it’s simplified here for the sake of this post)

  1. publicclassMovieRating
  2. {
  3.     publicint Id { get; set; }
  4.     publicstring MovieName { get; set; }
  5.     publicint MovieRating { get; set; }
  6. }

And after the change, it gained a new property:

  1. publicclassMovieRating
  2. {
  3.     publicint Id { get; set; }
  4.     publicstring UserId { get; set; }
  5.     publicstring MovieName { get; set; }
  6.     publicint MovieRating { get; set; }
  7. }

When it’s time to do the operations, we fill the UserId property with the id of the logged on user in the mobile service client, then continue with the operation. For reading, we filter based on that user id, and only retrieve the appropriate reviews.

  1. privateasyncvoid btnInsertRating_Click_1(object sender, RoutedEventArgs e)
  2. {
  3.     var i = newRandom().Next(TestRatings.Count);
  4.     MovieRating rating = newMovieRating
  5.     {
  6.         Movie = TestRatings[i].Key,
  7.         Rating = TestRatings[i].Value,
  8.     };
  9.  
  10.     MobileServiceUser user = MobileService.CurrentUser;
  11.     rating.UserId = user.UserId;
  12.     await MobileService.GetTable<MovieRating>().InsertAsync(rating);
  13.     AddToDebug("Inserted rating: {0}", rating);
  14. }
  15.  
  16. privateasyncvoid btnReadRatings_Click_1(object sender, RoutedEventArgs e)
  17. {
  18.     var table = MobileService.GetTable<MovieRating>();
  19.     MobileServiceUser user = MobileService.CurrentUser;
  20.     var ratings = await table.Where(r => r.UserId == user.UserId).ToListAsync();
  21.     AddToDebug("Ratings for this user:");
  22.     foreach (var rating in ratings)
  23.     {
  24.         AddToDebug(" {0} - {1}", rating.Movie, rating.Rating);
  25.     }
  26. }

So that works fine so far – for delete operations we first query the object we want to delete (again, filtering by the user id) then call the DeleteAsync method to delete it. To update an item, same thing: retrieve the object, then update it via UpdateAsync.

Before we go any further, let me just start saying that this is not the correct approach, so don’t do that (I hope that nobody stopped reading this post before this line). Back to the normal flow…

First of all, this approach doesn’t feel right. We are adding a property (UserId) which really doesn’t belong to the model, and for all operations, we need to remember to fill that property with the appropriate value. There is, however, a much more severe problem with this approach – security. By adding the user id in the model, that id is now being passed in the body of the requests for inserting and updating data. Even if we change the permissions in our MovieRating table to “Only Authenticated Users” (see below), that means that any authenticated user can access that table. And since the tables can be accessed using its REST interface (which is what the Mobile Services client SDK use; more on the REST interface on a future post), it’s possible that a bad (albeit valid) user of the application can create, read, update and delete data that belongs to another (good) user.

TablePermissions

The request can be created using any networking tools such as Fiddler, the old HttpWebRequest class, or any networking client written in any language or platform which is capable of sending HTTPS requests. So any solution which deals with the client only will just not work.

Ensuring per-user data on the server side

So, client doesn’t work, we need to go to the server side. What we need to do, besides locking down the table permissions to only authenticated users as we did before, is to validate that the user is who claims he/she is. And to do this, we can check the user object which is passed to all operations in the table. For example, during insert we can verify that the user id passed in the item being inserted is the actual user which is calling the operation, and if they don’t match then don’t proceed with the operation.

  1. function insert(item, user, request) {
  2.     if (item.UserId !== user.userId) {
  3.         request.respond(statusCodes.BAD_REQUEST, "Invalid user id");
  4.     } else {
  5.         request.execute();
  6.     }
  7. }

That’s good, and doing that on other operations would work as well. But now that we have that information at the server side, we can do better. One of the problems of having the UserId property in the client model (IMO) is that it doesn’t really belong there – the model is cleaner with only the information for the data it represents (i.e., movie title and movie rating). Since we have the data at the server side, we can actually remove that from the client, and leave all of the user handling to the server side. We’re now back to the original client model:

  1. publicclassMovieRating
  2. {
  3.     publicint Id { get; set; }
  4.     publicstring Movie { get; set; }
  5.     publicint Rating { get; set; }
  6. }

And the operations in the client are now simplified:

  1. privateasyncvoid btnInsertRating_Click_1(object sender, RoutedEventArgs e)
  2. {
  3.     var i = newRandom().Next(TestRatings.Count);
  4.     MovieRating rating = newMovieRating
  5.     {
  6.         Movie = TestRatings[i].Key,
  7.         Rating = TestRatings[i].Value,
  8.     };
  9.  
  10.     await MobileService.GetTable<MovieRating>().InsertAsync(rating);
  11.     AddToDebug("Inserted rating: {0}", rating);
  12. }
  13.  
  14. privateasyncvoid btnReadRatings_Click_1(object sender, RoutedEventArgs e)
  15. {
  16.     var table = MobileService.GetTable<MovieRating>();
  17.     var ratings = await table.ToListAsync();
  18.     AddToDebug("Ratings for this user:");
  19.     foreach (var rating in ratings)
  20.     {
  21.         AddToDebug(" {0} - {1}", rating.Movie, rating.Rating);
  22.     }
  23. }

Now that we removed the logic from the client, we need to insert that in the server, so let’s go over each operation. Insert is trivial: prior to executing the request, we add the extra property in the object being inserted, and let the Mobile Service runtime take care of the rest.

  1. function insert(item, user, request) {
  2.     item.UserId = user.userId;
  3.     request.execute();
  4. }

Querying is also simple. What we need to do is to take whatever filter parameters which were sent, and add one more, to filter only the records in the table whose UserId property is equal to the id of the logged on user.

  1. function read(query, user, request) {
  2.     query.where({ UserId: user.userId });
  3.     request.execute();
  4. }

Delete and update operations are a little trickier. On a delete script, we are passed the object id and the user object. So to ensure that someone is only deleting their own objects, we first need to query the table based on that id, and only if it matches that we can proceed with the deletion.

  1. function del(id, user, request) {
  2.     var table = tables.getTable('MovieRating');
  3.     table.where({ id: id }).read({
  4.         success: function (results) {
  5.             if (results.length) {
  6.                 var item = results[0];
  7.                 if (item.UserId === user.userId) {
  8.                     request.execute();
  9.                 } else {
  10.                     request.respond(statusCodes.BAD_REQUEST, "Invalid user"); // users don't match
  11.                 }
  12.             } else {
  13.                 request.respond(statusCodes.NOT_FOUND);
  14.             }
  15.         }, error: function () {
  16.             request.respond(statusCodes.NOT_FOUND);
  17.         }
  18.     });
  19. }

And in the client:

  1. privateasyncvoid btnDeleteRating_Click_1(object sender, RoutedEventArgs e)
  2. {
  3.     var table = MobileService.GetTable<MovieRating>();
  4.     var ratings = await table.ToListAsync();
  5.     if (ratings.Count > 0)
  6.     {
  7.         MovieRating toDelete = ratings[newRandom().Next(ratings.Count)];
  8.         await table.DeleteAsync(toDelete);
  9.         AddToDebug("Deleted rating: {0}", toDelete.Id);
  10.     }
  11.     else
  12.     {
  13.         AddToDebug("No items for this user");
  14.     }
  15. }

So what if some bad client tried to delete data from another user? It’s not too hard to do. With Fiddler, I captured the request sent from the DeleteAsync call:

Request

DELETE https://movies.azure-mobile.net/tables/MovieRating/7 HTTP/1.1
Accept: application/json
X-ZUMO-INSTALLATION-ID: <REMOVED>
X-ZUMO-APPLICATION: <REMOVED>
X-ZUMO-AUTH: <REMOVED>
Content-Type: application/json
Host: movies.azure-mobile.net
Content-Length: 60
Expect: 100-continue

{"id":7,"Movie":"The Empire Strikes Back (1980)","Rating":5}

Response:

HTTP/1.1 204 No Content
Cache-Control: no-cache
Content-Type: application/json
Server: Microsoft-IIS/7.5
Set-Cookie: ARRAffinity=…
x-zumo-version: Zumo.Main.0.1.6.419.Runtime
X-Powered-By: ARR/2.5
X-Powered-By: ASP.NET
Date: Fri, 14 Sep 2012 17:06:06 GMT

Notice that the request body isn’t really necessary (as is the case for most DELETE handlers). Now with Fiddler’s Composer, we can try to send a request to delete an item which belongs to another user:

Request:

DELETE https://movies.azure-mobile.net/tables/MovieRating/2 HTTP/1.1
Accept: application/json
X-ZUMO-INSTALLATION-ID: <REMOVED>
X-ZUMO-APPLICATION: <REMOVED>
X-ZUMO-AUTH: <REMOVED>
Content-Type: application/json
Host: movies.azure-mobile.net
Content-Length: 0
Expect: 100-continue

If we hadn’t added the check on the script, that request would have been accepted, and the data deleted. But with the check we added in the delete script, we get a proper response back:

Response:

HTTP/1.1 400 Bad Request
Cache-Control: no-cache
Content-Type: application/json
Server: Microsoft-IIS/7.5
Set-Cookie: ARRAffinity=…
x-zumo-version: Zumo.Main.0.1.6.419.Runtime
X-Powered-By: ASP.NET
X-Powered-By: ARR/2.5
Date: Fri, 14 Sep 2012 17:16:36 GMT
Content-Length: 14

"Invalid user"

For update we need to do something similar to delete. First query the table, check if the id is the same, and only then proceed.

  1. function update(item, user, request) {
  2.     var table = tables.getTable('MovieRating');
  3.     table.where({ id: item.id }).read({
  4.         success: function (results) {
  5.             if (results.length) {
  6.                 var existingItem = results[0];
  7.                 if (existingItem.UserId === user.userId) {
  8.                     request.execute();
  9.                 } else {
  10.                     request.respond(statusCodes.BAD_REQUEST, "Invalid user"); // users don't match
  11.                 }
  12.             } else {
  13.                 request.respond(statusCodes.NOT_FOUND);
  14.             }
  15.         }, error: function () {
  16.             request.respond(statusCodes.NOT_FOUND);
  17.         }
  18.     });
  19. }

And that’s it. Right now, my Azure Mobile Service is storing per-user data, and it’s also protected against malicious users of my application from accessing other users’ data.

Wrapping up

As with the previous posts, the code shown here is for the preview version of Windows Azure Mobile Services. If you have any comments / suggestions, feel free to post them as comments in this (or other) post, or use the Windows Azure Mobile Services forum for that as well. Let us know what you think!

MovieRatings.zip

Comments

  • Anonymous
    December 12, 2012
    I noticed that in the update script the item parameter is being overwritten by the result from the db.var item = results[0];Doesnt that mean that whatever came from the client was pretty much erased? thus not actually updating the resource at all.
  • Anonymous
    December 12, 2012
    The comment has been removed
  • Anonymous
    December 13, 2012
    Thanks Carlos,One thing still troubles me though.On the update script, the item element will not come with a UserId property. So when you update the resource wouldnt that set the UserId property to null?
  • Anonymous
    December 14, 2012
    No, the update will only set the fields present on the request. It doesn't replace the existing object with the given one. The REST API actually maps that: instead of a PUT request, an update operation is implemented as a PATCH request to indicate the behavior.
  • Anonymous
    April 06, 2013
    Fantastic boiler plate code. Exactly what I had in mind, but did not have the skills to write the code for (just starting out with mobile services). Thank you.
  • Anonymous
    September 25, 2013
    Excellent post - thanks!
  • Anonymous
    May 22, 2014
    Hi Carlos,I just posted the same comment on this article: blogs.msdn.com/.../deep-dive-on-the-offline-support-in-the-managed-client-sdk.aspx but as I don't know who can answer me and also as it concerns this article, I ask you directly :)I'm about to build an app with this scenario: One AMS, a .Net backend service, Authenticated users, UserId stored on server tables, Offline synchronization to SQLite.I want to synchronize only users elements into SQLite table for an offline use but also beeing able to access to full table for an online use context.To do that, on the server side, I was thinking about creating one table like CustomElements wich hold all the data and one other like MyCustomElements wich is always empty.Then I plan to modify the MyCustomElementsController so that it will get user filtered data from the CustomElements table.On the client side, I'll create only the MyCustomElements table wich synchronize with the same table on the server.Well, I didn't try it now, but I think it should work. I don't know if there's other solution like creating a custom controller (non table) and create a synchronization with it on local app table...Do you know how to deal with this synchronization scenario of filtered user's data?