SURFsecureID Second Factor Only (SFO) Authentication

Problem this snippet solves:

Second Factor Only authentication allows a SP to authenticate only the second factor of a user. With SFO you can add two factor authentication to your institutions application gateway (e.g. Citrix Netscaler or F5 BIG-IP) or to the authentication or authorization gateway (e.g. Microsoft ADFS or Novell/NetIQ). SFO has its own authentication endpoint at SURFsecureID (SURFconext Strong Authentication has been renamed to SURFsecureID).

More information about SURFsecureID SFO

SAML Request requirements
  1. must use the urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect binding
  2. must be signed using the http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 algorithm. (Note that the HTTP-Redirect binding does not use XML Signatures)
  3. must include a RequestedAuthnContext with an AuthnContextClassRef with one of the defined levels.
  4. must include the SURFconext identifier of the user in the Subject element of the AuthnRequest. (see the description of the AuthnRequest element in the https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf, section 3.4.1, line 2017) as a SAML NameID with Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".

Currently the last requirement (#4) isn't supported within APM (v12.1.1 HF2). But with use of iRules LX a workaround can be build.

iRules workround (version 14.1.0 and higher)

From BIG-IP version 14.1.0 and higher it's possible to manipulate SAML requests and responses with use of SAML related iRule commands and events. These features makes it very simple apply a workaround.

See: https://github.com/nvansluis/f5.surfsecureid

iRules LX workaround (version 14.0.0 and lower)

To work around the current limitations of BIG-IP APM we need build two virtual servers. I'll call them 'SURFconext_SP_frontend' and 'SURFconext_SP_backend'. The 'SURFconext_SP_backend' is the virtual server that is doing the actual authentication. This server holds the Access Policy. The 'SURFconext_SP_frontend' will be the virtual server that modifies the SAML request that is build by the backend virtual server. The 'SURFconext_SP_frontend' virtual server makes use of iRules LX to do this.

Prepare BIG-IP
  • Put private key that is being used by the SP to sign requests in a iFile: surfconext_sp_key.
  • Create workspace: SURFconext_SFO_workspace.
  • Create irule: SURFconext_SFO_irule.
  • Create extension: SURFconext_SFO_extension.
  • Create plugin: SURFconext_SFO_plugin
Install node.js modules
# /var/ilx/workspaces/Common/SURFconext_SFO_workspace/extensions/SURFconext_SFO_extension/node_modules
# npm install pako urlencode string crypto --save
crypto@0.0.3 crypto

string@3.3.3 string

urlencode@1.1.0 urlencode
└── iconv-lite@0.4.15

pako@1.0.4 pako
#

How to use this snippet:

SURFconext_SFO_irule
when HTTP_REQUEST {
    virtual SURFconext_SP_backend
}

when HTTP_RESPONSE {
    set SSOServiceUrl "https://gateway.pilot.stepup.surfconext.nl/second-factor-only/single-sign-on"
    set SURFconextIdentifier "urn:collab:person:some-organisation.example.org"
    set PrivateKey [ifile get "/Common/surfconext_sp_key"]

    if { [HTTP::header value Location] starts_with $SSOServiceUrl } {

        # set SURFconextIdentifier
        set UserID "[ACCESS::session data get session.logon.last.username]"
        set SURFconextIdentifier "${SURFconextIdentifier}:${UserID}"

        # extract SAMLRequest and SigAlg parameters
        set location_url [HTTP::header value Location]
        set SAMLRequest [URI::query $location_url "SAMLRequest"]
        set SigAlg [URI::query $location_url "SigAlg"]

        # create new SAMLRequest with ILX
        set rpc_handle [ILX::init SURFconext_SFO_plugin SURFconext_SFO_extension]
        if {[catch {ILX::call $rpc_handle createNewSAMLRequest $SAMLRequest $SURFconextIdentifier $PrivateKey $SigAlg} newURI]} {
            log local0.error  "Client - [IP::client_addr], ILX failure: $newURI"
            # Send user graceful error message, then exit event
            HTTP::respond 400 content "<html>Error. Failed to create SAML request.</html>"
            return
        }

        # send new SAMLRequest
        HTTP::header replace Location "$SSOServiceUrl?$newURI"
    }
}
ILX: index.js
'use strict';

// Import the Node.js modules
var objF5 = require('f5-nodejs');
var pako = require('pako');
var urlencode = require('urlencode');
var string = require('string');
var crypto = require('crypto');

// Create a new RPC server for listening to TCL iRule calls
var objILX = new objF5.ILXServer();

// Create method
objILX.addMethod('createNewSAMLRequest', function(objArgs, objResponse) {
    var SAMLRequest = objArgs.params()[0];
    var SURFconextIdentifier = objArgs.params()[1];
    var PrivateKey = objArgs.params()[2];
    var SigAlg_encoded = objArgs.params()[3];

    // decode and inflate AuthnRequest
    var SAMLRequest_decoded = new Buffer(urlencode.decode(SAMLRequest), 'base64');
    var SAMLRequest_inflated = pako.inflateRaw(SAMLRequest_decoded, {to:'string'});

    // build Subject element
    var subject = '<saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">' + SURFconextIdentifier + '</saml:NameID></saml:Subject>';

    // rebuild AuthnRequest
    var firstPart = string(SAMLRequest_inflated).between('','<samlp:NameIDPolicy');
    var lastPart = string(SAMLRequest_inflated).between('<samlp:NameIDPolicy');
    var newSAMLRequest = firstPart + subject + "<samlp:NameIDPolicy" + lastPart;

    // deflate and encode modified AuthnRequest
    var newSAMLRequest_deflated = new Buffer(pako.deflateRaw(newSAMLRequest));
    var newSAMLRequest_encoded = urlencode.encode(newSAMLRequest_deflated.toString('base64'));

    // create new signature
    var newURI = "SAMLRequest=" + newSAMLRequest_encoded + "&SigAlg=" + SigAlg_encoded;
    var key = PrivateKey.toString('ascii');
    var sign = crypto.createSign('RSA-SHA256');
    sign.update(newURI);
    var signature = urlencode.encode(sign.sign(key,'base64'));

    // create new URI
    newURI += "&Signature=" + signature;

    // return AuthnRequest to Tcl iRule
    objResponse.reply(newURI);
});

// Start listening for ILX::call events
objILX.listen();

Code :

72843
Updated Jun 06, 2023
Version 2.0
  • Could someone explain the need for two virtuals and how/why the first calls the second? In my case I have built a portal which provides webtop links and portal links to internal apps. I am trying to use RSA for SFO Authentication. The requirements are exactly as described by this snippet which is the inclusion of the user name in the Subject element of the AuthnRequest.

     

    Using the terminology in this article, I believe my existing Portal which has the access policy, performs the primary authentication and ultimately presents the portal links is the backend server. I am not sure exactly how the front-end virtual is supposed to be defined, addressed and called. Why can the iRules not be assigned to the main portal virtual?

     

    Any additional details that could be provided would be helpful. Thanks.

     

    APM 12.1.2

     

  • The frontend virtual server is kind of a wrapper for the virtual server that holds the actual access policy. The reason why this extra virtual server is needed has to do with the internal working of the SAML process that is performed by the access policy. This process will not trigger the HTTP_RESPONSE iRule event, which makes it impossible to intercept and alter the SAML request. However when using this layered virtual server structure, the frontend virtual server that is logically between the backend virtual server and the IDP will trigger the HTTP_RESPONSE iRule event and makes it possible to intercept and alter the SAML request.

     

    I hope this clarifies the need for an extra virtual server.