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 TaskWe 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.Register(RegisterBindingModel model) { // registration logic }