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.

Authenticating architecture

The logic works as follows:
Initial page load:

  1. Step A: Login info is detected, and user information is determined (ASP Page codebehind).
  2. Step B: User information is encrypted and saved to a client-side cookie (ASP Page codebehind).

Web service request:

  1. Step C: AngularJS picks up cookie and passes it in request header.
  2. 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.
  3. 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.

Comments
  1. 3 years ago
    • 3 years ago
  2. 3 years ago
    • 3 years ago

Leave a Reply

Your email address will not be published. Required fields are marked *