samlify

repository·master·Indexed 20 days ago

https://github.com/tngan/samlify

A highly configurable Node.js library for implementing SAML 2.0 Single Sign-On (SSO). It provides tools to configure Service Providers (SP) and Identity Providers (IdP) via XML metadata or programmatically, parse inbound SAML responses, and handle encrypted assertions. The library supports multiple schema validators and includes integration examples for OneLogin, GitLab, and Okta.

Tokens
33.4K
Snippets
95
Records
118
Agent score
71%

What's inside samlify

  1. Overview of SAML basics in samlify

    master

    The samlify basics guide provides a step-by-step walkthrough for implementing SAML functionality, starting from fundamental SSO concepts to practical integration. This specific tutorial focuses on the simplest possible configuration: a setup with no signature and no encryption.

    Note that for production environments, you should refer to the Advanced documentation to implement security features like signatures and encryption.

  2. Supported SAML 2.0 use cases

    master

    samlify provides implementations for both Identity Provider (IdP) and Service Provider (SP) roles, supporting the following workflows:

    • IdP-initiated Single Sign-On
    • IdP-initiated Single Logout
    • SP-initiated Single Sign-On
    • SP-initiated Single Logout (currently in development)

    The package includes a minimal identity provider implementation for testing and educational purposes, and supports integration with third-party identity providers.

  3. How SAML signatures are generated for different bindings

    master

    The method of signature generation depends on the SAML binding used:

    HTTP-Redirect binding

    The signature is computed over a URL-encoded concatenation of the following parameters: SAMLRequest, RelayState (optional), and SigAlg.

    • Format: SAMLRequest=xxx&RelayState=yyy&SigAlg=zzz
    • If RelayState is absent: SAMLRequest=xxx&SigAlg=zzz
    • The resulting signature is base64-encoded.

    HTTP-POST binding

    The signature is an XML digital signature embedded directly inside the <samlp:AuthnRequest> XML element.

  4. Understand the `parseResult` object structure

    master

    The sp.parseLoginResponse method resolves to a parseResult object containing the raw SAML content and an extract object. The extract object contains processed fields where inner attribute keys are camelCased (e.g., NotBefore becomes notBefore).

    Key fields in extract include:

    • response: Metadata about the response (id, issueInstant, destination, inResponseTo).
    • issuer: The IdP issuer URL.
    • nameID: The unique identifier for the user.
    • audience: The intended audience for the assertion.
    • conditions: Time-based constraints (notBefore, notOnOrAfter).
    • sessionIndex: Session details (authnInstant, sessionNotOnOrAfter, sessionIndex).
    • attributes: A map of user attributes (e.g., email, firstName).
    {
      samlContent: "<samlp:Response ...",
      extract: {
        response: {
          id: "_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6",
          issueInstant: "2015-10-26T11:41:43.500Z",
          destination: "https://sp.example.org/sso/acs",
          inResponseTo: "_4fee3b046395c4e751011e97f8900b5273d56685"
        },
        issuer: "https://idp.example.org/sso/metadata",
        nameID: "user@esaml2.com",
        audience: "https://sp.example.org/sso/metadata",
        conditions: {
          notBefore: "2015-10-26T11:41:43.500Z",
          notOnOrAfter: "2015-10-26T11:46:43.500Z"
        },
        sessionIndex: {
          authnInstant: "2015-10-26T11:41:43.500Z",
          sessionNotOnOrAfter: "2015-10-26T19:41:43.500Z",
          sessionIndex: "_be9967abd904ddcae3c0eb4189adbe3f71e327cf93"
        },
        attributes: {
          email: "user@esaml2.com",
          lastName: "Samuel",
          firstName: "E"
        }
      }
    }
  5. Support multiple Identity Providers (IdPs) and multiple Service Providers (SPs)

    master

    When different Identity Providers require different Service Provider configurations (for example, if one IdP requires signed requests while another does not), you should maintain one ServiceProvider instance per IdP. At runtime, you must select both the correct sourceSP and the correct targetIdP based on the incoming request parameters.

    // Define the default SP.
    const defaultSP = saml.ServiceProvider({
      metadata: fs.readFileSync('./metadata_sp.xml')
    });
    
    // Define an SP specifically configured for OneLogin.
    const oneloginSP = saml.ServiceProvider({
      metadata: fs.readFileSync('./metadata_sp_for_oneLogin.xml')
    });
    
    // Define the default IdP.
    const defaultIdP = saml.IdentityProvider({
      metadata: fs.readFileSync('./metadata_idp_default.xml')
    });
    
    // Define the OneLogin IdP.
    const oneloginIdP = saml.IdentityProvider({
      metadata: fs.readFileSync('./metadata_idp_onelogin.xml')
    });
    
    // SP-initiated SSO route, parameterised by IdP name.
    router.get('/spinitsso-post/:idp', (req, res) => {
      let targetIdP;
      let sourceSP;
      switch (req.params.idp || '') {
        case 'onelogin':
          targetIdP = oneloginIdP;
          sourceSP = oneloginSP;
          break;
        default:
          targetIdP = defaultIdP;
          sourceSP = defaultSP;
          break;
      }
      return sourceSP.createLoginRequest(targetIdP, 'post', (req, res) => res.render('actions', req));
    });
  6. Use HTTP-POST binding for SAML requests

    master

    The HTTP-POST binding delivers the SAML request via an auto-submitting HTML form. This is useful when the request size exceeds URL length limits.

    To implement this, use sp.createLoginRequest(idp, 'post'). This returns an object containing the necessary data (like entityEndpoint, type, context, and relayState) to be rendered into an HTML form. The form should use method="post" and then automatically submit via JavaScript.

    // Server-side: render the form object returned by createLoginRequest
    router.get('/spinitsso-redirect', (req, res) => {
      res.render('actions', sp.createLoginRequest(idp, 'post'));
    });
    
    // Client-side template (e.g., Handlebars):
    <form id="saml-form" method="post" action="{{entityEndpoint}}" autocomplete="off">
        <input type="hidden" name="{{type}}" value="{{context}}" />
        {{#if relayState}}
            <input type="hidden" name="RelayState" value="{{relayState}}" />
        {{/if}}
    </form>
    <script type="text/javascript">
        (function () {
            document.forms[0].submit();
        })();
    </script>
  7. Configure an Identity Provider (IdP) from metadata

    master

    You can construct an IdentityProvider instance by providing an existing SAML metadata document. This document serves as the contract between the Service Provider (SP) and the IdP for SSO/SLO. When using metadata, you can also provide private keys for signing and encryption.

    Required parameter:

    • metadata: string — The IdP-issued metadata XML string.

    Common optional parameters:

    • privateKey: string — The private key used for signing.
    • privateKeyPass: string — The passphrase for the private key.
    • encPrivateKey: string — The private key used for encryption.
    • encPrivateKeyPass: string — The passphrase for the encryption private key.
    • isAssertionEncrypted: boolean — Whether the IdP encrypts the assertion in the response (Note: This option is deprecated and will be removed; samlify will detect encryption automatically).
    const idp = new IdentityProvider({
      // Required.
      metadata: readFileSync('./test/misc/idpmeta.xml'),
      // Optional.
      privateKey: readFileSync('./test/key/idp/privkey.pem'),
      privateKeyPass: 'q9ALNhGT5EhfcRmp8Pg7e9zTQeP2x1bW',
      encPrivateKey: readFileSync('./test/key/idp/encryptKey.pem'),
      encPrivateKeyPass: 'g7hGcRmp8PxT5QeP2q9Ehf1bWe9zTALN',
      isAssertionEncrypted: true,
    });
  8. Configure SAML assertion signing requirements

    master

    To consume signed SAML responses, the Service Provider (SP) must declare its requirement for signed assertions. This is done by setting WantAssertionsSigned="true" in the SPSSODescriptor of the SP metadata XML.

    If you are not providing a metadata document to the ServiceProvider constructor, you must set wantAssertionsSigned: true in the options object instead.

    <SPSSODescriptor
        AuthnRequestsSigned="true"
        WantAssertionsSigned="true"
        protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
  9. Configure a Service Provider in samlify

    master

    To act as a Service Provider (SP) in a SAML flow, you must initialize a ServiceProvider instance using saml.ServiceProvider().

    Starting from v2, the metadata configuration option accepts a string or a Buffer. This flexibility allows you to load your SP metadata from various sources such as the filesystem, a database, a public URL, or in-memory storage.

    Note: The previous API pattern of passing a file path directly to ServiceProvider() is deprecated. You must now pass an object with a metadata key.

    const saml = require('samlify');
    const fs = require('fs');
    
    // v2 and later configuration pattern
    const sp = saml.ServiceProvider({
      metadata: fs.readFileSync('./metadata/sp.xml')
    });
  10. Generate testing keys and certificates using OpenSSL

    master

    For testing purposes, you can use OpenSSL to generate a private key and a self-signed certificate. It is recommended to use passphrase protection on the private key.

    1. Generate a private key: Use openssl genrsa to create a 4096-bit RSA key. The example below uses -passout pass:foobar to protect the key with the passphrase foobar and outputs it to encryptKey.pem.
    2. Generate a self-signed certificate: Use openssl req with the -x509 flag to create a certificate (encryptionCert.cer) based on the private key, valid for 3650 days.
    $ openssl genrsa -passout pass:foobar -out encryptKey.pem 4096
    $ openssl req -new -x509 -key encryptKey.pem -out encryptionCert.cer -days 3650
  11. Construct a ServiceProvider from a configuration object

    master

    If you do not have a metadata document, you can construct a ServiceProvider entirely from a configuration object. This allows you to manually define the SP's identity and capabilities.

    Required parameter:

    • entityID: string — Entity identifier used to identify the SP and match values in SAML requests/responses.

    Common optional parameters:

    • authnRequestsSigned: boolean — Whether the SP signs its AuthnRequest. Defaults to false.
    • wantAssertionsSigned: boolean — Whether the SP requires signed assertions. Defaults to false.
    • wantMessageSigned: boolean — Whether the SP requires signed SAML messages. Defaults to false.
    • signingCert: string — Certificate used for signing.
    • encryptCert: string — Certificate used for encryption.
    • elementsOrder: string[] — DOM element ordering for generated metadata. Defaults to ['KeyDescriptor', 'NameIDFormat', 'SingleLogoutService', 'AssertionConsumerService'].
    • nameIDFormat: NameIDFormat[] — Supported NameID formats. The first entry is used in outbound requests.
    • singleLogoutService: Service[] — Single Logout endpoints.
    • assertionConsumerService: Service[] — Endpoints where the IdP posts SAML responses.
    • signatureConfig: SignatureConfig — Signature placement and layout options.
    const sp = new ServiceProvider({
      // Required.
      entityID: 'http://hello-saml-sp.com/metadata',
      // Optional parameters listed below.
    });
  12. Parse and verify a SAML Response using `sp.parseLoginResponse`

    master

    To handle a SAML response from an Identity Provider (IdP), you must implement an Assertion Consumer Service (ACS) endpoint. This endpoint receives the SAML response (typically via HTTP-POST) and uses sp.parseLoginResponse to parse the XML and extract authentication data.

    Implementation Steps

    1. Define an ACS Endpoint: Create a route (e.g., in Express) that matches the Location declared in your Service Provider (SP) metadata.
    2. Call parseLoginResponse: Pass the IdP instance, the binding type ('post'), and the request object.
    3. Handle the Result: On success, use the parseResult.extract object to access user attributes and session info. On failure, catch the error (e.g., if the SAML status code indicates failure).

    Note: If WantAssertionsSigned is set in your SP metadata, parseLoginResponse will automatically verify the XML signature and the Issuer name.

    router.post('/acs', (req, res) => {
      sp.parseLoginResponse(idp, 'post', req)
        .then(parseResult => {
          // Use parseResult.extract to run your business logic.
        })
        .catch(console.error);
    });