Hi SharePoint guys,
In the past few weeks, I focused on Node.js and how to leverage this awesome technology to build stuff in the SharePoint area. It is really a great opportunity to better understand what is behind the scene in SharePoint provider-hosted addins. During my experimentation, I implemented a node module to help handling SharePoint common tasks (such as authentication and REST requests, …). But this will be the topic of a future blog post !
Today, we will discuss something else. Great fresh news for the Office 365 & SharePoint developers community,
SharePoint Webhooks are now Generally Available !
https://dev.office.com/blogs/sharepoint-webhooks-is-now-generally-available-build-service-oriented-processes-in-sharepoint
What is a Webhook ?
Webhooks tend to become a standard since most of the biggest platforms on the Web ( GitHub, Facebook, WordPress, VisualStudioOnline, DropBox, PayPal, …) leverage them. In a few words, a Webhook is a simple HTTP callback triggered by an event on a system.
Yes… a new name for Remote Event Receivers !
Not at all! Even if the Remote Event Receivers and Webhooks allow to trigger a process after an event, RER and Webhooks do not offer the same capabilities and specificities.
Webhooks
- Webhooks are simple HTTP POST handlers
- Can be easily implemented in any technology that offers HTTP capabilities
- Only notify “something happened” but not the involved item details or even the kind of event. The event nature should be discovered subsequently via the ChangeItem API
- Asynchronously triggered after an item changed
- Available only for list item changes
- Long-running processes should be implemented in background jobs triggered by the webhook
- The callback should send a response within 5 seconds
Remote Event Receivers
- More complex and heavyweight software architecture than Webhooks (WCF services with .NET classes)
- Handles synchronous (-ing) before the actual update and asynchronous events (-ed) after the actual update on items
- Can be triggered by specific events (add, update, delete)
To summary, RERs will remain for synchronous events handling while Webhooks coupled with background jobs can meet all other needs.
Implement a Webhook with Node.js
It is pretty easy to implement a SharePoint WebHook with Node.js!
let’s prepare our application.
Prerequisites
- You need a proper Node environment (Check my previous post about Node.js)
- You need to get the Ngrok tool
- You need a SharePoint online site collection
Let’s start !
- Let’s start Ngrok to get your temporary public URL:
> ngrok http 3000 –host-header=localhost:3000
Copy the given HTTPS url.
- In SharePoint, register a new App (http://tenant.sharepoint.com/<path>/_layouts/15/appregnew.aspx)
- Paste the public url as the Redirect URI, and paste the domain
(Note: It doesn’t really matter since we will stick to app-only in this sample app. However, it would be necessary in a app+user context since SharePoint has to post the app context to your app) - Click the Create button and copy the Client ID and Client Secret in a safe location.
- Make sure your app has sufficient and app-only policy enabled permissions
- Go to http://tenant.sharepoint.com/<path>/_layouts/15/appinv.aspx
- Copy/Paste the Client Id into the field App Id (1)
- Click the Lookup button (2)
- Copy/Paste the permissions XML into the text area (3)
<AppPermissionRequests AllowAppOnlyPolicy="true"<AppPermissionRequest Scope="http://sharepoint/content/sitecollection" Right="FullControl" /> </AppPermissionRequests>
- Click the “Create” button to apply the permissions
- Paste the public url as the Redirect URI, and paste the domain
- Create a workspace directory on your local dev environment
- In your directory, from the terminal, type
> npm install express-generator -g
> express - In the generated package.json file, add the following dependencies
"node-fetch": "^1.6.3",
"spaddin-helper": "git+https://github.com/ypcode/node-spaddin-helper.git"(Notice the spaddin-helper which is my node module currently hosted in my github repo)
- Install the modules
> npm install –save - Besides the app.js file, create a config.js file with the following content :
var config = { ClientId : "<your client Id>", ClientSecret : "<your client secret>", SPHostUrl : "<your SharePoint site url>", WebhooksUrl: "<your ngrok public url>/webhooks" }; module.exports = config;
(Replace the values by yours)
- In the app.js file, add the following lines (for example at line 10)
var config = require("./config"); var sp = require("spaddin-helper"); sp.SharePointAddinConfiguration.init(config.ClientId, config.ClientSecret);
Test our app
To make sure our app is properly configured, we can try to display a simple property from our SharePoint site.
CAUTION: Since we are in App-Only mode, we cannot query users information.
For instance, you can replace the content of the routes/index.js file by the following
var express = require('express'); var router = express.Router(); var sp = require("spaddin-helper"); var config = require("../config"); /* GET home page. */ router.get('/', function(req, res, next) { let ctx = new sp.SharePointContext(config.SPHostUrl); ctx.createAppOnlyClientForSPHost().then(client => { client.retrieve('_api/web/Title') .then(data => { let webTitle = data.d; res.render('index', { title: "WebHook on site " + webTitle }); }).catch((error) => { res.render('error', { error: error }); }); }); }); module.exports = router;
You can run your app by executing the npm start command, try to reach the public (ngrok) URL of your site:
We are now ready to continue with the Webhook matters
Subscribe a Webhook
Currently, the only way to subscribe to a SharePoint Webhook is through a call to the SharePoint REST API.
How does it work ?
You have to issue a HTTP POST request to <site>/_api/web/lists(‘id-of-list’)/subscriptions with the following body (Content-Type should be JSON):
{ resource: "<webUrl>/_api/web/lists('list-of-id')", notificationUrl: "https://yourserver/foo/bar/", expirationDateTime: "2017-06-01T00:00:00+00:00" }
When receiving the subscription request, SharePoint will try to validate your Webhook by sending it a POST request with a query string parameter called “validationtoken” that has a random value, your endpoint should respond to this request with the random value in the body.
In our Node app
We will build a basic UI that allows to subscribe a Webhook to a specific list.
Let’s add the UI, in the routes/index.js file, add the following right after the / route handler
router.get('/subscriptions', (req, res) => { let ctx = new sp.SharePointContext(config.SPHostUrl); ctx.createAppOnlyClientForSPHost().then(client => { let vm = { lists: [] }; // We retrieve all the lists of the current web client.retrieve('_api/web/lists?$select=Title,Id') .then(data => { // Add the results to the ViewModel vm.lists = data.d.results; }).then(() => { res.render('subscriptions', vm); }).catch((error) => { res.render('error', {error:error}); }); }); });
In this route handler, we retrieve all the lists of the current web and pass them to the view. Let’s add this view. Add a new file subscriptions.jade in the views folder with the following content
extends layout block content h1 SharePoint Webhook subscriptions form(action="/subscriptions", method="POST") if subscriptionCreated div Subscription has been requested for list #{listId} else div select(name="listId") each list in lists option(value=list.Id) #{list.Title} input(type="submit",name="submit",value="submit")
We have now to implement the backend for this UI, in the routes/index.js file, add the following
router.post('/subscriptions', (req, res) => { let ctx = new sp.SharePointContext(config.SPHostUrl); // We verify the listId POST argument let listId = req.body.listId; if (!listId) { res.send(400); // Bad request return; } ctx.createAppOnlyClientForSPHost().then(client => { // Content Type is not ODATA but regular JSON client.odataVerbose = false; client.create(`_api/web/lists('${listId}')/subscriptions`,{ resource: ctx.SPHostUrl + `_api/web/lists('${listId}')`, notificationUrl: config.WebhooksUrl, expirationDateTime: "2017-02-18T00:00:00+00:00" }) .then((resp) => { let error = resp["odata.error"]; if (error) { res.render('error', {message: error.message.value ,error:{status:"",stack:""}}); } else { res.render("subscriptions", {subscriptionCreated:true, listId: listId}); } }) .catch((error) => { res.render('error', {error:error}); }); }); });
And we have the subscriptions basic (and UGLY 😉 ) UI.
With this in place, we almost have everything to subscribe the Webhook.
Remember I mentionned above that the Webhook endpoint has to be validated on registration, this is achieved by a reply from the Webhook to SharePoint
The Webhook endpoint
The Webhook endpoint is responsible for the notification handling as well as its self validation during the subscription process. As already stated, it is nothing more than a handler to a HTTP POST request.
To stay clean, we will implement a dedicated route for the WebHook business.
In the routes folder, add a webhooks.js file and put the following content in it :
var express = require('express'); var router = express.Router(); const handleNotification = (data) => { console.log("============NOTIFICATION=============="); console.log(`Subscription: ${data.subscriptionId}`); console.log(`Client State: ${data.clientState}`); console.log(`Expiration: ${data.expirationDateTime}`); console.log(`Resource: ${data.resource}`); console.log(`Site Url: ${data.siteUrl}`); console.log(`Web ID: ${data.webId}`); console.log("===========/NOTIFICATION=============="); }; // Webhook handler router.post('/', (req, res) => { // Validate if new subscription if (req.query.validationtoken) { // Return a text/plain Success response // with the validationtoken query string parameter res.setHeader("Content-Type", "text/plain"); res.send(200, req.query.validationtoken); return; } let payload = req.body.value; if (!payload) { res.send(400, "Bad Request"); return; } if (Array.isArray(payload)) { payload.forEach(notification => { handleNotification(notification); }); } else { handleNotification(payload); } res.send(200, "OK"); }); module.exports = router;
The code above will validate the Webhook in the case of a subscription validation, otherwise it will process the notification, in our case, it will simply output the notification content in the console.
The final thing to do is to configure this route in our app.js file
- Replace the line 9
var users = require(‘./routes/users’);)
by
var webhooks = require(‘./routes/webhooks’); - Replace the line 28
app.use(‘/users’, users);
by
app.use(‘/webhooks’, webhooks); - (You can also delete the routes/users.js file which is useless in our case)
You can now
- launch your application (npm start)
- go to the subscriptions page (<your site>/subscriptions )
- Subscribe a Webhook to the list of your choice
- Test modifying list items in this list
- See the incoming notifications in your debug console
Here we go, a simple SharePoint Webhook implemented in Node.js.
You can check out the complete sample implementation here:
https://github.com/ypcode/samples/tree/master/sp-webhooks-sample-pub
Hope you enjoyed reading this, please give your feedback !
Yannick
Hi Yannick,
great post! Unfortunately I don’t know where to add the following code:
Can you give me a hint? When requesting content from the site in the section “Test our app” I get “Webhook on site undefined” as a result.
Thanks in advance and have a nice Sunday,
Steven
LikeLike
sorry, here the code I talked about:
LikeLike
Okay, the code gets deleted… I am talking about the AppPermissionRequests code block.
LikeLike
Hi Steven,
Thanks a lot for your feedback.
Indeed, the application will not have the sufficient permissions if they are not properly specified.
I realized that was not super clear, I updated that section.
To summary you have to:
– go to the https:////_layouts/15/appinv.aspx
– copy/Paste your registered Client Id
– copy/Paste the XML in the “Permission Request XML” text area
– click the “Create” button to validate
LikeLiked by 1 person
what is this
var sp = require(“spaddin-helper”);
LikeLike
As mentioned in the post it’s a custom module I built back then… by today I would rather recommend using something like pnpjs 🙂
LikeLike