passport-azure-ad
Version:
OIDC and Bearer Passport strategies for Azure Active Directory
160 lines (125 loc) • 5.84 kB
JavaScript
/**
* 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 restrict';
var crypto = require('crypto');
var createBuffer = require('./jwe').createBuffer;
/*
* the handler for state/nonce/policy
* @maxAmount - the max amount of {state: x, nonce: x, policy: x} tuples you want to save in cookie
* @maxAge - when a tuple in session expires in seconds
* @cookieEncryptionKeys
* - keys used to encrypt and decrypt cookie
*/
function CookieContentHandler(maxAmount, maxAge, cookieEncryptionKeys) {
if (!maxAge || (typeof maxAge !== 'number' || maxAge <= 0))
throw new Error('CookieContentHandler: maxAge must be a positive number');
this.maxAge = maxAge; // seconds
if (!maxAmount || (typeof maxAmount !== 'number' || maxAmount <= 0 || maxAmount % 1 !== 0))
throw new Error('CookieContentHandler: maxAmount must be a positive integer');
this.maxAmount = maxAmount;
if (!cookieEncryptionKeys || !Array.isArray(cookieEncryptionKeys) || cookieEncryptionKeys.length === 0)
throw new Error('CookieContentHandler: cookieEncryptionKeys must be a non-emptry array');
for (var i = 0; i < cookieEncryptionKeys.length; i++) {
var item = cookieEncryptionKeys[i];
if (!item.key || !item.iv)
throw new Error(`CookieContentHandler: array item ${i+1} in cookieEncryptionKeys must have the form { key: , iv: }`);
if (item.key.length !== 32)
throw new Error(`CookieContentHandler: key number ${i+1} is ${item.key.length} bytes, expected: 32 bytes`);
if (item.iv.length !== 12)
throw new Error(`CookieContentHandler: iv number ${i+1} is ${item.iv.length} bytes, expected: 12 bytes`);
}
this.cookieEncryptionKeys = cookieEncryptionKeys;
}
CookieContentHandler.prototype.findAndDeleteTupleByState = function(req, res, stateToFind) {
if (!req.cookies)
throw new Error('Cookie is not found in request. Did you forget to use cookie parsing middleware such as cookie-parser?');
var cookieEncryptionKeys = this.cookieEncryptionKeys;
var tuple = null;
// try every key and every cookie
for (var i = 0; i < cookieEncryptionKeys.length; i++) {
var item = cookieEncryptionKeys[i];
var key = createBuffer(item.key);
var iv = createBuffer(item.iv);
for (var cookie in req.cookies) {
if (req.cookies.hasOwnProperty(cookie) && cookie.startsWith('passport-aad.')) {
var encrypted = cookie.substring(13);
try {
var decrypted = decryptCookie(encrypted, key, iv);
tuple = JSON.parse(decrypted);
} catch (ex) {
continue;
}
if (tuple.state === stateToFind) {
res.clearCookie(cookie);
return tuple;
}
}
}
}
return null;
};
CookieContentHandler.prototype.add = function(req, res, tupleToAdd) {
var cookies = [];
// collect the related cookies
for (var cookie in req.cookies) {
if (req.cookies.hasOwnProperty(cookie) && cookie.startsWith('passport-aad.'))
cookies.push(cookie);
}
// only keep the most recent maxAmount-1 many cookies
if (cookies.length > this.maxAmount - 1) {
cookies.sort();
var numberToRemove = cookies.length - (this.maxAmount - 1);
for (var i = 0; i < numberToRemove; i++) {
res.clearCookie(cookies[0]);
cookies.shift();
}
}
// add the new cookie
var tupleString = JSON.stringify(tupleToAdd);
var item = this.cookieEncryptionKeys[0];
var key = createBuffer(item.key);
var iv = createBuffer(item.iv);
var encrypted = encryptCookie(tupleString, key, iv);
res.cookie('passport-aad.' + Date.now() + '.' + encrypted, 0, { maxAge: this.maxAge * 1000, httpOnly: true });
};
var encryptCookie = function(content, key, iv) {
var cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
var encrypted = cipher.update(content, 'utf8', 'hex');
encrypted += cipher.final('hex');
var authTag = cipher.getAuthTag().toString('hex');
return encrypted + '.' + authTag;
};
var decryptCookie = function(encrypted, key, iv) {
var parts = encrypted.split('.');
if (parts.length !== 3)
throw new Error('invalid cookie');
// the first part is timestamp, ignore it
var content = createBuffer(parts[1], 'hex');
var authTag = createBuffer(parts[2], 'hex');
var decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
var decrypted = decipher.update(content, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
exports.CookieContentHandler = CookieContentHandler;