diff --git a/README.md b/README.md index 88e53c9e..3baa9b37 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,19 @@ The `decryptionCert` argument should be a public certificate matching the `decry The `signingCert` argument should be a public certificate matching the `privateKey` and is required if the strategy is configured with a `privateKey`. An array of certificates can be provided to support certificate rotation. When supplying an array of certificates, the first entry in the array should match the current `privateKey`. Additional entries in the array can be used to publish upcoming certificates to IdPs before changing the `privateKey`. +### generateServiceProviderMetadata( params ) + +The underlying `generateServiceProviderMetadata` function is also exported directly. This is useful if you want to generate metadata without creating a strategy object. + +```js +const { generateServiceProviderMetadata } = require("@node-saml/node-saml"); + +const metadata = generateServiceProviderMetadata({ + issuer: "https://example.com", + callbackUrl: "https://example.com/callback", +}); +``` + ## Security and signatures Node-SAML uses the HTTP Redirect Binding for its `AuthnRequest`s (unless overridden with the `authnRequestBinding` parameter), and expects to receive the messages back via the HTTP POST binding. diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..95735baa --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_IDENTIFIER_FORMAT = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"; +export const DEFAULT_WANT_ASSERTIONS_SIGNED = true; diff --git a/src/index.ts b/src/index.ts index 469b5747..40022164 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import { SAML } from "./saml"; +import { generateServiceProviderMetadata } from "./metadata"; import { CacheItem, CacheProvider, @@ -18,6 +19,7 @@ import { export { SAML, + generateServiceProviderMetadata, CacheItem, CacheProvider, SamlOptions, diff --git a/src/metadata.ts b/src/metadata.ts index a70b1599..91865a2a 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -7,6 +7,8 @@ import { } from "./types"; import { assertRequired, signXmlMetadata } from "./utility"; import { buildXmlBuilderObject } from "./xml"; +import { generateUniqueId as generateUniqueIdDefault } from "./crypto"; +import { DEFAULT_IDENTIFIER_FORMAT, DEFAULT_WANT_ASSERTIONS_SIGNED } from "./constants"; export const generateServiceProviderMetadata = ( params: GenerateServiceProviderMetadataParams, @@ -15,13 +17,14 @@ export const generateServiceProviderMetadata = ( issuer, callbackUrl, logoutCallbackUrl, - identifierFormat, - wantAssertionsSigned, decryptionPvk, privateKey, metadataContactPerson, metadataOrganization, - generateUniqueId, + identifierFormat = DEFAULT_IDENTIFIER_FORMAT, + wantAssertionsSigned = DEFAULT_WANT_ASSERTIONS_SIGNED, + // This matches the default used in the `SAML` class. + generateUniqueId = generateUniqueIdDefault, } = params; let { signingCerts, decryptionCert } = params; diff --git a/src/saml.ts b/src/saml.ts index 8c50cb93..2583a3f2 100644 --- a/src/saml.ts +++ b/src/saml.ts @@ -43,6 +43,7 @@ import { keyInfoToPem, generateUniqueId } from "./crypto"; import { dateStringToTimestamp, generateInstant } from "./date-time"; import { signAuthnRequestPost } from "./saml-post-signing"; import { generateServiceProviderMetadata } from "./metadata"; +import { DEFAULT_IDENTIFIER_FORMAT, DEFAULT_WANT_ASSERTIONS_SIGNED } from "./constants"; const debug = Debug("node-saml"); const inflateRawAsync = util.promisify(zlib.inflateRaw); @@ -125,11 +126,11 @@ class SAML { audience: ctorOptions.audience ?? ctorOptions.issuer ?? "unknown_audience", // use issuer as default identifierFormat: ctorOptions.identifierFormat === undefined - ? "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" + ? DEFAULT_IDENTIFIER_FORMAT : ctorOptions.identifierFormat, allowCreate: ctorOptions.allowCreate ?? true, spNameQualifier: ctorOptions.spNameQualifier, - wantAssertionsSigned: ctorOptions.wantAssertionsSigned ?? true, + wantAssertionsSigned: ctorOptions.wantAssertionsSigned ?? DEFAULT_WANT_ASSERTIONS_SIGNED, wantAuthnResponseSigned: ctorOptions.wantAuthnResponseSigned ?? true, authnContext: ctorOptions.authnContext ?? [ "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport", diff --git a/src/types.ts b/src/types.ts index b23b35a9..9cbb0221 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,6 +84,7 @@ export type CertCallback = ( export interface MandatorySamlOptions { cert: string | string[] | CertCallback; issuer: string; + callbackUrl: string; } export interface SamlIDPListConfig { @@ -141,7 +142,6 @@ export enum ValidateInResponseTo { */ export interface SamlOptions extends Partial, MandatorySamlOptions { // Core - callbackUrl: string; entryPoint?: string; decryptionPvk?: string | Buffer; @@ -216,7 +216,7 @@ export interface GenerateServiceProviderMetadataParams { callbackUrl: SamlOptions["callbackUrl"]; logoutCallbackUrl?: SamlOptions["logoutCallbackUrl"]; identifierFormat?: SamlOptions["identifierFormat"]; - wantAssertionsSigned: SamlOptions["wantAssertionsSigned"]; + wantAssertionsSigned?: SamlOptions["wantAssertionsSigned"]; decryptionPvk?: SamlOptions["decryptionPvk"]; privateKey?: SamlOptions["privateKey"]; signatureAlgorithm?: SamlOptions["signatureAlgorithm"]; @@ -225,7 +225,7 @@ export interface GenerateServiceProviderMetadataParams { signMetadata?: SamlOptions["signMetadata"]; metadataContactPerson?: SamlOptions["metadataContactPerson"]; metadataOrganization?: SamlOptions["metadataOrganization"]; - generateUniqueId: SamlOptions["generateUniqueId"]; + generateUniqueId?: SamlOptions["generateUniqueId"]; } export type SamlConfig = Partial & MandatorySamlOptions; diff --git a/test/tests.spec.ts b/test/tests.spec.ts index 05d002a1..3d351d36 100644 --- a/test/tests.spec.ts +++ b/test/tests.spec.ts @@ -12,6 +12,7 @@ import * as assert from "assert"; import { FAKE_CERT, TEST_CERT } from "./types"; import { assertRequired, signXmlResponse } from "../src/utility"; import { parseDomFromString, validateSignature } from "../src/xml"; +import { generateServiceProviderMetadata } from "../src/metadata"; const BAD_TEST_CERT = "MIIEOTCCAyGgAwIBAgIJAKZgJdKdCdL6MA0GCSqGSIb3DQEBBQUAMHAxCzAJBgNVBAYTAkFVMREwDwYDVQQIEwhWaWN0b3JpYTESMBAGA1UEBxMJTWVsYm91cm5lMSEwHwYDVQQKExhUYWJjb3JwIEhvbGRpbmdzIExpbWl0ZWQxFzAVBgNVBAMTDnN0cy50YWIuY29tLmF1MB4XDTE3MDUzMDA4NTQwOFoXDTI3MDUyODA4NTQwOFowcDELMAkGA1UEBhMCQVUxETAPBgNVBAgTCFZpY3RvcmlhMRIwEAYDVQQHEwlNZWxib3VybmUxITAfBgNVBAoTGFRhYmNvcnAgSG9sZGluZ3MgTGltaXRlZDEXMBUGA1UEAxMOc3RzLnRhYi5jb20uYXUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0NuMcflq3rtupKYDf4a7lWmsXy66fYe9n8jB2DuLMakEJBlzn9j6B98IZftrilTq21VR7wUXROxG8BkN8IHY+l8X7lATmD28fFdZJj0c8Qk82eoq48faemth4fBMx2YrpnhU00jeXeP8dIIaJTPCHBTNgZltMMhphklN1YEPlzefJs3YD+Ryczy1JHbwETxt+BzO1JdjBe1fUTyl6KxAwWvtsNBURmQRYlDOk4GRgdkQnfxBuCpOMeOpV8wiBAi3h65Lab9C5avu4AJlA9e4qbOmWt6otQmgy5fiJVy6bH/d8uW7FJmSmePX9sqAWa9szhjdn36HHVQsfHC+IUEX7AgMBAAGjgdUwgdIwHQYDVR0OBBYEFN6z6cuxY7FTkg1S/lIjnS4x5ARWMIGiBgNVHSMEgZowgZeAFN6z6cuxY7FTkg1S/lIjnS4x5ARWoXSkcjBwMQswCQYDVQQGEwJBVTERMA8GA1UECBMIVmljdG9yaWExEjAQBgNVBAcTCU1lbGJvdXJuZTEhMB8GA1UEChMYVGFiY29ycCBIb2xkaW5ncyBMaW1pdGVkMRcwFQYDVQQDEw5zdHMudGFiLmNvbS5hdYIJAKZgJdKdCdL6MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAMi5HyvXgRa4+kKz3dk4SwAEXzeZRcsbeDJWVUxdb6a+JQxIoG7L9rSbd6yZvP/Xel5TrcwpCpl5eikzXB02/C0wZKWicNmDEBlOfw0Pc5ngdoh6ntxHIWm5QMlAfjR0dgTlojN4Msw2qk7cP1QEkV96e2BJUaqaNnM3zMvd7cfRjPNfbsbwl6hCCCAdwrALKYtBnjKVrCGPwO+xiw5mUJhZ1n6ZivTOdQEWbl26UO60J9ItiWP8VK0d0aChn326Ovt7qC4S3AgDlaJwcKe5Ifxl/UOWePGRwXj2UUuDWFhjtVmRntMmNZbe5yE8MkEvU+4/c6LqGwTCgDenRbK53Dgg"; @@ -3273,4 +3274,34 @@ describe("node-saml /", function () { }); }); }); + + describe("generateServiceProviderMetadata", function () { + it("only requires issuer and callbackUrl parameters", function () { + const metadata = generateServiceProviderMetadata({ + issuer: "https://www.example.com", + callbackUrl: "https://www.example.com/callback", + }); + + expect(metadata).to.be.a("string"); + expect(metadata).to.contain('entityID="https://www.example.com"'); + expect(metadata).to.contain('Location="https://www.example.com/callback"'); + }); + + it("matches metadata from SAML object", function () { + const saml = new SAML({ + cert: "no_cert_needed_for_metadata", + issuer: "https://www.example.com", + callbackUrl: "https://www.example.com/callback", + generateUniqueId: () => "d700077e-60ad-49c1-b93a-dd1753528708", + }); + + expect( + generateServiceProviderMetadata({ + issuer: "https://www.example.com", + callbackUrl: "https://www.example.com/callback", + generateUniqueId: () => "d700077e-60ad-49c1-b93a-dd1753528708", + }), + ).to.equal(saml.generateServiceProviderMetadata(null, null)); + }); + }); });