Suppress MFA for a period of time
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.
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
Empty Event - SuppressMFA
Add a new Branch Rule named 'Yes' and add the following expression:
expr { [mcget {session.custom.suppressmfa.skip}] == 1 }
Variable Assign - Set Cookie
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- Stanislas_Piro2Cumulonimbus
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
- Slayer001Cirrus
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
- Slayer001Cirrus
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.
- Slayer001Cirrus
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 } }
- Slayer001Cirrus
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 } } } } } }
- wbrowneAltostratus
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 } }