APM Sharepoint authentication
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- Stanislas_Piro2Cumulonimbus
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.
- domokos_23867Nimbostratus
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
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
- Stanislas_Piro2Cumulonimbus
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.
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_Piro2Cumulonimbus
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.
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
syntax.[ACCESS::user getsid $user_key]
Line 51: Okay, I got the intention behind. Maybe you should issue a
at the beginning of theif { [catch {HTTP::payload replace 0 0 {}}] } then { return }
/HTTP_REQUEST
events to cover every write access on already responded HTTP requests/responses.HTTP_RESPONSE
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
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...MRHSession_R
Cheers, Kai
- Stanislas_Piro2Cumulonimbus
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.
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_Piro2Cumulonimbus
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 } } }