SharePoint Online: App Only policy PowerShell tasks with ACS

Tags: SharePoint, Office 365, PowerShell, Apps

Here’s a little nugget that I’ve planned to blog about for some time, that I needed today for a small task. I needed to do a background job to SharePoint Online that at a scheduled interval downloads list data, process them and optionally updates some data in my site. This can of course be done by creating an executable storing username and password combos, and with the help of the TokenHelper.cs class from the App for SharePoint Web Toolkit NuGet package and some stored username and password combos we can make the Auth pieces quite easy. I don’t like that approach. There’s two big drawbacks with that approach. The first one is storing the username and password – we can solve that with an AppOnly policy, which I blogged about in the SharePoint 2013: Using the App Only policy and App Principals instead of username and password combos post. The second issue is that I very much prefer to script these kind of tasks, it makes it more flexible. Problem with that approach is that we need to manually do the Auth pieces. But from now on you just copy and paste from this post.

Creating the App Principal

In order to create our PowerShell script we need to create an App for it. This step is exactly the same as we did in the blog post mentioned above. But let’s repeat it. Note! I do use the traditional way of registering apps in this scenario using ACS – I do not use an Azure AD app. The reason for this is I want every Site Collection admin to be able to script like this. Azure AD apps requires way to much permissions for the normal user.

imageIn your site collection, navigate to /_layouts/15/appregnew.aspx. Click on both Generate buttons, so that you get one Client Id and one Client Secret. Next enter a Title, an App Domain and the Redirect URI. The App Domain and Redirect URI can be basically anything in this scenario. Then click Create to create the App Principal. On the next screen you will get all the details of your App. Copy and paste that data so you don’t loose it.

Next head on over to /_layouts/15/appinv.aspx. Paste your Client Id in the App Id (Microsoft has never been good in naming conventions) text box and click Lookup. This will retrieve the rest of the app details. Since we will not have any UI or install any App we need to manually ask for permissions and then grant the permissions. What we do is that we create a permission request XML. Depending on your requirements your XML may be different from the one below. The following permission request XML asks for Full Read permissions on the whole web.

<AppPermissionRequests AllowAppOnlyPolicy="true">
  <AppPermissionRequest
    Scope="http://sharepoint/content/sitecollection/web"
    Right="Read"/>
</AppPermissionRequests>

Note the AllowAppOnlyPolicy=”true” attribute – that one is the key to allowing the App to run without username and password. Once you paste the XML into the permission request XML textbox and then click on Create. You will see the Trust screen for your app. Make sure the permission request is what you expect and if so, click on Trust it!.

Auth in PowerShell

Now, let’s get to the core of this post and let’s create a PowerShell script that uses this app to read items from a list. I split it up in a few different parts to make it easier to follow. In the end of the post you will get a link to the full code sample.

Defining some constants

Let’s start by defining some constants:

$clientId = "16119847-8ac7-4a3a-a2e5-18debd9fc9d2"
$secret = "KuZj5UD22oy2.....=";
$redirecturi = "https://localhost"

$url = "https://tenant.sharepoint.com/sites/thesite/"
$domain = "tenant.sharepoint.com"
$identifier = "00000003-0000-0ff1-ce00-000000000000"

The $clientId, $secret and $redirecturi are copied directly from the results of my app registration. The $url parameter is the URL of the site where I registered the app, and $domain is just the server part of that URL. Finally the $identifier is a static Guid value, which represents SharePoint (Exchange, Lync, Workflow etc has their own Id’s).

Retrieving the Realm

The next step is to retrieve the Realm or Tenant Id. You might already know this or you might just run these commands once and store it as a static variable.

$realm = ""
$headers = @{Authorization = "Bearer "} 
try { 
    $x = Invoke-WebRequest -Uri "$($url)_vti_bin/client.svc" -Headers $headers -Method POST -UseBasicParsing
} catch {
    #We will get a 401 here
      $realm = $_.Exception.Response.Headers["WWW-Authenticate"].Substring(7).Split(",")[0].Split("=")[1].Trim("`"")
}

What we do here is to send a request to the client.svc endpoint and actually expect to get thrown a 401 back. When we get the 401 we’ll locate the WWW-Authenticate headers and retrieve the Realm property. Yea, that PoSh line could be a bit more prettier and failsafe, but it works on my machine.

Retrieving the access token

When we have the realm we can create the authorization code. This is how we combine all our variables into an authorization code:

[System.Reflection.Assembly]::LoadWithPartialName("System.Web") | Out-Null
$body = "grant_type=client_credentials"
$body += "&client_id=" +[System.Web.HttpUtility]::UrlEncode( $clientId + "@" + $realm)
$body += "&client_secret=" +[System.Web.HttpUtility]::UrlEncode( $secret)
$body += "&redirect_uri=" +[System.Web.HttpUtility]::UrlEncode( $redirecturi)
$body += "&resource=" +[System.Web.HttpUtility]::UrlEncode($identifier + "/" + $domain + "@" + $realm)

Let’s walk this through. First of all I load the System.Web assembly, if you run this as a scheduled task this assembly is not loaded in your app domain, compared to when running it in PowerShell ISE for instance, and we need that assembly for some encoding.

The actual authorization code starts with a grant_type which we set to the static variable of client_credentials, which means that we do not pass any user credentials or refresh tokens. Client_Id is not exactly the same Client Id as above, here we need to append “@” and the realm to scope the request to our tenant. The Client secret and redirect Uri is the same as when creating the app. Finally we have the resource token which is a combination of the SharePoint identifier, the domain and the realm. Note that if you’re targeting the anything in a Personal Site, you not only have to update the $url variable but also the $domain variable.

We send all this data to the Azure Access Control Services (ACS), remember we did not use Azure AD, endpoint like this:

$or = Invoke-WebRequest -Uri "https://accounts.accesscontrol.windows.net/$realm/tokens/OAuth/2" `
    -Method Post -Body $body `
    -ContentType "application/x-www-form-urlencoded"
$json = $or.Content | ConvertFrom-Json
When invoking the endpoint, using our authorization code above, we will get a JSON formatted string back. We convert this string into an object using ConvertFrom-Json.

Use the access token

Finally we can use the result from the ACS endpoint and get our access token which we’ll pass into the REST end point (as an Authorization Bearer token) of the site where we want to do operations.

$headers = @{
    Authorization = "Bearer " + $json.access_token;
    Accept ="application/json"
} 

Invoke-RestMethod -Uri "$($url)_api/lists/GetByTitle('Documents')/Items" -Method Get -Headers $headers

Summary

That wasn’t to hard right? All we needed to know was the basic process of OAuth 2.0 and know how to create and pars the requests and responses. The full code sample can be found here: https://gist.github.com/wictorwilen/db67725a66a3e40789e3

5 Comments

  • Erwin said

    Nice description!

    <ShamelessPromotion>
    The PnP PowerShell cmdlets (https://github.com/OfficeDev/PnP/tree/master/Solutions/PowerShell.Commands) have functionality for app only calls too:

    Connect-SPOnline -Url <yoururl> -AppId <appid> -AppSecret <appsecret>
    $context = Get-SPOContext
    </ShamelessPromotion>
    The cmdlets are focussed on using the CSOM library and not use REST, so if you feel more comfy with REST calls your example is absolutely great :-)

  • Wictor said

    Thanks Erwin, there's a purpose of using those cmdlets but not in this case. You want something portable, not relying on compiled code or requiring to install something.
    Also this can very easily be modified to support other Auth mechanisms such as Azure AD without being code savvy at all.

  • Erwin said

    Absolutely fair remark. The PnP cmdlets are more intented to be installed on a machine you use for day to day admin tasks/dev tasks. You describe a fully portable solution not requiring any of the CSOM libraries to be present on the machine you're running it on, which is elegant and absolutely has its benefits.

Comments have been disabled for this content.

About Wictor...

Wictor Wilén is the Nordic Digital Workplace Lead working at Avanade. Wictor has achieved the Microsoft Certified Architect (MCA) - SharePoint 2010, Microsoft Certified Solutions Master (MCSM) - SharePoint  and Microsoft Certified Master (MCM) - SharePoint 2010 certifications. He has also been awarded Microsoft Most Valuable Professional (MVP) for seven consecutive years.

And a word from our sponsors...