Udostępnij za pośrednictwem


Exposing authenticated data from Azure Mobile Services via an ASP.NET MVC application

After seeing a couple of posts in this topic, I decided to try to get this scenario working – and having never used the OAuth support in MVC, it was a good opportunity for me to learn a little about it. So here’s a very detailed, step-by-step of what I did (and worked for me), hopefully it will be useful if you got to this post. As in my previous step-by-step post, it can have more details than some people care about, so if you’re only interested in the connection between the MVC application and Azure Mobile Services, feel free to skip to the section 3 (using the Azure Mobile Service from the MVC app). The project will be a simple contact list, which I used in other posts in the past.

1. Create the MVC project

Let’s start with a new MVC 4 project on Visual Studio (I’m using VS 2012):

01-NewProject

And select “Internet Application” in the project type:

02-InternetApp

Now the app is created and can be run. Now let’s create the model for our class, by adding a new class to the project:

  1. public class Contact
  2. {
  3.     public int Id { get; set; }
  4.     public string Name { get; set; }
  5.     public string Telephone { get; set; }
  6.     public string EMail { get; set; }
  7. }

And after building the project, we can create a new controller (called ContactController), and I’ll choose the MVC Controller with actions and views using EF, since it gives me for free the nice scaffolding views, as shown below. In the data context class, choose “<New data context...>” – and choose any name, since it won’t be used once we start talking to the Azure Mobile Services for the data.

11-AddContactsController

Now that the template created the views for us, we can update the layout to start using the new controller / views. Open the _Layout.cshtml file under Views / Shared, and add a new action link to the new controller so we can access it (I’m not going to touch the rest of the page to keep this post short).

  1. <div class="float-right">
  2.     <section id="login">
  3.         @Html.Partial("_LoginPartial")
  4.     </section>
  5.     <nav>
  6.         <ul id="menu">
  7.             <li>@Html.ActionLink("Home", "Index", "Home")</li>
  8.             <li>@Html.ActionLink("Contact List", "Index", "Contact")</li>
  9.             <li>@Html.ActionLink("About", "About", "Home")</li>
  10.             <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
  11.         </ul>
  12.     </nav>
  13. </div>

At this point the project should be “F5-able” – try and run it. If everything is ok, you should see the new item in the top menu (circled below), and after clicking it you should be able to enter data (currently being stored in the local DB).

15-TestingMvcApp

Now since I want to let each user have their own contact list, I’ll enable authentication in the MVC application. I found the Using OAuth Providers with MVC 4 tutorial to be quite good, and I was able to add Facebook login to my app in just a few minutes. First, you have to register a new Facebook Application with the Facebook Developers Portal (and the “how to: register for Facebook authentication” guide on the Windows Azure documentation shows step-by-step what needs to be done). Once you have a client id and secret for your FB app, open the AuthConfig.cs file under the App_Start folder, and uncomment the call to RegisterFacebookClient:

  1. OAuthWebSecurity.RegisterFacebookClient(
  2.     appId: "YOUR-FB-APP-ID",
  3.     appSecret: "YOUR-FB-APP-SECRET");

At this point we can now change our controller class to require authorization (via the [Authorize] attribute) so that it will redirect us to the login page if we try to access the contact list without logging in first.

  1. [Authorize]
  2. public class ContactController : Controller
  3. {
  4.     // ...
  5. }

Now if we either click the Log in button, or if we try to access the contact list while logged out, we’ll be presented with the Login page.

21-FacebookLogin

Notice the two choices for logging in. In this post I’ll talk about the Facebook login only (so we can ignore the local account option), but this could also work with Azure Mobile Services, as shown in this post by Josh Twist.

And the application now works with the data stored in the local database. Next step: consume the data via Azure Mobile Services.

2. Create the Azure Mobile Service backend

Let’s start with a brand new mobile service for this example, by going to the Azure Management Portal and selecting to create a new Mobile Service:

36-AzureCreateMobileService

Once the service is created, select the “Data” tab as shown below:

37-DataTab

And create a new table. Since we only want authenticated users to access the data, we should set the permissions for the table operations accordingly.

38-CreateNewTable

Now, as I talked about in the “storing per-user data” post, we should modify the table scripts to make sure that no malicious client tries to access data from other users. So we need to update the insert script:

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

Read:

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

Update:

  1. function update(item, user, request) {
  2.     tables.current.where({ id: item.id, userId: user.userId }).read({
  3.         success: function(results) {
  4.             if (results.length) {
  5.                 request.execute();
  6.             } else {
  7.                 request.respond(401, { error: 'Invalid operation' });
  8.             }
  9.         }
  10.     });
  11. }

And finally delete:

  1. function del(id, user, request) {
  2.     tables.current.where({ id: id, userId: user.userId }).read({
  3.         success: function(results) {
  4.             if (results.length) {
  5.                 request.execute();
  6.             } else {
  7.                 request.respond(401, { error: 'Invalid operation' });
  8.             }
  9.         }
  10.     });
  11. }

We’ll also need to go to the “Identity” tab in the portal to add the same Facebook credentials which we added to the MVC application (that’s how the Azure Mobile Services runtime will validate with Facebook the login call)

44-AddFacebookCredentials

The mobile service is now ready to be used, we need now to start calling it from the web app.

3. Using the Azure Mobile Service from the MVC app

In a great post about this topic a while back, Filip W talked about using the REST API to talk to the service. While that is still a valid option, the version 1.0 of the Mobile Service SDK NuGet package also supports the “full” .NET Framework 4.5 (not only Windows Store or Windows Phone apps, as it did in the past). So we can use it to make our code simpler. First, right-click on the project references, and select “Mange NuGet Packages…”

51-ManageNuGetPackages

And on the Online tab, search for “mobileservices”, and install the “Windows Azure Mobile Services” package.

52-WindowsAzureMobileServicesPackage

We can now start updating the contacts controller to use that instead of the local DB. First, remove the declaration of the ContactContext property, and replace it with a mobile service client one. Notice that since we’ll use authentication, we don’t need to pass the application key.

  1. //private ContactContext db = new ContactContext();
  2. private static MobileServiceClient MobileService = new MobileServiceClient(
  3.     "https://YOUR-SERVICE-NAME.azure-mobile.net/"
  4. );

Now to the controller actions. For all operations, we need to ensure that the client is logged in. And to log in, we need the Facebook access token. As suggested in the Using OAuth Providers with MVC 4 tutorial, I updated the ExternalLoginCallback method to store the facebook token in the session object.

  1. [AllowAnonymous]
  2. public ActionResult ExternalLoginCallback(string returnUrl)
  3. {
  4.     AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback", new { ReturnUrl = returnUrl }));
  5.     if (!result.IsSuccessful)
  6.     {
  7.         return RedirectToAction("ExternalLoginFailure");
  8.     }
  9.  
  10.     if (result.ExtraData.Keys.Contains("accesstoken"))
  11.     {
  12.         Session["facebooktoken"] = result.ExtraData["accesstoken"];
  13.     }
  14.  
  15.     //...
  16. }

Now we can use that token to log in the web application to the Azure Mobile Services backend. Since we need to ensure that all operations are executed within a logged in user, the ideal component would be an action (or authentication) filter. To make this example simpler, I’ll just write a helper method which will be called by all action methods. In the method, shown below, we take the token from the session object, package it in the format expected by the service (an object with a member called “access_token” with the value of the actual token), and make a call to the LoginAsync method. If the call succeeded, then the user is logged in. If the MobileService object had already been logged in, its ‘CurrentUser’ property would not be null, so we bypass the call and return a completed task.

  1. private Task<bool> EnsureLogin()
  2. {
  3.     if (MobileService.CurrentUser == null)
  4.     {
  5.         var accessToken = Session["facebooktoken"] as string;
  6.         var token = new JObject();
  7.         token.Add("access_token", accessToken);
  8.         return MobileService.LoginAsync(MobileServiceAuthenticationProvider.Facebook, token).ContinueWith<bool>(t =>
  9.         {
  10.             if (t.Exception != null)
  11.             {
  12.                 return true;
  13.             }
  14.             else
  15.             {
  16.                 System.Diagnostics.Trace.WriteLine("Error logging in: " + t.Exception);
  17.                 return false;
  18.             }
  19.         });
  20.     }
  21.  
  22.     TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
  23.     tcs.SetResult(true);
  24.     return tcs.Task;
  25. }

Now for the actions themselves. When listing all contacts, we first ensure that the client is logged in, then retrieve all items from the mobile service. This is a very simple and naïve implementation – it doesn’t do any paging, so it will only work for small contact lists – but it illustrates the point of this post. Also, if the login fails the code simply redirects to the home page; in a more realistic scenario it would send some better error message to the user.

  1. //
  2. // GET: /Contact/
  3. public async Task<ActionResult> Index()
  4. {
  5.     if (!await EnsureLogin())
  6.     {
  7.         return this.RedirectToAction("Index", "Home");
  8.     }
  9.  
  10.     var list = await MobileService.GetTable<Contact>().ToListAsync();
  11.     return View(list);
  12. }

Displaying the details for a specific contact is similar – retrieve the contacts from the service based on the id, then display it.

  1. //
  2. // GET: /Contact/Details/5
  3. public async Task<ActionResult> Details(int id = 0)
  4. {
  5.     if (!await EnsureLogin())
  6.     {
  7.         return this.RedirectToAction("Index", "Home");
  8.     }
  9.  
  10.     var contacts = await MobileService.GetTable<Contact>().Where(c => c.Id == id).ToListAsync();
  11.     if (contacts.Count == 0)
  12.     {
  13.         return HttpNotFound();
  14.     }
  15.     return View(contacts[0]);
  16. }

Likewise, creating a new contact involves getting the table and inserting the item using the InsertAsync method.

  1. //
  2. // POST: /Contact/Create
  3. [HttpPost]
  4. [ValidateAntiForgeryToken]
  5. public async Task<ActionResult> Create(Contact contact)
  6. {
  7.     if (ModelState.IsValid)
  8.     {
  9.         if (!await EnsureLogin())
  10.         {
  11.             return RedirectToAction("Index", "Home");
  12.         }
  13.  
  14.         var table = MobileService.GetTable<Contact>();
  15.         await table.InsertAsync(contact);
  16.         return RedirectToAction("Index");
  17.     }
  18.  
  19.     return View(contact);
  20. }

And, for completeness sake, the other operations (edit / delete)

  1. //
  2. // GET: /Contact/Edit/5
  3. public async Task<ActionResult> Edit(int id = 0)
  4. {
  5.     if (!await EnsureLogin())
  6.     {
  7.         return RedirectToAction("Index", "Home");
  8.     }
  9.  
  10.     var contacts = await MobileService.GetTable<Contact>().Where(c => c.Id == id).ToListAsync();
  11.     if (contacts.Count == 0)
  12.     {
  13.         return HttpNotFound();
  14.     }
  15.     return View(contacts[0]);
  16. }
  17.  
  18. //
  19. // POST: /Contact/Edit/5
  20. [HttpPost]
  21. [ValidateAntiForgeryToken]
  22. public async Task<ActionResult> Edit(Contact contact)
  23. {
  24.     if (ModelState.IsValid)
  25.     {
  26.         if (!await EnsureLogin())
  27.         {
  28.             return RedirectToAction("Index", "Home");
  29.         }
  30.  
  31.         await MobileService.GetTable<Contact>().UpdateAsync(contact);
  32.         return RedirectToAction("Index");
  33.     }
  34.  
  35.     return View(contact);
  36. }
  37.  
  38. //
  39. // GET: /Contact/Delete/5
  40. public async Task<ActionResult> Delete(int id = 0)
  41. {
  42.     if (!await EnsureLogin())
  43.     {
  44.         return RedirectToAction("Index", "Home");
  45.     }
  46.  
  47.     var contacts = await MobileService.GetTable<Contact>().Where(c => c.Id == id).ToListAsync();
  48.     if (contacts.Count == 0)
  49.     {
  50.         return HttpNotFound();
  51.     }
  52.  
  53.     return View(contacts[0]);
  54. }
  55.  
  56. //
  57. // POST: /Contact/Delete/5
  58. [HttpPost, ActionName("Delete")]
  59. [ValidateAntiForgeryToken]
  60. public async Task<ActionResult> DeleteConfirmed(int id)
  61. {
  62.     if (!await EnsureLogin())
  63.     {
  64.         return RedirectToAction("Index", "Home");
  65.     }
  66.  
  67.     await MobileService.GetTable<Contact>().DeleteAsync(new Contact { Id = id });
  68.     return RedirectToAction("Index");
  69. }

That should be it. If you run the code now, try logging in to your Facebook account, inserting a few items then going to the portal to browse the data – it should be there. Deleting / editing / querying the data should also work.

Wrapping up

Logging in via an access (or authorization) token currently only works for Facebook, Microsoft and Google accounts; Twitter isn’t supported yet. So the example below could work (although I haven’t tried) just as well for the other two supported account types.

The code for this post can be found in the MSDN Code Samples at https://code.msdn.microsoft.com/Flowing-authentication-08b8948e.

Comments

  • Anonymous
    June 24, 2013
    Hi Carlos, great post, I ended with a solution like your.Here I see only two downside:1) The default provider of MVC for Google is an OpenID and it does not give you an access_token. You have to create a custom OAuth provider for Google, I used this one: nuget.org/.../DotNetOpenAuth.GoogleOAuth22) The MS live apps, so the MS live account, doesn't support more than one redirection domain for OAuth... so the question is: the accesstoken I get via my MVC application hosted on www.domain.com could be used with mobile service that is hosted on domain.azure-mobile.net? Or AMS try to revalidate this token and probably will fail cause the request comes from a different domain?
  • Anonymous
    January 14, 2014
    Hi Carlos, great post. I know its been a while but I was wondering if you can help me out. Which URL did you add for website address in Facebook. I used the Windows Azure URL for my app, however I am not able to use it to log into my local app? I read that I have to add Localhost @ FB.By any chance would you know if your Helper class will work in VS 2013?ThanksValetnine
  • Anonymous
    August 07, 2014
    Hi Carlos, great post, I have a question, I am trying to implement this same example using MVC 5 and OWIN, and I want to know if you used two different urls, because for example, at facebook if I use the mobile service URL, I get an URL redirection error in behalf of facebook, and I cannot log in, but if I use my website URL, I can log in, get the access token, but when I am trying to pass this one as the access token parameter to the loginAsync method, I keep receiving the unauthorized error.Please help, I have I week trying to fix this with no luck.
  • Anonymous
    August 28, 2014
    Great post Carlos.  I tried to follow your examples, using MVC 5 and version 1.2.3 of the Azure Mobile Services SDK, but it doesn't work for me.  I get a compilation error on the MobileService.GetTable<Contact>.  Visual Studio 2013 tells me "The type 'Microsoft.WindowsAzure.Mobile.Service.EntityData' is defined in an assebly that is not referenced."  It says I need to add a reference to 'Microsoft.WindowsAzure.Mobile.Service.Entity"Any Ideas.  I am thinking of using an older nuget package.
  • Anonymous
    July 25, 2015
    Liza, I'm getting the same unauthorized error..have you been able to fix it?