Securely Transmitting Information Across AngularJS to .NET Web API Services
Working with angularJS and .NET WebAPI 2 presents some challenges. In this scenario, the client side of our app is a single-page rich java client application written in AngularJS, and makes heavy use of web services. The web services are written in WebApi. Ultimately this app will be a SharePoint 2013 provider hosted app (and appear in an IFrame in SharePoint).
Here’s the issue: Our internal data retrieval process requires user information (i.e. Login ID), but the web services (although the are protected by forms authentication) are not aware of WHO the user is. We need a way to securely pass the user ID whenever a web service request is made.
You can do this by saving user information to a client side cookie. Information is encrypted, so it’s not possible for one user to impersonate another. The encryption key is located on the server.
Note: AES 256 bit Encryption code is needed to implement this. I got mine from this excellent article.
The logic works as follows:
Initial page load:
- Step A: Login info is detected, and user information is determined (ASP Page codebehind).
- Step B: User information is encrypted and saved to a client-side cookie (ASP Page codebehind).
Web service request:
- Step C: AngularJS picks up cookie and passes it in request header.
- Step D: On server side, the user token is picked up from the header and validated (using a FilterAttribute). note: so it applies to all.
- Step E: User token is decrypted, and can be used internally for data retrieval etc.
Steps A and B: User Authentication and Encryption
When the initial page loads, you’ll want to know if the user is logged in. The default app (when setting up in Visual Studio) comes with the wiring necessary to determine the SharePoint user. In my case there’s a third-party authentication provider, but the logic here is the same.
public clientSideUserToken; protected void Page_Load(object sender, EventArgs e) { bool isLoggedIn = false; string userName = null; //Authentication Provider logic for determining login. //SharePoint logic illustrated here. var spContext = SharePointContextProvider.Current.GetSharePointContext(Context); if (spContext != null) { using (var clientContext = spContext.CreateUserClientContextForSPHost()) { if (clientContext != null) { Microsoft.SharePoint.Client.User spUser = clientContext.Web.CurrentUser; clientContext.Load(spUser, user => user.Title); clientContext.ExecuteQuery(); isLoggedIn = true; userName = spUser.Title; } } } if (isLoggedIn) { string token = EncryptUsername(Session, userName); //drop cookie for angular to pick up. string cookieName = "MYCOOKIE"; Response.Cookies[cookieName]["USERTOKEN"] = token; Response.Cookies[cookieName].Expires = DateTime.Now.AddMinutes(1); } else { //HANDLE USER NOT LOGGED IN. } } private string EncryptUsername(HttpSessionState session, string userName) { byte[] byUserName = Encoding.ASCII.GetBytes(userName); byte[] byEncryptedUserName = AESEncryption.AES_Encrypt(byUserName, Config.CookieEncryptionKey); string encryptedUserName = Convert.ToBase64String(byEncryptedUserName); return encryptedUserName; }
Step C: AngularJS Submits User Information Header to Services
In this step, AnglularJS picks up the cookie dropped by the server on the initial page load (see step B), and puts it into the header of the next request.
This code is best located in an AngluarJS Interceptor (read more about interceptors here). The following ‘request’ function will execute every time the user makes a service request.
MyModule.factory('serviceLogger', function ($q, $rootScope, serviceErrorHandler, globalData, $cookies, $cookieStore) { return { 'request': function (config) { //pick up the asp cookie if present, pull out the encrypted user token, and put it into the header. var aspcookie = $cookies["MYCOOKIE"]; if (aspcookie) { var jsonCookie = {}; var items = aspcookie.split("&"); for (var i = 0; i < items.length; i++) { var splitPos = items[i].indexOf('='); jsonCookie[items[i].substring(0, splitPos)] = items[i].substring(splitPos + 1); } config.headers.HEADERTOKEN = jsonCookie.USERTOKEN; } return config; }, };
Steps D and E: Add a Custom Authentication Filter into WebApi to Check for Valid User
Finally, the header from step C is decoded and validated. If validation fails, the user gets a HTTP 401 (forbidden) error.
Using WebAPI, you can write code which can intercept all requests. To do this, write a custom authenticator implementing IAuthenticationFilter. Then apply it to ALL web services using the following code in your Global.asax:
protected void Application_Start(object sender, EventArgs e) { //whatever else you've got here GlobalConfiguration.Configuration.Filters.Add(new ValidateUserToken()); }
IMPLEMENTATION NOTE: tO implement the IAuthenticationFilter, I used an abstract filter to do some of the heavy lifting. I found a great example of that here. Another great help was this blog, which provides a straightforward way to implment IHttpActionResult. This implementation even allows us to send a custom object back to the client, along with the appropriate HTTP code!
Here’s an example of that class:
public class ValidateUserToken : AAuthenticationFilterAttribute { public override void OnAuthentication(System.Web.Http.Filters.HttpAuthenticationContext context) { try { DecryptUsername(context.Request); } catch { AuthResult msg = new AuthResult() { Message = "Server rejected the request. Authorization invalid or expired." }; context.ErrorResult = new SimpleHttpResult<AuthResult>(context.Request, HttpStatusCode.Forbidden, msg); } //otherwise pass through and everything is ok. } //note (step E): if you make this method static, you can retrieve the user name at will. private static string DecryptUsername(HttpRequestMessage request) { string encryptedUserName = request.Headers.GetValues("HEADERTOKEN").First(); byte[] byEncryptedUserName = Convert.FromBase64String(encryptedUserName); byte[] byUserName = AESEncryption.AES_Decrypt(byEncryptedUserName, Config.CookieEncryptionKey); string userName = Encoding.ASCII.GetString(byUserName); return userName; } } //Very simple result object. Any properties added here will be passed to the client side, as JSON. public class AuthResult { public string Message; }
Once these methods are in place, no further code modifications are necessary, and we can now write services and use them with confidence.
Hi Michael,
Great post! Do you have a full example or source code for this? I am not clear on steps C and step D. How can step D pick up the request values from step C? Please advise! Thanks
Hi Viet,
Unfortunately I don’t have a full working example at this time. The solution was devised as part of a larger project, so providing a sample is lower on my priority list. I will let you know if this changes.
The cookie is picked up on the server side by an authentication filter, which is applied to every webapi web service. In this example, the filter is called ValidateUserToken. The best way to implment this is to include an abstract authentication filter class and base the filter on that. Once you’ve got that part wired up, then focus on step E, the decryption.
Hope this helps!
Michael,
Thank you so much for your prompt response. I am a .NET developer but I am new to AngularJS development. As a result, I am having an issue with step C. I don’t know how where I can place step C and how it gets called so that step D can pick up the cookie values.
By the way, My app is a SharePoint 2013 Provider Hosted App using John Papa Hot Towel AngularJS Breeze template. Thanks
If you’re new to AngularJS, here’s my suggestion: First get your basic Angular project set up. Then get some HTTP requests in place, WITHOUT the authentication. Once that is all working, read up on interceptors – I have a post on that: http://intown.biz/2015/02/04/angularjs-interceptors/. The interceptor is where you put code that applies to all http requests (or whichever ones you define using your own logic), and that is where the client side code goes for this.