APM Full Step Up Authentication

Problem this snippet solves:

By default, APM is not able to handle several authentication during a session. Once you are logged in, it’s finished, you can’t ask for authentication again.

Since v12.1.0, we can see a new feature in EA called “Step-up Authentication” and the introduction of subroutines that is currently limited to ldap authentication or a confirm box.

The irule and configuration below allow the administrator to define 2 levels of authentication based on URIs. The concept can be extended to have multiple authentication levels.

This concept can be extended to define several Level of authentication. You can also change the element that trigger the additionnal authentication process.

How to use this snippet:

Installation

irule

To make it works, you need to install the irule on the Virtual Server that publish your application with APM authentication.

datagroup

You need to create a datagroup of string type. This dg must contains http path that need an additional authentication step. The dg is named loa3_uri in the irule example.

access profile

If you already have an existing access profile, you will need to modify it and include some additionnal configuration in your VPE. If you have no access profile, you can starts building your own based on the description we provide below.

Scenarios

1) User try to reach strong uri after first authentication process

In this scenario, the user first authenticate using a standard authentication mecanism. Once authenticated, if the user request content that is behing strong uris, the user restart an authentication process in the "Strong Auth" and "Already Auth" branch of the VPE.

2) User try to reach strong uri during the first authentication process

If the user try to access a strong uri on its first attempt, he will need to complete the full authentication process. Then, he can access every part of the web application without any additional prompt.

Special considerations

Client certificate Authentication

You may need to use Client certificate authentication as a primary factor or second factor. We highly recommend to use "SSl on-demand authentication" if you need it as primary factor. Client Certificate is not supported as a second factor, you need to use SSl on-demand authentication.

WebSSO

When first authentication has already been allowed and the user try to access a protected uri, the system will invite the user to complete the new authentication (second factor). This process will restart a webSSO action on the backend. Basic, NTLM and Kerberos webSSO have been tested with success.

Configuring the Visual Policy Editor

The printscreen below is a minimal Visual Policy Editor used to make Step up Authentication works properly :

Strong Auth

The strong Auth block is an "Empty Action" with two branch.

The branch named "Strong" contains the following condition :

expr { [mcget {session.server.landinguri}] starts_with "/strong" || [mcget {session.custom.last.strong}] == 1 }

We check that the uri starts with strong (used in scenario 1) or if a custom variable is set to 1 (second scenario)

Already Auth

This is an empty action with two branch. The branch named "yes" contains the following expression :

expr { [mcget {session.custom.last.authresult}] contains "true" }

2-factor Ending

session.custom.last.authtype
variable must be set to 1

session.policy.result.redirect.url
must be changed. The
session.server.landinguri
contains the true origin uri.

To set this variable, you must use the tcl script below :

        proc urldecode str {
            variable map
            variable alphanumeric a-zA-Z0-9
            for {set i 0} {$i <= 256} {incr i} {        set c [format %c $i]
                if {![string match \[$alphanumeric\] $c]} {
                    set map($c) %[format %.2x $i]
                }
            }
            array set map { " " + \n %0d%0a }
            set str [string map [list + { } "\\" "\\\\"] $str]
            regsub -all -- {%([A-Fa-f0-9][A-Fa-f0-9])} $str {\\u00\1} str
            return [subst -novar -nocommand $str]
        }
        set decoded_uri [urldecode [string range [mcget {session.server.landinguri}] [expr { [string last = [mcget {session.server.landinguri}]] + 1 }] end]]
        return $decoded_uri

Full strong Ending

session.custom.last.authtype
variable must be set to 1

Standard Ending

session.custom.last.authtype
variable must be set to 0

Session variables

The following variables can be used in the 2-factor section of the Visual Policy Editor :

  • session.custom.last.username
  • session.custom.last.password

Features

  • 2-step authentication
  • Retrieve username and password from first authentication
  • Encrypt Session1 cookie to avoid session Hijacking

External links

Github : https://github.com/e-XpertSolutions/f5

Code :

when RULE_INIT {
# to be changed prior to any publishing
set passphrase "hEuoYjmFUpB4PcpO3bUdQtLP4ic7jjm"
}

when HTTP_REQUEST  {
if { [HTTP::cookie exists MRHSession] and [ACCESS::session exists -state_allow -sid [HTTP::cookie MRHSession]] } {
set strong_auth [ACCESS::session data get session.custom.last.authtype]
if { [class match [HTTP::path] starts_with loa3_uri] and $strong_auth == 0 } {
HTTP::cookie encrypt "MRHSession" $passphrase
HTTP::respond 302 noserver "Location" "/strong?return_url=[URI::encode [HTTP::uri]]" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "MRHSession=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "LastMRH_Session=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "Session1=[HTTP::cookie MRHSession];path=/"
}
}
}

when ACCESS_SESSION_STARTED {

# decrypt Session1 cookie value
set decrypted [HTTP::cookie decrypt "Session1" $passphrase]
    
if { [HTTP::cookie exists Session1] and [ACCESS::session exists -state_allow -sid $decrypted] } {

## section : retrieve session variables from the first session

ACCESS::session data set session.custom.last.username [ACCESS::session data get session.logon.last.username -sid $decrypted]
ACCESS::session data set session.custom.last.password [ACCESS::session data get session.logon.last.password -sid $decrypted]

## End section

ACCESS::session data set session.custom.last.authresult "true"

# remove the first created session during standard authentication to avoid multiple active sessions
ACCESS::session remove -sid $decrypted

} elseif { [class match [HTTP::path] starts_with loa3_uri] } {
ACCESS::session data set session.custom.last.strong 1
}
}

Tested this on version:

11.5
Updated Jun 06, 2023
Version 2.0
  • if you add "programmability contest" as a tag, you'll be entered into our Codeshare Challenge contest we're running this month!
  • Very well done, congratulations! Does someone have an example to implement the above but with SAML, preferred with Auth Context Class which is in the SAML 2.0 Standard?
  • Hi Yann, thanks for sharing this! I'am trying to implement this on a 12.1.1 HF1 platform and I ran into some problems. The session cookies didn't get deleted when the browser was redirected to /strong. So it ran into a loop. I had to alter the irule and add the domain setting to the cookies.

    HTTP::respond 302 noserver "Location" "/strong?return_url=[URI::encode [HTTP::uri]]" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "MRHSession=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT; domain=example.com;path=/" Set-Cookie "LastMRH_Session=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT; domain=example.com;path=/" Set-Cookie "Session1=[HTTP::cookie MRHSession]; domain=example.com;path=/"
    

    Thanks again for sharing this. Best regards, Niels

  • Hi Niels,

     

    Thank you for your comment. The behavior you get is basically independant from the BIG-IP version. In fact, when you specify a domain or host in the Multi-Domain SSO tab in your access policy, the BIG-IP generate MRHSession cookies including the domain you specified. In that case, you effectively need to change the line of code you highlight.

     

    Thank you for your remark, I will add a note on that in the main post.

     

    Best Regards

     

    Yann

     

  • Hi Yann,

    Thanks for clarifying that. I found another thing in the iRule that doesn't seem to work for me. It has to do with the password setting between the old and the new session. This rule (line 26):

    ACCESS::session data set session.custom.last.password [ACCESS::session data get session.logon.last.password -sid $decrypted]
    

    The APM logon password variable isn't accessible from an iRule. I had to implement a workaround as described here:

    Best regards,

    Niels

  • Hi Yann,

    Nice Job! I found another way for step-up authentication using ACL instead of Datagroup.

    when CLIENT_ACCEPTED {
         retreive ACCESS Profile cookie settings to restore in cookie changes.
        if {[PROFILE::access domain_mode] equals 0 } {
            if { [set cookieproperties ";domain=[PROFILE::access domain_cookie]"] equals ";domain="} {set cookieproperties ""}
            if { [PROFILE::access secure_cookie]} {append cookieproperties ";secure"}
            if { [PROFILE::access httponly_cookie]} {append cookieproperties ";httponly"}
        }
    }
    
    when HTTP_REQUEST  {
         If user fails to authenticate in strong auth, restore the previous session cookie.
    if { [HTTP::cookie exists AuthRollBack] & [HTTP::cookie exists Session1] } {
            HTTP::respond 302 noserver "Location" "[HTTP::uri]" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "MRHSession=[HTTP::cookie Session1]$cookieproperties;path=/" Set-Cookie "LastMRH_Session=deleted$cookieproperties;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "Session1=deleted$cookieproperties;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "AuthRollBack=deleted;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/"
        }
        set referer [HTTP::header value Referer]
    }
    when ACCESS_ACL_DENIED {
          If User is denied by ACL and authnetication level is not strong, force to create a new session for strong authentication.
        if {[ACCESS::session data get session.custom.last.strong] == 0 } {
            ACCESS::respond 302 noserver "Location" "[HTTP::uri]" "Cache-Control" "no-cache, must-revalidate" Set-Cookie "MRHSession=deleted$cookieproperties;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "LastMRH_Session=deleted$cookieproperties;expires=Thu, 01-Jan-1970 00:00:10 GMT;path=/" Set-Cookie "Session1=[HTTP::cookie MRHSession]$cookieproperties;path=/"
            ACCESS::session data set session.custom.last.Referer $referer
        }
    }
    
    when ACCESS_SESSION_STARTED {
        If new session and previous session is store in a cookie, prepare policy for strong authentication
        if { [HTTP::cookie exists Session1] and [ACCESS::session exists -state_allow -sid [set sessionid [HTTP::cookie value "Session1"]]] } {
             section : retrieve session variables from the first session
    
            ACCESS::session data set session.custom.last.username [ACCESS::session data get session.logon.last.username -sid $sessionid]
            ACCESS::session data set session.custom.last.password [ACCESS::session data get session.logon.last.password -sid $sessionid]
    
             End section
    
             store the first created session during standard authentication to avoid multiple active sessions
            ACCESS::session data set session.custom.last.session $sessionid
            ACCESS::session data set session.custom.last.strong 1
        } else {
            ACCESS::session data set session.custom.last.strong 0
        }
    }
    
    when ACCESS_POLICY_COMPLETED {
         If Strong authentication process
        if {[ACCESS::session data get session.custom.last.strong]} {
            if { ([ACCESS::policy result] equals "deny") } {
                 restore previous session if the user failed to authenticate.
                ACCESS::session remove
                ACCESS::respond 302 noserver "Location" [ACCESS::session data get session.custom.last.Referer -sid [ACCESS::session data get session.custom.last.session]] "Cache-Control" "no-cache, must-revalidate" Set-Cookie "AuthRollBack=1;path=/"
            } else {
                 Remove the first created session during standard authentication to avoid multiple active sessions
                ACCESS::session remove -sid [ACCESS::session data get session.custom.last.session]
            }
        }
    }
    

    The VPE is quite simple (I removed authentication boxes to test... it is simpler)!

    In Strong Auth, I set the branch expression

    expr { [mcget {session.custom.last.strong}] == 1 }

    With this solution, you can assign different ACL based on AD group and manage step-up per user and not a global URL list.

    If a user fails to authenticate on strong authentication, he is redirected to the referer stored in a session variable of the previous session.

    This irule also manage the access profile cookie parameters (Secure, httponly, domain).

    I removed the cookie encryption as F5 does not encrypt the default session cookie and I preserve cookie security parameters for session1 cookie.

    This solution does not manage 2 way to authenticate (standard + strong / standard then strong) but always the same process : standard then strong.

  • do you have the configuration in text mode ? it's not really clear for me what is configured on the boxes "set redirect url"

     

    regards,