@boxyhq/saml-jackson 0.5.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/controller/api.d.ts +1 -0
- package/dist/controller/api.js +37 -5
- package/dist/controller/health-check.d.ts +11 -0
- package/dist/controller/health-check.js +53 -0
- package/dist/controller/logout.d.ts +18 -0
- package/dist/controller/logout.js +199 -0
- package/dist/controller/oauth/redirect.d.ts +1 -1
- package/dist/controller/oauth/redirect.js +6 -1
- package/dist/controller/oauth.d.ts +5 -3
- package/dist/controller/oauth.js +156 -27
- package/dist/controller/utils.d.ts +2 -1
- package/dist/controller/utils.js +11 -24
- package/dist/db/mem.js +31 -12
- package/dist/db/mongo.js +3 -10
- package/dist/db/redis.js +17 -6
- package/dist/db/sql/sql.d.ts +1 -1
- package/dist/db/sql/sql.js +10 -9
- package/dist/db/utils.d.ts +2 -0
- package/dist/db/utils.js +3 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +14 -2
- package/dist/typings.d.ts +50 -21
- package/package.json +22 -25
- package/dist/saml/saml.d.ts +0 -12
- package/dist/saml/saml.js +0 -211
    
        package/README.md
    CHANGED
    
    | @@ -14,7 +14,7 @@ npm i @boxyhq/saml-jackson | |
| 14 14 |  | 
| 15 15 | 
             
            ## Documentation
         | 
| 16 16 |  | 
| 17 | 
            -
            For full documentation, visit [boxyhq.com/docs/jackson/npm-library](https://boxyhq.com/docs/jackson/npm-library)
         | 
| 17 | 
            +
            For full documentation, visit [boxyhq.com/docs/jackson/deploy/npm-library](https://boxyhq.com/docs/jackson/deploy/npm-library)
         | 
| 18 18 |  | 
| 19 19 | 
             
            ## License
         | 
| 20 20 |  | 
    
        package/dist/controller/api.d.ts
    CHANGED
    
    
    
        package/dist/controller/api.js
    CHANGED
    
    | @@ -50,7 +50,7 @@ exports.APIController = void 0; | |
| 50 50 | 
             
            const crypto_1 = __importDefault(require("crypto"));
         | 
| 51 51 | 
             
            const dbutils = __importStar(require("../db/utils"));
         | 
| 52 52 | 
             
            const metrics = __importStar(require("../opentelemetry/metrics"));
         | 
| 53 | 
            -
            const  | 
| 53 | 
            +
            const saml20_1 = __importDefault(require("@boxyhq/saml20"));
         | 
| 54 54 | 
             
            const x509_1 = __importDefault(require("../saml/x509"));
         | 
| 55 55 | 
             
            const error_1 = require("./error");
         | 
| 56 56 | 
             
            const utils_1 = require("./utils");
         | 
| @@ -58,6 +58,19 @@ class APIController { | |
| 58 58 | 
             
                constructor({ configStore }) {
         | 
| 59 59 | 
             
                    this.configStore = configStore;
         | 
| 60 60 | 
             
                }
         | 
| 61 | 
            +
                _validateRedirectUrl({ redirectUrlList, defaultRedirectUrl }) {
         | 
| 62 | 
            +
                    if (redirectUrlList) {
         | 
| 63 | 
            +
                        if (redirectUrlList.length > 100) {
         | 
| 64 | 
            +
                            throw new error_1.JacksonError('Exceeded maximum number of allowed redirect urls', 400);
         | 
| 65 | 
            +
                        }
         | 
| 66 | 
            +
                        for (const url of redirectUrlList) {
         | 
| 67 | 
            +
                            (0, utils_1.validateAbsoluteUrl)(url, 'redirectUrl is invalid');
         | 
| 68 | 
            +
                        }
         | 
| 69 | 
            +
                    }
         | 
| 70 | 
            +
                    if (defaultRedirectUrl) {
         | 
| 71 | 
            +
                        (0, utils_1.validateAbsoluteUrl)(defaultRedirectUrl, 'defaultRedirectUrl is invalid');
         | 
| 72 | 
            +
                    }
         | 
| 73 | 
            +
                }
         | 
| 61 74 | 
             
                _validateIdPConfig(body) {
         | 
| 62 75 | 
             
                    const { encodedRawMetadata, rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product, description } = body;
         | 
| 63 76 | 
             
                    if (!rawMetadata && !encodedRawMetadata) {
         | 
| @@ -168,11 +181,13 @@ class APIController { | |
| 168 181 | 
             
                        const { encodedRawMetadata, rawMetadata, defaultRedirectUrl, redirectUrl, tenant, product, name, description, } = body;
         | 
| 169 182 | 
             
                        metrics.increment('createConfig');
         | 
| 170 183 | 
             
                        this._validateIdPConfig(body);
         | 
| 184 | 
            +
                        const redirectUrlList = extractRedirectUrls(redirectUrl);
         | 
| 185 | 
            +
                        this._validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
         | 
| 171 186 | 
             
                        let metaData = rawMetadata;
         | 
| 172 187 | 
             
                        if (encodedRawMetadata) {
         | 
| 173 188 | 
             
                            metaData = Buffer.from(encodedRawMetadata, 'base64').toString();
         | 
| 174 189 | 
             
                        }
         | 
| 175 | 
            -
                        const idpMetadata = yield  | 
| 190 | 
            +
                        const idpMetadata = yield saml20_1.default.parseMetadataAsync(metaData);
         | 
| 176 191 | 
             
                        // extract provider
         | 
| 177 192 | 
             
                        let providerName = extractHostName(idpMetadata.entityID);
         | 
| 178 193 | 
             
                        if (!providerName) {
         | 
| @@ -195,7 +210,7 @@ class APIController { | |
| 195 210 | 
             
                        const record = {
         | 
| 196 211 | 
             
                            idpMetadata,
         | 
| 197 212 | 
             
                            defaultRedirectUrl,
         | 
| 198 | 
            -
                            redirectUrl:  | 
| 213 | 
            +
                            redirectUrl: redirectUrlList,
         | 
| 199 214 | 
             
                            tenant,
         | 
| 200 215 | 
             
                            product,
         | 
| 201 216 | 
             
                            name,
         | 
| @@ -296,6 +311,8 @@ class APIController { | |
| 296 311 | 
             
                        if (description && description.length > 100) {
         | 
| 297 312 | 
             
                            throw new error_1.JacksonError('Description should not exceed 100 characters', 400);
         | 
| 298 313 | 
             
                        }
         | 
| 314 | 
            +
                        const redirectUrlList = redirectUrl ? extractRedirectUrls(redirectUrl) : null;
         | 
| 315 | 
            +
                        this._validateRedirectUrl({ defaultRedirectUrl, redirectUrlList });
         | 
| 299 316 | 
             
                        const _currentConfig = yield this.getConfig(clientInfo);
         | 
| 300 317 | 
             
                        if (_currentConfig.clientSecret !== (clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.clientSecret)) {
         | 
| 301 318 | 
             
                            throw new error_1.JacksonError('clientSecret mismatch', 400);
         | 
| @@ -306,7 +323,7 @@ class APIController { | |
| 306 323 | 
             
                        }
         | 
| 307 324 | 
             
                        let newMetadata;
         | 
| 308 325 | 
             
                        if (metaData) {
         | 
| 309 | 
            -
                            newMetadata = yield  | 
| 326 | 
            +
                            newMetadata = yield saml20_1.default.parseMetadataAsync(metaData);
         | 
| 310 327 | 
             
                            // extract provider
         | 
| 311 328 | 
             
                            let providerName = extractHostName(newMetadata.entityID);
         | 
| 312 329 | 
             
                            if (!providerName) {
         | 
| @@ -321,7 +338,7 @@ class APIController { | |
| 321 338 | 
             
                                throw new error_1.JacksonError('Tenant/Product config mismatch with IdP metadata', 400);
         | 
| 322 339 | 
             
                            }
         | 
| 323 340 | 
             
                        }
         | 
| 324 | 
            -
                        const record = Object.assign(Object.assign({}, _currentConfig), { name: name ? name : _currentConfig.name, description: description ? description : _currentConfig.description, idpMetadata: newMetadata ? newMetadata : _currentConfig.idpMetadata, defaultRedirectUrl: defaultRedirectUrl ? defaultRedirectUrl : _currentConfig.defaultRedirectUrl, redirectUrl:  | 
| 341 | 
            +
                        const record = Object.assign(Object.assign({}, _currentConfig), { name: name ? name : _currentConfig.name, description: description ? description : _currentConfig.description, idpMetadata: newMetadata ? newMetadata : _currentConfig.idpMetadata, defaultRedirectUrl: defaultRedirectUrl ? defaultRedirectUrl : _currentConfig.defaultRedirectUrl, redirectUrl: redirectUrlList ? redirectUrlList : _currentConfig.redirectUrl });
         | 
| 325 342 | 
             
                        yield this.configStore.put(clientInfo === null || clientInfo === void 0 ? void 0 : clientInfo.clientID, record, {
         | 
| 326 343 | 
             
                            // secondary index on entityID
         | 
| 327 344 | 
             
                            name: utils_1.IndexNames.EntityID,
         | 
| @@ -495,3 +512,18 @@ const extractHostName = (url) => { | |
| 495 512 | 
             
                    return null;
         | 
| 496 513 | 
             
                }
         | 
| 497 514 | 
             
            };
         | 
| 515 | 
            +
            const extractRedirectUrls = (urls) => {
         | 
| 516 | 
            +
                if (!urls) {
         | 
| 517 | 
            +
                    return [];
         | 
| 518 | 
            +
                }
         | 
| 519 | 
            +
                if (typeof urls === 'string') {
         | 
| 520 | 
            +
                    if (urls.startsWith('[')) {
         | 
| 521 | 
            +
                        // redirectUrl is a stringified array
         | 
| 522 | 
            +
                        return JSON.parse(urls);
         | 
| 523 | 
            +
                    }
         | 
| 524 | 
            +
                    // redirectUrl is a single URL
         | 
| 525 | 
            +
                    return [urls];
         | 
| 526 | 
            +
                }
         | 
| 527 | 
            +
                // redirectUrl is an array of URLs
         | 
| 528 | 
            +
                return urls;
         | 
| 529 | 
            +
            };
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            import { IHealthCheckController, Storable } from '../typings';
         | 
| 2 | 
            +
            export declare class HealthCheckController implements IHealthCheckController {
         | 
| 3 | 
            +
                healthCheckStore: Storable;
         | 
| 4 | 
            +
                constructor({ healthCheckStore }: {
         | 
| 5 | 
            +
                    healthCheckStore: any;
         | 
| 6 | 
            +
                });
         | 
| 7 | 
            +
                init(): Promise<void>;
         | 
| 8 | 
            +
                status(): Promise<{
         | 
| 9 | 
            +
                    status: number;
         | 
| 10 | 
            +
                }>;
         | 
| 11 | 
            +
            }
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            "use strict";
         | 
| 2 | 
            +
            var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
         | 
| 3 | 
            +
                function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
         | 
| 4 | 
            +
                return new (P || (P = Promise))(function (resolve, reject) {
         | 
| 5 | 
            +
                    function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
         | 
| 6 | 
            +
                    function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
         | 
| 7 | 
            +
                    function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
         | 
| 8 | 
            +
                    step((generator = generator.apply(thisArg, _arguments || [])).next());
         | 
| 9 | 
            +
                });
         | 
| 10 | 
            +
            };
         | 
| 11 | 
            +
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 12 | 
            +
            exports.HealthCheckController = void 0;
         | 
| 13 | 
            +
            const error_1 = require("./error");
         | 
| 14 | 
            +
            const healthKey = 'amihealthy';
         | 
| 15 | 
            +
            const healthValue = 'fit';
         | 
| 16 | 
            +
            const g = global;
         | 
| 17 | 
            +
            class HealthCheckController {
         | 
| 18 | 
            +
                constructor({ healthCheckStore }) {
         | 
| 19 | 
            +
                    this.healthCheckStore = healthCheckStore;
         | 
| 20 | 
            +
                }
         | 
| 21 | 
            +
                init() {
         | 
| 22 | 
            +
                    return __awaiter(this, void 0, void 0, function* () {
         | 
| 23 | 
            +
                        this.healthCheckStore.put(healthKey, healthValue);
         | 
| 24 | 
            +
                    });
         | 
| 25 | 
            +
                }
         | 
| 26 | 
            +
                status() {
         | 
| 27 | 
            +
                    return __awaiter(this, void 0, void 0, function* () {
         | 
| 28 | 
            +
                        try {
         | 
| 29 | 
            +
                            if (!g.isJacksonReady) {
         | 
| 30 | 
            +
                                return {
         | 
| 31 | 
            +
                                    status: 503,
         | 
| 32 | 
            +
                                };
         | 
| 33 | 
            +
                            }
         | 
| 34 | 
            +
                            const response = yield Promise.race([
         | 
| 35 | 
            +
                                this.healthCheckStore.get(healthKey),
         | 
| 36 | 
            +
                                new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 1000)),
         | 
| 37 | 
            +
                            ]);
         | 
| 38 | 
            +
                            if (response === healthValue) {
         | 
| 39 | 
            +
                                return {
         | 
| 40 | 
            +
                                    status: 200,
         | 
| 41 | 
            +
                                };
         | 
| 42 | 
            +
                            }
         | 
| 43 | 
            +
                            return {
         | 
| 44 | 
            +
                                status: 503,
         | 
| 45 | 
            +
                            };
         | 
| 46 | 
            +
                        }
         | 
| 47 | 
            +
                        catch (err) {
         | 
| 48 | 
            +
                            throw new error_1.JacksonError('Service not available', 503);
         | 
| 49 | 
            +
                        }
         | 
| 50 | 
            +
                    });
         | 
| 51 | 
            +
                }
         | 
| 52 | 
            +
            }
         | 
| 53 | 
            +
            exports.HealthCheckController = HealthCheckController;
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            import { SAMLResponsePayload, SLORequestParams } from '../typings';
         | 
| 2 | 
            +
            export declare class LogoutController {
         | 
| 3 | 
            +
                private configStore;
         | 
| 4 | 
            +
                private sessionStore;
         | 
| 5 | 
            +
                private opts;
         | 
| 6 | 
            +
                constructor({ configStore, sessionStore, opts }: {
         | 
| 7 | 
            +
                    configStore: any;
         | 
| 8 | 
            +
                    sessionStore: any;
         | 
| 9 | 
            +
                    opts: any;
         | 
| 10 | 
            +
                });
         | 
| 11 | 
            +
                createRequest({ nameId, tenant, product, redirectUrl }: SLORequestParams): Promise<{
         | 
| 12 | 
            +
                    logoutUrl: string | null;
         | 
| 13 | 
            +
                    logoutForm: string | null;
         | 
| 14 | 
            +
                }>;
         | 
| 15 | 
            +
                handleResponse({ SAMLResponse, RelayState }: SAMLResponsePayload): Promise<{
         | 
| 16 | 
            +
                    redirectUrl: any;
         | 
| 17 | 
            +
                }>;
         | 
| 18 | 
            +
            }
         | 
| @@ -0,0 +1,199 @@ | |
| 1 | 
            +
            "use strict";
         | 
| 2 | 
            +
            var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
         | 
| 3 | 
            +
                if (k2 === undefined) k2 = k;
         | 
| 4 | 
            +
                var desc = Object.getOwnPropertyDescriptor(m, k);
         | 
| 5 | 
            +
                if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
         | 
| 6 | 
            +
                  desc = { enumerable: true, get: function() { return m[k]; } };
         | 
| 7 | 
            +
                }
         | 
| 8 | 
            +
                Object.defineProperty(o, k2, desc);
         | 
| 9 | 
            +
            }) : (function(o, m, k, k2) {
         | 
| 10 | 
            +
                if (k2 === undefined) k2 = k;
         | 
| 11 | 
            +
                o[k2] = m[k];
         | 
| 12 | 
            +
            }));
         | 
| 13 | 
            +
            var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
         | 
| 14 | 
            +
                Object.defineProperty(o, "default", { enumerable: true, value: v });
         | 
| 15 | 
            +
            }) : function(o, v) {
         | 
| 16 | 
            +
                o["default"] = v;
         | 
| 17 | 
            +
            });
         | 
| 18 | 
            +
            var __importStar = (this && this.__importStar) || function (mod) {
         | 
| 19 | 
            +
                if (mod && mod.__esModule) return mod;
         | 
| 20 | 
            +
                var result = {};
         | 
| 21 | 
            +
                if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
         | 
| 22 | 
            +
                __setModuleDefault(result, mod);
         | 
| 23 | 
            +
                return result;
         | 
| 24 | 
            +
            };
         | 
| 25 | 
            +
            var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
         | 
| 26 | 
            +
                function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
         | 
| 27 | 
            +
                return new (P || (P = Promise))(function (resolve, reject) {
         | 
| 28 | 
            +
                    function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
         | 
| 29 | 
            +
                    function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
         | 
| 30 | 
            +
                    function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
         | 
| 31 | 
            +
                    step((generator = generator.apply(thisArg, _arguments || [])).next());
         | 
| 32 | 
            +
                });
         | 
| 33 | 
            +
            };
         | 
| 34 | 
            +
            var __importDefault = (this && this.__importDefault) || function (mod) {
         | 
| 35 | 
            +
                return (mod && mod.__esModule) ? mod : { "default": mod };
         | 
| 36 | 
            +
            };
         | 
| 37 | 
            +
            Object.defineProperty(exports, "__esModule", { value: true });
         | 
| 38 | 
            +
            exports.LogoutController = void 0;
         | 
| 39 | 
            +
            const crypto_1 = __importDefault(require("crypto"));
         | 
| 40 | 
            +
            const util_1 = require("util");
         | 
| 41 | 
            +
            const xml2js_1 = __importDefault(require("xml2js"));
         | 
| 42 | 
            +
            const xmlbuilder_1 = __importDefault(require("xmlbuilder"));
         | 
| 43 | 
            +
            const zlib_1 = require("zlib");
         | 
| 44 | 
            +
            const dbutils = __importStar(require("../db/utils"));
         | 
| 45 | 
            +
            const saml20_1 = __importDefault(require("@boxyhq/saml20"));
         | 
| 46 | 
            +
            const error_1 = require("./error");
         | 
| 47 | 
            +
            const redirect = __importStar(require("./oauth/redirect"));
         | 
| 48 | 
            +
            const utils_1 = require("./utils");
         | 
| 49 | 
            +
            const deflateRawAsync = (0, util_1.promisify)(zlib_1.deflateRaw);
         | 
| 50 | 
            +
            const relayStatePrefix = 'boxyhq_jackson_';
         | 
| 51 | 
            +
            const logoutXPath = "/*[local-name(.)='LogoutRequest']";
         | 
| 52 | 
            +
            class LogoutController {
         | 
| 53 | 
            +
                constructor({ configStore, sessionStore, opts }) {
         | 
| 54 | 
            +
                    this.opts = opts;
         | 
| 55 | 
            +
                    this.configStore = configStore;
         | 
| 56 | 
            +
                    this.sessionStore = sessionStore;
         | 
| 57 | 
            +
                }
         | 
| 58 | 
            +
                // Create SLO Request
         | 
| 59 | 
            +
                createRequest({ nameId, tenant, product, redirectUrl }) {
         | 
| 60 | 
            +
                    return __awaiter(this, void 0, void 0, function* () {
         | 
| 61 | 
            +
                        let samlConfig = null;
         | 
| 62 | 
            +
                        if (tenant && product) {
         | 
| 63 | 
            +
                            const samlConfigs = yield this.configStore.getByIndex({
         | 
| 64 | 
            +
                                name: utils_1.IndexNames.TenantProduct,
         | 
| 65 | 
            +
                                value: dbutils.keyFromParts(tenant, product),
         | 
| 66 | 
            +
                            });
         | 
| 67 | 
            +
                            if (!samlConfigs || samlConfigs.length === 0) {
         | 
| 68 | 
            +
                                throw new error_1.JacksonError('SAML configuration not found.', 403);
         | 
| 69 | 
            +
                            }
         | 
| 70 | 
            +
                            samlConfig = samlConfigs[0];
         | 
| 71 | 
            +
                        }
         | 
| 72 | 
            +
                        if (!samlConfig) {
         | 
| 73 | 
            +
                            throw new error_1.JacksonError('SAML configuration not found.', 403);
         | 
| 74 | 
            +
                        }
         | 
| 75 | 
            +
                        const { idpMetadata: { slo, provider }, certs: { privateKey, publicKey }, } = samlConfig;
         | 
| 76 | 
            +
                        if ('redirectUrl' in slo === false && 'postUrl' in slo === false) {
         | 
| 77 | 
            +
                            throw new error_1.JacksonError(`${provider} doesn't support SLO or disabled by IdP.`, 400);
         | 
| 78 | 
            +
                        }
         | 
| 79 | 
            +
                        const { id, xml } = buildRequestXML(nameId, this.opts.samlAudience, slo.redirectUrl);
         | 
| 80 | 
            +
                        const sessionId = crypto_1.default.randomBytes(16).toString('hex');
         | 
| 81 | 
            +
                        let logoutUrl = null;
         | 
| 82 | 
            +
                        let logoutForm = null;
         | 
| 83 | 
            +
                        const relayState = relayStatePrefix + sessionId;
         | 
| 84 | 
            +
                        const signedXML = yield signXML(xml, privateKey, publicKey);
         | 
| 85 | 
            +
                        yield this.sessionStore.put(sessionId, {
         | 
| 86 | 
            +
                            id,
         | 
| 87 | 
            +
                            redirectUrl,
         | 
| 88 | 
            +
                        });
         | 
| 89 | 
            +
                        // HTTP-Redirect binding
         | 
| 90 | 
            +
                        if ('redirectUrl' in slo) {
         | 
| 91 | 
            +
                            logoutUrl = redirect.success(slo.redirectUrl, {
         | 
| 92 | 
            +
                                SAMLRequest: Buffer.from(yield deflateRawAsync(signedXML)).toString('base64'),
         | 
| 93 | 
            +
                                RelayState: relayState,
         | 
| 94 | 
            +
                            });
         | 
| 95 | 
            +
                        }
         | 
| 96 | 
            +
                        // HTTP-POST binding
         | 
| 97 | 
            +
                        if ('postUrl' in slo) {
         | 
| 98 | 
            +
                            logoutForm = saml20_1.default.createPostForm(slo.postUrl, [
         | 
| 99 | 
            +
                                {
         | 
| 100 | 
            +
                                    name: 'RelayState',
         | 
| 101 | 
            +
                                    value: relayState,
         | 
| 102 | 
            +
                                },
         | 
| 103 | 
            +
                                {
         | 
| 104 | 
            +
                                    name: 'SAMLRequest',
         | 
| 105 | 
            +
                                    value: Buffer.from(signedXML).toString('base64'),
         | 
| 106 | 
            +
                                },
         | 
| 107 | 
            +
                            ]);
         | 
| 108 | 
            +
                        }
         | 
| 109 | 
            +
                        return { logoutUrl, logoutForm };
         | 
| 110 | 
            +
                    });
         | 
| 111 | 
            +
                }
         | 
| 112 | 
            +
                // Handle SLO Response
         | 
| 113 | 
            +
                handleResponse({ SAMLResponse, RelayState }) {
         | 
| 114 | 
            +
                    var _a;
         | 
| 115 | 
            +
                    return __awaiter(this, void 0, void 0, function* () {
         | 
| 116 | 
            +
                        const rawResponse = Buffer.from(SAMLResponse, 'base64').toString();
         | 
| 117 | 
            +
                        const sessionId = RelayState.replace(relayStatePrefix, '');
         | 
| 118 | 
            +
                        const session = yield this.sessionStore.get(sessionId);
         | 
| 119 | 
            +
                        if (!session) {
         | 
| 120 | 
            +
                            throw new error_1.JacksonError('Unable to validate state from the origin request.', 403);
         | 
| 121 | 
            +
                        }
         | 
| 122 | 
            +
                        const parsedResponse = yield parseSAMLResponse(rawResponse);
         | 
| 123 | 
            +
                        if (parsedResponse.status !== 'urn:oasis:names:tc:SAML:2.0:status:Success') {
         | 
| 124 | 
            +
                            throw new error_1.JacksonError(`SLO failed with status ${parsedResponse.status}.`, 400);
         | 
| 125 | 
            +
                        }
         | 
| 126 | 
            +
                        if (parsedResponse.inResponseTo !== session.id) {
         | 
| 127 | 
            +
                            throw new error_1.JacksonError(`SLO failed with mismatched request ID.`, 400);
         | 
| 128 | 
            +
                        }
         | 
| 129 | 
            +
                        const samlConfigs = yield this.configStore.getByIndex({
         | 
| 130 | 
            +
                            name: utils_1.IndexNames.EntityID,
         | 
| 131 | 
            +
                            value: parsedResponse.issuer,
         | 
| 132 | 
            +
                        });
         | 
| 133 | 
            +
                        if (!samlConfigs || samlConfigs.length === 0) {
         | 
| 134 | 
            +
                            throw new error_1.JacksonError('SAML configuration not found.', 403);
         | 
| 135 | 
            +
                        }
         | 
| 136 | 
            +
                        const { idpMetadata, defaultRedirectUrl } = samlConfigs[0];
         | 
| 137 | 
            +
                        if (!(yield saml20_1.default.validateSignature(rawResponse, null, idpMetadata.thumbprint))) {
         | 
| 138 | 
            +
                            throw new error_1.JacksonError('Invalid signature.', 403);
         | 
| 139 | 
            +
                        }
         | 
| 140 | 
            +
                        try {
         | 
| 141 | 
            +
                            yield this.sessionStore.delete(sessionId);
         | 
| 142 | 
            +
                        }
         | 
| 143 | 
            +
                        catch (_err) {
         | 
| 144 | 
            +
                            // Ignore
         | 
| 145 | 
            +
                        }
         | 
| 146 | 
            +
                        return {
         | 
| 147 | 
            +
                            redirectUrl: (_a = session.redirectUrl) !== null && _a !== void 0 ? _a : defaultRedirectUrl,
         | 
| 148 | 
            +
                        };
         | 
| 149 | 
            +
                    });
         | 
| 150 | 
            +
                }
         | 
| 151 | 
            +
            }
         | 
| 152 | 
            +
            exports.LogoutController = LogoutController;
         | 
| 153 | 
            +
            // Create the XML for the SLO Request
         | 
| 154 | 
            +
            const buildRequestXML = (nameId, providerName, sloUrl) => {
         | 
| 155 | 
            +
                const id = '_' + crypto_1.default.randomBytes(10).toString('hex');
         | 
| 156 | 
            +
                const xml = {
         | 
| 157 | 
            +
                    'samlp:LogoutRequest': {
         | 
| 158 | 
            +
                        '@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
         | 
| 159 | 
            +
                        '@xmlns:saml': 'urn:oasis:names:tc:SAML:2.0:assertion',
         | 
| 160 | 
            +
                        '@ID': id,
         | 
| 161 | 
            +
                        '@Version': '2.0',
         | 
| 162 | 
            +
                        '@IssueInstant': new Date().toISOString(),
         | 
| 163 | 
            +
                        '@Destination': sloUrl,
         | 
| 164 | 
            +
                        'saml:Issuer': {
         | 
| 165 | 
            +
                            '#text': providerName,
         | 
| 166 | 
            +
                        },
         | 
| 167 | 
            +
                        'saml:NameID': {
         | 
| 168 | 
            +
                            '@Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
         | 
| 169 | 
            +
                            '#text': nameId,
         | 
| 170 | 
            +
                        },
         | 
| 171 | 
            +
                    },
         | 
| 172 | 
            +
                };
         | 
| 173 | 
            +
                return {
         | 
| 174 | 
            +
                    id,
         | 
| 175 | 
            +
                    xml: xmlbuilder_1.default.create(xml).end({}),
         | 
| 176 | 
            +
                };
         | 
| 177 | 
            +
            };
         | 
| 178 | 
            +
            // Parse SAMLResponse
         | 
| 179 | 
            +
            const parseSAMLResponse = (rawResponse) => __awaiter(void 0, void 0, void 0, function* () {
         | 
| 180 | 
            +
                return new Promise((resolve, reject) => {
         | 
| 181 | 
            +
                    xml2js_1.default.parseString(rawResponse, { tagNameProcessors: [xml2js_1.default.processors.stripPrefix] }, (err, { LogoutResponse }) => {
         | 
| 182 | 
            +
                        if (err) {
         | 
| 183 | 
            +
                            reject(err);
         | 
| 184 | 
            +
                            return;
         | 
| 185 | 
            +
                        }
         | 
| 186 | 
            +
                        resolve({
         | 
| 187 | 
            +
                            issuer: LogoutResponse.Issuer[0]._,
         | 
| 188 | 
            +
                            id: LogoutResponse.$.ID,
         | 
| 189 | 
            +
                            status: LogoutResponse.Status[0].StatusCode[0].$.Value,
         | 
| 190 | 
            +
                            destination: LogoutResponse.$.Destination,
         | 
| 191 | 
            +
                            inResponseTo: LogoutResponse.$.InResponseTo,
         | 
| 192 | 
            +
                        });
         | 
| 193 | 
            +
                    });
         | 
| 194 | 
            +
                });
         | 
| 195 | 
            +
            });
         | 
| 196 | 
            +
            // Sign the XML
         | 
| 197 | 
            +
            const signXML = (xml, signingKey, publicKey) => __awaiter(void 0, void 0, void 0, function* () {
         | 
| 198 | 
            +
                return yield saml20_1.default.sign(xml, signingKey, publicKey, logoutXPath);
         | 
| 199 | 
            +
            });
         | 
| @@ -1 +1 @@ | |
| 1 | 
            -
            export declare const success: (redirectUrl: string, params: Record<string, string>) => string;
         | 
| 1 | 
            +
            export declare const success: (redirectUrl: string, params: Record<string, string | string[] | undefined>) => string;
         | 
| @@ -4,7 +4,12 @@ exports.success = void 0; | |
| 4 4 | 
             
            const success = (redirectUrl, params) => {
         | 
| 5 5 | 
             
                const url = new URL(redirectUrl);
         | 
| 6 6 | 
             
                for (const [key, value] of Object.entries(params)) {
         | 
| 7 | 
            -
                     | 
| 7 | 
            +
                    if (Array.isArray(value)) {
         | 
| 8 | 
            +
                        value.forEach((v) => url.searchParams.append(key, v));
         | 
| 9 | 
            +
                    }
         | 
| 10 | 
            +
                    else if (value !== undefined) {
         | 
| 11 | 
            +
                        url.searchParams.set(key, value);
         | 
| 12 | 
            +
                    }
         | 
| 8 13 | 
             
                }
         | 
| 9 14 | 
             
                return url.href;
         | 
| 10 15 | 
             
            };
         | 
| @@ -12,12 +12,14 @@ export declare class OAuthController implements IOAuthController { | |
| 12 12 | 
             
                    tokenStore: any;
         | 
| 13 13 | 
             
                    opts: any;
         | 
| 14 14 | 
             
                });
         | 
| 15 | 
            +
                private resolveMultipleConfigMatches;
         | 
| 15 16 | 
             
                authorize(body: OAuthReqBody): Promise<{
         | 
| 16 | 
            -
                    redirect_url | 
| 17 | 
            -
                    authorize_form | 
| 17 | 
            +
                    redirect_url?: string;
         | 
| 18 | 
            +
                    authorize_form?: string;
         | 
| 18 19 | 
             
                }>;
         | 
| 19 20 | 
             
                samlResponse(body: SAMLResponsePayload): Promise<{
         | 
| 20 | 
            -
                    redirect_url | 
| 21 | 
            +
                    redirect_url?: string;
         | 
| 22 | 
            +
                    app_select_form?: string;
         | 
| 21 23 | 
             
                }>;
         | 
| 22 24 | 
             
                /**
         | 
| 23 25 | 
             
                 * @swagger
         |