A simple node.js SAML SDK for wicked.haufe.io
JavaScript
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
.dockerignore
.gitignore
.npmignore
Dockerfile-publish
README.md
package.json
wicked-saml.js

README.md

wicked.haufe.io SAML SDK

This library helps implementing Authorization Servers for federating SAML identites (SSO identities) into a wicked.haufe.io OAuth2.0 Implicit Grant Flow API implementation. It assumes your SAML IdP supports the HTTP-POST Binding and encrypted SAML Assertions.

It does the heavy lifting regarding implementing a SAML SP (Service Provider) which talks to the IdP.

You can find more information on wicked.haufe.io here:

Usage

To install the SDK into your node.js application, run

$ npm install wicked-saml --save --save-exact

Please note that you will also need to inject the wicked-sdk when initializing the wicked-saml SDK; check out the wicked-sdk NPM package for more information: npmjs.com/packages/wicked-sdk.

The SDK will be kept downwards-compatible for as long as possible; it will be tried hard to make earlier versions of the SDK compatible with a later release of wicked.haufe.io, so using the --save-exact is a safe bet.

Prerequisites

The wicked-saml package is intended for use with ExpressJS 4.x and wicked.haufe.io. It is not intended for other types of usage.

You SAML IdP needs to support the HTTP-POST-Binding and Encrypted Assertions. In some cases (like OpenAM), assertion encryption has to be explicitly turned on and cannot be part of the metadata.xml. If you receive errors static that EncryptedAssertion lengths is zero but was expected to be one, this may be the mistake.

Example

var wicked = require('wicked-sdk');
var wickedSaml = require('wicked-saml')
var async = require('async'); // another requirement for this sample

async.series([
    callback => wicked.initialize(callback),
    callback => wickedSaml.initialize(wicked, 'your-server-id', callback)
], function (err) {
    if (err)
        throw err; // or do whatever you need
    
    // start server
});

The single most interesting point is the string 'your-server-id' in the above example. It relies on an an Authorization Server being registered in your wicked configuration. The wicked-saml will do the following thing:

  • Retrieve the /auth-server/your-server-id information from the API (that's what the wicked-sdkis needed for)
  • Read out the information in the saml properties and use that to initialize the implementation of the SAML Service Provider with that

More information can be found in the wicked documentation under Authorization Servers.

A sample your-server-id.json file could look like this:

{
  "name": "your-server-id",
  "id": "your-server-id",
  "auth": "none",
  "desc": "Authorization Server for SAML Federation",
  "url": "https://${PORTAL_NETWORK_APIHOST}/auth-server/{{apiId}}?client_id=(your app's client id)",
  "config": {
    "api": {
      "upstream_url": "http://auth-server:3005",
      "request_path": "/auth-server"
    },
    "plugins": [
      {
        "config": {
          "header_name": "Correlation-Id",
          "generator": "uuid"
        },
        "name": "correlation-id"
      }
    ]
  },
  "saml": {
    "spOptions": {
      "entity_id": "https://api.company.com/auth-server/metadata.xml",
      "assert_endpoint": "https://api.company.com/auth-server/assert",
      "nameid_format": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient",
      "certificate": "-----BEGIN CERTIFICATE-----\nMIIDIDCCA...awot98FReb\n-----END CERTIFICATE-----",
      "private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBA...Vy4HpO2KPg==\n-----END RSA PRIVATE KEY-----"
    },
    "idpOptions": {
      "sso_login_url": "https://your-saml-idp.com:443/auth/SSORedirect/metaAlias/idp1",
      "certificates": [
        "-----BEGIN CERTIFICATE-----\nMIICrTCC...g8t2tGs=\n-----END CERTIFICATE-----"
      ]
    }
  }
}

The options which can be used can be found in the documentation of saml2-js, which is the library which is used "under the hood" of the wicked-saml package: npmjs.com/package/saml2-js.

The example properties above (when correctly filled) will work with e.g. OpenAM.

Library description

The following functions are exported by wicked-saml.

wickedSaml.initialize(wicked, serverId, callback)

Initialize the SAML library; calls the wicked API to retrieve information on the Authorization Server registration of the wicked configuration (see above). The serverId has to match the auth-server definition ID.

Pass your wicked-sdk instance to the library here; wicked-saml will use its apiGet function.

Callback signature: function(err) -- Does not return anything but an error, or null if successful.

wickedSaml.metadata()

Returns a function which can be used directly as the metadata.xml end point, when using express.

app.get('/auth-server/metadata.xml', wickedSaml.metadata());

wickedSaml.login(callback)

Create a request identifier and login URL for redirecting to the SAML IdP.

Example:

// Assume /auth-server/:apiId?client_id=3498wzio4e57648576348756345
app.get('/auth-server/:apiId', function (req, res, next) {
    req.session.apiId = req.params.apiId;
    req.session.clientId = req.query.client_id;
    wickedSaml.login(function (err, loginInfo) {
        if (err)
            return next(err);
        req.session.requestId = loginInfo.requestId;
        res.redirect(loginInfo.loginUrl);
    });
});

Note that there is a bunch of validity checking and security measures missing in the above code.

Callback signature': function(err, loginInfo), whereas loginInfo:

loginInfo = {
    loginUrl: 'https://...../idp1',
    requestId: '7hf5irutzerwiutzhw384765h8w47658w4f'
}

Use the loginUrl to redirect to the IdP and store the requestId in your session for checking when you get called back in /assert.

wickedSaml.assert(req, requestId, callback)

Use this function to decrypt a SAML assertion. Call this from the /assert end point you specified in your configuration (spOptions.assert_endpoint):

app.post('/auth-server/assert', function (req, res, next) {
    const requestId = req.session.requestId;
    wickedSaml.assert(req, requestId, function (err, userInfo, samlResponse) {
        if (err)
            return next(err); // More elaborate error handling if needed

        // userInfo will contain "authenticated_userid" property (most of the time)
        // If you need other things, use getAttributeValue() to retrieve from
        // the samlResponse:
        userInfo.authenticated_userid = wickedSaml.getAttributeValue(samlResponse, 'our_company_id');

        // Fill in the other values for use with the Kong Adapter, stored
        // in session (see login())
        userInfo.api_id = req.session.apiId;
        userInfo.client_id = req.session.clientId;

        // In case you need to do some authorization step (this is only authentication),
        // this is the place to do that, e.g. check for licenses for the authenticated
        // user, which could be passed on as OAuth2 scopes:
        userInfo.scope = ['some_scope', 'other_scope'];

        wicked.getRedirectUriWithAccessToken(userInfo, function (err, redirect) {
            if (err)
                return next(err);
            // Yay, done! Redirect back to web app
            res.redirect(redirect.redirect_uri);
        })
    });
});

Callback signature: function(err, userInfo, samlResponse)

The userInfo looks as follows:

userInfo = {
    authenticated_userid: "some-id-we-found"
}

wickedSaml.assert will try to extract these two values (as needed for getRedirectUriWithAccessToken), but cannot guarantee it will work out. Additionally, if you have multiple fields in your SAML response which ends with id, any one will be picked. So it's recommended that you explicitly set those values manually using the samlResponse and the getAttributeValue() function (see below).

wickedSaml.getAttributeNames(samlResponse)

Lists all attribute names of the user tag of the given SAML response (samlRespose). Takes the SAML response from assert() as an argument and returns a string array.

Note: The attribute names will be converted to lower case.

wickedSaml.getAttributeValue(samlResponse, wantedAttribute)

Retrieve the value of an attribute in the samlResponse. The wantedAttribute parameter is not case-sensitive. If the attribute cannot be found, null is returned.

wickedSaml.getConfig()

Returns the configuration object the SAML SDK retrieved from the wicked API (e.g., the auth-saml.json settings from the auth-servers configuration of your API portal).

wickedSaml.getLogoutResponseUrl(inResponseTo, relayState, callback)

To be written.