UNPKG

passport-azure-ad

Version:

OIDC and Bearer Passport strategies for Azure Active Directory

269 lines (230 loc) 8.31 kB
/** * Copyright (c) Microsoft Corporation * All Rights Reserved * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT * OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ 'use strict'; const base64url = require('base64url'); const crypto = require('crypto'); const util = require('util'); exports.getLibraryProduct = () => { return 'passport-azure-ad' }; exports.getLibraryVersionParameterName = () => { return "x-client-Ver" }; exports.getLibraryProductParameterName = () => { return 'x-client-SKU' }; exports.getLibraryVersion = () => { require('pkginfo')(module, 'version'); return module.exports.version; }; exports.getElement = (parentElement, elementName) => { if (parentElement[`saml:${elementName}`]) { return parentElement[`saml:${elementName}`]; } else if (parentElement[`samlp:${elementName}`]) { return parentElement[`samlp:${elementName}`]; } else if (parentElement[`wsa:${elementName}`]) { return parentElement[`wsa:${elementName}`]; } return parentElement[elementName]; }; exports.getFirstElement = (parentElement, elementName) => { const element = exports.getElement(parentElement, elementName); return Array.isArray(element) ? element[0] : element; }; /** * Reconstructs the original URL of the request. * * This function builds a URL that corresponds the original URL requested by the * client, including the protocol (http or https) and host. * * If the request passed through any proxies that terminate SSL, the * `X-Forwarded-Proto` header is used to detect if the request was encrypted to * the proxy. * * @return {String} * @api private */ exports.originalURL = (req) => { const headers = req.headers; const protocol = (req.connection.encrypted || req.headers['x-forwarded-proto'] === 'https') ? 'https' : 'http'; const host = headers.host; const path = req.url || ''; return `${protocol}://${host}${path}`; }; /** * Merge object b with object a. * * var a = { something: 'bar' } * , b = { bar: 'baz' }; * * utils.merge(a, b); * // => { something: 'bar', bar: 'baz' } * * @param {Object} a * @param {Object} b * @return {Object} * @api private */ exports.merge = (a, b) => { return util._extend(a, b); // eslint-disable-line no-underscore-dangle }; /** * Return a unique identifier with the given `len`. * * utils.uid(10); * // => "FDaS435D2z" * * CREDIT: Connect -- utils.uid * https://github.com/senchalabs/connect/blob/2.7.2/lib/utils.js * * @param {Number} len * @return {String} * @api private */ exports.uid = (len) => { var bytes = crypto.randomBytes(Math.ceil(len * 3 / 4)); return base64url.encode(bytes).slice(0,len); }; function prepadSigned(hexStr) { const msb = hexStr[0]; if (msb < '0' || msb > '7') { return `00${hexStr}`; } return hexStr; } function toHex(number) { const nstr = number.toString(16); if (nstr.length % 2) { return `0${nstr}`; } return nstr; } // encode ASN.1 DER length field // if <=127, short form // if >=128, long form function encodeLengthHex(n) { if (n <= 127) { return toHex(n); } const nHex = toHex(n); const lengthOfLengthByte = 128 + nHex.length / 2; // 0x80+numbytes return toHex(lengthOfLengthByte) + nHex; } // http://stackoverflow.com/questions/18835132/xml-to-pem-in-node-js exports.rsaPublicKeyPem = (modulusB64, exponentB64) => { const modulus = new Buffer(modulusB64, 'base64'); const exponent = new Buffer(exponentB64, 'base64'); const modulusHex = prepadSigned(modulus.toString('hex')); const exponentHex = prepadSigned(exponent.toString('hex')); const modlen = modulusHex.length / 2; const explen = exponentHex.length / 2; const encodedModlen = encodeLengthHex(modlen); const encodedExplen = encodeLengthHex(explen); const encodedPubkey = `30${encodeLengthHex( modlen + explen + encodedModlen.length / 2 + encodedExplen.length / 2 + 2 )}02${encodedModlen}${modulusHex}02${encodedExplen}${exponentHex}`; const derB64 = new Buffer(encodedPubkey, 'hex').toString('base64'); const pem = `-----BEGIN RSA PUBLIC KEY-----\n${derB64.match(/.{1,64}/g).join('\n')}\n-----END RSA PUBLIC KEY-----\n`; return pem; }; // used for c_hash and at_hash validation // case (1): content = access_token, hashProvided = at_hash // case (2): content = code, hashProvided = c_hash exports.checkHashValueRS256 = (content, hashProvided) => { if (!content) return false; // step 1. hash the content var digest = crypto.createHash('sha256').update(content, 'ascii').digest(); // step2. take the first half of the digest, and save it in a buffer var buffer = new Buffer(digest.length/2); for (var i = 0; i < buffer.length; i++) buffer[i] = digest[i]; // step 3. base64url encode the buffer to get the hash var hashComputed = base64url(buffer); return (hashProvided === hashComputed); }; // This function is used for handling the tuples containing nonce/state/policy/timeStamp in session // remove the additional tuples from array starting from the oldest ones // remove expired tuples in array exports.processArray = function(array, maxAmount, maxAge) { // remove the additional tuples, start from the oldest ones if (array.length > maxAmount) array.splice(0, array.length - maxAmount); // count the number of those already expired var count = 0; for (var i = 0; i < array.length; i++) { var tuple = array[i]; if (tuple.timeStamp + maxAge * 1000 <= Date.now()) count++; else break; } // remove the expired ones if (count > 0) array.splice(0, count); }; // This function is used to find the tuple matching the given state, remove the tuple // from the array and return the tuple // @array - array of {state: x, nonce: x, policy: x, timeStamp: x} tuples // @state - the tuple which matches the given state exports.findAndDeleteTupleByState = (array, state) => { if (!array) return null; for (var i = 0; i < array.length; i++) { var tuple = array[i]; if (tuple['state'] === state) { // remove the tuple from the array array.splice(i, 1); return tuple; } } return null; }; // copy the fields from source to dest exports.copyObjectFields = (source, dest, fields) => { if (!source || !dest || !fields || !Array.isArray(fields)) return; for (var i = 0; i < fields.length; i++) dest[fields[i]] = source[fields[i]]; }; exports.getErrorMessage = (err) => { if (typeof err === 'string') return err; if (err instanceof Error) return err.message; // if not string or Error, we try to stringify it var str; try { str = JSON.stringify(err); } catch (ex) { return err; } return str; }; exports.concatUrl = (url, rest) => { if (typeof rest === 'string' || rest instanceof String) { rest = [rest]; } if (!url) { return `?${rest.join('&')}`; } var hasParam = url.indexOf('?') !== -1; return rest ? url.concat(hasParam ? '&' : '?').concat(rest.join('&')) : url; };