├── ActionableMessageTokenValidator.js ├── LICENSE ├── OpenIdMetadata.js ├── README.md ├── app.js ├── outlook-actionable-messages-node-token-validation.yml └── package.json /ActionableMessageTokenValidator.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // All rights reserved. 5 | // 6 | // This code is licensed under the MIT License. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files(the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions : 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | //------------------------------------------------------------------------------ 27 | 28 | /** 29 | * Module for token validation. 30 | */ 31 | 32 | "use strict"; 33 | 34 | var request = require('request'); 35 | var getPem = require('rsa-pem-from-mod-exp'); 36 | var base64url = require('base64url'); 37 | var oid = require('./OpenIdMetadata'); 38 | var jwt = require('jsonwebtoken'); 39 | 40 | const O365_APP_ID = "48af08dc-f6d2-435f-b2a7-069abd99c086"; 41 | const O365_OPENID_METADATA_URL = "https://substrate.office.com/sts/common/.well-known/openid-configuration"; 42 | const O365_TOKEN_ISSUER = "https://substrate.office.com/sts/"; 43 | 44 | /** 45 | * Result from token validation. 46 | */ 47 | var ActionableMessageTokenValidationResult = (function() { 48 | function ActionableMessageTokenValidationResult() { 49 | this.sender = ""; 50 | this.actionPerformer = ""; 51 | } 52 | 53 | return ActionableMessageTokenValidationResult; 54 | }()); 55 | 56 | /** 57 | * Token validator for actionable message. 58 | */ 59 | var ActionableMessageTokenValidator = (function () { 60 | /** 61 | * Constructor. 62 | */ 63 | function ActionableMessageTokenValidator() { 64 | }; 65 | 66 | /** 67 | * Validates an actionable message token. 68 | * @param token 69 | * A JWT issued by Microsoft. 70 | * 71 | * @param targetUrl 72 | * The expected URL in the token. This should the web service URL. 73 | * 74 | * @param cb 75 | * The callback when the validation is completed. 76 | */ 77 | ActionableMessageTokenValidator.prototype.validateToken = function (token, targetUrl, cb) { 78 | var decoded = jwt.decode(token, { complete: true }); 79 | var verifyOptions = { 80 | issuer: O365_TOKEN_ISSUER, 81 | audience: targetUrl 82 | }; 83 | 84 | var openIdMetadata = new oid.OpenIdMetadata(O365_OPENID_METADATA_URL) 85 | 86 | openIdMetadata.getKey(decoded.header.kid, key => { 87 | var result = new ActionableMessageTokenValidationResult(); 88 | 89 | if (key) { 90 | try { 91 | jwt.verify(token, key, verifyOptions); 92 | 93 | if (decoded.payload.appid.toLowerCase() != O365_APP_ID.toLowerCase()) { 94 | var error = new Error("Invalid app id"); 95 | Error.captureStackTrace(error); 96 | cb(error); 97 | } else { 98 | result.sender = decoded.payload.sender; 99 | result.actionPerformer = decoded.payload.sub; 100 | } 101 | } catch (err) { 102 | cb(err); 103 | return; 104 | } 105 | } else { 106 | var error = new Error("invalid key"); 107 | Error.captureStackTrace(error); 108 | cb(error); 109 | } 110 | 111 | cb(null, result); 112 | }); 113 | }; 114 | 115 | return ActionableMessageTokenValidator; 116 | }()); 117 | 118 | exports.ActionableMessageTokenValidationResult = ActionableMessageTokenValidationResult; 119 | exports.ActionableMessageTokenValidator = ActionableMessageTokenValidator; 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Microsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OpenIdMetadata.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // All rights reserved. 5 | // 6 | // This code is licensed under the MIT License. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files(the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions : 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | //------------------------------------------------------------------------------ 27 | 28 | /** 29 | * Module for Open ID metadata. 30 | */ 31 | 32 | "use strict"; 33 | 34 | var request = require('request'); 35 | var getPem = require('rsa-pem-from-mod-exp'); 36 | var base64url = require('base64url'); 37 | 38 | /** 39 | * Represents an OpenID configuration. 40 | */ 41 | var OpenIdMetadata = (function () { 42 | function OpenIdMetadata(url) { 43 | this.lastUpdated = 0; 44 | this.url = url; 45 | } 46 | 47 | /** 48 | * Gets a public key from the cache given the key ID. 49 | * @param keyId 50 | * The ID of the key to retrieve. 51 | * 52 | * @param cb 53 | * The callback after the key search is completed. 54 | */ 55 | OpenIdMetadata.prototype.getKey = function (keyId, cb) { 56 | var _this = this; 57 | // If keys are more than 5 days old, refresh them 58 | var now = new Date().getTime(); 59 | 60 | if (this.lastUpdated < (now - 1000 * 60 * 60 * 24 * 5)) { 61 | this._refreshCache(function (err) { 62 | if (err) { 63 | } 64 | // Search the cache even if we failed to refresh 65 | var key = _this._findKey(keyId); 66 | cb(key); 67 | }); 68 | } else { 69 | // Otherwise read from cache 70 | var key = this.findKey(keyId); 71 | cb(key); 72 | } 73 | }; 74 | 75 | /** 76 | * Refresh the internal cache. 77 | * @param cb 78 | * The callback after the cache is refreshed. 79 | */ 80 | OpenIdMetadata.prototype._refreshCache = function (cb) { 81 | var _this = this; 82 | var options = { 83 | method: 'GET', 84 | url: this.url, 85 | json: true 86 | }; 87 | 88 | request(options, function (err, response, body) { 89 | if (!err && (response.statusCode >= 400 || !body)) { 90 | err = new Error('Failed to load openID config: ' + response.statusCode); 91 | } 92 | 93 | if (err) { 94 | cb(err); 95 | } else { 96 | var openIdConfig = body; 97 | var options = { 98 | method: 'GET', 99 | url: openIdConfig.jwks_uri, 100 | json: true 101 | }; 102 | request(options, function (err, response, body) { 103 | if (!err && (response.statusCode >= 400 || !body)) { 104 | err = new Error("Failed to load Keys: " + response.statusCode); 105 | } 106 | if (!err) { 107 | _this.lastUpdated = new Date().getTime(); 108 | _this.keys = body.keys; 109 | } 110 | cb(err); 111 | }); 112 | } 113 | }); 114 | }; 115 | 116 | /** 117 | * Find the key given the key ID. 118 | * @param keyId 119 | * The ID of the key. 120 | * 121 | * @return 122 | * The value of the key if found; else null. 123 | */ 124 | OpenIdMetadata.prototype._findKey = function (keyId) { 125 | if (!this.keys) { 126 | return null; 127 | } 128 | 129 | for (var i = 0; i < this.keys.length; i++) { 130 | if (this.keys[i].kid == keyId) { 131 | var key = this.keys[i]; 132 | if (!key.n || !key.e) { 133 | // Return null for non-RSA keys 134 | return null; 135 | } 136 | var modulus = base64url.toBase64(key.n); 137 | var exponent = key.e; 138 | return getPem(modulus, exponent); 139 | } 140 | } 141 | 142 | return null; 143 | }; 144 | 145 | return OpenIdMetadata; 146 | }()); 147 | 148 | exports.OpenIdMetadata = OpenIdMetadata; 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | products: 4 | - office-outlook 5 | - office-365 6 | languages: 7 | - javascript 8 | extensions: 9 | contentType: samples 10 | technologies: 11 | - Actionable messages 12 | createdDate: 11/17/2016 11:23:12 AM 13 | --- 14 | # Action Request Token Verification Node.js Sample 15 | 16 | Services can send actionable messages to users to complete simple tasks against their services. When a user performs one of the actions in a message, an action request will be sent by Microsoft to the service. The request from Microsoft will contain a bearer token in the authorization header. This code sample shows how to verify the token to ensure the action request is from Microsoft, and use the claims in the token to validate the request. 17 | 18 | app.post('/api/expense', function (req, res) { 19 | var token; 20 | 21 | // Get the token from the Authorization header 22 | if (req.headers && req.headers.hasOwnProperty('authorization')) { 23 | var auth = req.headers['authorization'].trim().split(' '); 24 | if (auth.length == 2 && auth[0].toLowerCase() == 'bearer') { 25 | token = auth[1]; 26 | } 27 | } 28 | 29 | if (token) { 30 | var validator = new validation.ActionableMessageTokenValidator(); 31 | 32 | // This will validate that the token has been issued by Microsoft for the 33 | // specified target URL i.e. the target matches the intended audience (“aud” claim in token) 34 | // 35 | // In your code, replace https://api.contoso.com with your service’s base URL. 36 | // For example, if the service target URL is https://api.xyz.com/finance/expense?id=1234, 37 | // then replace https://api.contoso.com with https://api.xyz.com 38 | validator.validateToken( 39 | token, 40 | "https://api.contoso.com", 41 | function (err, result) { 42 | if (err) { 43 | console.error('error: ' + err.message); 44 | res.status(401); 45 | res.end(); 46 | } else { 47 | // We have a valid token. We will now verify that the sender and action performer are who 48 | // we expect. The sender is the identity of the entity that initially sent the Actionable 49 | // Message, and the action performer is the identity of the user who actually 50 | // took the action (“sub” claim in token). 51 | // 52 | // You should replace the code below with your own validation logic 53 | // In this example, we verify that the email is sent by expense@contoso.com (expected sender) 54 | // and the email of the person who performed the action is john@contoso.com (expected recipient) 55 | // 56 | // You should also return the CARD-ACTION-STATUS header in the response. 57 | // The value of the header will be displayed to the user. 58 | 59 | if (result.sender.toLowerCase() != 'expense@contoso.com' || 60 | result.action_performer.toLowerCase() != 'john@contoso.com') { 61 | res.set('CARD-ACTION-STATUS', 'Invalid sender or the action performer is not allowed.') 62 | res.status(403); 63 | res.end(); 64 | return; 65 | } 66 | 67 | // Further business logic code here to process the expense report. 68 | 69 | res.set('CARD-ACTION-STATUS', 'The expense was approved.') 70 | res.status(200); 71 | res.end(); 72 | } 73 | }); 74 | } else { 75 | res.status(401); 76 | res.end(); 77 | } 78 | }); 79 | 80 | The code sample is using the following library for JWT validation. 81 | 82 | [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) 83 | 84 | More information Outlook Actionable Messages is available [here](https://dev.outlook.com/actions). 85 | 86 | ## Copyright 87 | Copyright (c) 2017 Microsoft. All rights reserved. 88 | 89 | 90 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 91 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // Copyright (c) Microsoft Corporation. 4 | // All rights reserved. 5 | // 6 | // This code is licensed under the MIT License. 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files(the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions : 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | //------------------------------------------------------------------------------ 27 | 28 | 'use strict'; 29 | 30 | var express = require('express'); 31 | var request = require('request'); 32 | var validation = require('./ActionableMessageTokenValidator'); 33 | var app = express(); 34 | 35 | app.post('/api/expense', function (req, res) { 36 | var token; 37 | 38 | if (req.headers && req.headers.hasOwnProperty('authorization')) { 39 | var auth = req.headers['authorization'].trim().split(' '); 40 | if (auth.length == 2 && auth[0].toLowerCase() == 'bearer') { 41 | token = auth[1]; 42 | } 43 | } 44 | 45 | if (token) { 46 | var validator = new validation.ActionableMessageTokenValidator(); 47 | 48 | // validateToken will verify the following 49 | // 1. The token is issued by Microsoft and its digital signature is valid. 50 | // 2. The token has not expired. 51 | // 3. The audience claim matches the service domain URL. 52 | // 53 | // Replace https://api.contoso.com with your service domain URL. 54 | // For example, if the service URL is https://api.xyz.com/finance/expense?id=1234, 55 | // then replace https://api.contoso.com with https://api.xyz.com 56 | validator.validateToken( 57 | token, 58 | "https://api.contoso.com", 59 | function (err, result) { 60 | if (err) { 61 | console.error('error: ' + err.message); 62 | res.status(401); 63 | res.end(); 64 | } else { 65 | // We have a valid token. We will verify the sender and the action performer. 66 | // You should replace the code below with your own validation logic. 67 | // In this example, we verify that the email is sent by expense@contoso.com 68 | // and the action performer is someone with a @contoso.com email address. 69 | // 70 | // You should also return the CARD-ACTION-STATUS header in the response. 71 | // The value of the header will be displayed to the user. 72 | 73 | if (result.sender.toLowerCase() != 'expense@contoso.com' || 74 | !result.action_performer.toLowerCase().endsWith('@contoso.com')) { 75 | res.set('CARD-ACTION-STATUS', 'Invalid sender or the action performer is not allowed.') 76 | res.status(403); 77 | res.end(); 78 | return; 79 | } 80 | 81 | // Further business logic code here to process the expense report. 82 | 83 | res.set('CARD-ACTION-STATUS', 'The expense was approved.') 84 | res.status(200); 85 | res.end(); 86 | } 87 | }); 88 | } else { 89 | res.status(401); 90 | res.end(); 91 | } 92 | }); 93 | 94 | app.listen(3000); 95 | console.log('listening on 3000'); 96 | -------------------------------------------------------------------------------- /outlook-actionable-messages-node-token-validation.yml: -------------------------------------------------------------------------------- 1 | ### YamlMime:Sample 2 | sample: 3 | - name: Action Request Token Verification Node.js Sample 4 | path: '' 5 | description: Node.js code sample to validate bearer token for Outlook Actionable Messages 6 | readme: '' 7 | generateZip: FALSE 8 | isLive: TRUE 9 | technologies: 10 | - Office Add-in 11 | azureDeploy: '' 12 | author: jamescro 13 | platforms: [] 14 | languages: 15 | - JavaScript 16 | extensions: 17 | products: 18 | - Outlook 19 | scenarios: [] 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "outlook_actionable_messages_node_token_validation", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "express": "3.x", 7 | "jsonwebtoken": "7.x", 8 | "request": "2.x", 9 | "rsa-pem-from-mod-exp": "^0.8.4", 10 | "base64url": "^1.0.6" 11 | } 12 | } --------------------------------------------------------------------------------