diff --git a/README.md b/README.md index e1da81c8..7b494a14 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Passport-SAML -[![Build Status](https://github.com/node-saml/passport-saml/workflows/Build%20Status/badge.svg)](https://github.com/node-saml/passport-saml/actions?query=workflow%3ABuild%Status) [![GitHub version](https://badge.fury.io/gh/node-saml%2Fpassport-saml.svg)](https://badge.fury.io/gh/node-saml%2Fpassport-saml) [![npm version](https://badge.fury.io/js/passport-saml.svg)](http://badge.fury.io/js/passport-saml) [![NPM](https://nodei.co/npm/passport-saml.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/passport-saml/) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) +[![Build Status](https://github.com/node-saml/passport-saml/workflows/Build%20Status/badge.svg)](https://github.com/node-saml/passport-saml/actions?query=workflow%3ABuild%Status) [![GitHub version](https://badge.fury.io/gh/node-saml%2Fpassport-saml.svg)](https://badge.fury.io/gh/node-saml%2Fpassport-saml) [![npm version](https://badge.fury.io/js/passport-saml.svg)](http://badge.fury.io/js/passport-saml) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) + +[![NPM](https://nodei.co/npm/passport-saml.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/passport-saml/) This is a [SAML 2.0](http://en.wikipedia.org/wiki/SAML_2.0) authentication provider for [Passport](http://passportjs.org/), the Node.js authentication library. @@ -10,9 +12,9 @@ Passport-SAML has been tested to work with Onelogin, Okta, Shibboleth, [SimpleSA ## Installation - $ npm install passport-saml - -/ +```shell +npm install passport-saml +``` ## Usage @@ -88,27 +90,28 @@ Using multiple providers supports `validateInResponseTo`, but all the `InRespons The profile object referenced above contains the following: ```typescript -type Profile = { - issuer?: string; +export interface Profile { + issuer: string; sessionIndex?: string; - nameID?: string; - nameIDFormat?: string; + nameID: string; + nameIDFormat: string; nameQualifier?: string; spNameQualifier?: string; + ID?: string; mail?: string; // InCommon Attribute urn:oid:0.9.2342.19200300.100.1.3 email?: string; // `mail` if not present in the assertion - getAssertionXml(): string; // get the raw assertion XML - getAssertion(): object; // get the assertion XML parsed as a JavaScript object - getSamlResponseXml(): string; // get the raw SAML response XML - ID?: string; -} & { + ["urn:oid:0.9.2342.19200300.100.1.3"]?: string; + getAssertionXml?(): string; // get the raw assertion XML + getAssertion?(): Record; // get the assertion XML parsed as a JavaScript object + getSamlResponseXml?(): string; // get the raw SAML response XML [attributeName: string]: unknown; // arbitrary `AttributeValue`s -}; +} ``` #### Config parameter details: -- **Core** +**Core** + - `callbackUrl`: full callbackUrl (overrides path/protocol if supplied) - `path`: path to callback; will be combined with protocol and server host information to construct callback url if `callbackUrl` is not specified (default: `/saml/consume`) - `protocol`: protocol for callback; will be combined with path and server host information to construct callback url if `callbackUrl` is not specified (default: `http://`) @@ -122,7 +125,9 @@ type Profile = { - `signatureAlgorithm`: optionally set the signature algorithm for signing requests, valid values are 'sha1' (default), 'sha256', or 'sha512' - `digestAlgorithm`: optionally set the digest algorithm used to provide a digest for the signed data object, valid values are 'sha1' (default), 'sha256', or 'sha512' - `xmlSignatureTransforms`: optionally set an array of signature transforms to be used in HTTP-POST signatures. By default this is `[ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#' ]` -- **Additional SAML behaviors** + +**Additional SAML behaviors** + - `additionalParams`: dictionary of additional query params to add to all requests; if an object with this key is passed to `authenticate`, the dictionary of additional query params will be appended to those present on the returned URL, overriding any specified by initialization options' additional parameters (`additionalParams`, `additionalAuthorizeParams`, and `additionalLogoutParams`) - `additionalAuthorizeParams`: dictionary of additional query params to add to 'authorize' requests - `identifierFormat`: optional name identifier format to request from identity provider (default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) @@ -147,31 +152,38 @@ type Profile = { { entries: [ // required { - providerId: 'yourProviderId', // required for each entry - name: 'yourName', // optional - loc: 'yourLoc', // optional - } + providerId: "yourProviderId", // required for each entry + name: "yourName", // optional + loc: "yourLoc", // optional + }, ], - getComplete: 'URI to your complete IDP list', // optional + getComplete: "URI to your complete IDP list", // optional }, ], proxyCount: 2, // optional - requesterId: 'requesterId', // optional -} + requesterId: "requesterId", // optional +}; ``` -- **InResponseTo Validation** +**InResponseTo Validation** + - `validateInResponseTo`: if truthy, then InResponseTo will be validated from incoming SAML responses - `requestIdExpirationPeriodMs`: Defines the expiration time when a Request ID generated for a SAML request will not be valid if seen in a SAML response in the `InResponseTo` field. Default is 8 hours. - `cacheProvider`: Defines the implementation for a cache provider used to store request Ids generated in SAML requests as part of `InResponseTo` validation. Default is a built-in in-memory cache provider. For details see the 'Cache Provider' section. -- **Issuer Validation** + +**Issuer Validation** + - `idpIssuer`: if provided, then the IdP issuer will be validated for incoming Logout Requests/Responses. For ADFS this looks like `https://acme_tools.windows.net/deadbeef` -- **Passport** + +**Passport** + - `passReqToCallback`: if truthy, `req` will be passed as the first argument to the verify callback (default: `false`) - `name`: Optionally, provide a custom name. (default: `saml`). Useful If you want to instantiate the strategy multiple times with different configurations, allowing users to authenticate against multiple different SAML targets from the same site. You'll need to use a unique set of URLs for each target, and use this custom name when calling `passport.authenticate()` as well. -- **Logout** + +**Logout** + - `logoutUrl`: base address to call with logout requests (default: `entryPoint`) - `additionalLogoutParams`: dictionary of additional query params to add to 'logout' requests - `logoutCallbackUrl`: The value with which to populate the `Location` attribute in the `SingleLogoutService` elements in the generated service provider metadata. @@ -188,7 +200,10 @@ const bodyParser = require("body-parser"); app.post( "/login/callback", bodyParser.urlencoded({ extended: false }), - passport.authenticate("saml", { failureRedirect: "/", failureFlash: true }), + passport.authenticate("saml", { + failureRedirect: "/", + failureFlash: true, + }), function (req, res) { res.redirect("/"); } @@ -241,11 +256,11 @@ Authentication requests sent by Passport-SAML can be signed using RSA signature To select hashing algorithm, use: -```js +```javascript ... - signatureAlgorithm: 'sha1' // (default, but not recommended anymore these days) - signatureAlgorithm: 'sha256', // (preferred - your IDP should support it, otherwise think about upgrading it) - signatureAlgorithm: 'sha512' // (most secure - check if your IDP supports it) + signatureAlgorithm: "sha1" // (default, but not recommended anymore these days) + signatureAlgorithm: "sha256" // (preferred - your IDP should support it, otherwise think about upgrading it) + signatureAlgorithm: "sha512" // (most secure - check if your IDP supports it) ... ``` @@ -255,14 +270,14 @@ Formats supported for `privateKey` field are, 1. Well formatted PEM: -``` +```text -----BEGIN PRIVATE KEY----- -----END PRIVATE KEY----- ``` -``` +```text -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- @@ -290,8 +305,8 @@ cert: "MIICizCCAfQCCQCY8tKaMc0BMjANBgkqh ... W=="; If you have a certificate in the binary DER encoding, you can convert it to the necessary PEM encoding like this: -```bash - openssl x509 -inform der -in my_certificate.cer -out my_certificate.pem +```shell +openssl x509 -inform der -in my_certificate.cer -out my_certificate.pem ``` If the Identity Provider has multiple signing certificates that are valid (such as during the rolling from an old key to a new key and responses signed with either key are valid) then the `cert` configuration key can be an array: @@ -359,17 +374,17 @@ To support this scenario you can provide an implementation for a cache provider ```javascript { - saveAsync: async function(key, value) { - // saves the key with the optional value, returns the saved value - }, - getAsync: async function(key) { - // returns the value if found, null otherwise - }, - removeAsync: async function(key) { - // removes the key from the cache, returns the - // key removed, null if no key is removed - } -} + saveAsync: async function (key, value) { + // saves the key with the optional value, returns the saved value + }, + getAsync: async function (key) { + // returns the value if found, null otherwise + }, + removeAsync: async function (key) { + // removes the key from the cache, returns the + // key removed, null if no key is removed + }, +}; ``` Provide an instance of an object which has these functions passed to the `cacheProvider` config option when using Passport-SAML. diff --git a/package-lock.json b/package-lock.json index 931c26a4..49a185d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,10 +5,11 @@ "requires": true, "packages": { "": { + "name": "passport-saml", "version": "3.0.0", "license": "MIT", "dependencies": { - "node-saml": "^4.0.0-beta.0", + "node-saml": "4.0.0-beta.0", "passport-strategy": "^1.0.0" }, "devDependencies": { @@ -28,7 +29,7 @@ "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.1", "github-release-notes": "^0.17.3", - "mocha": "^9.1.1", + "mocha": "^9.1.3", "onchange": "^7.1.0", "passport": "^0.4.1", "prettier": "^2.4.1", @@ -4018,16 +4019,16 @@ "dev": true }, "node_modules/mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "dependencies": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.2", - "debug": "4.3.1", + "debug": "4.3.2", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", @@ -4038,12 +4039,11 @@ "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.23", + "nanoid": "3.1.25", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", "workerpool": "6.1.5", "yargs": "16.2.0", "yargs-parser": "20.2.4", @@ -4055,6 +4055,10 @@ }, "engines": { "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mochajs" } }, "node_modules/mocha/node_modules/argparse": { @@ -4063,24 +4067,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4141,9 +4127,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -11570,15 +11556,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2" - } - }, "node_modules/widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", @@ -15302,16 +15279,16 @@ "dev": true }, "mocha": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.1.tgz", - "integrity": "sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", + "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.2", - "debug": "4.3.1", + "debug": "4.3.2", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", @@ -15322,12 +15299,11 @@ "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.23", + "nanoid": "3.1.25", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", "workerpool": "6.1.5", "yargs": "16.2.0", "yargs-parser": "20.2.4", @@ -15340,23 +15316,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -15407,9 +15366,9 @@ "dev": true }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.1.25", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", + "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", "dev": true }, "natural-compare": { @@ -21226,15 +21185,6 @@ "isexe": "^2.0.0" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "widest-line": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", diff --git a/package.json b/package.json index 565a6813..5bc9c665 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eslint-plugin-prettier": "^4.0.0", "express": "^4.17.1", "github-release-notes": "^0.17.3", - "mocha": "^9.1.1", + "mocha": "^9.1.3", "onchange": "^7.1.0", "passport": "^0.4.1", "prettier": "^2.4.1", diff --git a/src/multiSamlStrategy.ts b/src/multiSamlStrategy.ts index ab9dcabe..00bf6f66 100644 --- a/src/multiSamlStrategy.ts +++ b/src/multiSamlStrategy.ts @@ -47,7 +47,7 @@ export class MultiSamlStrategy extends AbstractStrategy { logout( req: RequestWithUser, callback: (err: Error | null, url?: string | null | undefined) => void - ) { + ): void { this._options.getSamlOptions(req, (err, samlOptions) => { if (err) { return callback(err); @@ -65,7 +65,7 @@ export class MultiSamlStrategy extends AbstractStrategy { decryptionCert: string | null, signingCert: string | null, callback: (err: Error | null, metadata?: string) => void - ) { + ): void { if (typeof callback !== "function") { throw new Error("Metadata can't be provided synchronously for MultiSamlStrategy."); } diff --git a/src/strategy.ts b/src/strategy.ts index 3185d2c4..879cbf3e 100644 --- a/src/strategy.ts +++ b/src/strategy.ts @@ -101,18 +101,18 @@ export abstract class AbstractStrategy extends PassportStrategy { } }; - if (req.query && (req.query.SAMLResponse || req.query.SAMLRequest)) { - const originalQuery = url.parse(req.url).query; + if (req.query?.SAMLResponse || req.query?.SAMLRequest) { + const originalQuery = url.parse(req.url).query ?? ""; this._saml .validateRedirectAsync(req.query, originalQuery) .then(validateCallback) .catch((err) => this.error(err)); - } else if (req.body && req.body.SAMLResponse) { + } else if (req.body?.SAMLResponse) { this._saml .validatePostResponseAsync(req.body) .then(validateCallback) .catch((err) => this.error(err)); - } else if (req.body && req.body.SAMLRequest) { + } else if (req.body?.SAMLRequest) { this._saml .validatePostRequestAsync(req.body) .then(validateCallback) @@ -130,8 +130,8 @@ export abstract class AbstractStrategy extends PassportStrategy { const host = req.headers && req.headers.host; if (this._saml.options.authnRequestBinding === "HTTP-POST") { const data = await this._saml.getAuthorizeFormAsync(RelayState, host); - const res = req.res!; - res.send(data); + const res = req.res; + res?.send(data); } else { // Defaults to HTTP-Redirect this.redirect(await this._saml.getAuthorizeUrlAsync(RelayState, host, options)); @@ -191,6 +191,9 @@ export abstract class AbstractStrategy extends PassportStrategy { error(err: Error): void { super.error(err); } + redirect(url: string, status?: number): void { + super.redirect(url, status); + } } export class Strategy extends AbstractStrategy { diff --git a/src/types.ts b/src/types.ts index c74b41ef..ae40716c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,6 @@ import type * as express from "express"; import * as passport from "passport"; -import { Profile, SamlConfig } from "."; +import { Profile, SamlConfig } from "node-saml"; export interface AuthenticateOptions extends passport.AuthenticateOptions { samlFallback?: "login-request" | "logout-request"; @@ -16,9 +16,11 @@ export interface StrategyOptions { passReqToCallback?: boolean; } +export type User = Record; + export interface RequestWithUser extends express.Request { - samlLogoutRequest: any; - user?: Profile; + samlLogoutRequest: Profile; + user: User; } export type VerifiedCallback = (