on 16-Jul-2019 05:52
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.
Add a new Branch Rule named 'Yes' and add the following expression:
expr { [mcget {session.custom.suppressmfa.skip}] == 1 }
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.0you 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
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
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.
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
}
}
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
}
}
}
}
}
}
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.
Niels 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
}
}
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
}
}
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.