@pagopa/io-spid-commons
Version:
Common code for integrating SPID authentication
146 lines • 9.69 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.withSpid = exports.withSpidAuthMiddleware = exports.noopCacheProvider = void 0;
/**
* Exports a decorator function that applies
* a SPID authentication middleware to an express application.
*
* Setups the endpoint to generate service provider metadata
* and a scheduled process to refresh IDP metadata from providers.
*/
const express_1 = require("@pagopa/ts-commons/lib/express");
const responses_1 = require("@pagopa/ts-commons/lib/responses");
const function_1 = require("fp-ts/lib/function");
const O = require("fp-ts/lib/Option");
const T = require("fp-ts/lib/Task");
const t = require("io-ts");
const passport = require("passport");
const xml2js_1 = require("xml2js");
const E = require("fp-ts/Either");
const config_1 = require("./config");
const redis_cache_provider_1 = require("./strategy/redis_cache_provider");
Object.defineProperty(exports, "noopCacheProvider", { enumerable: true, get: function () { return redis_cache_provider_1.noopCacheProvider; } });
const logger_1 = require("./utils/logger");
const metadata_1 = require("./utils/metadata");
const middleware_1 = require("./utils/middleware");
const response_1 = require("./utils/response");
const saml_1 = require("./utils/saml");
const saml_2 = require("./utils/saml");
const samlUtils_1 = require("./utils/samlUtils");
/**
* Wraps assertion consumer service handler
* with SPID authentication and redirects.
*/
const withSpidAuthMiddleware = (acs, clientLoginRedirectionUrl, clientErrorRedirectionUrl, extraRequestParamsCodec) => (req, res, next) => {
const maybeDoc = (0, saml_1.getXmlFromSamlResponse)(req.body);
// in case the SAMLResponse is missing
// we redirect the user to a specific error
if (O.isNone(maybeDoc)) {
logger_1.logger.error("Spid Authentication|Authentication Error|ERROR=%s|ISSUER=UNKNOWN", samlUtils_1.ERROR_SAML_RESPONSE_MISSING);
return res.redirect(clientErrorRedirectionUrl +
`?errorMessage=${samlUtils_1.ERROR_SAML_RESPONSE_MISSING}`);
}
passport.authenticate("spid", (err, user) => __awaiter(void 0, void 0, void 0, function* () {
const issuer = (0, function_1.pipe)(maybeDoc, O.chain(saml_1.getSamlIssuer), O.getOrElse(() => "UNKNOWN"));
if (err) {
const redirectionUrl = clientErrorRedirectionUrl +
(0, function_1.pipe)(maybeDoc, O.chain(saml_1.getErrorCodeFromResponse), O.map((errorCode) => `?errorCode=${errorCode}`), O.getOrElse(() => `?errorMessage=${err}`));
logger_1.logger.error("Spid Authentication|Authentication Error|ERROR=%s|ISSUER=%s|REDIRECT_TO=%s", err, issuer, redirectionUrl);
return res.redirect(redirectionUrl);
}
if (!user) {
logger_1.logger.error("Spid Authentication|Authentication Error|ERROR=user_not_found|ISSUER=%s", issuer);
return res.redirect(clientLoginRedirectionUrl);
}
const _a = user, { extraLoginRequestParams } = _a, userBaseProps = __rest(_a, ["extraLoginRequestParams"]);
const response = yield acs(Object.assign(Object.assign({}, userBaseProps), { getAcsOriginalRequest: () => req }), (0, function_1.pipe)(extraRequestParamsCodec, E.fromNullable(undefined), E.chainW((codec) => codec.decode(extraLoginRequestParams)), E.getOrElseW(() => undefined)));
response.apply(res);
}))(req, res, next);
};
exports.withSpidAuthMiddleware = withSpidAuthMiddleware;
/**
* Apply SPID authentication middleware
* to an express application.
*/
// eslint-disable-next-line max-params
const withSpid = ({ acs, app, appConfig, doneCb = function_1.constVoid, logout, redisClient, samlConfig, serviceProviderConfig, lollipopMiddleware = (_, __, next) => next(), }) => {
const loadSpidStrategyOptions = (0, middleware_1.getSpidStrategyOptionsUpdater)(samlConfig, serviceProviderConfig);
const metadataTamperer = (0, saml_2.getMetadataTamperer)(new xml2js_1.Builder(), serviceProviderConfig, samlConfig);
const authorizeRequestTamperer = (0, saml_1.getAuthorizeRequestTamperer)(
// spid-testenv does not accept an xml header with utf8 encoding
new xml2js_1.Builder({ xmldec: { encoding: undefined, version: "1.0" } }), samlConfig);
const maybeStartupIdpsMetadata = O.fromNullable(appConfig.startupIdpsMetadata);
// If `startupIdpsMetadata` is provided, IDP metadata
// are initially taken from its value when the backend starts
return (0, function_1.pipe)(maybeStartupIdpsMetadata, O.map(metadata_1.parseStartupIdpsMetadata), O.map((idpOptionsRecord) => T.of((0, middleware_1.makeSpidStrategyOptions)(samlConfig, serviceProviderConfig, idpOptionsRecord))), O.getOrElse(loadSpidStrategyOptions), T.map((spidStrategyOptions) => {
(0, middleware_1.upsertSpidStrategyOption)(app, spidStrategyOptions);
return (0, middleware_1.makeSpidStrategy)(spidStrategyOptions, saml_1.getSamlOptions, redisClient, authorizeRequestTamperer, metadataTamperer, (0, saml_1.getPreValidateResponse)(serviceProviderConfig.strictResponseValidation, appConfig.eventTraker, appConfig.hasClockSkewLoggingEvent), doneCb, appConfig.extraLoginRequestParamConfig);
}), T.map((spidStrategy) => {
var _a;
// Even when `startupIdpsMetadata` is provided, we try to load
// IDP metadata from the remote registries
(0, function_1.pipe)(maybeStartupIdpsMetadata, O.map(() => {
(0, function_1.pipe)(loadSpidStrategyOptions(), T.map((opts) => (0, middleware_1.upsertSpidStrategyOption)(app, opts)))().catch((e) => {
logger_1.logger.error("loadSpidStrategyOptions|error:%s", e);
});
}));
// Fetch IDPs metadata from remote URL and update SPID passport strategy options
const idpMetadataRefresher = () => (0, function_1.pipe)(loadSpidStrategyOptions(), T.map((opts) => (0, middleware_1.upsertSpidStrategyOption)(app, opts)));
// Initializes SpidStrategy for passport
passport.use("spid", spidStrategy);
const spidAuth = passport.authenticate("spid", {
session: false,
});
// Setup SPID login handler
app.get(appConfig.loginPath, (0, response_1.middlewareCatchAsInternalError)((req, res, next) => {
(0, function_1.pipe)(O.fromNullable(req.query), O.chainNullableK((q) => q.authLevel), O.filter(t.keyof(config_1.SPID_LEVELS).is), O.chain(O.fromPredicate((authLevel) => appConfig.spidLevelsWhitelist.includes(authLevel))), O.fold(() => {
var _a;
logger_1.logger.error(`Missing or invalid authLevel [${(_a = req === null || req === void 0 ? void 0 : req.query) === null || _a === void 0 ? void 0 : _a.authLevel}]`);
return (0, responses_1.ResponseErrorValidation)("Bad Request", "Missing or invalid authLevel").apply(res);
}, (_) => next()));
}), (0, response_1.middlewareCatchAsInternalError)(lollipopMiddleware), (0, response_1.middlewareCatchAsInternalError)(spidAuth));
// Setup SPID metadata handler
app.get(appConfig.metadataPath, (0, express_1.toExpressHandler)((req) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((resolve) => spidStrategy.generateServiceProviderMetadataAsync(req, null, // certificate used for encryption / decryption
serviceProviderConfig.publicCert, (err, metadata) => {
if (err || !metadata) {
resolve((0, responses_1.ResponseErrorInternal)(err
? err.message
: `Error generating service provider metadata ${err}.`));
}
else {
resolve((0, responses_1.ResponseSuccessXml)(metadata));
}
}));
})));
// Setup SPID assertion consumer service.
// This endpoint is called when the SPID IDP
// redirects the authenticated user to our app
app.post(appConfig.assertionConsumerServicePath, (0, response_1.middlewareCatchAsInternalError)((0, exports.withSpidAuthMiddleware)(acs, appConfig.clientLoginRedirectionUrl, appConfig.clientErrorRedirectionUrl, (_a = appConfig.extraLoginRequestParamConfig) === null || _a === void 0 ? void 0 : _a.codec)));
// Setup logout handler
app.post(appConfig.sloPath, (0, express_1.toExpressHandler)(logout));
return { app, idpMetadataRefresher };
}));
};
exports.withSpid = withSpid;
//# sourceMappingURL=index.js.map