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.5Hi 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.
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.[ACCESS::user getsid $user_key]
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_Piro2Cumulonimbus
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
Hi Stanislas,
if you're going to implement an account lockout then make sure you issue
before sending the lockout 401 response. Otherwise it will be possible for an attacker to detect that the lockout is active.after "[PROFILE::access min_failure_delay]000"
Cheers, Kai
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
- Stanislas_Piro2Cumulonimbus
OK,
I did not see what did your irule. I now understand what you mean.
I think usage of
with password hash is as secure as usage of session cookie.[ACCESS::user getsid $user_key]
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.
Hi Stanislas,
you can't use locadb to secure the usage of
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[ACCESS::user getsid $user_key]
.[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
nor[info commands]
comamnds. Also any new 8.5 syntaxes (e.g.[interp hidden]
aka. nipple expand) are not available and the performance of{*}
is still suboptimal.[info exists]
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_Piro2Cumulonimbus
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:
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
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_Piro2Cumulonimbus
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.
 
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
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.[md5 "$apm_password$src_ip"]
Cheers, Kai