What Every Developer Needs to Know About SharePoint Apps, CSOM, and Anonymous Publishing Sites

This post will show what works and what doesn’t with CSOM and REST in a SharePoint 2013 publishing site that permits anonymous access.  More importantly, we show what you should and should not do… and why.

Overview

I frequently see questions about using SharePoint apps with “public-facing web sites” where the web content is available to anonymous users.  There are a lot of misconceptions about what is possible.  This post will dive into some of the gory details of CSOM with anonymous access.  This demonstration will use an on-premises lab environment instead of O365.

Setting Up the Demonstration

I created a new web application that allows anonymous access (https://anonymous.contoso.lab) and a site collection using the Publishing template.  I then enabled anonymous access for the entire site by going to Site Settings/ Site Permissions and clicking the Anonymous Access button in the ribbon.  Notice that checkbox “Require Use Remote Interfaces permission” that is checked by default… leave it checked for now.

image

Next, I created a SharePoint-hosted app.  I just slightly modified the out of box template.

 'use strict';

var context = SP.ClientContext.get_current();
var user = context.get_web().get_currentUser();

$(document).ready(function () {
    getUserName();
});

function getUserName() {
    context.load(user);
    context.executeQueryAsync(onGetUserNameSuccess, onGetUserNameFail);
}


function onGetUserNameSuccess() {
    if (null != user) {
        //The user is not null
        var userName = null;
        try {
            userName = user.get_title();
        } catch (e) {
            userName = "Anonymous user!";
        }
    }
    $('#message').text('Hello ' + userName);
}


function onGetUserNameFail(sender, args) {
    alert('Failed to get user name. Error:' + args.get_message());
}

Next, I add a client app part to the project.  The client app part isn’t going to do anything special, it just says Hello World.

 <%@ Page language="C#" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage, Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<WebPartPages:AllowFraming ID="AllowFraming" runat="server" />

<html>
<head>
    <title></title>

    <script type="text/javascript" src="../Scripts/jquery-1.8.2.min.js"></script>
    <script type="text/javascript" src="/_layouts/15/MicrosoftAjax.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.runtime.js"></script>
    <script type="text/javascript" src="/_layouts/15/sp.js"></script>

    <script type="text/javascript">
        'use strict';

        // Set the style of the client web part page to be consistent with the host web.
        (function () {
            var hostUrl = '';
            if (document.URL.indexOf('?') != -1) {
                var params = document.URL.split('?')[1].split('&');
                for (var i = 0; i < params.length; i++) {
                    var p = decodeURIComponent(params[i]);
                    if (/^SPHostUrl=/i.test(p)) {
                        hostUrl = p.split('=')[1];
                        document.write('<link rel="stylesheet" href="' + hostUrl + '/_layouts/15/defaultcss.ashx" />');
                        break;
                    }
                }
            }
            if (hostUrl == '') {
                document.write('<link rel="stylesheet" href="/_layouts/15/1033/styles/themable/corev15.css" />');
            }
        })();
    </script>
</head>
<body>
    <h1>Hello, world!</h1>
</body>
</html>

Next, I went to Central Administration / Apps / Manage App Catalog and created an app catalog for the web application.  I published the SharePoint-hosted app in Visual Studio (which just generates the .app package) and then uploaded the .app package to the App Catalog.

image

Next, as the site collection administrator, I added the app to the publishing site.

image

Finally, I edit the main page of the publishing site and add the client app part and test that it works.  Check in the page and publish and you should see something like this:

image

What Do Anonymous Users See?

The question on everybody’s mind is what happens if there is not an authenticated user.  In our simple test, recall that the only thing we are showing is a simple IFRAME with some styling obtained from SharePoint.  The only thing our IFRAME is showing is a page that contains static HTML, “Hello, world!”.  I highlighted the “Sign In” link to show that I really am an anonymous user.

image

Now, click the link “SPHostedClientWebPart Title” (yeah, I know, I am lazy… I should have given it a better name) and you are taken to the full-page experience for the app.  What do we see?  We get an error.

image

That error is saying that the anonymous user does not have access to use the Client Side Object Model.  Just for grins, let’s try the REST API with the URL https://anonymous.contoso.lab/\_api/Web.   First, you get a login prompt.  Next, you see error text that says you do not have access.

image

This makes sense because the CSOM and REST API are not available by default to anonymous users.

Enabling CSOM for Anonymous Users

Let me start this section by saying what I am about to show you comes with some risk that you will need to evaluate for your environment.  Continue reading the entire article to learn about the risks.

That said, go back to Site Settings / Site Permissions and then click the Anonymous Access button in the ribbon.  Remember that rather cryptic-sounding checkbox Require Use Remote Interfaces permission?  Uncheck it and click OK.

image

That check box decouples use of CSOM from the Use Remote Interfaces permission.  When checked, it simply means that the user must possess the Use Remote Interfaces permission which allows access to SOAP, Web DAV, the Client Object Model.  You can remove this permission from users, disabling their ability to use SharePoint Designer.  There are cases where you still want to remove this permission, such as for anonymous users, but you still want to use the CSOM.  This is exactly what the checkbox is letting you do; you are enabling use of CSOM without requiring the users to have that permission.

To test the change, go back to the main page for your site.  Of course, we still see the IFRAME from before, that’s not doing anything with CSOM.  Click the title of the web part to see the full-page immersive experience.  This time, we do not see an error message, instead we see that our code fell into an exception handler because the Title property of the User object was not initialized.  Our error handling code interprets this as an anonymous user. 

image

You just used the app model in a public-facing SharePoint site with anonymous users.

In case you are interested, you can set the same property with PowerShell using a comically long yet self-descriptive method UpdateClientObjectModelUseRemoteAPIsPermissionSetting.

 PS C:\> $site = Get-SPSite https://anonymous.contoso.lab
PS C:\> $site.UpdateClientObjectModelUseRemoteAPIsPermissionSetting($false)

How about that REST API call?  What happens with anonymous users now?

image

Now that you know how to fire the shotgun, let’s help you move it away from your foot.

All Or Nothing

There is no way to selectively enable parts of the CSOM EXCEPT search.

UPDATE: Thanks to Sanjay for pointing out that it is possible to enable search for anonymous without enabling the entire CSOM or REST API, and thanks to Waldek Matykarz for a great article showing how to do it.

Enabling use of CSOM for anonymous users presents a possible information disclosure risk in that it potentially divulges much more information than you would anticipate.  Let me make that clear:  If you remove the require remote interface permission for an anonymous site, the entire CSOM is now available to anonymous users.  Of course, that doesn’t mean they can do anything they want, SharePoint permissions still apply.  If the a list is not made available to anonymous users, then you can’t use the CSOM to circumvent that security requirement.  Similarly, an anonymous user will only be able to see lists or items that have been explicitly made available to anonymous users.  It’s important to know that more than just what you see on the web page is now available via CSOM or REST.

ViewFormPagesLockDown?  Ha!

In a public-facing web site using SharePoint, you want to make sure that users cannot go to form pages such as Pages/Forms/AllItems.aspx where we would see things like CreatedBy and ModifiedBy. The ViewFormPagesLockDown feature is enabled by default for publishing sites to prevent against this very scenario.  This feature reduces fine-grained permissions for the limited access permission level, removing the permission to View Application Pages or Use Remote Interfaces.  This means that an anonymous user cannot go to Pages/Forms/AllItems.aspx and see all of the pages in that library.  If we enable CSOM for anonymous users, you still wont’ be able to access the CreatedBy and ModifiedBy via the HTML UI in the browser, but you can now access that information using CSOM or REST.

To demonstrate, let’s use the REST API as an anonymous user to look through the items in the Pages library by appending _api/Web/Lists/Pages/Items to the site.

image

I’ll give you a moment to soak that one in.

Let me log in as Dan Jump, the CEO of my fictitious Contoso company in my lab environment.  Dan authors a page and publishes it.  An anonymous user now uses the REST API (or CSOM, but if you are reading this hopefully you get that they are the same endpoint) using the URL:

https://anonymous.contoso.lab/\_api/Web/Lists/Pages/Items(4)/FieldValuesAsText

image

The resulting text shows his domain account (contoso\danj) and his name (Dan Jump).  This may not be a big deal in many organizations, but for some this would be a huge deal and an unintended disclosure of personally identifiable information.  Understand that if you enable the CSOM for your public-facing internet site, you run the risk of information disclosure. 

For those that might be confused about my using the term “CSOM” but showing examples using REST, here is some code to show you it works.  I don’t need OAuth or anything here, and I run this from a non-domain joined machine to prove that you can now get to the data.

 using Microsoft.SharePoint.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (ClientContext ctx = new ClientContext(https://anonymous.contoso.lab))
            {
                ctx.Credentials = System.Net.CredentialCache.DefaultCredentials;
                Console.WriteLine("Enter the list name: ");
                string listName = Console.ReadLine();
                List list = ctx.Web.Lists.GetByTitle(listName);

                Console.WriteLine("Enter the field name: ");
                string fieldName = Console.ReadLine();


                CamlQuery camlQuery = new CamlQuery();
                
                ListItemCollection listItems = list.GetItems(camlQuery);
                ctx.Load(
                     listItems,
                     items => items
                         .Include(
                             item => item[fieldName],
                             item => item["Author"],
                             item => item["Editor"]));
                try
                {
                    ctx.ExecuteQuery();

                    foreach (var myListItem in listItems)
                    {
                        Console.WriteLine("{0} = {1}, Created By={2}, Modified By={3}", 
 fieldName, myListItem[fieldName], ((FieldUserValue)myListItem["Author"]).LookupValue,  
 ((FieldUserValue)myListItem["Editor"]).LookupValue );
                    }
                }
                catch (Exception oops)
                {
                    Console.WriteLine(oops.Message);
                }
            }
        }
    }
}

This code simply asks for a list name and a column name that you would like to query data for, such as “Title”. I also include the Created By and Modified By fields as well to demonstrate the potential disclosure risk. Since the CSOM is available to anonymous users, I can call it from a remote machine and gain information that was not intended to be disclosed.

clip_image002

You see that CSOM and REST are calling the same endpoint and getting the same data.

Security Trimming Still in Effect

At this point I have probably freaked a few readers out who didn’t understand the full consequences when they unchecked that checkbox (or, more likely, people who skimmed and did not read this section).  Does this mean that anonymous users can do ANYTHING they want to with the CSOM?  Of course not.  When you configured anonymous access for the web application, you specified the anonymous policy, likely with “Deny Write – Has no write access”. 

image

This means what it says: Anonymous users cannot write, even with the REST API or by using CSOM code.  Further, anonymous users can only see the information that you granted them to see when you configured anonymous access for the site. 

image

If there is content in the site that you don’t want anonymous users to access, you have to break permission inheritance and remove the right for viewers to read.

image

Additionally, there is some information that is already locked down.  Logged in as the site collection administrator, I can go to the User Information List and see information about the site users.

https://anonymous.contoso.lab/\_api/web/lists/getbytitle('User%20Information%20List')/Items

If I try that same URL as an anonymous user, I simply get a 404 not found. 

To summarize this section and make it perfectly clear: security trimming is still in effect.  Unpublished pages are not visible by default to anonymous users.  They can only see the lists that enable anonymous access.  If, despite what I’ve written so far, you decide to enable CSOM for anonymous users, then you will want to make sure that you don’t accidentally grant access for anonymous users to things they shouldn’t have access to.

Potential for DoS

When you use SharePoint to create web pages, you carefully construct the information that is shown on the page and you control how frequently it is queried or seen.  Hopefully you perform load tests to confirm your environment can sustain the expected traffic load before putting it into production.

With CSOM enabled for anonymous users, all that testing was pointless.

There is no caching with the CSOM, so this would open up the possibility for me to do things like query batches of information to query 2000 items from multiple lists simultaneously, staying under the default list view threshold while taxing your database. Now if I can get a few other people to run that code, say by using some JavaScript exploit and posting it to Twitter, then I now have the makings for a DoS attack… or at least one hell of a stressful day for my databases.

You Really Need to Understand How OAuth Works

Hopefully by now I have convinced you that enabling the CSOM for anonymous users to directly access is not advised (and hence why it is turned off by default).  At this point in the conversation, I usually hear someone chime in about using the app only policy.  Let me tell you why this is potentially a MONUMENTALLY bad idea.

With provider-hosted apps, I can use this thing called the App Only Policy.  This lets my app perform actions that the current user is not authorized to do.  As an example, an administrator installs the app and grants it permission to write to a list.  A user who has read-only permission can use the app, and the app can still write to the list even though the user does not have permission.  Pretty cool! 

I have presented on the SharePoint 2013 app model around the world, and I can assure you that it all comes down to security and permissions.  Simply put, you must invest the time to understand how OAuth works in SharePoint 2013.  Watch my Build 2013 talk Understanding Authentication and Permissions with Apps for SharePoint and Office as a good starter.

The part to keep in mind is how OAuth works and why we say you ABSOLUTELY MUST USE SSL WITH SHAREPOINT APPS.  It works by the remote web site sending an access token in the HTTP header “Authorization” with a value of “Bearer “ + a base64 encoded string.  Notice I didn’t say encrypted, I said encoded.   That means that the information can easily be decoded by anybody who can read the HTTP header value.

I showed a working example of this when I wrote a Fiddler extension to inspect SharePoint tokens.  It isn’t that hard to crack open an access token to see what’s in it.

image

If you have a public-facing web site, you are likely letting everyone access it using HTTP and not requiring HTTPS (of course, you are doing this).  If you tried to use a provider-hosted app without using SSL, then anyone can get to the Authorization token and replay the action, or worse.  They could create a series of tests against lists, the web, the root web, the site collection, additional site collections, or the tenant to see just what level of permission the app had.  If the app has been granted Full Control permission to a list, it has the ability to do anything it wants to that list including delete it.  Even though your app may only be writing a document to the list, your app is authorized to do anything with that list.  Start playing around with CSOM and REST, and I can do some nasty stuff to your environment. 

One more thing… the access token is good for 12 hours.  That’s a pretty large window of time for someone to get creative and make HTTP calls to your server, doing things that you never intended.

Doing It The Right Way

Suffice to say, you DO NOT want to try using CSOM calls in a provider-hosted app to an anonymous site, and you DO NOT want to enable CSOM for anonymous users.  Does this completely rule out using the app model?  

You could set up a different zone with a different URL that uses SSL, and your app will communicate to SharePoint using only server-side calls to an SSL protected endpoint.  To achieve this, the app would have to use the app-only policy because no user information would be passed as part of the token (see my blog post, SharePoint 2013 App Only Policy Made Easy for more information).

image

The reason that I stressed server-side calls in the previous paragraph is simple: if they were client-side calls using the Silverlight CSOM or JavaScript CSOM implementation, we’d be back at the previous problem of exposing the CSOM directly to anonymous users.

This pattern means there are certain interactions with apps that are not going to work easily.  For instance, using this pattern with an ACS trust means that a context token will not be passed to your app because your app is using HTTP.  You can still communicate with SharePoint, but the coding is going to look a bit different. 

 string realm = TokenHelper.GetRealmFromTargetUrl(siteUri);

//Get the access token for the URL.  
//Requires this app to be registered with the tenant
string accessToken = TokenHelper.GetAppOnlyAccessToken(
    TokenHelper.SharePointPrincipal, 
    siteUri.Authority, 
    realm).AccessToken;

//Get client context with access token
using(var clientContext = TokenHelper.GetClientContextWithAccessToken(
     siteUri.ToString(),accessToken))
{
    //Do work here
}

Instead of reading information from the context token, we are simply making a request to Azure ACS to get the access token based on the realm and the URL of the SharePoint site.  This keeps the access token completely away from users, uses SSL when communicating with SharePoint, and allows you to control the frequency and shape of information that is queried rather than opening access to the whole API to any developer who wants to have a go at your servers.

Enabling Search for Anonymous Users

Sanjay Narang pointed out in the comments to this post that it is possible to enable anonymous search REST queries without removing the requires remote interface permission setting.  This is detailed in the article SharePoint Search REST API overview.  administrators can restrict what query parameters to expose to anonymous users by using a file called queryparametertemplate.xml. To demonstrate, I first make sure that the site has the requires remote interface permission.

image

Next, I make sure that search results will return something.  I have a library, Members Only, that contains a single document.

image

That document contains the following text.

image

I break permissions for the library and remove the ability for anonymous users to access it.

image

A search for “dan” returns two results if I am an authenticated user.

image

A search for “dan” only returns 1 result as an anonymous user.

image

If I attempt to use the search REST API as an authenticated user, I get results.

image

If I attempt it as an anonymous user, I get an HTTP 500 error. 

image

Looking in the ULS logs, you will see that the following error occurs.

Microsoft.Office.Server.Search.REST.SearchServiceException: The SafeQueryPropertiesTemplateUrl "The SafeQueryPropertiesTemplateUrl &quot;{0}&quot; is not a valid URL." is not a valid URL.  

To address this, we will use the approach detailed by Waldek Mastykarz in his blog post Configuring SharePoint 2013 Search REST API for anonymous users.  I copy the XML from the SharePoint Search REST API overview article and paste into notepad.  As instructed in that article, you need to replace the farm ID, site ID, and web ID.  You can get those from PowerShell.

 PS C:\> Get-SPFarm | select ID

Id                                                                             
--                                                                             
bfa6aff8-2dd4-4fcf-8f80-926f869f63e8                                           



PS C:\> Get-SPSite https://anonymous.contoso.lab | select ID

ID                                                                             
--                                                                             
6a851e78-5065-447a-9094-090555b6e855                                           



PS C:\> Get-SPWeb https://anonymous.contoso.lab | select ID

ID                                                                             
--                                                                             
8d0a1d1a-cdae-4210-a794-ba0206af1751   

Given those values, I can now replace them in the XML file.

image

Save the XML file to a document library named QueryPropertiesTemplate.

image

Finally, append &QueryTemplatePropertiesUrl='spfile://webroot/queryparametertemplate.xml' to the query.

image

Now try the query as an anonymous user, and you get results even though we still require the use remote interfaces permission.   The rest of the CSOM and REST API is not accessible, only search is, and only for the properties allowed through that file.

image

Summary

This was a long read (took even longer to write), but hopefully it helps to answer the question if you should use apps for your public-facing site.  As you saw, you can easily create client app parts and even custom actions.  You should not enable CSOM for anonymous access unless you are OK with the risks and can mitigate against them.  You can still use the app model, but it’s going to take a little more engineering and will require your SharePoint site have an endpoint protected by SSL that is different than the endpoint that users will access.

For More Information

Understanding Authentication and Permissions with Apps for SharePoint and Office

SharePoint 2013 App Only Policy Made Easy

Fiddler extension to inspect SharePoint tokens

Configuring SharePoint 2013 Search REST API for anonymous users.

SharePoint Search REST API overview