Building Electron application to interact with SharePoint using OAuth implicit grant flow

Introduction


This article can be used to build cross platform desktop application to interacts with Office 365 resources using electron js. It further explains interaction of the application with SharePoint Online using Oauth implicit flow via graph API.

Purpose


Purpose of this article is to explain how OAuth Implicit flow works and how it can be implemented in a Desktop application built using Electron JS to interact with O365 products via . For better explanation, we will build a Employee Registration desktop application which will call graph api to get and add list item in SharePoint.

Why Electron JS ?

Electron JS is widely used to easily setup a cross platform desktop application. Most of the top desktop application from Microsoft like Teams & Visual Studio Code is built using Electron JS. See the list of application build using electron js here.

Architectural Flow


Below is a pictorial representation of over all process flow that occurs during this application life cycle.

The over all architectural flow can be divided into three major actions specified below:

  1. Application prompts for login credentials and user provides it, the application then request an access token from Microsoft Identity platform.
  2. Microsoft Identity Platform verifies the user and handovers the access token.
  3. Application make graph API call, passing the access token received to get & add SharePoint list item.

Getting started on building an application using Electron JS


This section is divided into two sub sections.

  1. Create \ Register an Azure Application and Grant permission the required permission.
  2. Build Electron JS application to interact with SharePoint Online.

 

Create \ Register an Azure Application and Grant the required permission.

Follow the steps specified below to register a new application and assign the required permission.

1 ) Sign in to the Azure portal using tenant admin account.

If the account that is being used has access to more than one tenant, select the particular account in the top right corner, and set portal session to the Azure AD tenant that needs to be used.

  1. In the left-hand navigation pane, select the Azure Active Directory service and then select App registrations > New registration.
     Provide a name (Here its EmployeeRegisteration) and under Redirect URI enter "**https://login.microsoftonline.com/common/oauth2/nativeclient**"
     Click Register.

  2. Go to API permission and click on Add permission. Select Microsoft Graph option and then select Delegated Permission. Since we are requesting sign-in credentials from logged in user, we are selecting delegated permission. Select "Sites.Manage.All" under Sites category & Click Add permissions.

 
4) Go to Authentication, select 'Access Tokens' under Advance Settings -> Implicit Grant category & Click 'Save'.

Make note of Client ID & Tenant ID which will be required while building the application.

Build Electron JS application to interact with SharePoint Online

We are building a Employee Registration desktop application which will call graph api to gets and add list item in SharePoint. Here, we will first create a login window to collect credentials from user and request access token from Microsoft Identity Platform. On receiving of the access token successfully, we will call the graph api.

Follow the below steps to build the application.

  1. Git Clone the below repo in any preferred location. (Note : Any boiler plate code can be used).
git clone https://github.com/alexdevero/electron-react-webpack-boilerplate.git
  1. Open the project in Visual Studio Code or any other editor.

Add a new file in the root directory with the name settings.json. Include the below content and save it. Include the client id and tenant id noted in previous section. Also provide the list id and tenant name.

{
    "tenant":"<yourtenant>.sharepoint.com",
    "tenantID":"<tenant ID>",
    "ClientID":"<client ID>",
    "resource":"https://graph.microsoft.com",
    "redirectURL":"https://login.microsoftonline.com/common/oauth2/nativeclient",
    "listID":"<SharePoint List ID>"
}
  1. Inside Main.js, add the below code. Here we are writing a function called loginPrompt. 
    From Line 29 to 37, The idea is to create a new browser window & redirect the login user to enter credentials to acquire access token from Microsoft Identity Platform.
01.ipcMain.on('loginPrompt', (event, args) => {
02.  let authWindow = new  BrowserWindow(
03.    {
04.      alwaysOnTop: true, // keeps this window on top of others
05.      modal: true,
06.      autoHideMenuBar: true,
07.      parent: mainWindow,
08.      frame: true,
09.      show: false,
10.      webPreferences: {
11.        nodeIntegration: false, // No need to specify these if Electron v4+ but showing for demo
12.        contextIsolation: true  // we can isolate this window
13.      }
14.    }
15.  );
16. 
17.  authWindow.on('closed', () => {
18.    this.authWindow = null;
19.  });
20. 
21.  authWindow.setMenu(null);
22. 
23.  let filter = { urls: [settings.redirectURL] };
24. 
25.  authWindow.webContents.on('did-finish-load', () => {
26.    authWindow.show();
27.  });
28. 
29.  authWindow.loadURL(`https://login.microsoftonline.com/` + settings.tenantID + `/oauth2/authorize?
30.    client_id=`+ settings.ClientID + `
31.    &response_type=token
32.    &scope=openid
33.    &redirect_uri=`+settings.redirectURL+`
34.    &response_mode=fragment
35.    &nonce=678911
36.    &state=12345
37.    &resource=`+ encodeURIComponent(settings.resource));
38. 
39. 
40.  session.defaultSession.webRequest.onCompleted(filter, (details) => {
41.    var url = details.url;
42.    let accessToken = url.match(/\#(?:access_token)\=([\S\s]*?)\&/)[1];
43.    event.returnValue = accessToken;
44.    authWindow.close();
45.  });
46.});

In Line 43, We send the accessToken back to the event from where it is called.

  1. In App.js located inside src > components add the following code
componentDidMount() {
    accessToken = ipcRenderer.sendSync('loginPrompt', {});
  }

Once our component is mounted, we are calling the loginPrompt function request for user sign in.

More info on ipcMain & ipcRender below https://electronjs.org/docs/api/ipc-main | https://electronjs.org/docs/api/ipc-renderer

Building the UI

To build the UI, we will use Office UI Fabric for React.

  1. To Include Office UI Fabric for react, Include the below command from the terminal of Visual Studio Code.
npm i office-ui-fabric-react
  1. Replace the below code in App.js file.  
001.import React, { Component } from 'react'
002.import { TextField, PrimaryButton, DefaultButton } from 'office-ui-fabric-react';
003.import '../assets/css/App.css';
004.import * as settings from '../../settings';
005.const electron = window.require("electron");
006.const ipcRenderer = electron.ipcRenderer
007.const request = require('request');
008.let accessToken;
009.class App extends React.Component {
010.  constructor(props) {
011.    super(props);
012.    this.state = {
013.      tableContent: null
014.    }
015.    this.buttonClick = this.buttonClick.bind(this);
016.  }
017. 
018.  buttonClick(e) {
019.    e.preventDefault();
020.    
021.    this.addEmployee(this.employeeName.value, this.employeeDesignation.value, accessToken).then(() => {
022.      this.getAllEmployees().then((htmlContent) => { this.setState({ tableContent: htmlContent }); });
023.    });
024.  }
025. 
026.  componentDidMount() {
027.    accessToken = ipcRenderer.sendSync('loginPrompt', {});
028.    if (accessToken !== undefined) {
029.      this.getAllEmployees().then((htmlContent) => { this.setState({ tableContent: htmlContent }); });
030.    }
031.  }
032. 
033.  addEmployee(employeeName, desgination) {
034.    return new  Promise((resolve, reject) => {
035.      const options = {
036.        method: 'POST',
037.        url: 'https://graph.microsoft.com/v1.0/sites/' + settings.tenant + '/lists/' + settings.listID + '/items',
038.        headers: {
039.          'Authorization': 'Bearer ' + accessToken,
040.          'content-type': 'application/json'
041.        },
042.        body: JSON.stringify({
043.          "fields": {
044.            "Title": employeeName,
045.            "Designation": desgination
046.          }
047.        })
048.      };
049. 
050.      request(options, (error, response, body) => {
051.        const result = JSON.parse(body);
052.        if (response.statusCode == 201) {
053.          resolve(result);
054.        } else  {
055.          reject(result);
056.        }
057.      });
058.    });
059.  }
060. 
061.  getAllEmployees() {
062.    return new  Promise((resolve, reject) => {
063.      const options = {
064.        method: 'GET',
065.        url: 'https://graph.microsoft.com/v1.0/sites/' + settings.tenant + '/lists/' + settings.listID + '/items?expand=fields(select=Title,Designation)',
066.        headers: {
067.          'Authorization': 'Bearer ' + accessToken,
068.          'content-type': 'application/json'
069.        }
070.      };
071.      request(options, (error, response, body) => {
072.        const result = JSON.parse(body);
073.        let htmlContent = [];
074.        if (response.statusCode == 200) {
075.          Promise.all(result.value.map((eachItem, i) => {
076.            htmlContent.push(<div className="w3-container w3-border-bottom w3-border-blue ms-Grid-col ms-sm12 ms-md10 ms-lg9">
077.              <div className="ms-Grid-col ms-sm6 ms-md6 ms-lg6">
078.                {result.value[i].fields.Title}
079.              </div>
080.              <div className="w3-border-blue ms-Grid-col ms-sm6 ms-md6 ms-lg6">
081.                {result.value[i].fields.Designation}
082.              </div>
083.            </div>);
084.          })).then(() => {
085.            resolve(htmlContent);
086.          });
087.        } else  {
088.          reject(result);
089.        }
090.      });
091.    });
092.  }
093. 
094.  render() {
095.    return <div className="ms-Grid center">
096.      <div className="ms-Grid-row">
097.        <div className="ms-Grid-col ms-sm12 ms-md10 ms-lg9">
098.          <TextField label="Employee Name"  componentRef={(ref) => { this.employeeName = ref }} placeholder="Enter Name" />
099.        </div></div>
100.      <div className="ms-Grid-row">
101.        <div className="ms-Grid-col ms-sm12 ms-md10 ms-lg9">
102.          <TextField label="Designation"  componentRef={(ref) => { this.employeeDesignation = ref }} placeholder="Enter Name" />
103.        </div></div><br />
104.      <div className="ms-Grid-row">
105.        <div className="ms-Grid-col ms-sm12 ms-md10 ms-lg6">
106.          <PrimaryButton onClick={this.buttonClick} >Add Employee</PrimaryButton>
107.        </div>
108.        <div className="ms-Grid-col ms-sm12 ms-md10 ms-lg6">
109.          <DefaultButton>Cancel</DefaultButton>
110.        </div>
111.      </div>
112.      <br />
113.      <br />
114.      <br />
115.      <div className="ms-Grid-row">
116.        <div className="w3-container w3-pale-blue w3-leftbar w3-border-blue ms-Grid-col ms-sm12 ms-md10 ms-lg9">
117.          <div className="ms-Grid-col ms-sm6 ms-md6 ms-lg6">
118.            Employee Name
119.          </div>
120.          <div className="ms-Grid-col ms-sm6 ms-md6 ms-lg6">
121.            Designation
122.          </div>
123.        </div>
124.        {this.state.tableContent}
125.      </div>
126.    </div>
127.  }
128.}
129.export default  App
Explanation

In Line 33 & 61, we have two function which get and add item in SharePoint list (Here we have a list with a custom column name "Designation"). It calls respective graph API and pass the access token received along with it. Access token is received as soon as the component is mounted in Line 27.

  1. Add the below CSS rules to App.css located in src -> assets -> App.css
.w3-border-blue, .w3-hover-border-blue:hover {
    border-color: skyblue!important;
}
.w3-border-bottom {
    border-bottom: 1px solid #ccc!important;
}
.w3-container, .w3-panel {
    padding: 0.01em 16px;
}
*, *:before, *:after {
    box-sizing: inherit;
}
div.linebreak::before{
    display: block;
}
 
.center {
    margin: auto;
    width: 60%;
    padding: 10px;
  }
 
.w3-border-blue, .w3-hover-border-blue:hover {
    border-color: #2196F3!important;
}
.w3-pale-blue, .w3-hover-pale-blue:hover {
    color: #000!important;
    background-color: #ddffff!important;
}
.w3-leftbar {
    border-left: 6px solid #0466ad!important;
}
.w3-container, .w3-panel {
    padding: 0.01em 16px;
}
 
body {
    background-color: #6dbef9;
}
  1. Package the application for production

Run the below commands

npm run package
npm run prod

The application exe file will be present inside a newly created build folder. On running the application exe file, login screen will be presented to user and once the valid credentials are provided, following screen shown in the Glimpse of the desktop application should appear. 

Solution can also be downloaded from link provided in download this solution section

Glimpse of the desktop application

Conclusion

Thus in this article, we saw how to implement OAuth implicit grant flow in a desktop application built using Electron JS.

Download this solution

This solution can be downloaded from here

See Also