Azure Functions, JS and App-Only Updates to SharePoint Online

Have you ever, really wanted to have your JavaScript perform a RunWithElevatedPrivileges against SharePoint Online?  Do something that the current user just don't have permission to do?

Today we tackle this core problem that every SharePoint JavaScript developer has thought about at least once.  And we will do it with AzureFunctions.

1.  Register Azure Active Directory App

You can go through dev.office.com to get started - but you'll need various bits of the portal.  So do this from old portal https://manage.windowsazure.com

You need to write copy out your Client ID and Client Secret

Lets give this some App-Only Permissions

Also need to allow implicit flow, that means download the manifest, change the json (below) and upload it back.

 

2.  Create Azure Function

Go to http://functions.azure.com and sign in. 

 This picture looks like an ad in my blog...

This picture looks like an ad in my blog...

Create a new function - I choose the HttpTrigger with Node template.

2.1 Add NPM modules

NodeJS modules (= C# reference libraries) are loaded via npm install (= nuget).  To access this, you need to go to

Function app settings > Go to App Settings > Tools(tipped by @crandycodes)

npm install adal-node
npm install request

The errors are because I don't have a package.json file in the folder.  Add adal-node and also request.  Check they are here.

2.2 NodeJS code

Go back to the function and add this code

var request = require("request");
var adal = require("adal-node");

module.exports = function(context, req) {
    context.log('Node.js HTTP trigger function processed a request. RequestUri=%s', req.originalUrl);

    var authorityHostUrl = 'https://login.microsoftonline.com';
    var tenant = 'johnliu365.onmicrosoft.com';
    var authorityUrl = authorityHostUrl + '/' + tenant;
    var clientId = '37ded58a-YOUR-CLIENT-ID';
    var clientSecret = 'fE4kulPjYOUR-CLIENT-SECRET=';
    var resource = 'https://graph.microsoft.com';

    var authContext = new adal.AuthenticationContext(authorityUrl);

    authContext.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function(err, tokenResponse) {
        if (err) {
            context.log('well that didn\'t work: ' + err.stack);
            context.done();
            return;
        }
        context.log(tokenResponse);

        var accesstoken = tokenResponse.accessToken;
        var options = { 
            method: 'GET', 
            uri: "https://graph.microsoft.com/beta/groups", 
            headers: { 
                'Accept': 'application/json;odata.metadata=full',
                'Authorization': 'Bearer ' + accesstoken
            }
        };

        context.log(options);
        request(options, function(error, res, body){
            context.log(error);
            context.log(body);
            context.res = { body: body || '' };
            context.done();
        });
    });
};

Run it

Got our auth token, and got our list of groups.  Perfect.

2.3 Call SharePoint Online

var request = require("request");
var adal = require("adal-node")

module.exports = function(context, req) {
    context.log('Node.js HTTP trigger function processed a request. RequestUri=%s', req.originalUrl);

    var authorityHostUrl = 'https://login.microsoftonline.com';
    var tenant = 'johnliu365.onmicrosoft.com';
    var authorityUrl = authorityHostUrl + '/' + tenant;
    var clientId = '37ded58a-YOUR-CLIENT-ID';
    var clientSecret = 'fE4kulYOUR-CLIENT-SECRET=';
    //var resource = 'https://graph.microsoft.com';
    var resource = 'https://johnliu365.sharepoint.com';

    var authContext = new adal.AuthenticationContext(authorityUrl);

    authContext.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function(err, tokenResponse) {
        if (err) {
            context.log('well that didn\'t work: ' + err.stack);
            context.done();
            return;
        }
        context.log(tokenResponse);

        var accesstoken = tokenResponse.accessToken;
        /*
        var options = { 
            method: 'GET', 
            uri: "https://graph.microsoft.com/beta/groups", 
            headers: { 
                'Accept': 'application/json;odata.metadata=full',
                'Authorization': 'Bearer ' + accesstoken
            }
        };
        */
        var options = {
            method: "POST",
            uri: "https://johnliu365.sharepoint.com/_api/web/lists/getbytitle('Poked')/items",
            body: JSON.stringify({ '__metadata': { 'type': 'SP.Data.PokedListItem' }, 'Title': 'Test ' + (req.body.name || "hello!") }),
            headers: {
                'Authorization': 'Bearer ' + accesstoken, 
                'Accept': 'application/json; odata=verbose',
                'Content-Type': 'application/json; odata=verbose'
            }
        };


        context.log(options);
        request(options, function(error, res, body){
            context.log(error);
            context.log(body);
            context.res = { body: body || '' };
            context.done();
        });
    });
};

Changed resource to sharepoint.com, also changed the POST options - I want to insert a list item.

This is so sad!  "Unsupported app only token."  :-( :-(

1.1 Backtrack

To move on, we need to backtrack a bit.  @richdizz has a great blog post that documents this - to perform App Only operations on SharePoint Online, the client ID / Client Secret doesn't cut it.  You need to get auth token via certificate.

https://blogs.msdn.microsoft.com/richard_dizeregas_blog/2015/05/03/performing-app-only-operations-on-sharepoint-online-through-azure-ad/

Rich also pointed me to Travis Tidwell (Form.IO) 's excellent

https://github.com/formio/keycred

npm install -g keycred
keycred
(go through the options)

You need three things:

1. The generated keyCredentials JSON

You need to add this part to your AD App Manifest json file - under keyCredentials.  Your JSON should have a really long value for "value"

2. The private key

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAveX4jNn/eBPM1kdRNMPAlh6rT/JFoah9QkUbeYPkYGqWvn7X
~~snipped~~
0ibMc7T5K7AGVT0q0ppBLheFQkeSnPbHJrX40xILEkzd/0RLvC8X
-----END RSA PRIVATE KEY-----

Copy the entire thing, including -----BEGIN and END ----- and save this into a file.  I called mine funky.pem
Upload the funky.pem file into the folder.  I use the VS Online tool.
 

 

3. Certificate Fingerprint:

85b82741408c6c3af462b3a378e3e8963efaad70

2.4 Update to acquireTokenWithClientCertificate

var request = require("request");
var adal = require("adal-node");
var fs = require("fs");

module.exports = function(context, req) {
    context.log('Node.js HTTP trigger function processed a request. RequestUri=%s', req.originalUrl);

    var authorityHostUrl = 'https://login.microsoftonline.com';
    var tenant = 'johnliu365.onmicrosoft.com';
    var authorityUrl = authorityHostUrl + '/' + tenant;
    var clientId = '37ded58a-YOUR-CLIENT-ID';
    var clientSecret = 'DONT NEED USE CERT';
    //var resource = 'https://graph.microsoft.com';
    var resource = 'https://johnliu365.sharepoint.com';

    var thumbprint = '85b82741408c6c3af462b3a378e3e8963efaad70';
    var certificate = fs.readFileSync(__dirname + '/funky.pem', { encoding : 'utf8'});

    var authContext = new adal.AuthenticationContext(authorityUrl);

    authContext.acquireTokenWithClientCertificate(resource, clientId, certificate, thumbprint, function(err, tokenResponse) {
    //authContext.acquireTokenWithClientCredentials(resource, clientId, clientSecret, function(err, tokenResponse) {
        if (err) {
            context.log('well that didn\'t work: ' + err.stack);
            context.done();
            return;
        }
        context.log(tokenResponse);

        var accesstoken = tokenResponse.accessToken;
        /*
        var options = { 
            method: 'GET', 
            uri: "https://graph.microsoft.com/beta/groups", 
            headers: { 
                'Accept': 'application/json;odata.metadata=full',
                'Authorization': 'Bearer ' + accesstoken
            }
        };
        */
        var options = {
            method: "POST",
            uri: "https://johnliu365.sharepoint.com/_api/web/lists/getbytitle('Poked')/items",
            body: JSON.stringify({ '__metadata': { 'type': 'SP.Data.PokedListItem' }, 'Title': 'Test ' + (req.body.name || "hello!") }),
            headers: {
                'Authorization': 'Bearer ' + accesstoken, 
                'Accept': 'application/json; odata=verbose',
                'Content-Type': 'application/json; odata=verbose'
            }
        };


        context.log(options);
        request(options, function(error, res, body){
            context.log(error);
            context.log(body);
            context.res = { body: body || '' };
            context.done();
        });
    });
};

Use fs.readFileSync to read funky.pem file and acquireTokenWithClientCertificate instead of ClientSecret

Result

Note - updated by "funky"

Triggers

There are several Azure Functions triggers that immediately jumps out at me as being really useful.

HTTP Trigger

Your JavaScript can call the azure function and trigger the action.  You will need to turn on CORS.  And filter to only your domain.

But this will give your client side javascript ability to call an elevated function.

Timer Jobs

You can trigger your function based on a timer.  In this scenario, your javascript will run against SharePoint like a timer job. 

WebHooks

Currently only Exchange in Office 365 supports WebHooks.  But SharePoint will support it in the future.  When this becomes available, a webhook can trigger an Azure Function and essentially, you have a List Item event receiver.

Except all in JavaScript.

So much fun haha I'm so excited!

Further Reading