Monday, 24 February 2014

Windows Azure Mobile Services - Web API - Custom APIs

So what about the API tab that is in the Node.js service and missing from the Web API service? Well, the clue is in the name! We can simply add a Web API 2 controller to our solution like this:


And start coding in our controller methods. The 'RequiresAuthorization' security attributes we looked at in this article still work and we can create a reference to the 'ApiServices' object to access logging, push client etc:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Microsoft.WindowsAzure.Mobile.Service;
using Microsoft.WindowsAzure.Mobile.Service.Security;

namespace TileTapperWebAPIService.Controllers
{
    public class HighScoreController : ApiController
    {
        // GET api/<controller>
        [RequiresAuthorization(AuthorizationLevel.Application)]
        public IEnumerable<string> Get()
        {
            var service = new ApiServices(base.Configuration);
            service.Log.Info("Hello from HighScoreController!");

            return new string[] { "value1", "value2" };
        }

        // GET api/<controller>/5
        public string Get(int id)
        {
            return "value";
        }

        // POST api/<controller>
        public void Post([FromBody]string value)
        {
        }

        // PUT api/<controller>/5
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/<controller>/5
        public void Delete(int id)
        {
        }
    }

}

Windows Azure Mobile Services - Web API - Push Notifications

Node.js services have the option of manually managing device push channels (through your own channel registration table) and directly pushing to a devices URI handle or using the Notifications hub to take care of the devices for you (I notice that the Node.js services still have this, but also have an 'ENABLE ENHANCED PUSH' button in the portal to automatically integrate a notifications hub).

Web API services don't have the option to make direct platform-specific push requests and by default come with a Notification Hub instance created and ready to use.

Last year I wrote an article about using the Notifications Hub in Node.js. The implementation is very similar in Web API. This article explains how to integrate the hub into your applications.

Here is an implementation of a scheduled job which is used to create a level board for a fictitious game, it calls two methods which send toast and tile notifications for Windows Phone MPNS via the hub:

using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.WindowsAzure.Mobile.Service;
using System;

namespace TileTapperWebAPIService.ScheduledJobs
{
    public class LevelJob : ScheduledJob
    {
        public override async Task ExecuteAsync()
        {
            // Level name
            string levelName = string.Format("Daily Level {0}", DateTime.Now.ToShortDateString());

            // Logic to create level ommited
            //
            //

            string title = "New Level!";

            await this.SendToastMpns(title, levelName);
            await this.SendTileMpns(title, levelName);

            Services.Log.Info(string.Format("{0} - Created", levelName));
        }

        private async Task SendToastMpns(string text1, string text2)
        {
            try
            {
                var toast = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                "<wp:Notification xmlns:wp=\"WPNotification\">" +
                    "<wp:Toast>" +
                        "<wp:Text1>" + text1 + "</wp:Text1>" +
                        "<wp:Text2>" + text2 + "</wp:Text2>" +
                    "</wp:Toast> " +
                "</wp:Notification>";

                await base.Services.Push.HubClient.SendMpnsNativeNotificationAsync(toast);
            }
            catch (Exception ex)
            {
                base.Services.Log.Error(ex);
            }
        }

        private async Task SendTileMpns(string backTitle, string backContent)
        {
            try
            {
                var tile = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                "<wp:Notification xmlns:wp=\"WPNotification\" Version=\"2.0\">" +
                    "<wp:Tile Template=\"FlipTile\">" +
                        "<wp:BackTitle>" + backTitle + "</wp:BackTitle>" +
                        "<wp:BackContent>" + backContent + "</wp:BackContent>" +
                        "<wp:WideBackContent>" + backContent + "</wp:WideBackContent>" +
                    "</wp:Tile> " +
                "</wp:Notification>";

                await base.Services.Push.HubClient.SendMpnsNativeNotificationAsync(tile);
            }
            catch (Exception ex)
            {
                base.Services.Log.Error(ex);
            }
        }
    }

}

We can configure this in the 'SCHEDULER' tab of the portal and run on a timed schedule or on demand, or make an HTTP POST request to call it like this:

Request:
POST https://tiletapperwebapi.azure-mobile.net/jobs/level HTTP/1.1
x-zumo-master: XXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Host: tiletapperwebapi.azure-mobile.net
Content-Length: 0

Response:
HTTP/1.1 200 OK
Content-Length: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=dd8538851a2df7249d42a39ee57c8edadae2824386ae2790373823ba2f34a746;Path=/;Domain=tiletapperwebapi.azure-mobile.net
Set-Cookie: WAWebSiteSID=71043e6c932a454d8c4cf9d17a9ed735; Path=/; HttpOnly
Date: Mon, 24 Feb 2014 10:59:04 GMT


Sunday, 23 February 2014

Windows Azure Mobile Services - Web API - Security

If we continue examining the template project we downloaded in this article, we notice there is no mention of security in the controller and no way of configuring permissions in the portal as there are for a Node.js service; so is our service secure and how do we adjust permissions for each controller method so we have the same level of control as in Node.js?


Default Permissions

To do a quick test and see if we have any security, we can quickly fire up fiddler and compose a simple GET request on the TodoItem, without even having to do any client-side coding:

Request:
GET https://tiletapperwebapi.azure-mobile.net/tables/todoitem HTTP/1.1
Host: tiletapperwebapi.azure-mobile.net

Response:
HTTP/1.1 403 Forbidden
Content-Length: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=d3b0dd468eddabcdcdade2ddbc3126e837df5031a73d3c0f5da9eda3a33b354e;Path=/;Domain=tiletapperwebapi.azure-mobile.net
Set-Cookie: WAWebSiteSID=7f59130dd78d4d8b99c890c1868d4dac; Path=/; HttpOnly
Date: Sat, 22 Feb 2014 18:36:44 GMT

You can see we get a 403 Forbidden which means we don't have access to the API and the service is secure.

If we put the app key in the header (get this from 'MANAGE KEYS' in the portal root toolbar), we are granted access get the result we want:

Request:
GET https://tiletapperwebapi.azure-mobile.net/tables/todoitem HTTP/1.1
Host: tiletapperwebapi.azure-mobile.net
X-ZUMO-APPLICATION: XXXXXXXXXXXXXxxxxxxxxxxxxxxxxxx

Response:
HTTP/1.1 200 OK
Content-Length: 98
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=d3b0dd468eddabcdcdade2ddbc3126e837df5031a73d3c0f5da9eda3a33b354e;Path=/;Domain=tiletapperwebapi.azure-mobile.net
Set-Cookie: WAWebSiteSID=88d016736ed8442f81ff998d1db4d832; Path=/; HttpOnly
Date: Sat, 22 Feb 2014 18:48:34 GMT

[{"id":"1","complete":false,"text":"First item"},{"id":"2","complete":false,"text":"Second item"}]

This means with the default controller configuration we are secured with the application key which equates to the 'Anybody with the application key' permission in a Node.js service.

Modifying Permissions

If we want to modify the table method permissions which we are likely to do, it's not obvious what to do from the template application. I had a dig about in the 'Microsoft.WindowsAzure.Mobile.Service.Security' namespace and found that there is an attribute called 'RequiresAuthorizationAttribute' with a nice description of it's function:

"Apply this attribute to System.Web.Http.ApiController actions or controllers access to them. Based on the Microsoft.WindowsAzure.Mobile.Service.Security.AuthorizationLevel specified, access to the target action will be restricted to requests that have been granted that level or higher."

We can apply this attribute to our controller methods to achieve 4 different permissions:

Admin

This is equivalent to 'Only scripts and admins' in Node.js services and only permits requests bearing the master key (Z-ZUMO-MASTER header) or direct access from other scripts:

[RequiresAuthorization(AuthorizationLevel.Admin)]
public IQueryable<TodoItem> GetAllTodoItems()
{
    return Query();

}

Anonymous

This is equivalent to 'Everyone' in Node.js services and permits anybody on the internet to access the method which is to be used with caution:

[RequiresAuthorization(AuthorizationLevel.Anonymous)]
public IQueryable<TodoItem> GetAllTodoItems()
{
    return Query();
}

If we make a request with no auth header now we get our data.

Application

This is equivalent to 'Anybody with the application key' in Node.js services and only permits requests bearing the application key and is the same as the default with not attribute used:

[RequiresAuthorization(AuthorizationLevel.Application)]
public IQueryable<TodoItem> GetAllTodoItems()
{
    return Query();
}

User

This is equivalent to 'Only authenticated users' in Node.js services and only permits users bearing a valid OAuth2 token from one of the 4 available OAuth providers:

[RequiresAuthorization(AuthorizationLevel.User)]
public IQueryable<TodoItem> GetAllTodoItems()
{
    return Query();
}

Finally

Now we know how to set our permissions it's important to think about them for each method (if all methods require the same permissions, we can apply a single attribute to the controller). In my book I wrote a whole chapter on security and basically advised applying Admin permissions to anything which was not used by the application, then User for most other cases. Anonymous is one to be careful of as it allows access to anyone on the internet who knows the service URI.

Saturday, 22 February 2014

Publishing a Windows Azure Mobile Service Web API Project

In my previous article, we started looking at what's in the template Azure Mobile Service solution. This is a quick walk-through how to publish the solution:

1. From the dashboard in the Mobile Service portal, click the 'Download publish profile' link on the dashboard to download the services publish profile

2. From the 'Build' menu in Visual Studio, select 'Publish':




3. Click on the 'Import' button:




4. Browse to the downloaded profile and click OK:




5. Check the connection details and click 'Publish':



6. The solution should publish to our service. Check the output window for any problems.

Next we'll start looking at security in the controllers...

Tuesday, 11 February 2014

Listing Windows Azure Mobile Services NPM Packages

It's useful to know what NPM packages are pre-installed and available to use out-of-the-box in our Mobile Services. I thought it might be good to get a list of these packages and decided to create an API method which listed the packages and dependencies, with a description of what they are.

NPM list Command

The list (or ls for short) command lists the packages and can be called using the child_process.exec command. This function does this and starts examining the packages:

function npmls() {
        var exec = require('child_process').exec;
   
        // In node you can use --long switch to show description but it causes maxBuffer exceeded on parse
        var child = exec('npm ls --json',
          function (error, stdout, stderr) {
           
            var tree = JSON.parse(stdout);
            npminfo(tree, '');
           
            setNpmHttp(dequeuePackage);

            if (error !== null) {
              console.log('exec error: ' + error);
            }
        });
    }

The JSON looks like this if we log it:

{
  "problems": [
    "invalid: msnodesql@0.2.1 D:\\home\\site\\wwwroot\\node_modules\\sqlserver"
  ],
  "dependencies": {
    "apn": {
      "version": "1.3.8",
      "dependencies": {
        "q": {
          "version": "0.9.6"
        }
      }
    },
    "azure": {
      "version": "0.6.7-zumo",
      "from": "https://github.com/WindowsAzure/azure-sdk-for-node/tarball/v0.6.7-zumo",
      "dependencies": {
        "azure": {
          "version": "0.7.15",
          "from": "git://github.com/WindowsAzure/azure-sdk-for-node.git#v0.7.15-August2013",
          "dependencies": {
            "xml2js": {
              "version": "0.4.0",
              "dependencies": {
                "sax": {
                  "version": "0.5.5"
                }
              }
            },
            "request": {
              "version": "2.25.0",
              "dependencies": {
                "qs": {
                  "version": "0.6.5"
                },
                "json-stringify-safe": {
                  "version": "5.0.0"
                },
                "forever-agent": {
                  "version": "0.5.0"
                },
                "tunnel-agent": {
                  "version": "0.3.0"
                },
                "http-signature": {
                  "version": "0.10.0",
                  "dependencies": {
                    "assert-plus": {
                      "version": "0.1.2"
                    },

HTTPS Certificate Errors

This method was needed to make the npm view GET use http instead of https due to certificate trust issues:

// Used http instead of https to stop certificate errors
    function setNpmHttp (callback) {
       
        var exec = require('child_process').exec;
   
            var child = exec('npm config set registry="http://registry.npmjs.org/"',
              function (error, stdout, stderr) {
                callback();
            });      
    }

NPM Info

Once we've parsed the JSON, we can start examining each package and it's dependencies. This function walks the dependencies and builds an array of objects used for processing the package information:

// Recursive function for getting package info for all dependencies
    function npminfo(node, depth) {
         
        var dp = node.dependencies;
        if(dp !== null) {      
            for (var key in dp) {
              if (dp.hasOwnProperty(key)) {                      
               
                // Build info
                var link = 'https://www.npmjs.org/package/' + key;
                var info = depth + '<a href=\'' + link + '\', \'' + key + '\' onclick="window.open(this.href, \'' + key + '\' ); return false" target="_blank">' + key + '</a>' + ' V' + dp[key].version;
               
                packages.push({ 'key' : key, 'info' : info, 'depth' : depth.length })
                               
                // Loop through dependencies
                npminfo(dp[key], depth + '&#45;');
              }
            }
        }
    }

Extra Information

Once we have a list of package objects, we can process them to get extra description information:

function dequeuePackage(){
        // Dequeue and execute
        if(packages.length > 0){
            var pack = packages.shift();
            if(pack.depth == 0){
               
                writeLine();
               
                // Get detail
                var exec = require('child_process').exec;
       
                var child = exec('npm view ' + pack.key + ' description',
                  function (error, stdout, stderr) {
                     
                    if (error !== null) {
                      console.error('exec error: ' + error);
                    }
                    else{          
                        pack.info = pack.info + ' - ' + stdout;                        
                        writeLine('<b>' + pack.info + '</b>');
                    }
                   
                    // Do next
                    dequeuePackage();
                });
            }
            else {
                // Do next
                writeLine(pack.info);
                dequeuePackage();
            }
        }
        else {      
            // We're finished      
            opText = opText + '</body></html>';
           
            response.send(statusCodes.OK, opText);
        }  
    }

This is only done for top-level packages because it take's too long to make requests for all packages and the API request will time-out!

Full Script

When we put it all together, the API method looks like this:

exports.get = function(request, response) {
   
    // Array of package info objects
    var packages = [];
   
    // html start
    var opText = '<!DOCTYPE html><html><head><title>NPM Modules</title></head><body>';
    opText += '<h1>Windows Azure Mobile Services</h1>';
    opText += '<h2>NPM Packages and Dependencies</h2>';
   
    // List packages
    npmls();
   
    // Adds a line with break
    function writeLine(text){
        if(text)
            opText += text;
        opText += '<br />';
    }
   
    function npmls() {
        var exec = require('child_process').exec;
   
        // In node you can use --long switch to show description but it causes maxBuffer exceeded on parse
        var child = exec('npm ls --json',
          function (error, stdout, stderr) {
           
            var tree = JSON.parse(stdout);
            console.log(stdout);
            npminfo(tree, '');
           
            setNpmHttp(dequeuePackage);

            if (error !== null) {
              console.log('exec error: ' + error);
            }
        });
    }
   
    // Used http instead of https to stop certificate errors
    function setNpmHttp (callback) {
       
        var exec = require('child_process').exec;
   
            var child = exec('npm config set registry="http://registry.npmjs.org/"',
              function (error, stdout, stderr) {
                callback();
            });
       
    }
   
    // Recursive function for getting package info for all dependencies
    function npminfo(node, depth) {
         
        var dp = node.dependencies;
        if(dp !== null) {      
            for (var key in dp) {
              if (dp.hasOwnProperty(key)) {                      
               
                // Build info
                var link = 'https://www.npmjs.org/package/' + key;
                var info = depth + '<a href=\'' + link + '\', \'' + key + '\' onclick="window.open(this.href, \'' + key + '\' ); return false" target="_blank">' + key + '</a>' + ' V' + dp[key].version;
               
                packages.push({ 'key' : key, 'info' : info, 'depth' : depth.length })
                               
                // Loop through dependencies
                npminfo(dp[key], depth + '&#45;');
              }
            }
        }
    }
   
    function dequeuePackage(){
        // Dequeue and execute
        if(packages.length > 0){
            var pack = packages.shift();
            if(pack.depth == 0){
               
                writeLine();
               
                // Get detail
                var exec = require('child_process').exec;
       
                var child = exec('npm view ' + pack.key + ' description',
                  function (error, stdout, stderr) {
                     
                    if (error !== null) {
                      console.error('exec error: ' + error);
                    }
                    else{          
                        pack.info = pack.info + ' - ' + stdout;                        
                        writeLine('<b>' + pack.info + '</b>');
                    }
                   
                    // Do next
                    dequeuePackage();
                });
            }
            else {
                // Do next
                writeLine(pack.info);
                dequeuePackage();
            }
        }
        else {      
            // We're finished      
            opText = opText + '</body></html>';
           
            response.send(statusCodes.OK, opText);
        }  
    }
};

Finally

You can test it out here (although I may take it off-line at some point!).