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
- must use the urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect binding
- 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)
- must include a RequestedAuthnContext with an AuthnContextClassRef with one of the defined levels.
- 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