wsfed
Version:
WSFed server middleware
148 lines (129 loc) • 6.53 kB
JavaScript
var templates = require('./templates');
var PassportProfileMapper = require('./claims/PassportProfileMapper');
var utils = require('./utils');
var saml11 = require('saml').Saml11;
var jwt = require('jsonwebtoken');
var interpolate = require('./interpolate');
var xtend = require('xtend');
function asResource(res) {
if(res.substr(0, 6) !== 'http:/' &&
res.substr(0, 6) !== 'https:' &&
res.substr(0, 4) !== 'urn:') {
return 'urn:' + res;
}
return res;
}
/**
* WSFederation middleware.
*
* This middleware creates a WSFed endpoint based on the user logged in identity.
*
* @param {object} options
* @param {function} options.getPostURL REQUIRED the function receives 4 parameters from the request (wtrealm, wreply, request, callback). It must return, via the callback,
* the URL to post the result response to.
* @param {function} options.getUserFromRequest the function receives one parameter, the request. It must return the user being authenticated, as an object.
* This object will be passed to the profile mapper to determine attributes of the response.
* @param {PassportProfileMapper} options.profileMapper a ProfileMapper implementation to convert a user profile to claims (PassportProfile).
* @param {Buffer} options.cert The public key / certificate of the issuer, in PEM format.
* @param {Buffer} options.key The private key of the issuer, used to sign the response, in PEM format.
* @param {string} options.issuer the name of the issuer of the response.
* @param {string} options.audience the name of the audience of the response.
* @param {number} options.lifetime The lifetime of the response, in seconds. Default 8 hours.
* @param {string} options.signatureAlgorithm signature algorithm, options: rsa-sha1, rsa-sha256. default rsa-sha256.
* @param {string} options.digestAlgorithm digest algorithm, options: sha1, sha256. default sha256.
* @param {boolean} options.jwt if true, uses a JWT token for the signed assertion. default false.
* @param {object} options.extendJWT An object that, is passed, contains claims to be added to JWT signed assertion.
* @param {boolean} options.jwtAlgorithm If using JWT signed assertion, indicates the algorithm to be applied. Default RS256.
* @param {boolean} options.jwtAllowInsecureKeySizes Insecure and not recommended, for backward compatibility ONLY. If true, allows insecure key sizes to be used when signing with JWT. Default false.
* @param {boolean} options.jwtAllowInvalidAsymmetricKeyTypes Insecure and not recommended, for backward compatibility ONLY.
* If true, allows a mismatch between JWT algorithm and the actual key type provided to sign.
*
* @return {function} An Express middleware that acts as a WsFed endpoint.
*/
module.exports = function(options) {
options = options || {};
options.profileMapper = options.profileMapper || PassportProfileMapper;
options.getUserFromRequest = options.getUserFromRequest || function(req){ return req.user; };
if(typeof options.getPostURL !== 'function') {
throw new Error('getPostURL is required');
}
function renderResponse(res, postUrl, wctx, assertion) {
res.set('Content-Type', 'text/html');
var model = {
callback: postUrl,
wctx: wctx,
wresult: assertion
};
var form;
if (options.formTemplate) {
form = interpolate(options.formTemplate);
} else {
form = templates[(!options.plain_form ? 'form' : 'form_el')];
}
res.send(form(model));
}
var responseHandler = options.responseHandler || renderResponse;
function execute (postUrl, req, res, next) {
var audience = options.audience ||
req.query.wtrealm ||
req.query.wreply;
if(!audience){
return next(new Error('audience is required'));
}
audience = asResource(audience);
var user = options.getUserFromRequest(req);
if(!user) return res.send(401);
var ctx = options.wctx || req.query.wctx;
if (!options.jwt) {
var profileMap = options.profileMapper(user);
var claims = profileMap.getClaims(options);
var ni = profileMap.getNameIdentifier(options);
if (!ni || !ni.nameIdentifier) {
return next(new Error('No attribute was found to generate the nameIdentifier'));
}
saml11.create({
signatureAlgorithm: options.signatureAlgorithm,
digestAlgorithm: options.digestAlgorithm,
cert: options.cert,
key: options.key,
issuer: asResource(options.issuer),
lifetimeInSeconds: options.lifetime || options.lifetimeInSeconds || (60 * 60 * 8),
audiences: audience,
attributes: claims,
nameIdentifier: ni.nameIdentifier,
nameIdentifierFormat: ni.nameIdentifierFormat || options.nameIdentifierFormat,
encryptionPublicKey: options.encryptionPublicKey,
encryptionCert: options.encryptionCert
}, function(err, assertion) {
if (err) return next(err);
var escapedWctx = utils.escape(ctx);
assertion = '<t:RequestSecurityTokenResponse Context="'+ escapedWctx + '" xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><t:RequestedSecurityToken>' + assertion + '</t:RequestedSecurityToken></t:RequestSecurityTokenResponse>';
return responseHandler(res, postUrl, ctx, assertion);
});
} else {
if (options.extendJWT && typeof options.extendJWT === 'object') {
user = xtend(user, options.extendJWT);
}
jwt.sign(user, options.key.toString(), {
expiresIn: options.lifetime || options.lifetimeInSeconds || (60 * 60 * 8),
audience: audience,
issuer: asResource(options.issuer),
algorithm: options.jwtAlgorithm || 'RS256',
allowInsecureKeySizes: options.jwtAllowInsecureKeySizes || false,
allowInvalidAsymmetricKeyTypes: options.jwtAllowInvalidAsymmetricKeyTypes || false
}, (error, signed) => {
if (error) {
return next(error);
}
return responseHandler(res, postUrl, ctx, signed);
});
}
}
return function (req, res, next) {
options.getPostURL(req.query.wtrealm, req.query.wreply, req, function (err, postUrl) {
if (err) return next(err);
if (!postUrl) return res.send(400, 'postUrl is required');
execute(postUrl, req, res, next);
});
};
};