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- timon74NimbostratusPerfect for my needs. thanks for your help
- timon74NimbostratusI reduce the persistent cookie expiration time to 20 seconds. It permits to increase security in my case. Thanks for this really efficient Irule.
- rouanon_150376NimbostratusHi. 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_COLLETNimbostratusHi, 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
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_Piro2Cumulonimbus
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_SandyNimbostratus
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.
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_Piro2Cumulonimbus
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.
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
script block and add the other iRule events. But be aware that the query toelseif { $authschema eq "legacy" } then {
exposes some security risks, since it can be used to bypass active account lockouts.[ACCESS::user getsid $user_key]
Update: Changed the session cookie injection mechanism.
Cheers, Kai