CodeShare
Have some code. Share some code.
cancel
Showing results for 
Search instead for 
Did you mean: 
Custom Alert Banner

Problem this snippet solves:

This code snippet can be used if you want to suppress MFA for a period of time. This solution uses an encrypted persistent cookie, that will be set at a successful MFA logon. Upon subsequent logons the browser will send the persistent cookie (when not expired) and the cookie will be validated. When the cookie is valid, the 2nd authentication factor will be skipped.

How to use this snippet:

Create an Access Policy which uses MFA. For example see the following picture.

0151T000003kW39QAE.png

Note the following VPE agents in the example above: 'iRule Event - Check Cookie', 'Suppress MFA' and 'Variable Assign - Set Cookie'. These events need to be in place to cooperate with the iRule in this code snippet.


iRule Event - Check Cookie


0151T000003kW3EQAU.png


Empty Event - SuppressMFA


Add a new Branch Rule named 'Yes' and add the following expression:


expr { [mcget {session.custom.suppressmfa.skip}] == 1 }


0151T000003kW3OQAU.png


Variable Assign - Set Cookie


0151T000003kW3YQAU.png

Code :

when RULE_INIT {
    # change passphrase below before any publishing
    # set seconds after which the peristent cookie expires
    array set static::suppress_mfa {
        passphrase "hEuoYjmFUpB4PcpO3bUdQtLP4ic7jjm"
        cookie "SuppressMFA"
        seconds 86400
    }
}

when ACCESS_SESSION_STARTED {
    # store hash from cookie in APM variable
    if { [HTTP::cookie exists $static::suppress_mfa(cookie)] } {
        set hash [HTTP::cookie decrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)]
        ACCESS::session data set session.custom.suppressmfa.hash $hash
    }
}

when ACCESS_POLICY_COMPLETED {
    # if cookie should be set, create hash and store it into a APM variable
    if { [ACCESS::session data get session.custom.suppressmfa.setcookie] == 1 } {
        set username [ACCESS::session data get session.logon.last.username]
        set UA [ACCESS::session data get session.user.agent]
        set hash [b64encode [md5 "c:$username:$UA"]]
        ACCESS::session data set session.custom.suppressmfa.hash $hash
    }
}

when ACCESS_POLICY_AGENT_EVENT {
    # check if hash from cookie matches current session hash (username and user-agent)
    switch [ACCESS::policy agent_id] {
        "checkcookie" {
            set username [ACCESS::session data get session.logon.last.username]
            set UA [ACCESS::session data get session.user.agent]
            set hash [b64encode [md5 "c:$username:$UA"]]
            if { $hash equals [ACCESS::session data get session.custom.suppressmfa.hash] } {
                ACCESS::session data set session.custom.suppressmfa.skip 1
            }
        }
    }
}

when HTTP_RESPONSE {
    # if cookie should be set, insert an encrypted cookie containing the hash (username and user-agent)
    if { [ACCESS::session data get session.custom.suppressmfa.setcookie] == 1 } {
        HTTP::cookie insert name $static::suppress_mfa(cookie) value [ACCESS::session data get session.custom.suppressmfa.hash]
        HTTP::cookie expires $static::suppress_mfa(cookie) $static::suppress_mfa(seconds) relative
        HTTP::cookie encrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)
        HTTP::cookie path $static::suppress_mfa(cookie) "/"
        HTTP::cookie secure $static::suppress_mfa(cookie) enable
        ACCESS::session data set session.custom.suppressmfa.setcookie 0
    }
}

Tested this on version:

13.0
Comments
Stanislas_Piro2
Cumulonimbus
Cumulonimbus

you may merge HTTP_REQUEST and ACCESS_SESSION_STARTED events in ACCESS_SESSION_STARTED.

ACCESS_SESSION_STARTED raise just after HTTP_REQUEST if access session is found... this is just before HTTP redirect to /my.policy... you can use HTTP::cookie commands like in HTTP_REQUEST event.

when ACCESS_SESSION_STARTED {
    # store hash from cookie in APM variable
    if { [HTTP::cookie exists $static::suppress_mfa(cookie)] } {
	        ACCESS::session data set session.custom.suppressmfa.hash [HTTP::cookie decrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)]
        }
}

Instead of Irule Event, did you try this branch expression?

expr {[b64encode [md5 "c:[mcget session.logon.last.username]:[mcget session.user.agent]"]] == [mcget session.custom.suppressmfa.hash]}

I can't test now if it works... but I guess both b64encode and md5 commands works in VPE expressions

Hi Stanislas,

 

Thanks! I seems the wiki page on HTTP::cookie should get updated. The ACCESS_SESSION_STARTED event is not listed as a valid event. I've updated the code snippet. I tried the b64encode and md5 in the branch expression, but these commands are not allowed. One must ask if it is really a huge benefit to put a hashed combination of the username and User-Agent in the cookie. It seems that just putting the username in the encrypted cookie should be sufficient. Then just the VPE expression could be used.

 

Kind regards,

 

--Niels

Slayer001
Cirrus
Cirrus

Hi Niels,

 

I tried this iRule in our setup but it seems the HTTP_RESPONSE event is not hit when going to just the APM webtop after completing the VPE policy? When setting logging in this event it never gets triggered. I don't see the cookie appearing in the list of cookies.

 

Tried with HTTP_RESPONSE_RELEASE but that's already triggered when sending the session cookies back to the client which is to early.

Is there any other event that could be used?

Hi Slayer001,

 

Ah, yes I've seen that behaviour before when using SAML. You could probably workaround this by using an extra virtual server, like I explained here: https://devcentral.f5.com/s/articles/surfconext-second-factor-only-sfo-authentication-1012

 

Also see my comment on why using the extra virtual server:

 

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.

 

Kind regards,

 

--Niels

 

 

Slayer001
Cirrus
Cirrus

Thanks for the swift reply. Is the 2nd virtual server (frontend) an standard VS that serves as entry point and has the actual APM VS as a node?

What I tend to do is assign the same IP adress to both the front- and backend server. The frontend server is assigned to a VLAN (enabled on). And the backend server that is holding the Access Policy isn't enabled on any VLAN.

Slayer001
Cirrus
Cirrus

When I do that my virtual server is reachable on 443 but the access policy is not triggered? I just get a failed request to /my.policy

So what does the iRule on the frontend vs looks like? Something like this:

when RULE_INIT {
    # change passphrase below before any publishing
    # set seconds after which the peristent cookie expires
    array set static::suppress_mfa {
        passphrase "hEuoYjmFUpB4PcpO3bUdQtLP4ic7jjm"
        cookie "SuppressMFA"
        seconds 86400
    }
}
 
when HTTP_REQUEST {
    virtual vs_backend
}
 
when HTTP_RESPONSE {
    # if cookie should be set, insert an encrypted cookie containing the hash (username and user-agent)
    if { [ACCESS::session data get session.custom.suppressmfa.setcookie] == 1 } {
        HTTP::cookie insert name $static::suppress_mfa(cookie) value [ACCESS::session data get session.custom.suppressmfa.hash]
        HTTP::cookie expires $static::suppress_mfa(cookie) $static::suppress_mfa(seconds) relative
        HTTP::cookie encrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)
        HTTP::cookie path $static::suppress_mfa(cookie) "/"
        HTTP::cookie secure $static::suppress_mfa(cookie) enable
        ACCESS::session data set session.custom.suppressmfa.setcookie 0
    }
}
Slayer001
Cirrus
Cirrus

Got it to work, thanks for your help Niels.

Als added a timestamp in the cookie to avoid tampering with the expiration time of the cookie

This is what I have for the checkcookie event now:

when ACCESS_POLICY_AGENT_EVENT {
    # check if hash from cookie matches current session hash (username and user-agent)
    switch [ACCESS::policy agent_id] {
        "checkcookie" {
            set username [ACCESS::session data get session.logon.last.username]
            set UA [ACCESS::session data get session.user.agent]
            set hash [b64encode [md5 "c:$username:$UA"]]
            set currenttime [clock seconds]
            set starttime [string range [ACCESS::session data get session.custom.suppressmfa.hash] end-9 end]
            if {$starttime equals ""}{
            }
            else {
                set start_int $starttime
                set diff_int $static::suppress_mfa(seconds)
                set endtime [expr {$start_int + $diff_int}]
                #log local0. "endtime: $endtime"
                if { $currenttime <= $endtime } {
                    if { $hash equals [string range [ACCESS::session data get session.custom.suppressmfa.hash] 0 end-10] } {
                        ACCESS::session data set session.custom.suppressmfa.skip 1
                    }
                }
            }
        }
    }
}
wbrowne
Altostratus
Altostratus

I had this working perfectly until the 14.1.4 upgrade. When I got f5 support involve they said it is because of the way the new version treats the http::respond. The cookie will create one time for the first person who logs in. However no one else will get the cookie. When I put logging in on the when HTTP_RESPONSE it seems like the f5 is responding correctly with and creating the cookie for that first user because I see this in the log

<HTTP_RESPONSE>: Server: Apache

but then every time after the actual IIS web server of the pool member seems to be responding

HTTP Request Headers: Server: Microsoft-IIS/10.0

and then it looks like it responds twice. They suggested I needed an HTTP::close. That did seem to help for the first user as when the cookie expired and after the next login the cookie was created for that user. But only if he was using the same browser. I also noticed I can repeat the process if I force the active to standby and try again. The first person to login will get the cookie but no on else. I attached 2 examples of the logs created. Is anyone else on version 14.1.4 and experiencing this issue?

when HTTP_RESPONSE {
    # if table shoud be set then take record of the ClientIP and set encrytped cookie
   if { [ACCESS::session data get session.custom.suppressmfa.setauthtable] == 1 } {
		table set tab_amia:[IP::client_addr] Authed $static::suppress_mfa(seconds)
}
		HTTP::cookie insert name $static::suppress_mfa(cookie) value $static::suppress_mfa(value) path "/"
			if {$static::AMIADEV_Cookie_debug } {log local0. "cookie $static::suppress_mfa(cookie) set for $static::suppress_mfa(seconds)"}
		HTTP::cookie expires $static::suppress_mfa(cookie) $static::suppress_mfa(seconds) relative
			if {$static::AMIADEV_Cookie_debug } {log local0. "cookie expires in $static::suppress_mfa(seconds)"}
		HTTP::cookie secure $static::suppress_mfa(cookie) enable
	        HTTP::cookie httponly $static::suppress_mfa(cookie) enable
	        HTTP::cookie encrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)
		HTTP::header "Cache-Control" "max-age=$static::suppress_mfa(seconds)"
		HTTP::close
		foreach aHeader [HTTP::header names] {
			if {$static::AMIADEV_Cookie_debug } {log local0. "HTTP Request Headers: $aHeader: [HTTP::header value $aHeader]"}}
		ACCESS::session data set session.custom.suppressmfa.setauthtable 0
		}
    }	

I just tested the original iRule as shared with the community and it seems to be working fine in 14.1.4. One thing I notice in your version is that you removed the conditional 'set cookie' from the HTTP_RESPONSE part. The original version only sets a cookie when an user has successfully performed the MFA. Your version sets the cookie every time the HTTP_RESPONSE is being triggered. This doesn't seem right to me.

wbrowne
Altostratus
Altostratus

0691T00000C2X77QAF.pngNiels thank you for your response.

I have put my entire iRule here. I just entered the HTTP_RESPONSE originally because that is what I am having problems with. I am not sure what you mean by saying I removed the conditional 'set cookie'. I have the

if { [ACCESS::session data get session.custom.suppressmfa.setauthtable] == 1 } . I also have the line in there that sets it to ACCESS::session data set session.custom.suppressmfa.setauthtable 0 after the first response. Really the only thing that I have added was that I am creating a table called tab_amia [IP::client_addr] and added a value "Authed" and added that to the cookie check in the when ACCESS_POLICY_AGENT_EVENT. All this seems to be working accept for the actual cookie creation

when RULE_INIT {
    # set the cookie encryption passphrase
    # set the cookie name
    # set the encrypted value in the cookie
    # set seconds after which the peristent cookie expires
	# To see debug logs set to 1, turn off with 0. Logs can be viewed in /var/log/apm or in the TMUI under System -> Logs -> Local Traffic
	set static::AMIADEV_Cookie_debug 1
    array set static::suppress_mfa {
        passphrase "pw for decryption"
        cookie "AMIA_MFA"
        value "amia"
        seconds "300"
    }
}
when ACCESS_SESSION_STARTED {
    # store hash from cookie in APM variable
    if { [HTTP::cookie exists $static::suppress_mfa(cookie)] } {
		log local0. "amia cookie exists for [IP::client_addr]"
    set hash [HTTP::cookie decrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)]
		if {$static::AMIADEV_Cookie_debug } {log local0. "cookie decrypted $hash"}
    ACCESS::session data set session.custom.suppressmfa.hash $hash
    }
    else {
    table delete tab_amia:[IP::client_addr]
		if {$static::AMIADEV_Cookie_debug } {log local0. "no cookie found"}
		if {$static::AMIADEV_Cookie_debug } {log local0. "cookie name expected $static::suppress_mfa(cookie)"}
    }
}
when ACCESS_POLICY_AGENT_EVENT {
    # check if hash from cookie matches encrypted value
    switch [ACCESS::policy agent_id] {
        "checkauthed" {
			set checked [table lookup tab_amia:[IP::client_addr]] 
            if { [ACCESS::session data get session.custom.suppressmfa.hash] equals $static::suppress_mfa(value) and $checked contains "Authed" } {
                ACCESS::session data set session.custom.suppressmfa.skip 1 
            }
        }
    }
}
when HTTP_RESPONSE {
    # if table shoud be set then take record of the ClientIP and set encrytped cookie
   if { [ACCESS::session data get session.custom.suppressmfa.setauthtable] == 1 } {
		table set tab_amia:[IP::client_addr] Authed $static::suppress_mfa(seconds)
		set taba [table lookup tab_amia:[IP::client_addr]]
			if {$static::AMIADEV_Cookie_debug } {log local0. "$taba"}
		HTTP::cookie insert name $static::suppress_mfa(cookie) value $static::suppress_mfa(value) path "/"
		HTTP::cookie expires $static::suppress_mfa(cookie) $static::suppress_mfa(seconds) relative
		HTTP::cookie secure $static::suppress_mfa(cookie) enable
	    HTTP::cookie httponly $static::suppress_mfa(cookie) enable
	    HTTP::cookie encrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)
		HTTP::header "Cache-Control" "max-age=$static::suppress_mfa(seconds)"
		HTTP::close
		ACCESS::session data set session.custom.suppressmfa.setauthtable 0
		}
    }	
wbrowne
Altostratus
Altostratus

I was able to get this fixed with the help of a colleague. No matter what we tried in the response it always worked for the first person but no one after. We took the when HTTP_RESPONSE out completely and added a when ACCESS_POLICY_COMPLETES.

when ACCESS_POLICY_COMPLETED {
 
    if { [ACCESS::session data get session.custom.suppressmfa.setauthtable] == 1 }{
    	set sessionauth [ACCESS::session data get session.custom.suppressmfa.setauthtable]
			if {$static::AMIADEV_Cookie_debug } {log local0. "AMIA set auth table is $sessionauth"}
		table set tab_amia:[IP::client_addr] Authed $static::suppress_mfa(seconds)
		set taba [table lookup tab_amia:[IP::client_addr]]
			if {$static::AMIADEV_Cookie_debug } {log local0. "$taba"}
        HTTP::cookie insert name $static::suppress_mfa(cookie) value $static::suppress_mfa(value) path "/"
            if {$static::AMIADEV_Cookie_debug } {log local0. "cookie $static::suppress_mfa(cookie) set for $static::suppress_mfa(seconds)"}
        HTTP::cookie expires $static::suppress_mfa(cookie) $static::suppress_mfa(seconds) relative
            if {$static::AMIADEV_Cookie_debug } {log local0. "cookie expires in $static::suppress_mfa(seconds)"}
        HTTP::cookie secure $static::suppress_mfa(cookie) enable
        HTTP::cookie httponly $static::suppress_mfa(cookie) enable
        HTTP::cookie encrypt $static::suppress_mfa(cookie) $static::suppress_mfa(passphrase)
        ACCESS::respond 302 noserver "Location" [ACCESS::session data get session.policy.result.start_uri] "Cache-Control" "no-cache, must-revalidate" Set-Cookie "$static::suppress_mfa(cookie)=[HTTP::cookie $static::suppress_mfa(cookie)];path=/;secure;httponly;Max-age=$static::suppress_mfa(seconds)"
            if {$static::AMIADEV_Cookie_debug } {log local0. "policy completed"}
        foreach aHeader [HTTP::header names] {
			if {$static::AMIADEV_Cookie_debug } {log local0. "$aHeader: [HTTP::header value $aHeader]"}}
        unset sessionauth
		unset taba
        }
 }
Harri
Nimbostratus
Nimbostratus

Totally overkill to play with cookies. Just use session table with the table-command to add username to table when mfa is first successful and inspect the table in the beginning of the policy in irule agent to set up a suppress variable. 

Version history
Last update:
‎16-Jul-2019 05:52
Updated by:
Contributors