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

Problem this snippet solves:

Updated version to support Webdav with windows explorer after Nicolas's comment.

APM is a great authentication service but it does it only with forms.

The default behavior is to redirect user to /my.policy to process VPE. this redirect is only supported for GET method.

Sharepoint provide 3 different access types:

  • browsing web site with a browser
  • Editing documents with Office
  • browser folder with webdav client (or editing documents with libreoffice through webdav protocol)

This irule display best authentication method for each of these access types:

  • browsers authenticate with default authentication method (form based authentication)
  • Microsoft office authenticate with Form based authentication (with support of MS-OFBA protocol)
  • Libreoffice and webdav clients authenticate with 401 basic authentication

Form based authentication (browser and Microsoft office) is compatible (validated for one customer) with SAML authentication

Editing documents is managed with a persistent cookie expiring after 5 minutes. to be shared between IE and Office, it requires :

  • cookie is persistent (expiration date instead of deleted at the end of session)
  • web site defined as "trusted sites" in IE.

How to use this snippet:

install this irule and enable it on the VS.

Code :

when RULE_INIT {
    array set static::MSOFBA {
        ReqHeader "X-FORMS_BASED_AUTH_REQUIRED"
        ReqVal "/sp-ofba-form"
        ReturnHeader "X-FORMS_BASED_AUTH_RETURN_URL"
        ReturnVal "/sp-ofba-completed"
        SizeHeader "X-FORMS_BASED_AUTH_DIALOG_SIZE"
        SizeVal "800x600"
    }
   set static::ckname "MRHSession_SP"
   set static::Basic_Realm_Text "SharePoint Authentication"
}

when HTTP_REQUEST {
   set apmsessionid [HTTP::cookie value MRHSession]
   set persist_cookie [HTTP::cookie value $static::ckname]
   set clientless_mode 0
   set form_mode 0
   # Identify User-Agents type
   if {[HTTP::header exists "X-FORMS_BASED_AUTH_ACCEPTED"] && (([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t") || ([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f"))} {
      set clientless_mode 0; set form_mode 1
   } else {
      switch -glob [string tolower [HTTP::header "User-Agent"]] {
        "*microsoft-webdav-miniredir*" { set clientless_mode 1 }
         "*microsoft data access internet publishing provider*" -
         "*office protocol discovery*" -
         "*microsoft office*" -
         "*non-browser*" -
         "msoffice 12*" { set form_mode 1 }
         "*mozilla/4.0 (compatible; ms frontpage*" {
            if { [ string range [getfield [string tolower [HTTP::header "User-Agent"]] "MS FrontPage " 2] 0 1]  > 12 } {
               set form_mode 1
            } else {
               set clientless_mode 1
            }
         }
         "*mozilla*" -
         "*opera*" { set clientless_mode 0 }
         default { set clientless_mode 1
         }
      }
   }
   if { $clientless_mode || $form_mode } {
      if { [HTTP::cookie exists "MRHSession"] } {set apmstatus [ACCESS::session exists -state_allow $apmsessionid]} else {set apmstatus 0}
      if { !($apmstatus) && [HTTP::cookie exists $static::ckname] } {set apmpersiststatus [ACCESS::session exists -state_allow $persist_cookie]} else {set apmpersiststatus 0}
      if { ($apmpersiststatus) && !($apmstatus) } {
         # Add MRHSession cookie for non browser user-agent first request and persistent cookie present
         if { [catch {HTTP::cookie insert name "MRHSession" value $persist_cookie} ] } {log local0. "[IP::client_addr]:[TCP::client_port] : TCL error on HTTP cookie insert MRHSession : URL : [HTTP::host][HTTP::path] - Headers : [HTTP::request]"} else {return}
      }
   } else { return }      
   if { $clientless_mode && !($apmstatus)} {
      if { !([HTTP::header Authorization] == "") } {
         set clientless(insert_mode) 1
         set clientless(username)    [ string tolower [HTTP::username] ]
         set clientless(password)    [HTTP::password]
         binary scan [md5 "$clientless(password)"] H* clientless(hash)
         set user_key "$clientless(username).$clientless(hash)"
         set clientless(cookie_list)             [ ACCESS::user getsid $user_key ]
         if { [ llength $clientless(cookie_list) ] != 0 } {
            set clientless(cookie) [ ACCESS::user getkey [ lindex $clientless(cookie_list) 0 ] ]
            if { $clientless(cookie) != "" } {
               HTTP::cookie insert name MRHSession value $clientless(cookie)
               set clientless(insert_mode) 0
            }
         }
         if { $clientless(insert_mode) } {
            HTTP::header insert "clientless-mode" 1
            HTTP::header insert "username" $clientless(username)
            HTTP::header insert "password" $clientless(password)
        }
        unset clientless
      } else {
        HTTP::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close
        return
      }
   } elseif {$form_mode && !($apmstatus) && !([HTTP::path] equals $static::MSOFBA(ReqVal))}{
      HTTP::respond 403 -version "1.1" noserver \
         $static::MSOFBA(ReqHeader) "https://[HTTP::host]$static::MSOFBA(ReqVal)" \
         $static::MSOFBA(ReturnHeader) "https://[HTTP::host]$static::MSOFBA(ReturnVal)" \
         $static::MSOFBA(SizeHeader) $static::MSOFBA(SizeVal) \
         "Connection" "Close"
      return
   }
}


when HTTP_RESPONSE {
   # Insert persistent cookie for html content type and private session
   if { [HTTP::header "Content-Type" ] contains "text/html" } {
      HTTP::cookie remove $static::ckname
      HTTP::cookie insert name $static::ckname value $apmsessionid path "/"
      HTTP::cookie expires $static::ckname 120 relative
      HTTP::cookie secure $static::ckname enable
   }
   # Insert session cookie if session was recovered from persistent cookie
   if { ([info exists "apmpersiststatus"]) && ($apmpersiststatus) } {
         HTTP::cookie insert name MRHSession value $persist_cookie path "/"
         HTTP::cookie secure MRHSession enable
   }
}

when ACCESS_SESSION_STARTED {
   if {([info exists "clientless_mode"])} {
      ACCESS::session data set session.clientless $clientless_mode
   }
   if { [ info exists user_key ] } {
      ACCESS::session data set "session.user.uuid" $user_key
   }
}

when ACCESS_POLICY_COMPLETED {
   if { ([info exists "clientless_mode"]) && ($clientless_mode) && ([ACCESS::policy result] equals "deny") } {
      ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=$static::Basic_Realm_Text" Connection close
      ACCESS::session remove
   } 
}

when ACCESS_ACL_ALLOWED {
   switch -glob [string tolower [HTTP::path]] {
      "/sp-ofba-form" {
         ACCESS::respond 302 noserver Location "https://[HTTP::host]$static::MSOFBA(ReturnVal)"
      }
      "/sp-ofba-completed" {
         ACCESS::respond 200 content {
            
               Authenticated
               Good Work, you are Authenticated
            
         } noserver
      }
      "*/signout.aspx" {
         # Disconnect session and redirect to APM logout Page
         ACCESS::respond 302 noserver Location "/vdesk/hangup.php3"
         return
      }
      "/_layouts/accessdenied.aspx" {
         # Disconnect session and redirect to APM Logon Page
         if {[string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } {
            ACCESS::session remove
            ACCESS::respond 302 noserver Location "/"
            return
         }
      }
      default {
         # No Actions
      }
   }
}

Tested this on version:

11.5
Comments
timon74
Nimbostratus
Nimbostratus
Perfect for my needs. thanks for your help
timon74
Nimbostratus
Nimbostratus
I reduce the persistent cookie expiration time to 20 seconds. It permits to increase security in my case. Thanks for this really efficient Irule.
rouanon_150376
Nimbostratus
Nimbostratus
Hi. Just to make sure : Would this solve the issue of editing office documents through APM (with a rewrite profile) ? I'm trying to publish Sharepoint with APM and everything seems to be working except that editing part. Many thanks
Nicolas_COLLET
Nimbostratus
Nimbostratus
Hi, we need to adapt / correct this irule for managed network drive because Windows network drive used user-agent like this : Microsoft-WebDAV-MiniRedir And it's doesn't manage authentification with form mode. So it's just need to change this : Before line 24 : "*microsoft-webdav-miniredir*" - After line 22 : "*microsoft-webdav-miniredir*" { set clientless_mode 1 } After operate this modification, Network drive it's ok. Best regards
Kai_Wilke
MVP
MVP

Hi Stanislas,

 

Thanks for sharing this iRule to Devcentral.

 

I'm even wondering why this iRule hasn't won the 2016 coding challenge. The ability to support MSOFBA is so much required for every SharePoint and esp. these WS-Federation based deployments. O well, but a (highly insecure) SCEP based certificate enrollment page for iPhones sounds so much cooler, isn't it? To bad that I have not found the time to vote for you... 😉

 

BTW: Your last code update has broken the formating of the code. It contains now a lot of & and some malformated .

 

Cheers, Kai

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

Thank you for the comment.

I rollback to the previous version. I am not able to upload new content without & and HTML header...

the only difference with the new version is line 113. I forgot to insert quote before and after realm name.

the line must be:

ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Connection close
Andy_from_Sandy
Nimbostratus
Nimbostratus

Thank you for sharing, it has got me a long way to a successful configuration.

 

I have built up a sharepoint installation using SSO with PKI to Kerberos. So the user presents there certificate because of client ssl profile. After client cert inspection and OCSP in access profile a SSO configuration does the Kerberos lookup to pass to SharePoint.

 

Now I have added OWA on a separate VIP which also checks client certificate.

 

The iRule takes care of switching to clientless mode when OWA connects to SharePoint VIP. I have modified it to also check for the OWA request is from a known list. I have modified the access policy to bypass client cert inspection based on above change to iRule. I also found I had to switch to clientless mode when user-agent = microsoft office protocol discovery.

 

I don't think I have any forms based authentication so I have removed that code from the iRule.

 

What I would be interested in is some commentary as to how the iRule works please. Are you able to share the access policy as well please? Thank you.

 

Kai_Wilke
MVP
MVP

Hey Stanislas,

 

I'm currently in the process in writing an iRule for ADFS protected SharePoints. Recycled some parts of your iRule and optimized some parts here and there. Also found some interesting tweaks for native Windows WebDav Client. Will send you the outcome once finished... 😉

 

Cheers, Kai

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

 

This irule is working on VS with following access profiles:

 

  • SAML SP with SAML IdP with fallback to AD Auth for non MSOFBA clients (session.clientless variable condition to choose authentication method)
  • Standard Logon Page / AD Auth
  • Standard Logon Page / AD Auth with Captcha

If you write an irule to improve APM / sharepoint compatibility, please share it.

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

In a native ADFS-based SharePoint collaboration scenario you cannot use AD fallbacks. The SAML user accounts are most likely stored in many decentralized repositories, without a direct trust nor network access to them.

My goal was to get everything working without using the AD fallback at all. Well, I had to skip the support for outdated MS Office products and changed the handling of Microsoft WebDav Clients slightly. In addition I've restructured the code here and there to reduce the complexity and increase the performance...

when CLIENT_ACCEPTED {
    set inject_session_cookie 0
}
when HTTP_REQUEST {
     Check if APM session cookie is present and valid
    if { ( [set sessionid [HTTP::cookie value "MRHSession"]] ne "" ) and
         ( [ACCESS::session exists -state_allow $sessionid] ) } then {
         Allow the successfully pre authenticated request to pass
    } else {
         Check if persistent APM session cookie is present and valid
        if { ( [set sessionid [HTTP::cookie value "MRHSession_SP"]] ne "" ) and
         ( [ACCESS::session exists -state_allow $sessionid] ) } then {
             Restore APM session cookie value
            HTTP::cookie insert name "MRHSession" value $sessionid
            set inject_session_cookie 1 
             Allow the successfully pre authenticated request to pass
        } else {
             Enumerate explicit MS-OFBA authentication capabilities
             Background: https://msdn.microsoft.com/en-us/library/office/cc313069(v=office.12).aspx
            if { ( [HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t" ) or 
                 ( [HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f" ) } then {
                 Explicit MSOFBA support detected. 
                set authschema "ms-ofba"
            } else {
                 Enumerate implicit MS-OFBA authentication capabilities
                switch -glob -- [string tolower [HTTP::header "User-Agent"]] "*office protocol discovery*" - \
                  "*microsoft office*" - \
                  "*microsoft data access internet publishing provider*" - \
                  "*non-browser*" - \
                  "msoffice 12*" - \
                  "*microsoft-webdav-miniredir*" - \
                  {*ms frontpage 1[23456789]*} {
                     Implicit MSOFBA support detected.   
                    set authschema "ms-ofba"
                } "*ms frontpage*" {
                     Legacy client detected
                    set authschema "legacy"
                } "*mozilla*" - \
                  "*opera*" {
                     Regular web browser detected.  
                    set authschema "browser"
                } default { 
                    set authschema "legacy"
                }
            }
            if { $authschema eq "ms-ofba" } then {
                 Send a MSOFBA compatible Access Denied response 
                if { [HTTP::path] ne "/sp-msofba-form" } then {
                    HTTP::respond 403 -version "1.1" \
                                      content "Access Denied. Make sure that your client is correctly configured. See https://support.microsoft.com/en-us/kb/932118 for further information." \
                                      noserver \
                                      "Content-Type" "text/html" \
                                      "X-FORMS_BASED_AUTH_REQUIRED" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-form" \
                                      "X-FORMS_BASED_AUTH_RETURN_URL" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-completed" \
                                      "X-FORMS_BASED_AUTH_DIALOG_SIZE" "800x600" \
                                      "Set-Cookie" "MRHSession=deleted;path=/;secure" \
                                      "Set-Cookie" "LastMRH_Session=deleted;path=/;secure" \
                                      "Set-Cookie" "MRHSession=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure" \
                                      "Set-Cookie" "LastMRH_Session=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure"
                }
            } elseif { $authschema eq "legacy" } then {
                 Send a regular Access Denied response 
                HTTP::respond 403 content "Access denied. An unsupported client access has been detected."
            } else {
                 Let the regular web browser request pass to the APM policy
            }
        }
    }
}

when ACCESS_SESSION_STARTED {
    if { [HTTP::cookie value "SAML_Realm"] ne "" } then {
        ACCESS::session data set "session.irule.realmcookie" [HTTP::cookie value "SAML_Realm"]
    }
}
when ACCESS_POLICY_COMPLETED {
    if { [set realm_cookie [ACCESS::session data get "session.irule.setrealmcookie"]] ne "" } then {
        ACCESS::respond 302 "Location" "[ACCESS::session data get "session.server.landinguri"]" "Set-Cookie" "SAML_Realm=$realm_cookie;path=/;secure"
    } else {
        ACCESS::respond 302 "Location" "[ACCESS::session data get "session.server.landinguri"]" 
    }
}
when ACCESS_ACL_ALLOWED {
    switch -glob -- [string tolower [HTTP::path]] "/sp-msofba-form" {
         Successfully APM authenticated request MS-OFBA request detected. Redirect to MS-OFBA return URL
        ACCESS::respond 302 noserver Location "/sp-msofba-completed"
    } "/sp-msofba-completed" {
         Successfully APM authenticated request MS-OFBA request detected. Sending MS-OFBA return response
        ACCESS::respond 200 content "AuthenticatedGood Work, you are Authenticated" noserver
    } "*/signout.aspx" {
         SharePoint SignOut signature detected. Disconnect session and redirect to APM logout Page
        ACCESS::respond 302 noserver Location "/vdesk/hangup.php3"
    } "/_layouts/accessdenied.aspx" {
         SharePoint AccessDenied signature detected.
        if { [string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } then {
             SharePoint LoginAsAnotherUser request detected. Killing the APM session an sending redirect to www-root.
            ACCESS::session remove
            ACCESS::respond 302 noserver Location "/"
            return
        }
    } default {
          Let the authenticated request pass
    }
}
when HTTP_RESPONSE {
    if { [HTTP::header "Content-Type" ] contains "text/html" } then {
         Insert persistent APM session cookie into HTTP response.
        HTTP::header insert "Set-Cookie" "MRHSession_SP=$sessionid;path=/;secure"
        HTTP::cookie expires "MRHSession_SP" 120 relative
    }
    if { $inject_session_cookie } then {
         Insert APM session cookie into HTTP response.
        HTTP::header insert "Set-Cookie" "MRHSession=$sessionid;path=/;secure"
        set inject_session_cookie 0
    }
}

Note: Your Clientless_Mode support can be easily included in the provided script. You just need to insert your existing code into the

elseif { $authschema eq "legacy" } then {
script block and add the other iRule events. But be aware that the query to
[ACCESS::user getsid $user_key]
exposes some security risks, since it can be used to bypass active account lockouts.

Update: Changed the session cookie injection mechanism.

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

the use of

[ACCESS::user getsid $user_key]
is from F5 exchange irule. this irule is also used when enabling exchange profile.

this is based on a user key from username and password. if you think it is insecure, we can set a the variable session.max_session_timeout to 1 hour. it will force a new authentication every hour if the user changed his password or if the account is locked. this can be seamless as the authentication header is inserted in every request.

The main reason I filter on MRHSession_SP cookie only for non browser is to secure persistent cookie usage.

When managing persistent cookie for non browser, it will allow only non-browser to recover session cookie from sharepoint cookie. when user close the browser without logout, next user on a shared computer can access previous user session if persistent cookie is not expired.

if you manage the MRHSession_SP cookie for all user agents, the user will be allowed to close the browser, open again and access to sharepoint.

another reason I put ACCESS::session command only for clientless and OFBA clients is to solve performance issues.

ACCESS::session may be used carefully because it generate a pause in execution of irule waiting all other TMM answer about this session. executing this for every requests may cause latency. that's why I filtered first on user-agent, then on session status.

I understand that in my irule, there are static objects I can remove and fix OFBA urls and persist cookie name.

I agree your switch command to filter user-agents is more optimized (with frontage filter version), but I will keep irule architecture to stay generic for every client type.

I will update my irule soon with good points I found in yours.

Kai_Wilke
MVP
MVP

Hi Stanislas,

good point to allow the MRHSession_SP persistent cookie logins only to non-browsers. I've already changed my code to include this additional security mechanism (see below). The performance impact off this change shouldn't be that much, since the

HTTP_RESPONSE
event will inject a MRHSession cookie for the very next request.

I don't have any information that ACCESS::session will park the connection till every other TMM has been contacted. I thought its more like a

[table]
call where just a specific TMM (data owner) will be contacted if needed. SOL12962 does also not explain this behavior...

"Note: When you run the ACCESS::session command, iRule execution on the connection will be suspended until the operation completes only if the session database record is held by another TMM; this situation allows the current TMM to retrieve the data from the other TMM before processing the remainder of the iRule. The ACCESS:: commands are available only if your BIG-IP system is licensed for the BIG-IP APM system."

Do you have additional information on the ACCESS:: connection parking behavior?

when CLIENT_ACCEPTED {
    set inject_session_cookie 0
}
when HTTP_REQUEST {
     Check if APM session cookie is present and valid
    if { ( [set sessionid [HTTP::cookie value "MRHSession"]] ne "" ) and
         ( [ACCESS::session exists -state_allow $sessionid] ) } then {
         Allow the successfully pre authenticated request to pass
    } else {
         Enumerate explicit MS-OFBA authentication capabilities
         Background: https://msdn.microsoft.com/en-us/library/office/cc313069(v=office.12).aspx
        if { ( [HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t" ) or 
             ( [HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f" ) } then {
             Explicit MSOFBA support detected. 
            set authschema "ms-ofba"
        } else {
             Enumerate implicit MS-OFBA authentication capabilities
            switch -glob -- [string tolower [HTTP::header "User-Agent"]] "*office protocol discovery*" - \
              "*microsoft office*" - \
              "*microsoft data access internet publishing provider*" - \
              "*non-browser*" - \
              "msoffice 12*" - \
              "*microsoft-webdav-miniredir*" - \
              {*ms frontpage 1[23456789]*} {
                 Implicit MSOFBA support detected.   
                set authschema "ms-ofba"
            } "*ms frontpage*" {
                 Legacy client detected
                set authschema "legacy"
            } "*mozilla*" - \
              "*opera*" {
                 Regular web browser detected.  
                set authschema "browser"
            } default { 
                set authschema "legacy"
            }
        }
        if { not ( $authschema eq "browser" ) and  
             ( [set sessionid [HTTP::cookie value "MRHSession_SP"]] ne "" ) and
             ( [ACCESS::session exists -state_allow $sessionid] ) } then {
             Restore APM session cookie value
            HTTP::cookie insert name "MRHSession" value $sessionid
            set inject_session_cookie 1 
             Allow the successfully pre authenticated request to pass
        } else {
            if { $authschema eq "ms-ofba" } then {
                 Send a MSOFBA compatible Access Denied response 
                if { [HTTP::path] ne "/sp-msofba-form" } then {
                    HTTP::respond 403 -version "1.1" \
                                      content "Access Denied. Make sure that your client is correctly configured. See https://support.microsoft.com/en-us/kb/932118 for further information." \
                                      noserver \
                                      "Content-Type" "text/html" \
                                      "X-FORMS_BASED_AUTH_REQUIRED" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-form" \
                                      "X-FORMS_BASED_AUTH_RETURN_URL" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-completed" \
                                      "X-FORMS_BASED_AUTH_DIALOG_SIZE" "800x600" \
                                      "Set-Cookie" "MRHSession=deleted;path=/;secure" \
                                      "Set-Cookie" "LastMRH_Session=deleted;path=/;secure" \
                                      "Set-Cookie" "MRHSession=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure" \
                                      "Set-Cookie" "LastMRH_Session=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure"
                    }
            } elseif { $authschema eq "legacy" } then {
                 Send a regular Access Denied response 
                HTTP::respond 403 content "Access denied. An unsupported client access has been detected."
            } else {
                 Let the regular web browser request pass to the APM policy
            }
        }
    }
}
when ACCESS_ACL_ALLOWED {
    switch -glob -- [string tolower [HTTP::path]] "/sp-msofba-form" {
         Successfully APM authenticated request MS-OFBA request detected. Redirect to MS-OFBA return URL
        ACCESS::respond 302 noserver Location "/sp-msofba-completed"
    } "/sp-msofba-completed" {
         Successfully APM authenticated request MS-OFBA request detected. Sending MS-OFBA return response
        ACCESS::respond 200 content "AuthenticatedGood Work, you are Authenticated" noserver
    } "*/signout.aspx" {
         SharePoint SignOut signature detected. Disconnect session and redirect to APM logout Page
        ACCESS::respond 302 noserver Location "/vdesk/hangup.php3"
    } "/_layouts/accessdenied.aspx" {
         SharePoint AccessDenied signature detected.
        if { [string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } then {
             SharePoint LoginAsAnotherUser request detected. Killing the APM session an sending redirect to www-root.
            ACCESS::session remove
            ACCESS::respond 302 noserver Location "/"
            return
        }
    } default {
          Let the authenticated request pass
    }
}
when HTTP_RESPONSE {
    if { [HTTP::header "Content-Type" ] contains "text/html" } then {
         Insert persistent APM session cookie into HTTP response.
        HTTP::header insert "Set-Cookie" "MRHSession_SP=$sessionid;Path=/;Secure;HttpOnly"
        HTTP::cookie expires "MRHSession_SP" 120 relative
    }
    if { $inject_session_cookie } then {
         Insert APM session cookie into HTTP response.
        HTTP::header insert "Set-Cookie" "MRHSession=$sessionid;Path=/;Secure;HttpOnly"
        set inject_session_cookie 0
    }
}

Note: If the F5 exchange irule uses

[ACCESS::user getsid $user_key]
to offload authentication request, then its a even a bigger issue... 😞 The problem is, that you can easily bruteforce AD accounts if the offloading cache is not protected by an account lockout mechanism. You can guess passwords even if the account is already locked out and if the correct password is found you are allowed to enter or at least get a slightly different error message. A max session timeout period of 1 hour wouldn't make it better if SmartPhones polls the mailbox 24/7, isn't it?

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

the use of

[ACCESS::user getsid $user_key]
is not a big issue.

in ACCESS_SESSION_STARTED, the session uuid is changed from default tmm.policyname.logonname to username."md5 of user password"

if { [ info exists user_key ] } {
  ACCESS::session data set "session.user.uuid" $user_key
}

so, when a new request comes with same username and password and without session cookie, APM will not create a new session if a previous one exists with the same username / password.

This is first created for active sync to prevent multiple access session for one user.

in the active sync, there is a option to insert in the hash the client ip address to prevent a user with multiple devices to share the same access session (this is the default behavior for active sync). this can be added in the irule.

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

and I confirm I had some issues about ACCESS::session with more than 30K access session on BIGIP 10250.

 

BIGIP 10250 use 12 TMM process, I had some request dropped when access::session did not receive session status from all tmm.

 

In the first version, I used ACCESS::session to get session status, get session variable (for one customer, MRHSession_SP cookie is only sent if the user check "private computer" in the logon page).

 

I changed the irule to limit ACCESS::session command to optimize the code.

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Kai,

instead of creating a variable with previous user agent, you can check if the variable authschema exists

if { [info exists authschema] } then {
     Do nothing, keep previous request authschema value
} elseif { ( [HTTP::header value "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t" ) or 
    ( [HTTP::header value "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f" ) } then {

    
     Enumerate explicit MS-OFBA authentication capabilities
     Background: https://msdn.microsoft.com/en-us/library/office/cc313069(v=office.12).aspx
    


     Explicit MSOFBA support detected. 
    set authschema "ms-ofba"

} else {

    
     Enumerate implicit MS-OFBA authentication capabilities
     Background: https://msdn.microsoft.com/en-us/library/office/cc313069(v=office.12).aspx
     

    switch -glob -- [string tolower $last_ua_agent] "*office protocol discovery*" - \
      "*microsoft office*" - \
      "*microsoft data access internet publishing provider*" - \
      "*non-browser*" - \
      "msoffice 12*" - \
      "*microsoft-webdav-miniredir*" - \
      {*ms frontpage 1[23456789]*} {

         Implicit MSOFBA support detected.   
        set authschema "ms-ofba"

    } "*ms frontpage*" {

         Legacy client detected
        set authschema "legacy"

    } "*mozilla*" - \
      "*opera*" {

         Regular web browser detected.  
        set authschema "browser"

    } default { 

         Unknown user-agent detected. 
        set authschema "legacy"

    }
}
Kai_Wilke
MVP
MVP

Hi Stanislas,

[info exists]
has a rather poor performance on TCL8.4. With TCL8.5 the performance will become much better. In addition I also tried to cover the scenario, that the UA may change on a single TCP connection. But a good catch that I could combine the two
[if]
's into a single one. This will save some additional cycles... Thanks! 😉

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

I tried in version tcl 8.4 et tcl 8.5, and I confirm

[info exists]
is optimized in version 8.5. but
$last_ua_agent ne [set last_ua_agent XXX]
has also poor performance in tcl 8.4.

$ tclsh8.5
% set ua "Mozilla"
Mozilla
% set authschema "browser"
browser
% time { expr { [info exists authschema] } } 100000
0.32418845 microseconds per iteration
% time { expr { $ua != [ set ua "Mozilla"] } } 100000
0.56738861 microseconds per iteration


$ tclsh8.4
% set ua "Mozilla"
Mozilla
% set authschema "browser"
browser
% time { expr { [info exists authschema] } } 100000
0.98919 microseconds per iteration
% time { expr { $ua != [ set ua "Mozilla"] } } 100000
0.73574 microseconds per iteration

I understand the user agent can change on a single TCP connection, but the client type may not change (As I know, proxy servers does not reuse TCP connection for different client connections).

thank you very much for pointing me which part of this irule I can optimize, and share your experience about irule and APM interoperability.

I hope this feature will be included in future sharepoint iapp and deployment guide.

Next step will be to add Microsoft ADAL support with future version if F5 oauth implementation will be compatible.

Kai_Wilke
MVP
MVP

Hi Stanislas,

No please don't, I have to thank YOU for pointing me in the right direction. Your iRule simply rocks! 🙂

I'm rather unsure if certain SSL-Inspection Proxy may share upstream connections between different internal clients unless "Session-Based-Authentication" support is explicitly requested. At least certain HTTP Proxy will recycle connections. But since HTTP is per RFC a stateless protocol, its always better to prepare for the worst and accept the fact that certain Proxy's may recycle connections, isn't it?

Question: Rumers (e.g. sol36322151) are going around that TMOS v12+ supports TCL 8.5 for iRules. Do you know how to enable TCL8.5? If you have some sparetime left I would be glad if you could open a support call to find out. I don't have a valid support contract in my pocket... 😞

In addition I was able to repro my concerns regarding the mentioned account lockout bypass.

Behavior with APM Profile "Restrict to Single Client IP" Option =

DISABLED

1.) Access the site using a legacy client (aka. Basic Auth)

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - HTTP_REQUEST"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: close
Content-Length: 0

2.) Login as valid user using credential set: test:password1+

HTTP/1.1 302 Redirect
Location: /pages/default.aspx
Server: Microsoft-IIS/8.0
...
Set-Cookie: MRHSession=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly
Set-Cookie: LastMRH_Session=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly

3.) Login from a different host using credential set: test:password1-

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - ACCESS_POLICY_COMPLETED"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: Close
Set-Cookie: LastMRH_Session=e443275f;path=/;secure;HttpOnly
Set-Cookie: MRHSession=7884faddb7f22b02f86b38f4e443275f;path=/;secure;HttpOnly
Content-Length: 0

4.) Repeat step 3.) multiple times to lock the user account in your repository

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - ACCESS_POLICY_COMPLETED"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: Close
Set-Cookie: LastMRH_Session=3c9093b9;path=/;secure;HttpOnly
Set-Cookie: MRHSession=26212f5e9bec540efa77ad963c9093b9;path=/;secure;HttpOnly
Content-Length: 0

5.) Login from a different host using credential set: test:password1+ (aka. you have guessed the correct password!)

HTTP/1.1 302 Redirect
Location: /pages/default.aspx
Server: Microsoft-IIS/8.0
...
Set-Cookie: MRHSession=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly
Set-Cookie: LastMRH_Session=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly

Behavior with APM Profile "Restrict to Single Client IP" Option =

ENABLED

1.) Access the site using a legacy client (aka. Basic Auth)

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - HTTP_REQUEST"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: close
Content-Length: 0

2.) Login as valid user using credential set: test:password1+

HTTP/1.1 302 Redirect
Location: /pages/default.aspx
Server: Microsoft-IIS/8.0
...
Set-Cookie: MRHSession=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly
Set-Cookie: LastMRH_Session=100f33e0307120fd8f2d0e886cf2b7d1; expires=Wed, 14 Sep 2016 15:48:19 GMT;path=/;secure;HttpOnly

3.) Login from a different host using credential set: test:password1-

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - ACCESS_POLICY_COMPLETED"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: Close
Set-Cookie: LastMRH_Session=e443275f;path=/;secure;HttpOnly
Set-Cookie: MRHSession=7884faddb7f22b02f86b38f4e443275f;path=/;secure;HttpOnly
Content-Length: 0

4.) Repeat step 3. multiple times to lock the user account in your repository

HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="HelloWorld - ACCESS_POLICY_COMPLETED"
Set-Cookie: MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/
Server: BigIP
Connection: Close
Set-Cookie: LastMRH_Session=3c9093b9;path=/;secure;HttpOnly
Set-Cookie: MRHSession=26212f5e9bec540efa77ad963c9093b9;path=/;secure;HttpOnly
Content-Length: 0

5.) Login from a different host using credential set: test:password1+ (aka. you have guessed the correct password!)

HTTP/1.0 302 Found
Server: BigIP
Cache-Control: no-cache, no-store
Connection: Close
Content-Length: 0
Location: /vdesk/hangup.php3
Set-Cookie: LastMRH_Session=e443275f;path=/;secure;HttpOnly
Set-Cookie: MRHSession=7884faddb7f22b02f86b38f4e443275f;path=/;secure;HttpOnly

Note: As you've already pointed out, the Exchange iRule uses an optional

[md5 "$apm_password$src_ip"]
for per-src_ip uuie keying. But even then, it can still be used to by-pass repository account lockouts, if you sit behind the same NAT device (e.g. same conference center, cafe, etc.). The better approach would be to implement a tight account lookout before any credential caches are used.

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi kai,

 

the exchange irule does not use the APM Profile "Restrict to Single Client IP" value.

 

I already created a little irule to enable the use of this value with APM exchange profile:

 

https://devcentral.f5.com/s/feed/0D51T00006j1zdWSAQ

 

I will update the irule with the ability to limit session reuse for a single client ip like in exchange irule.

 

With sharepoint, it is interesting to limit session reuse per IP. with active sync, mobile phone often change client ip as they are natted with an address pool. in this case, limiting session per client IP can create lots of session for the same device.

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

 

tell me what you thing about this rather simple change. Its much more secure than any IP binding... 🙂

 

when RULE_INIT {
    set static::account_failed_auth_limit 5
    set static::account_failed_auth_window 300
    set static::account_lockout_duration 600    
}
when HTTP_REQUEST {

....

  if { !([HTTP::header Authorization] == "") } {
     set clientless(insert_mode) 1
     set username    [ string tolower [HTTP::username] ]
     if { [table lookup "$username\_lock"] ne 1 } then {
        set clientless(password)    [HTTP::password]
        binary scan [md5 "$clientless(password)"] H* clientless(hash)
        set user_key "$username.$clientless(hash)"
        set clientless(cookie_list)             [ ACCESS::user getsid $user_key ]
        if { [ llength $clientless(cookie_list) ] != 0 } {
           set clientless(cookie) [ ACCESS::user getkey [ lindex $clientless(cookie_list) 0 ] ]
           if { $clientless(cookie) != "" } {
              HTTP::cookie insert name MRHSession value $clientless(cookie)
              set clientless(insert_mode) 0
           }
        }
        if { $clientless(insert_mode) } {
           HTTP::header insert "clientless-mode" 1
           HTTP::header insert "username" $username
           HTTP::header insert "password" $clientless(password)
        }
        unset clientless
     } else {
        HTTP::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close
        unset clientless
        return
     }
  } else {
    HTTP::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close
    return
  }

...

}
when ACCESS_POLICY_COMPLETED {
   if { ([info exists "clientless_mode"]) && ($clientless_mode) && ([ACCESS::policy result] equals "deny") } {
      ACCESS::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close
      ACCESS::session remove
      if { $static::account_failed_auth_limit > 0 } then {
         if { [expr { [table keys -subtable "$username\_count" -count] + 1 }] >= $static::account_failed_auth_limit } {
            table set -notouch "$username\_lock" 1 indef $static::account_lockout_duration
            table delete -subtable "$username\_count" -all
         } else {
            table set -subtable "$username\_count" [clock clicks] 1 indef $static::account_failed_auth_window
         }

      }

   } 
}

Note: The provided code is recycled from one of my iRule based authentication module(s). The code is not tested in combination with your code and may contain some coding glitches...

 

Cheers, Kai

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

since version 11.4, you can use localdb instance to store lockout users instead of table.

 

localdb instance allow to dynamic user creation. in locadb write action, you can allow to create unknown users.

 

follow this link to configure it:

 

https://support.f5.com/kb/en-us/products/big-ip_apm/manuals/product/apm-authentication-single-sign-o...

 

about your question on tcl version 8.5 since TMOS 12.0, there is nothing to do. According to SOL36322151, irule use TCL 8.6.7 since version 12.0

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

you can't use locadb to secure the usage of

[ACCESS::user getsid $user_key]
within HTTP_REQUEST. Its only available within an APM Policy which is to late in the chain. Even then i think you will get much better results using iRules in combination with
[table]
.

Furthermore, my aged development strongbox is running on TMOS 12.0.0 with TCL version 8.4.6. Specifc 8.5 commands are not shown using

[info commands]
nor
[interp hidden]
comamnds. Also any new 8.5 syntaxes (e.g.
{*}
aka. nipple expand) are not available and the performance of
[info exists]
is still suboptimal.

You may try the code below on 12.1 to see if TCL8.5 is available on those plattforms. If not, then please open a ticket for me 🙂

when RULE_INIT {
        log local0.debug "TCL version : [info patchlevel]"
}

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

OK,

I did not see what did your irule. I now understand what you mean.

I think usage of

[ACCESS::user getsid $user_key]
with password hash is as secure as usage of session cookie.

there is the same security issue if the account is locked after the user signed on logon page.

the irule does not use username and password to authenticate but as a fingerprint to be sure this is the same user as the previous session, and reuse the same session, like session cookie does.

that's why I was thinking about insert client ip in the fingerprint as in exchange irule.

Kai_Wilke
MVP
MVP

Hi Stanislas,

 

without additional Account Lockouts inplace you will simply allow attacker to bypass the account lockouts of your internal repository. An IP binding (like implemented in the EAS iRules) will make it just a little harder and introduces certain side effects (opening multiple APM sessions if an IP change occours). But its by any means not a secure solution^^

 

Cheers, Kai

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

if you're going to implement an account lockout then make sure you issue

after "[PROFILE::access min_failure_delay]000"
before sending the lockout 401 response. Otherwise it will be possible for an attacker to detect that the lockout is active.

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

 

[ACCESS::user getsid $user_key] in only used when another session is already authenticated. so the user provided first the right login / password.

 

for next requests with same login / password, there is no need to check if the password is wrong with lockout prevention. the password was right during first logon, so password is used to build fingerprint.

 

I understand that brute force prevention may be the next security part for the irule allowing basic auth as APM disable brute force prevention with min / max failure delay.

 

to enable min / max failure delay, I can add the following code before ACCESS::respond in ACCESS_POLICY_COMPLETED event:

 

set min "[PROFILE::access min_failure_delay]000"
set max "[PROFILE::access max_failure_delay]000"

after [expr {int(rand() * ($max + 1 - $min)) + $min}]
ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Connection close
unset min max
Kai_Wilke
MVP
MVP

Hi Stanislas,

for next requests with same login / password, there is no need to check if the password is wrong with lockout prevention. the password was right during first logon, so password is used to build fingerprint.

To protect against bruteforce attacks, its required to block further authentication attemps using the same username if a certain threshold of wrong logins has been reached. If caches are deployed to offload repository authentication (e.g.

[ACCESS::user getsid $user_key]
is a credential cache!), then you MUST either make sure that the lockout mechanism will also cover access to the cached credentials, or you MUST make sure that the cached credentials of a given username are gettings invalidated once the account lockout is active.

Since invalidating the cache is not practical in our scenario (it will cause the existing session to become removed), its wise to enforce the lockout in front of the cache. If both methods are ignored, the caches can be used to bruteforce a currently active account, even in the case the repository has already enforced a lockout...

after [expr {int(rand() * ($max + 1 - $min)) + $min}]

Yours is even better.. 😉

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

I followed all your advices (except lockout, One solution instead of blocking with table can be deleting in ACCESS_POLICY_COMPLETED sessions of user with same username if VPE lockout occurs)

here is the new version :

when RULE_INIT {
   set static::Basic_Realm_Text "SharePoint Authentication"
}

when CLIENT_ACCEPTED {
    set inject_session_cookie 0
    set last_ua_agent "init"
}

when HTTP_REQUEST {
    if { ! [ info exists SP_PROFILE_RESTRICT_SINGLE_IP ] } {
        set SP_PROFILE_RESTRICT_SINGLE_IP        [PROFILE::access restrict_to_single_client_ip]
    } 
     Identify User-Agents type
    if { $last_ua_agent equals [set last_ua_agent [HTTP::header value "User-Agent"]] } {
         Do nothing, keep previous request authschema value
        log local0. $last_ua_agent
    } elseif {[HTTP::header exists "X-FORMS_BASED_AUTH_ACCEPTED"] && (([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t") || ([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f"))} {
        set clientless_mode 0; set form_mode 1
    } else {
        set clientless_mode 0
        set form_mode 0
        switch -glob -- [string tolower [HTTP::header "User-Agent"]] "*office protocol discovery*" - \
            "*microsoft office*" - \
            "*microsoft data access internet publishing provider*" - \
            "*non-browser*" - \
            "msoffice 12*" - \
            "*microsoft-webdav-miniredir*" - \
            {*ms frontpage 1[23456789]*} {
                 Implicit MSOFBA support detected.   
                set form_mode 1
            } "*ms frontpage*" {
                 Legacy client detected
                set clientless_mode 1
            } "*mozilla*" - \
            "*opera*" {
                 Regular web browser detected.  
                set clientless_mode 0
            } default { 
                set clientless_mode 1
            }
   }
   if { $clientless_mode || $form_mode } {
        if { ( [set sessionid [HTTP::cookie value "MRHSession"]] ne "" ) and ( [ACCESS::session exists -state_allow $sessionid] ) } then {
             Allow the successfully pre authenticated request to pass
            return
        } else {
             Check if persistent APM session cookie is present and valid
            if { ( [set sessionid [HTTP::cookie value "MRHSession_SP"]] ne "" ) and ( [ACCESS::session exists -state_allow $sessionid] ) } then {
                 Restore APM session cookie value
                if { [catch {HTTP::cookie insert name "MRHSession" value $sessionid} ] } {log local0. "[IP::client_addr]:[TCP::client_port] : TCL error on HTTP cookie insert MRHSession : URL : [HTTP::host][HTTP::path] - Headers : [HTTP::request]"} else {return}
                HTTP::cookie insert name "MRHSession" value $sessionid
                set inject_session_cookie 1 
                 Allow the successfully pre authenticated request to pass
                return
            }
        }
    } else {
        set sessionid [HTTP::cookie value "MRHSession"]
        return
    }      
    if { $clientless_mode } {
      if { [ string match -nocase {basic *} [HTTP::header Authorization] ] == 1 } {
         set clientless(insert_mode) 1
         set clientless(src_ip)      [IP::remote_addr]
         set clientless(username)    [ string tolower [HTTP::username] ]
         set clientless(password)    [HTTP::password]
         if { $SP_PROFILE_RESTRICT_SINGLE_IP == 0 } {
                binary scan [md5 "$clientless(password)"] H* clientless(hash)
         } else {
                binary scan [md5 "$clientless(password)$clientless(src_ip)"] H* clientless(hash)
         }
         set user_key "$clientless(username).$clientless(hash)"
         set clientless(cookie_list)             [ ACCESS::user getsid $user_key ]
         if { [ llength $clientless(cookie_list) ] != 0 } {
            set clientless(cookie) [ ACCESS::user getkey [ lindex $clientless(cookie_list) 0 ] ]
            if { $clientless(cookie) != "" } {
               HTTP::cookie insert name MRHSession value $clientless(cookie)
               set clientless(insert_mode) 0
            }
        }
        if { $clientless(insert_mode) } {
            HTTP::header insert "clientless-mode" 1
            HTTP::header insert "username" $clientless(username)
            HTTP::header insert "password" $clientless(password)
        }
        unset clientless
        } else {
            HTTP::respond 401 noserver WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close
            return
        }
    } elseif {$form_mode && [HTTP::path] ne "/sp-msofba-form"}{
        HTTP::respond 403 -version "1.1" \
            content "Access Denied. Make sure that your client is correctly configured. See https://support.microsoft.com/en-us/kb/932118 for further information." \
            noserver \
            "Content-Type" "text/html" \
            "X-FORMS_BASED_AUTH_REQUIRED" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-form" \
            "X-FORMS_BASED_AUTH_RETURN_URL" "https://[getfield [HTTP::host] ":" 1]/sp-msofba-completed" \
            "X-FORMS_BASED_AUTH_DIALOG_SIZE" "800x600" \
            "Set-Cookie" "MRHSession=deleted;path=/;secure" \
            "Set-Cookie" "LastMRH_Session=deleted;path=/;secure" \
            "Set-Cookie" "MRHSession=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure" \
            "Set-Cookie" "LastMRH_Session=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;secure"
        return
    }
}


when HTTP_RESPONSE {
     Insert persistent cookie for html content type and private session
    if { [HTTP::header "Content-Type" ] contains "text/html" } {
        HTTP::cookie remove MRHSession_SP
        HTTP::cookie insert name MRHSession_SP value $sessionid path "/"
        HTTP::cookie expires MRHSession_SP 120 relative
        HTTP::cookie secure MRHSession_SP enable
    }
     Insert session cookie if session was recovered from persistent cookie
    if { ([info exists "inject_session_cookie"]) && ($inject_session_cookie) } {
        HTTP::cookie insert name MRHSession value $sessionid path "/"
        HTTP::cookie secure MRHSession enable
    }
}

when ACCESS_SESSION_STARTED {
    if {([info exists "clientless_mode"])} {
        ACCESS::session data set session.clientless $clientless_mode
    }
    if { [ info exists user_key ] } {
        ACCESS::session data set "session.user.uuid" $user_key
    }
}

when ACCESS_POLICY_COMPLETED {
    if { ([info exists "clientless_mode"]) && ($clientless_mode) && ([ACCESS::policy result] equals "deny") } {
        set min "[PROFILE::access min_failure_delay]000"
        set max "[PROFILE::access max_failure_delay]000"
        after [expr {int(rand() * ($max + 1 - $min)) + $min}]
        ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Connection close
        unset min max
        ACCESS::session remove
    } 
}

when ACCESS_ACL_ALLOWED {
    switch -glob [string tolower [HTTP::path]] {
        "/sp-msofba-form" {
            ACCESS::respond 302 noserver Location "/sp-msofba-completed"
        }
        "/sp-msofba-completed" {
            ACCESS::respond 200 content {
                
                Authenticated
                Good Work, you are Authenticated
                
            } noserver
        }
        "*/signout.aspx" {
             Disconnect session and redirect to APM logout Page
            ACCESS::respond 302 noserver Location "/vdesk/hangup.php3"
            return
        }
        "/_layouts/accessdenied.aspx" {
             Disconnect session and redirect to APM Logon Page
            if {[string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } {
                ACCESS::session remove
                ACCESS::respond 302 noserver Location "/"
                return
            }
        }
        default {
             No Actions
        }
    }
}
Kai_Wilke
MVP
MVP

Hi Stanislas,

I have some feedback for you...

Line 11-13: Make this option a static::variable. 
Line 15: Unify the syntax within your script to either [HTTP::header MyHeader] or [HTTP::header value MyHeader].
Line 17: Leftover log line?
Line 18: The HTTP::header exists makes much sense. I've updated my own script to include this.
Line 19: I don't see a reason to use two independent variables for forms and client_less. Using a multivalue would make things easier. You may use 0 for browser, 1 for MSOFBA and 2 for Clientless. In this case you could apply certain script blocks for ==1 ==2 or even >0
Line 20,21: Make sure that each switch script block sets both variables. In this case you don't need set them to 0 at the beginning. It will safe some cycles.
Line 51: Remove this line. HTTP::header insert should not throw an error and line 52 will nevertheless insert the cookie for you.
Line 53: Keep in mind that you should reset this variable once a cookie has been issued. Currently it will issues a cookie on consecutive requests. Alternatively set this variable on each request depending on the enumerated client type.
Line 68: Remove the == 0 and flip the order of the [if]. It may save your some additional cycles....
Line 111: I would make sure that only browser are allowed to issue persistent cookie. I guess there is no reason to inject persistent cookie for non-browser clients.
Line 112: A SharePoint wouldn't use a cookie name of MRHSession_SP, so you don't need to remove it.
Line 113-114: Combine this two commands using a HTTP::header insert it will safe some cycles. Also try to issue a HttpOnly header. It will work with Office clients.
Line 135-137: There is no need to random-delay the 401 within ACCESS_POLICY_COMPLETED. Its already auto-random-delayed. The delay would be required only for 401 responses within the HTTP_REQUEST event.
Line 118: There is no need for the [info exists]. The variable is set during CLIENT_ACCEPTED and the not removed.

Deleting VPE session: Sounds interesting. How will you enumerate the sssions with an identical username?

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

 

here are my answers:

 

  • Line 11-13: Make this option a static::variable. : No, if it is static::variable, it won't change when changing Access profile since the irule is reload (static::variable goal is to be set in RULE_INIT).
  • Line 15: Unify the syntax within your script to either [HTTP::header MyHeader] or [HTTP::header value MyHeader]. : I agree. I will change it.
  • Line 17: Leftover log line? : debug log i forgot to remove
  • Line 18: The HTTP::header exists makes much sense. I've updated my own script to include this.
  • Line 19: I don't see a reason to use two independent variables for forms and client_less. Using a multivalue would make things easier. You may use 0 for browser, 1 for MSOFBA and 2 for Clientless. In this case you could apply certain script blocks for ==1 ==2 or even >0 : I agree, but I must change it and manage in ACCESS_SESSION_STARTED, time was missing
  • Line 20,21: Make sure that each switch script block sets both variables. In this case you don't need set them to 0 at the beginning. It will safe some cycles. I will change to one variable
  • Line 51: Remove this line. HTTP::header insert should not throw an error and line 52 will nevertheless insert the cookie for you. Cookie insert,as most of HTTP commands, can throw an error if previous HTTP response command was executed before (another irule or LTM Policy)
  • Line 53: Keep in mind that you should reset this variable once a cookie has been issued. Currently it will issues a cookie on consecutive requests. Alternatively set this variable on each request depending on the enumerated client type.
  • Line 68: Remove the == 0 and flip the order of the [if]. It may save your some additional cycles.... : OK, I got it from F5 exchange irule
  • Line 111: I would make sure that only browser are allowed to issue persistent cookie. I guess there is no reason to inject persistent cookie for non-browser clients.
  • Line 112: A SharePoint wouldn't use a cookie name of MRHSession_SP, so you don't need to remove it. : Right
  • Line 113-114: Combine this two commands using a HTTP::header insert it will safe some cycles. Also try to issue a HttpOnly header. It will work with Office clients. I agree, I will do it
  • Line 135-137: There is no need to random-delay the 401 within ACCESS_POLICY_COMPLETED. Its already auto-random-delayed. The delay would be required only for 401 responses within the HTTP_REQUEST event. I tested it an in clientless mode and the auto random delay was not working on version 12.0, I will try again
  • Line 118: There is no need for the [info exists]. The variable is set during CLIENT_ACCEPTED and the not removed. : Right

And Lines 100 - 101: same cookie remove than lines 102 - 103. lines 100 and 101 will be removed.

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

Line 11-13: Personally I would make the setting independent of the APM profile configuration. This would allow to distinct between regular browser and MSOFBA clients (not restricted) and clientless access (Single-IP restricted). Furthermore I would recoment to always use the Single-IP mode for clientless access to reduce the security risks associated with the

[ACCESS::user getsid $user_key]
syntax.

Line 51: Okay, I got the intention behind. Maybe you should issue a

if { [catch {HTTP::payload replace 0 0 {}}] } then { return }
at the beginning of the
HTTP_REQUEST
/
HTTP_RESPONSE
events to cover every write access on already responded HTTP requests/responses.

Line 135-137: Also tested on TMOS v12.0 and it has worked like a charm for me.

Additional Note: I've identified a problem within the MSOFBA authentication. If the APM session of an MSOFBA client is removed (e.g. session timeout) the MSOFBA client will not be able to re-authenticate using MSOFBA a second time. Only a fresh persistent cookie will allow him to save pending document changes, which is unfortunately not very intuitive for the end user. I've already developed a mechanism to restore/create a minimalistic APM session on-the-fly based on selected session meta-data (e.g. Username, Language-Settings, SSO settings) utilizing an AES encrypted

MRHSession_R
session cookie. The code already works like a charm and allows me to deploy strict APM Policy session inactivity timeouts (e.g. 900 seconds), while providing a solid backdoor for long-living MSOFBA client sessions (e.g. 1-2 day(s)) without wasting any APM concurrent user CALs. Will perform additional tests and then post my latest iRule for you...

Cheers, Kai

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi Kai,

 

I just tested to kill a session for a MSOFBA authenticated user.

 

When trying to save the document, Office client (In my test, Excel 2011 for Mac) display a message with a "reconnect" button. when I clicked on it, the document was uploaded on the server (as the user was authenticated on the SAML IdP, I was not prompted for user credentials).

 

I think this is not a problem but the expected behavior when session expires.

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

 

every Windows based Office client I've tested so far does not perform MSOFBA a second time. It receives the err403 and then just stops to follow the MSOFBA headers... 😞

 

Cheers, Kai

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

I just tried with Excel 2016 for windows. Excel displays the error message with connection button. when I click, the office browser open then close because of already saml session opened.

 

The irule used when I try is the one of this thread, not the last one in comments.

 

Kai_Wilke
MVP
MVP

Hi Stanislas,

 

Just tried your iRule and mentioned problem remains.

 

I'm currently testing with Excel14 (aka. Office 2010), but will test some newer Office versions next week, to get a felling which specific versions are afffected.

 

Cheers, Kai

 

domokos_23867
Nimbostratus
Nimbostratus

Hello,

 

I have used the irule and hit some limitations. I turned on a lot of logging and I see the following behavior. If I use IE I see the 2 cookies inserted (I have also a VPE that does SAML auth followed by Kerberos SSO and the access policy uses persistent cookies). I click on the link to open the document in WORD and word fires. It always has the MRH cookie but not the _SP one. The variable apmstatus is set to 1 and apmpersiststatus is set to 0. I guess it never tries to insert the _SP one because the MRH cookie is always there. So it seems that word shares the cookie of IE. Now when I use Firefox I see in the FF session the 2 cookies but when word comes up it does not use any of them. So I get a redirect inside WORD to go to the SAML iDP to authenticate. This comes from the fact that the cookie that FF has is not used by word, so I wonder if you have any ideas on how to link the word session to FF. I cannot find anything in common that would link the 2 of them.

 

Thanks in advance Carol

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Hi,

 

Firefox does not write persistent cookies on the same folder than Microsoft products, and I'm not sure the cookie storage format is the same between applications. re-authentication is required when editing document from FF.

 

in the irule, there is no link between apmpersiststatus and _SP cookie.

 

asaf_01_136490
Nimbostratus
Nimbostratus

Hey Kai , can you please publish the final version . another thing , you need to disable the protected mode in IE.

 

Daphne_Won
Legacy Employee
Legacy Employee

Everyone, I like to engage the folks here that have deployed OFBA iRules to support authentication access from native Office apps to Sharepoint on-prem. Can you email me to see what authentication methods you need, what version of the Office clients you need to support, and what versions of the Sharepoints you have deployed for? You can email me at d.won@.

 

ryanm99
Nimbostratus
Nimbostratus

Hello Everyone,

I am attempting to set up the iRule that was shared here to allow Microsoft Office Mobile apps to access SharePoint through APM. I am currently running version 13.0.0. When I attempt to access the environment, I get the following in the APM log file:

May 19 22:34:17 DV-BIGIP01 err tmm1[19118]: 01490514:3: (null):Common:00000000: Access encountered error: ERR_ARG. File: ../modules/hudfilter/access/access.c, Function: access_sanitize_portal_headers, Line: 15499
May 19 22:34:17 DV-BIGIP01 err tmm1[19118]: 01490514:3: (null):Common:00000000: Access encountered error: ERR_ARG. File: ../modules/hudfilter/access/access.c, Function: access_forward_request_to_portal, Line: 15578
May 19 22:34:17 DV-BIGIP01 err tmm1[19118]: 01490514:3: (null):Common:00000000: Access encountered error: ERR_ARG. File: ../modules/hudfilter/access/access.c, Function: access_process_state_client_enforce_session, Line: 7261
May 19 22:34:17 DV-BIGIP01 err tmm1[19118]: 01490514:3: (null):Common:00000000: Access encountered error: ERR_ARG. File: ../modules/hudfilter/access/access.c, Function: hud_access_handler, Line: 2735

Does anyone have any ideas on something I could try? On the mobile app side it just spins at connecting for a while and eventually times out. Thanks!​text

sergio_baza_alo
Altocumulus
Altocumulus

Hello all,

 

This iRule is working great for me in IE, Firefox and Chrome but I'm having problems with Edge. Everytime I try to open a Microsoft Office document I'm prompted for credentials.

 

Does someone know how to fix this?

 

Kind regards

 

Nicolas_COLLET
Nimbostratus
Nimbostratus

Hello,

 

It's possible to try the new version of this irule :

 

Best regards

 

sergio_baza_alo
Altocumulus
Altocumulus

Hello,

 

With the new version I'm still having the issue 😞

 

Kind regards

 

sergio_baza_alo
Altocumulus
Altocumulus

Actually I'm using the iRule v2.

 

What I've noticed, (putting logs in the iRule) is that the IE and the Edge uses the same user agent so the behaviour shouldn't be different

 

EDGE: Jan 23 10:36:54 F5 info tmm1[18133]: Rule /Common/iRule_edicion_documentos : UA: microsoft office excel 2014 (16.0.8431) windows nt 10.0

 

IE: Jan 23 10:39:40 F5 info tmm[18133]: Rule /Common/iRule_edicion_documentos : UA: microsoft office excel 2014 (16.0.8431) windows nt 10.0

 

Does anybody has an idea of why is this happening?

 

Kind regards

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

What is the exact behavior? You says you are prompted... but what kind of prompt?

 

OFBA? Basic auth pop-up?

 

With other browsers, is there any OFBA prompt?

 

asaf_01_136490
Nimbostratus
Nimbostratus

check that you getting the cookies- i had an issues that the time + timezone was not sync and the cookies was expired .

      HTTP::cookie remove $static::ckname
      HTTP::cookie insert name $static::ckname value $apmsessionid path "/"
      HTTP::cookie expires $static::ckname 120 relative
      HTTP::cookie secure $static::ckname enable
Stanislas_Piro2
Cumulonimbus
Cumulonimbus

it's not an irule issue but may be a browser behavior change.

 

JoeTheFifth
Altostratus
Altostratus

Here is one for you guys: i don't use adfs. I use a forms login page to authenticate users in an ldap then I get the username and do a kerberos sso. This works fine with sharepoint and Office web apps but opening office documents does not work. i have the login pop. I will need to take a look a look at the irule and try ti adapt it. if you have any hints please shoot 🙂

 

Stanislas_Piro2
Cumulonimbus
Cumulonimbus

Which login pop up?

 

If this is a form inside a browser it means the code works fine.

 

If you expected a transparent authentication because of browser auth, it works only with internet explorer, and only if the url is in trusted sites.

 

Authentication is stored in a persistent cookie written on disk by browser, but office only read cookies in ie directories.

 

Pål_Andre_Ropst
Nimbostratus
Nimbostratus

Hi Stanislas,

 

Thank you so much for your provided iRule. This is working great. Our only problem is getting it to work with Safari on Mac OS with Office for Mac (currently testing 2016). What happens is just like you describe. When your authenticated with Safari, and then try to open a Office document within SharePoint, this trigger the Office product (word, Excel, Powerpoint). But when the office application opens this trigger the access policy/authentication process within a browser - just like you describe. In reference to your last post - does that mean that this will not work with Safari on Mac?

 

Are there any customization that can be done to get it to work?

 

Thanks again.

 

Sincereley, Pål Andrè

 

Version history
Last update:
‎20-Apr-2016 04:02
Updated by:
Contributors