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.
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.
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
- http://www.jeremythake.com/2016/04/using-azure-functions-with-the-microsoft-graph-and-bing-translator-apis/
- https://twitter.com/crandycodes