Skip to content

Commit e5369ae

Browse files
committed
- improves certificate generation flow
- adds support for rich notifications E2E - updates unit test to check for tenant ID
1 parent c6630c5 commit e5369ae

File tree

5 files changed

+78
-22
lines changed

5 files changed

+78
-22
lines changed

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: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,22 @@ function ensureOpenSsl() {
1010
}
1111
}
1212

13-
export function createSelfSignedCertificate(certPath, keyPath, password) {
14-
ensureOpenSsl();
15-
pem.createCertificate({ selfSigned: true, serviceKeyPassword: password, days: 365 }, (err, result) => {
16-
fs.writeFileSync(path.join(__dirname, certPath), result.certificate);
17-
fs.writeFileSync(path.join(__dirname, keyPath), result.serviceKey);
18-
if (err) {
19-
// eslint-disable-next-line no-console
20-
console.error(err);
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();
2129
}
2230
});
2331
}

routes/auth.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import express from 'express';
33
import { getSubscription, saveSubscription, deleteSubscription } from '../helpers/dbHelper';
44
import { getAuthUrl, getTokenFromCode } from '../helpers/authHelper';
55
import { SubscriptionManagementService } from '../helpers/requestHelper';
6-
import { subscriptionConfiguration } from '../constants';
6+
import { subscriptionConfiguration, certificateConfiguration } from '../constants';
7+
import { getSerializedCertificate, createSelfSignedCertificateIfNotExists } from '../helpers/certificateHelper';
78

89
export const authRouter = express.Router();
910

@@ -23,12 +24,18 @@ authRouter.get('/signin', (req, res) => {
2324
authRouter.get('/callback', (req, res, next) => {
2425
getTokenFromCode(req.query.code, async (authenticationError, token) => {
2526
if (token) {
26-
// Request this subscription to expire one day from now.
27-
// Note: 1 day = 86400000 milliseconds
28-
subscriptionConfiguration.expirationDateTime = new Date(Date.now() + 86400000).toISOString();
27+
// Request this subscription to expire one hour from now.
28+
// Note: 1 hour = 3600000 milliseconds
29+
subscriptionConfiguration.expirationDateTime = new Date(Date.now() + 3600000).toISOString();
2930

3031
try {
3132
const subscriptionService = new SubscriptionManagementService(token.accessToken);
33+
if (subscriptionConfiguration.includeResourceData) {
34+
// we're registering a subscription for notifications with resource data and must attach certificate information to get the data
35+
await createSelfSignedCertificateIfNotExists(certificateConfiguration.relativeCertPath, certificateConfiguration.relativeKeyPath, certificateConfiguration.password);
36+
subscriptionConfiguration.encryptionCertificate = getSerializedCertificate(certificateConfiguration.relativeCertPath);
37+
subscriptionConfiguration.encryptionCertificateId = certificateConfiguration.certificateId;
38+
}
3239
const subscriptionData = await subscriptionService.createSubscription(subscriptionConfiguration);
3340

3441
subscriptionData.userId = token.userId;

routes/listen.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import http from 'http';
44
import { ioServer } from '../helpers/socketHelper';
55
import { SubscriptionManagementService } from '../helpers/requestHelper';
66
import { getSubscription } from '../helpers/dbHelper';
7-
import { subscriptionConfiguration } from '../constants';
7+
import { subscriptionConfiguration, certificateConfiguration, adalConfiguration } from '../constants';
8+
import { decryptSymetricKey, decryptPayload, verifySignature } from '../helpers/certificateHelper';
9+
import { isTokenValid } from '../helpers/tokenHelper';
810

911
export const listenRouter = express.Router();
1012

1113
/* Default listen route */
12-
listenRouter.post('/', (req, res, next) => {
14+
listenRouter.post('/', async (req, res, next) => {
1315
let status;
1416
let clientStatesValid;
1517

@@ -37,17 +39,39 @@ listenRouter.post('/', (req, res, next) => {
3739
}
3840
}
3941

42+
// if we're receiving notifications with resource data we have to validate the origin of the request by validating the tokens
43+
let areTokensValid = true;
44+
if (req.body.validationTokens) {
45+
const validationResults = await Promise.all(req.body.validationTokens.map((x) => isTokenValid(x, adalConfiguration.clientID, adalConfiguration.tenantID)));
46+
areTokensValid = validationResults.reduce((x, y) => x && y);
47+
}
48+
4049
// If all the clientStates are valid, then process the notification
41-
if (clientStatesValid) {
50+
if (clientStatesValid && areTokensValid) {
4251
for (let i = 0; i < req.body.value.length; i++) {
4352
const resource = req.body.value[i].resource;
4453
const subscriptionId = req.body.value[i].subscriptionId;
45-
processNotification(subscriptionId, resource, res, next);
54+
55+
if (req.body.value[i].encryptedContent) {
56+
// we have a notification with resource data, let's decrypt the enclosed data
57+
// eslint-disable-next-line no-loop-func
58+
const decryptedSymetricKey = decryptSymetricKey(req.body.value[i].encryptedContent.dataKey, certificateConfiguration.relativeKeyPath);
59+
const isSignatureValid = verifySignature(req.body.value[i].encryptedContent.dataSignature, req.body.value[i].encryptedContent.data, decryptedSymetricKey);
60+
if (isSignatureValid) {
61+
// the signature is valid, data hasn't been tampered with. We can proceed to displaying the data
62+
const decryptedPayload = decryptPayload(req.body.value[i].encryptedContent.data, decryptedSymetricKey);
63+
emitNotification(subscriptionId, decryptedPayload);
64+
} // otherwise data is invalid, ignore it
65+
} else {
66+
// we have a plain notification that doesn't contain data, let's call Microsoft Graph to get the resource data
67+
processNotification(subscriptionId, resource, res, next);
68+
}
4669
}
4770
// Send a status of 'Accepted'
4871
status = 202;
4972
} else {
5073
// Since the clientState field doesn't have the expected value,
74+
// or the validation tokens are invalid for notifications with data
5175
// this request might NOT come from Microsoft Graph.
5276
// However, you should still return the same status that you'd
5377
// return to Microsoft Graph to not alert possible impostors
@@ -58,16 +82,20 @@ listenRouter.post('/', (req, res, next) => {
5882
res.status(status).end(http.STATUS_CODES[status]);
5983
});
6084

85+
function emitNotification(subscriptionId, data) {
86+
ioServer.to(subscriptionId).emit('notification_received', data);
87+
}
88+
6189
// Get subscription data from the database
62-
// Retrieve the actual mail message data from Office 365.
90+
// Retrieve the entity from Microsoft Graph.
6391
// Send the message data to the socket.
6492
function processNotification(subscriptionId, resource, res, next) {
6593
getSubscription(subscriptionId, async (dbError, subscriptionData) => {
6694
if (subscriptionData) {
6795
try {
6896
const subscriptionManagementService = new SubscriptionManagementService(subscriptionData.accessToken);
6997
const endpointData = await subscriptionManagementService.getData(resource);
70-
ioServer.to(subscriptionId).emit('notification_received', endpointData);
98+
emitNotification(subscriptionId, endpointData);
7199
} catch (requestError) {
72100
res.status(500);
73101
next(requestError);

tests/confTest.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ var conf = require('../constants');
33

44
describe('ADAL', function () { // eslint-disable-line no-undef
55
it( // eslint-disable-line no-undef
6-
'Checking clientID and clientSecret in constants.js',
6+
'Checking clientID, clientSecret and tenantID in constants.js',
77
function () {
88
assert(
99
isADALConfigured(conf.adalConfiguration),
10-
'\nRegister clientID and clientSecret in file constants.js.\n'
10+
'\nRegister clientID. clientSecret, tenantID in file constants.js.\n'
1111
+ 'You don\'t have them? Get them by using the Office 365 app registration tool\n'
1212
+ 'http://dev.office.com/app-registration\n'
1313
+ 'App type: Web App\n'
@@ -37,12 +37,16 @@ function isADALConfigured(configuration) {
3737
&& configuration.clientID !== null
3838
&& configuration.clientID !== ''
3939
&& configuration.clientID !== 'ENTER_YOUR_CLIENT_ID';
40+
var tenantIDConfigured = typeof (configuration.tenantID) !== 'undefined'
41+
&& configuration.tenantID !== null
42+
&& configuration.tenantID !== ''
43+
&& configuration.tenantID !== 'ENTER_YOUR_TENANT_ID';
4044
var clientSecretConfigured = typeof (configuration.clientSecret) !== 'undefined'
4145
&& configuration.clientSecret !== null
4246
&& configuration.clientSecret !== ''
4347
&& configuration.clientSecret !== 'ENTER_YOUR_SECRET';
4448

45-
return clientIDConfigured && clientSecretConfigured;
49+
return clientIDConfigured && clientSecretConfigured && tenantIDConfigured;
4650
}
4751

4852
function isSubscriptionConfigured(configuration) {

0 commit comments

Comments
 (0)