Category: SharePoint 2013

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.

SharePoint App Part Resizing, When Using AngularJS

Provider Hosted Apps

Sometimes it’s necessary to embed rich, non-SharePoint functionality in a SharePoint site. The preferred way to do this in SharePoint 2013 is to use a Provider Hosted App. SharePoint then acts as a “host” for the app, which can function independently. This gives the ability to layer in complex business rules, perform development cycles AWAY from SharePoint, and then embed the functionality in the site.

IFrame Presentation

Taking this route, one challenge is to ensure that the app appears as part of the page. Provider Hosted Apps are hosted in an iframe, but users need not be aware of this fact. If not done properly, parts of the app might disappear or for scroll bars may appear in the frame. Generally the iframe content cannot control the size of the “parent” window.

So it’s up to the developer to ensure that the iframe is sized properly for the app. Fortunately, Microsoft has anticipated this issue and given us some javascript “hooks” to do this. There are a number of blogs on how to achieve this, from simple implementation to complex ones . Bottom line, your hosted javascript code needs to send a message similar to this one to the parent window:

<message senderId=12345ABCD>resize(100%,800px)</message>

Additional Challenges with AngularJS

Resizing the parent iFrame is a simpler task with a “traditional” (mostly server side) app – then page size changes when the page reloads, and the necessary logic can be run then.

But with AngularJS (a single page app), the iframe needs to resize when we move from one “view” to another, or the page size changes due to the underlying javascript logic. When resizing, we need to know what the size of our content will be. Fortunately, this is not rocket science – it just requires a “harmonic convergence” of code.

Step 1: Add wrapper HTML

Identify or create a wrapper div which will contain all HTML in your app. We will check this wrapper to determine how large the iframe should be.

<body ng-app="appName">
    <div main-wrapper>
        <!-- Add your site or application content here -->
        <div ng-view=""></div>
    <div>
</body>

Step 2: Add Resizing Code

Add some javascript code to resize the iframe. The following code assumes that only the height of your app will vary. Note the chrome fix – this is another issue that applies only to Angular implementations.

"use strict";
function adjustFrameSize(contentHeight) {
    var senderId,
        resizeMessage = '<message senderId={Sender_ID}>resize({Width}, {Height}px)</message>';

    var args = document.URL.split("?");
    if (args.length < 2) return;
    var params = args[1].split("&");
    for (var i = 0; i < params.length; i = i + 1) {
        var param = params[i].split("=");
        if (param[0].toLowerCase() == "senderid") {
            senderId = decodeURIComponent(param[1]);
            senderId = senderId.split("#")[0]; //for chrome - strip out #/viewname if present
        }
    }

    var step = 30, finalHeight;
    finalHeight = (step - (contentHeight % step)) + contentHeight;

    resizeMessage = resizeMessage.replace("{Sender_ID}", senderId);
    resizeMessage = resizeMessage.replace("{Height}", finalHeight);
    resizeMessage = resizeMessage.replace("{Width}", "100%");
    console.log(resizeMessage);
    window.parent.postMessage(resizeMessage, "*");
}

Step 3: Add an Angular Directive

In your AngularJS code, create a directive which will attach to your wrapper div. What you’re doing here is creating an event which can be fired at any time from your app, to trigger a resize of the iframe based on the height of your wrapper div.

  .directive('mainWrapper', function ($timeout) {
        return {
            restrict: 'A',
            link: function (s, e, attrs) {
                s.$on('resizeframe', function () {
                    $timeout(function () {
                        //timeout ensures that it's run after the DOM renders.
                        adjustFrameSize(e[0].offsetHeight);
                    }, 0, false);
                });
            }
        };
    })

Step 4: Trigger the Resizing Event

Finally, you need to instruct the event to fire at the appropriate times. This might be when a controller is intially accessed, or perhaps when data are accessed. The strength of this method is that you can control when resizing occurs. You need to inject $scope and use $broadcast. Here are a couple of examples:

.controller('FirstPageControler', function ($scope) {
      //resize frame when controller loads
      $scope.$parent.$broadcast('resizeframe');
})

.controller('AnotherControler', function ($scope, webServices) {
      webServices.reports().then(function (response) {
          $scope.reports = response;

          //resize frame upon successful return from a service.
          //presumably we have returned with data that will resize the page.
          $scope.$parent.$broadcast('resizeframe');
      }, function (response) {
          $scope.reports = response;
      });
})

That should do the job! Good luck, and feel free to leave a comment if you’ve found a better way.