Jan 19, 2017

Build a SharePoint Webhook with Node.js

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

Let's start !

  1. Let's start Ngrok to get your temporary public URL:
ngrok http 3000 --host-header=localhost:3000

Copy the given HTTPS url.

ngrok-public-url

  1. In SharePoint, register a new App (http://tenant.sharepoint.com//_layouts/15/appregnew.aspx)

    1. 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)
    2. Click the Create button and copy the Client ID and Client Secret in a safe location.

    Registered Webhook App.png

    1. Make sure your app has sufficient and app-only policy enabled permissions

      • Go to http://tenant.sharepoint.com//_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>
      

      App-permissions.png

      • Click the "Create" button to apply the permissions
  2. Create a workspace directory on your local dev environment

  3. In your directory, from the terminal, type

npm install express-generator -g > express
  1. 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)

  1. Install the modules
npm install --save
  1. 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)

  1. 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; 

WebhookNodeAppRunning.png

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 /_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

[code language="javascript"] 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', ); }); }); }); [/code]

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.

webhooksubscriptionsview

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 (/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 webhook_notifications.png

Here we go, a simple SharePoint Webhook implemented in Node.js.

github

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

Other posts