共用方式為


Introducing SharePointContext for Provider-Hosted SharePoint Apps!

One of the more frustrating parts of building provider-hosted apps for SharePoint 2013 was that you needed to choose ahead of time if you were targeting a low-trust app or a high-trust app and use the appropriate methods in TokenHelper.cs.  Not only that, but figuring out how to use the app-only policy was less than straightforward.  You were also left to your own devices for caching.  The code in Visual Studio 2013 addresses this with the new SharePointContext.

Choose Your Weapon

As an example, here is a snippet of code from my blog post SharePoint 2013 App Only Policy Made Easy that shows how to use a high-trust app with the app-only policy.

 string appOnlyAccessToken = 
   TokenHelper.GetS2SAccessTokenWithWindowsIdentity(_hostWeb, null);

using (ClientContext clientContext = 
    TokenHelper.GetClientContextWithAccessToken(_hostWeb.ToString(), appOnlyAccessToken))
{
    List list = clientContext.Web.Lists.GetByTitle("Announcements");
    ListItemCreationInformation info = new ListItemCreationInformation();
    Microsoft.SharePoint.Client.ListItem item = list.AddItem(info);
    item["Title"] = "Created from CSOM";
    item["Body"] = "Created from CSOM " + DateTime.Now.ToLongTimeString();

    item.Update();
    clientContext.Load(item);
    clientContext.ExecuteQuery();
}

Unless you heavily comment your code (guilty, I don’t), it would be difficult to figure out that the null parameter in the call to GetS2SAccessTokenWithWindowsIdentity actually meant “don’t pass in user information as part of the token”, therefore using the app-only policy.  To compound the problem, that code would only work with high-trust apps.  If you are building a low-trust app, the method used to obtain the access token would look very different, as the use of an app-only token is explicitly declared. 

 SharePointContextToken contextToken =
    TokenHelper.ReadAndValidateContextToken_contextTokenString, Request.Url.Authority);

//Get app only access token.
string appOnlyAccessToken = 
    TokenHelper.GetAppOnlyAccessToken(contextToken.TargetPrincipalName, 
            _hostWeb.Authority, contextToken.Realm).AccessToken;

using (ClientContext clientContext = 
    TokenHelper.GetClientContextWithAccessToken(_hostWeb.ToString(), appOnlyAccessToken))
{
    List list = clientContext.Web.Lists.GetByTitle("Announcements");
    ListItemCreationInformation info = new ListItemCreationInformation();
    Microsoft.SharePoint.Client.ListItem item = list.AddItem(info);
    item["Title"] = "Created from CSOM";
    item["Body"] = "Created from CSOM " + DateTime.Now.ToLongTimeString();

    item.Update();
    clientContext.Load(item);
    clientContext.ExecuteQuery();
}

Developers were left to their own devices to try to figure out how to create a factory pattern to abstract this low-level code.  So there’s two problems highlighted.  The first is that the way you obtain the app-only token is very different in both models, the second is that it’s not very straightforward to begin with.  Another thing in that code that I don’t particularly like is that you have to pass in the host web or app web URL. 

The Problem with the Cache

There’s another more insidious problem in there.  This code will cause the token to be obtained every time this code is run.  In the S2S case, this isn’t necessarily that huge of an impact, but in the case of the low-trust app, this is a performance hit because it requires a call to Azure ACS each time.  To avoid this, you had to implement your own cache strategy, and that often led to developers putting the access token in an HTTP cookie.  This is particularly bad because it opens you up to attacks where the access token could be obtained and reused.  Consider the access token your app’s username and password and keep the keys away from the clients who use your app.  There is a property in the context token called CacheKey (see my post Inside SharePoint 2013 OAuth Context Tokens for more information on the context token) that you should store in an HTTP cookie instead, and use that key to reference the refresh and access token stored in state on the server.  Without having that code, developers were left on their own to implement caching without understanding the ramifications.

One Code to Rule Them All

OK, you’ve trudged through the rather extensive setup of the problems being addressed.  Enough already, show me what’s changed to improve the situation!

The new SharePointContext abstracts the details of an app using ACS (a low-trust app) or S2S (a high-trust app).  The class structures supporting SharePointContext look like the following:

There are abstract classes for the provider and context, and concrete classes for high trust and low trust apps.  Very nicely done.

THE PAYOFF – RUNS EITHER AS S2S or ACS!

 var spContext = SharePointContextProvider.Current.GetSharePointContext(Context);
using (var appOnlyClientContext = spContext.CreateAppOnlyClientContextForSPHost())
            {
                List list = appOnlyClientContext.Web.Lists.GetByTitle("Announcements");
                ListItemCreationInformation info = new ListItemCreationInformation();
                Microsoft.SharePoint.Client.ListItem item = list.AddItem(info);

                item["Title"] = "Created from App Only CSOM " + DateTime.Now.ToLongTimeString();
                item["Body"] = "App Only created by CSOM";

                item.Update();
                appOnlyClientContext.Load(item);
                appOnlyClientContext.ExecuteQuery();
            }           

This same code runs whether my app is a high-trust app or a low-trust app!

The way this works is the SharePointContextProvider has logic that detects if the app is a high trust app or not.  The default static constructor for SharePointContextProvider checks to see if the app is a high trust app or not and creates a concrete provider class to handle the specific differences between each type of authorization.

         static SharePointContextProvider()
        {
            if (!TokenHelper.IsHighTrustApp())
            {
                SharePointContextProvider.current = new SharePointAcsContextProvider();
            }
            else
            {
                SharePointContextProvider.current = new SharePointHighTrustContextProvider();
            }
        }

That call to IsHighTrustApp, how does it figure out if this is a high trust app or not?  It checks for SigningCredentials.

         public static bool IsHighTrustApp()
        {
            return SigningCredentials != null;
        }

SigningCredentials is a property that’s populated by looking in the web.config for an appSetting value containing the certificate path.  If there is a certificate, SigningCredentials will be non-null, and it is deemed a high trust app.  In case it isn’t abundantly clear, you no longer have to create two versions of your app (hope you weren’t doing this), or write a bunch of code just to figure out how to write the app only once.

 

Fixing the Cache Issue

The other really nice part of this new model is that you don’t have to worry about the cache problem nearly as much.  When you make the call to GetSharePointContext, the implementation of that method makes a call to SaveSharePointContext.

 public SharePointContext GetSharePointContext(HttpContextBase httpContext)
        {
            if (httpContext == null)
            {
                throw new ArgumentNullException("httpContext");
            }

            Uri spHostUrl = SharePointContext.GetSPHostUrl(httpContext.Request);
            if (spHostUrl == null)
            {
                return null;
            }

            SharePointContext spContext = LoadSharePointContext(httpContext);

            if (spContext == null || !ValidateSharePointContext(spContext, httpContext))
            {
                spContext = CreateSharePointContext(httpContext.Request);

                if (spContext != null)
                {
                    SaveSharePointContext(spContext, httpContext);
                }
            }

            return spContext;
        }

Why is this a win?  For starters, it does the right thing by using a cookie to store the CacheKey, and stores the actual token in session state on the server referenced by the cache key.  Notice that call to LoadSharePointContext.  For the SharePointAcsContextProvider concrete class, this method looks like the following:

         protected override SharePointContext LoadSharePointContext(HttpContextBase httpContext)
        {
            return httpContext.Session[SPContextKey] as SharePointAcsContext;
        }

How sweet is that?  It does the right thing and looks in session state for the context based on the key.  That significantly reduces the amount of traffic.  Note, though, that it is using session state, which doesn’t survive nearly as long as the refresh token and access token.  You may have different caching needs.  Thankfully, the code is generated for you as part of your SharePoint provider-hosted app project, and the classes are not sealed.  This means you can modify or extend the behavior to suit your needs.  You could also change and extend TokenHelper.cs as well, such as in Steve Peschka’s sample in his post Using SharePoint Apps with SAML and FBA Sites in SharePoint 2013.

Host Web or App Web?

This is also a nice change in Visual Studio 2013 for the SharePointContext that lets you explicitly use the app-only or app+user context, and explicitly target the app web or host web.  Notice the members

image

Notice the methods for CreateAppOnlyClientContextForSP*, CreateUserClientContextForSP*, and each has suffixes of *AppWeb and *Host.  This makes the coding much more explicit and self-documenting without having to discover my old blog posts to figure it out.

Great job, Visual Studio team!  Well done!

You can find more information from Chris Johnson’s blog post, SharePoint app tools in Visual Studio 2013 preview, the new SharePointContext helper!  Chris highlights additional benefits, such as surviving postbacks in MVC apps and the [SharePointContextFilter] attribute that ensures that you have a context.  He provides additional links to talks from the Build Conference in that post. 

For More Information

Using SharePoint Apps with SAML and FBA Sites in SharePoint 2013

Inside SharePoint 2013 OAuth Context Tokens

SharePoint 2013 App Only Policy Made Easy

SharePoint app tools in Visual Studio 2013 preview, the new SharePointContext helper!

Comments

  • Anonymous
    April 15, 2014
    Great Article Kirk !!  I've been fighting with the TokenHelper class and have been struggling to get it to do what I need.  I hadn't even spotted that - in the move up from VS2012 to 2013 that I now have SharePointContext.cs in the solution. One question though .... is this only limited to the immediate HostWeb when getting a sharepoint context?  I have a scenario where my app is installed in sitecollection1 ... but I want to get data from sitecollection2 in my tenancy (autohosted on SharePoint Online). The problem is  - the call to sharepointcontextprovider.current.getsharepointcontext(Context) is looking in sitecollection for my lists.  Of course, they don't exist.  Can I get a context to a different url / sharepoint site collection in my tenancy?

  • Anonymous
    April 15, 2014
    The comment has been removed

  • Anonymous
    April 15, 2014
    Thanks for the speedy response.  It's not Mysites that I'm looking at; I'm wanting to work with a central pool of data and that's why my lists are in a predefined and known site collection.  Clearly, the app can be installed in any site collection by users with relevant permissions from the "corporate app catalog". I'm using the GetClientContextWithAccessToken already .... but it's the contextTokenString variable in the following that I struggle with beyond the initial page load :- protected ClientContext UserContext(String siteurl)        {            Uri sharepointUrl = new Uri(siteurl);            if (contextTokenString == "") { contextTokenString = TokenHelper.GetContextTokenFromRequest(HttpContext.Current.Request); }            //Get context token.            SharePointContextToken contextToken = TokenHelper.ReadAndValidateContextToken(contextTokenString, HttpContext.Current.Request.Url.Authority);            //Get access token.            string accessToken = TokenHelper.GetAccessToken(contextToken, sharepointUrl.Authority).AccessToken;            ClientContext clientContext = TokenHelper.GetClientContextWithAccessToken(sharepointUrl.ToString(), accessToken);            return clientContext;        } I pass in the sitecollection url where I'm wanting to work and attempt to return the clientContext with which to (use). The assignment of the contextTokenString only works on the initial call (presumably from the appredirect.aspx page??) .... so I'm saving that in a hidden field on my page for any subsequent calls back to SharePoint.  Is this the correct way / best practice?  I'm assuming that for most people, Apps are intented to provide a small specific piece of functionality and most will likely work in the app web or on artefacts in the host.  What I'm trying to achieve is kind of like a pseudo-central storage (think SQL) so that all transactions post to a single location. Cheers, Steve

  • Anonymous
    March 09, 2015
    The comment has been removed