Skip to content

Commit ba62c28

Browse files
authored
Merge pull request #43 from microsoftgraph/feature/rich-notifications
Feature/rich notifications
2 parents fef9197 + 3e85ebe commit ba62c28

File tree

15 files changed

+475
-35
lines changed

15 files changed

+475
-35
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,5 @@ typings
123123

124124
# tmp files generated by openssl
125125
.tmp/
126+
*.pfx
127+
*.pem

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ To use the Webhook sample, you need the following:
6969

7070
- [Node.js](https://nodejs.org/) version 10 or 12.
7171
- A [work or school account](http://dev.office.com/devprogram).
72+
- [OpenSSL](https://www.openssl.org/source/) when trying change notifications with resource data.
73+
74+
> You can install OpenSSL on windows using [chocolatey](https://chocolatey.org/install) with `choco install openssl -y` (run as administrator).
7275
7376
## Register the app
7477
<a name="Register-the-app"></a>
@@ -132,6 +135,7 @@ You'll need the `NGROK_ID` value in the next section.
132135
133136
1. Use a text editor to open `constants.js`.
134137
1. Replace `ENTER_YOUR_CLIENT_ID` with the client ID of your registered Azure application.
138+
1. Replace `ENTER_YOUR_TENANT_ID` with the ID of your organization. This information can be found next to the client ID on the application management page.
135139
1. Replace `ENTER_YOUR_SECRET` with the client secret of your registered Azure application.
136140
1. Replace `NGROK_ID` with the value in *https public URL* from the previous section.
137141
![](const)

constants.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@ exports.adalConfiguration = {
22
authority: 'https://login.microsoftonline.com/common',
33
clientID: 'ENTER_YOUR_CLIENT_ID',
44
clientSecret: 'ENTER_YOUR_SECRET',
5+
tenantID: 'ENTER_YOUR_TENANT_ID',
56
redirectUri: 'http://localhost:3000/callback'
67
};
78

89
exports.subscriptionConfiguration = {
910
changeType: 'Created',
1011
notificationUrl: 'https://NGROK_ID.ngrok.io/listen',
1112
resource: 'me/mailFolders(\'Inbox\')/messages',
12-
clientState: 'cLIENTsTATEfORvALIDATION'
13+
clientState: 'cLIENTsTATEfORvALIDATION',
14+
includeResourceData: false
1315
};
16+
17+
exports.certificateConfiguration = {
18+
certificateId: 'myCertificateId',
19+
relativeCertPath: './certificate.pem',
20+
relativeKeyPath: './key.pem',
21+
password: 'Password123',
22+
}; // the certificate will be generated during the first subscription creation, production solutions should rely on a certificate store

helpers/certificateHelper.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pem from 'pem';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import os from 'os';
5+
import crypto from 'crypto';
6+
7+
function ensureOpenSsl() {
8+
if (os.platform() === 'win32') {
9+
pem.config({ pathOpenSSL: 'C:/Program Files/OpenSSL-Win64/bin/openssl.exe' });
10+
}
11+
}
12+
13+
export function createSelfSignedCertificateIfNotExists(certPath, keyPath, password) {
14+
const certFullPath = path.join(__dirname, certPath);
15+
return new Promise((resolve) => {
16+
if (!fs.existsSync(certFullPath)) {
17+
ensureOpenSsl();
18+
pem.createCertificate({ selfSigned: true, serviceKeyPassword: password, days: 365 }, (err, result) => {
19+
fs.writeFileSync(certFullPath, result.certificate);
20+
fs.writeFileSync(path.join(__dirname, keyPath), result.serviceKey);
21+
if (err) {
22+
// eslint-disable-next-line no-console
23+
console.error(err);
24+
}
25+
resolve();
26+
});
27+
} else {
28+
resolve();
29+
}
30+
});
31+
}
32+
33+
export function getSerializedCertificate(certPath) {
34+
const pfx = fs.readFileSync(path.join(__dirname, certPath));
35+
const pfxAsString = pfx.toString().replace(/(\r\n|\n|\r|-|BEGIN|END|CERTIFICATE|\s)/gm, '');
36+
return pfxAsString;
37+
}
38+
39+
export function getPrivateKey(keyPath) {
40+
const privateKey = fs.readFileSync(path.join(__dirname, keyPath), 'utf8');
41+
return privateKey;
42+
}
43+
44+
export function decryptSymetricKey(base64encodedKey, keyPath) {
45+
const asymetricPrivateKey = getPrivateKey(keyPath);
46+
const decodedKey = Buffer.from(base64encodedKey, 'base64');
47+
const decryptedSymetricKey = crypto.privateDecrypt(asymetricPrivateKey, decodedKey);
48+
return decryptedSymetricKey;
49+
}
50+
51+
export function decryptPayload(base64encodedPayload, decryptedSymetricKey) {
52+
const iv = Buffer.alloc(16, 0);
53+
decryptedSymetricKey.copy(iv, 0, 0, 16);
54+
const decipher = crypto.createDecipheriv('aes-256-cbc', decryptedSymetricKey, iv);
55+
let decryptedPayload = decipher.update(base64encodedPayload, 'base64', 'utf8');
56+
decryptedPayload += decipher.final('utf8');
57+
return decryptedPayload;
58+
}
59+
60+
export function verifySignature(base64encodedSignature, base64encodedPayload, decryptedSymetricKey) {
61+
const hmac = crypto.createHmac('sha256', decryptedSymetricKey);
62+
hmac.write(base64encodedPayload, 'base64');
63+
return base64encodedSignature === hmac.digest('base64');
64+
}

helpers/requestHelper.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class SubscriptionManagementService {
2323

2424
async createSubscription(subscriptionCreationInformation) {
2525
const client = this.getGraphClient();
26-
const subscription = await client.api(this.subscriptionPath).create(subscriptionCreationInformation);
26+
const subscription = await client.api(this.subscriptionPath).version('beta').create(subscriptionCreationInformation);
2727
return subscription;
2828
}
2929

helpers/tokenHelper.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import jwt from 'jsonwebtoken';
2+
import jkwsClient from 'jwks-rsa';
3+
4+
const client = jkwsClient({
5+
jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys'
6+
});
7+
8+
export function getKey(header, callback) {
9+
client.getSigningKey(header.kid, (err, key) => {
10+
var signingKey = key.publicKey || key.rsaPublicKey;
11+
callback(null, signingKey);
12+
});
13+
}
14+
15+
export function isTokenValid(token, appId, tenantId) {
16+
return new Promise((resolve) => {
17+
const options = {
18+
audience: [appId],
19+
issuer: [`https://sts.windows.net/${tenantId}/`]
20+
};
21+
jwt.verify(token, getKey, options, (err) => {
22+
if (err) {
23+
// eslint-disable-next-line no-console
24+
console.error(err);
25+
resolve(false);
26+
} else {
27+
resolve(true);
28+
}
29+
});
30+
});
31+
}

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ createDatabase();
88
app.set('port', process.env.PORT || 3000);
99

1010
const server = app.listen(app.get('port'), () => {
11+
// eslint-disable-next-line no-console
1112
console.log('Express server listening on port ' + server.address().port);
1213
});

0 commit comments

Comments
 (0)