Sunday, November 24, 2013

Angular, Web Api 2 & Authentication

This post will take a look at registering an account and logging in with Angular and ASP.NET Web API 2 via cross origin requests (CORS). Authorization with roles and accessing restricted portions of a site will not be covered, just as a heads up. This is to serve more as a starting point.

Some specs regarding the setup:
Visual Studio 2013
Project for the Angular client
Project for Web Api
HotTowel.Angular.Breeze 2.0.1

Alright let's get moving. You can put both projects into one solution or you can put them in separate solutions if you wish. We'll start with the Angular end of things. After you install the HotTowel.Angular.Breeze package, we are good to go. In the app folder, add a folder called register. In the register folder, add a html file called register. The code:
<section id="register-view" class="mainbar" data-ng-controller="register as vm">
   <div class="row-fluid">
      <h4>Create a new account.</h4>
   </div>
   <div class="row-fluid">
      <div>
         <form id="register" class="form-inline">
            <div class="form-group">
               <label class="col-md-2 control-label" for="UserName">User name<label>
               <div class="col-md-10">
                  <input type="text" name="username" placeholder="User Name" data-ng-model="vm.registration.userName" />
               </div>
            </div>
            <div class="form-group">
               <label class="col-md-2 control-label" for="Password">Password<label>
               <div class="col-md-10">
                  <input type="text" name="password" placeholder="Password" data-ng-model="vm.registration.password" />
               </div>
            </div>
            <div class="form-group">
               <label class="col-md-2 control-label" for="ConfirmPassword">Confirm Password<label>
               <div class="col-md-10">
                  <input type="text" name="confirmPassword" placeholder="confirmPassword" data-ng-model="vm.registration.confirmPassword" />
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <input type="submit" class="btn btn-default" value="Register" data-ng-click="vm.sendRegistration()" />
               </div>
            </div>
         </form>
      </div>
   </div>
</section>

Now the code for the register controller, add a script file (register.js) to the register folder:
(function() {
   var controllerId = 'register';

   angular.module('app').controller(controllerId,
      ['$http', register]).config(['$sceDelegateProvider', function ($sceDelegateProvider) {
         $sceDelegateProvider.resourceUrlWhitelist(['self', /^https?:\/\/(cdn\.)?localhost:52635/]);
      }]);

   function register($http) {
      var vm = this;
      var url = "http://localhost:62757/api/Account/Register";

      vm.activate = activate;
      vm.title = 'Register';
      vm.sendRegistration = sendRegistration;
      vm.registration = {
         username = "",
         password = "",
         confirmPassword: ""
      };

      var headers = {
         'Access-Control-Allow-Origin': 'http://localhost:62714/',
         'Access-Control-Allow-Methods': ['POST', 'OPTIONS'],
         'Access-Control-Allow-Headers': 'Content-Type',
         'Access-Control-Allow-Credendtials': 'true',
         'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      };

      function activate() {
      }
      
      function sendRegistration() {
         var result = "username=" + vm.registration.userName + "&password=" + vm.registration.password + "&confirmPassword=" + vm.registration.confirmPassword;

         $http({
            url: url,
            method: 'POST',
            data: result,
            headers: headers
         }).success(function (data, status, headers, config) {
            // if successful, "" will be returned
         }).error(function (data, status, headers, config) {
            console.log('failed');
         });
      }
   }
})();

A few things to note....The sceDelegateProvider, this allows us a way to configure trusted urls used for client/server communication. Note how a headers object is constructed. It basically creates name/value pairs for the http headers.

Take special note of the Access-Control-Allow-Methods. Here we are configuring POST and OPTIONS. This is because on a POST request, the browser will send a preflight OPTIONS request. We must configure the server in the same fashion or else the POST operation will fail. It is a good idea to inspect the OPTIONS preflight headers and compare them to the headers of the POST request. Try to match them so they are the same. If you run into problems in this area, make sure that the headers for the POST and OPTIONS headers are the same and that the server is expecting those headers as well.

Lastly, if they call succeeds, you will see an empty string returned if you inspect the result in Chrome.

In the app folder, add a folder called login. Next add a html file called login. The code:
<section id="login-view" class="mainbar" data-ng-controller="login as vm">
   <div class="row-fluid">
      <h4>Log in.</h4>
   </div>
   <div class="row-fluid">
      <div>
         <form id="register" class="form-inline">
         <form id="register" class="form-inline">
            <div class="form-group">
               <label class="col-md-2 control-label" for="UserName">User name<label>
               <div class="col-md-10">
                  <input type="text" name="userName" placeholder="User Name" data-ng-model="vm.logIn.userName" />
               </div>
            </div>
            <div class="form-group">
               <label class="col-md-2 control-label" for="Password">Password<label>
               <div class="col-md-10">
                  <input type="text" name="password" placeholder="Password" data-ng-model="vm.logIn.password" />
               </div>
            </div>
            <div class="form-group">
               <div class="col-md-offset-2 col-md-10">
                  <input type="submit" class="btn btn-default" value="Log In" data-ng-click="vm.sendLogIn()" />
               </div>
            </div>
         </form>
      </div>
   </div>
</section>

Add a js file to the login folder called login.js. The code for the controller:
(function () {
   var controllerId = 'login';

   angular.module('app').controller(controllerId,
      ['$http', login]).config(['$sceDelegateProvider', function ($sceDelegateProvider) {
         $sceDelegateProvider.resourceUrlWhitelist(['self', /^https?:\/\/(cdn\.)?localhost:52635/]);
      }]);

   function login($http) {
      var vm = this;
      var accessToken = "";
      var url = "http://localhost:62757/Token";

      vm.activate = activate;
      vm.title = 'Log In';
      vm.sendLogIn = sendLogIn;
      vm.logIn = {
         username = "",
         password: ""
      }

      var headers = {
         'Access-Control-Allow-Origin': 'http://localhost:62714/',
         'Access-Control-Allow-Methods': ['POST', 'OPTIONS'],
         'Access-Control-Allow-Headers': 'Content-Type',
         'Access-Control-Allow-Credentials': 'true',
         'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      };

      function activate() {
      }

      function sendLogIn() {
         var result = "grant_type=password&" + "username=" + vm.logIn.userName + "&password=" + vm.logIn.password;

         $http({
            url: url,
            method: 'POST',
            data: result,
            headers: headers
         }).success(function (data, status, headers, config) {
            accessToken = data.access_token;
         }).error(function (data, status, headers, config) {
            console.log('failed');
         });
      }
   }
})();
The login setup is quite similar to register. Again, $sceDelegateProvider.resourceUrlWhiteList provides safe url communication from client to server. The headers are configured in the same fashion as noted above.

When the request is successful, the Token endpoint returns a token. This is then stored in the accessToken variable and can now be used for any request that requires authorization for a resource. More about the mysterious Token endpoint below.

There really isn't much to do configure Web Api to accept CORS requests. We will need to install two packages via Nuget. Those packages are:

Microsoft.AspNet.WebApi.Cors
Microsoft.Owin.Cors

The first change to make is in Startup.Auth.cs. In this file, there is a method called ConfigureAuth. We need to add one line of code, I put it at the top:

public partial class Startup
{
   // other members

   public void ConfigureAuth(IAppBuilder app)
   {
      app.UseCors(CorsOptions.AllowAll);
   }
}

There is also something else in this file that is a key piece of information. In the Startup method, there is some code for the creation and initialization of a class called OAuthAuthorizationServerOptions. You will see a property called TokenEndpointPath = new PathString("/Token"). Ok so what is it?

With every iteration of the framework, Microsoft like to change membership. If you open the Web Api AccountController, you will see a method for registering, the Register method. This has been around for awhile in ASP.NET MVC. Then you would expect that there would be a method for log in, that's the way it was before. Well, if you look in the AccountController, you will not find a Login method at all. What the heck happened?

It took a little digging. Recall the OAuthAuthorizationServerOptions from above and the TokenEndpointPath. In the login controller we also configured the url for /Token. The actual login now takes place in OWIN middleware. You route the request to the Token endpoint and then the request goes off into the ether.

We have one change left to make on the server. In the AccountController, locate the Register method. We need to decorate this method to allow CORS requests:
[AllowAnonymous]
[Route("Register")]
[EnableCors(origins: "http://localhost:62714", headers: "accept, content-type, origin", methods: "POST, OPTIONS")]
public async Task Register(RegisterBindingModel model)
{
   // registration logic
}
We have to ensure that the client and server and configured to expect the same headers. It was mentioned above that when configuring a POST operation, the preflight OPTIONS request has to be configured as well. Be very aware and careful about this as it may cause you more pain (and burning many hours) than you would want.

Wednesday, November 13, 2013

JavaScript: Working with Arrays

I was going to include this material with the last post on jQuery but this material really deserves it's own attention. Many of us .NET developers are hesitant about JavaScript, and for good reason, it's a pain in the ass. But, as with all things, it is a mater of practice, practice, practice. This post will present working with JavaScript arrays and some of the slight differences between them. I'm running this in Visual Studio 2012 with Chrome.

To get rolling, we will declare some arrays to hold the data, some variables, and pass the variables to a function. The function will create a key that will be looked up in the cache and add if the key is not present.
var productCache = [];
var productCache2 = [];

var value1 = 1;
var value2 = 2;
var value3 = 3;

addToCache(value1);
addToCache(value2);
addToCache(value3);

function addToCache(val) {
   var id = "productId";
   var value = val;
   var key = id + "-" + value;

   productCache[key] = value;
   productCache2.push(value);
}
As displayed above, there is two ways to shuttle data into an array. You can use an index, or use the push function. Now, let's call the length property on each array and see what we have, afterall, using the length property is a pretty common scenario.
console.log(productCache.length);
console.log(productCache2.length);

Result: 0
Result: 3
How about that. Can that really be? Let's take a look at the objects with the developer tools.

 
If you read the post on jQuery tips you would know why this is happening. Indexes, by default, are numeric. In the first cache, we are using strings for indexes, this is why we are seeing nothing when length is called and when we peek into the object. Rest assured, the data is in there, that is what we will look at next.
Now we will look at iterating over these arrays, how to get the keys, how to get the values. You can accomplish this several different ways. This part may be boring but as developers, we like our options don't we? We will iterate the following ways: plain for loop, hasOwnProperty, and using ECMA 5. Both of the arrays will produce the same value and the technique is the same.
// Get keys with plain for loop
for (var key in productCache) {
   console.log("logging key: " + key);
}

Result:
logging key: productId-1
logging key: productId-2
logging key: productId-3

// Get keys using hasOwnProperty
// hasOwnProperty will inspect properties that have been added, 'direct properties'
// it will not inspect properties inherited from Object through the prototype chain
for (var key in productCache) {
   if (productCache.hasOwnProperty(key)) {
      console.log("logging key: " + key);
   }
}

Result:
logging key: productId-1
logging key: productId-2
logging key: productId-3

// Get keys using ECMA 5
var keys = Object.keys(productCache).map(function(key) {
   console.log("logging key: " + key;
   return key;
});

console.log(keys);

Result:
logging key: productId-1
logging key: productId-2
logging key: productId-3

["productId-1", "productId-2", "productId-3"]

// Get values with plain for loop
for (var key in productCache) {
   var value = productCache[key];
   console.log("logging value: " + value);
}

Result:
logging value: 1
logging value: 2
logging value: 3

// Get values using hasOwnProperty
// hasOwnProperty will inspect properties that have been added, 'direct properties'
// it will not inspect properties inherited from Object through the prototype chain
for (var key in productCache) {
   if (productCache.hasOwnProperty(key)) {
      var value = productCache[key];
      console.log("logging value: " + value;
   }
}

Result:
logging value: 1
logging value: 2
logging value: 3

// Get values using ECMA 5
var values = Object.keys(productCache).map(function(key) {
   console.log("logging value: " + productCache[key]);
   return productCache[key];
});

console.log(values)

Result:
logging value: 1
logging value: 2
logging value: 3

[1, 2, 3]
Deleting from an array, you have to be careful when you are doing this. Depending on how you are storing items in the array and which way you choose to delete an item, can have some unexpected side effects. Just to recap and try to readjust our brains a little bit, we have two arrays, and this is what they look like:

productCache:
[productId-1] = 1
[productId-2] = 2
[productId-3] = 3

productCache2:
0: 1
1: 2
2: 3

Let's start deleting stuff and see what goes boom. To delete an item, you use the, well, delete keyword.
for (var index in productCache) {
   var item = productCache[index];
   if (item == "3") {
      delete productCache[index];
   }
}
Before showing the results, I'd like to call attention to something. Typically, when we deal with indexes we think of them as a number when we are iterating. But when you step outside of a language that possess the awesomeness of C#, you can't be guaranteed anything. Anyway, recall that in this array, we are storing key/value pairs and in our case, the key is a string, not a number. Above, when we are iterating through this array, the first time through, index is "productId-1", second time through, "productId-2", third time, "productId-3". Let's have a look at the results....

 
Cool, it worked, no surprises. Let's move on to the other array. Since we just pushed values into this array, it will have the normal indexes of 0, 1, and 2.
for (var index in productCache2) {
   var item = productCache2[index];
   if (item == "3") {
      delete productCache2[index];
   }
}
 
 
 
Uh oh indeed. See what happened there? The item did in fact get removed but the length did not change! Is there something wrong with our eyes??? Let's investigate a bit more...

 
Yes, that is the expected behavior when using delete with arrays. That "undefined x 1" will be put in the array for any item you delete. The alternative, is to use the splice method. The first parameter that splice takes is the index, the second parameter is a number indicating how many items to cleave.
for (var index in productCache2) {
   var item = productCache2[index];
   if (item == "3") {
      productCache2.splice(index, 1);
   }
}
 
 
 
 
Splice with not work with our other array. This is rather simple, the first parameter to splice expects a number. That array uses strings for keys.