APM authentication for a sessionless and clientless API
We are trying to use APM 13.1.1 to replace a TMG for authenticating API calls. While searching for existing solutions, the closest one I found is the iRule by Stanislas Piron provided for a similar case https://devcentral.f5.com/questions/clientless-mode-and-401-for-poor-client-48409 , but there's still too much session-oriented stuff there.
The problem is that the APM should be completely transparent to both the server and the requestor (not calling it a client, as it's not a full client). It should only show itself when an LDAP authentication fails, and then it should only send back a 401 reply. Using the iRule and access policy above, the cookies and headers we get are confusing the API server. Testing with curl (not using -b, so cookies should not be stored), we get a 401 reply when the session is established, and subsequent request pass through OK as long as the session is there. That means that the APM changes the first server request by inserting cookies and/or headers. On the client side I see the LastMRHSession and MRHSession cookies in the first reply, but not after that.
So the question is: how to configure APM to authenticate each and every request without changing the traffic in any way? As there's no client to handle the cookies, they should not even be sent out. Session tracking is unnecessary, so sessions should be deleted right after the API call is answered.
After extensive testing, and diving deep into HTTPS and APM event orders (https://devcentral.f5.com/questions/irule-event-order-https-ssl-client-server-side and https://devcentral.f5.com/articles/http-event-order-access-policy-manager) I managed to get APM working like I wanted it to. Briefly, here's what the APM combined with an iRule does:
- Authenticate every request via LDAP using the credentials supplied in the Authorization header
- Remove the APM session once the server response is ready
- Remove the APM cookies before sending the server response back to the requestor (still not calling it a client)
As mentioned, the starting point was the iRule and policy here https://devcentral.f5.com/questions/clientless-mode-and-401-for-poor-client-48409
This is the combined iRule I came up with:
This is essentially the iRule by Stanislas Piron when HTTP_REQUEST { set apmsessionid [HTTP::cookie value MRHSession] Check for existing APM session cookie if { [HTTP::cookie exists "MRHSession"] } {set apmstatus [ACCESS::session exists -state_allow $apmsessionid]} else {set apmstatus 0} If no APM status is found if { !($apmstatus)} { Find Authorization-header if { [ string match -nocase {basic *} [HTTP::header Authorization] ] == 1 } { Set clientless-mode, username and password for APM set clientless(insert_mode) 1 set clientless(username) [ string tolower [HTTP::username] ] set clientless(password) [HTTP::password] Create an md5 hash of the password binary scan [md5 "$clientless(password)"] H* clientless(hash) Use combination of username and password hash as the user_key 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) } { Set variables in headers for APM HTTP::header insert "clientless-mode" 1 HTTP::header insert "username" $clientless(username) HTTP::header insert "password" $clientless(password) } unset clientless } else { If there is no auth header, respond with 401 HTTP::respond 401 noserver WWW-Authenticate "Basic realm=\"[HTTP::host] Authentication\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close return } } } when ACCESS_SESSION_STARTED { Use the user_key as APM uuid if { [info exists user_key] } then { ACCESS::session data set {session.user.uuid} $user_key } } The rest I came up with on my own when ACCESS_ACL_ALLOWED { If APM auth is successfull, perform URL manipulation. I removed the rules, but this is apparently the right place to perform pool select based on URL when using APM. Pool selects under HTTP_REQUEST event may be overridden by APM. } when ACCESS_POLICY_COMPLETED { If auth fails, remove the APM session if { ([ACCESS::policy result] equals "deny") } { set host [ACCESS::session data get "session.network.name"] ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=\"$host Authentication\"" Connection close ACCESS::session remove } } when HTTP_REQUEST_SEND { Remove successfully authenticated sessions only when the HTTP request has been sent to the server. Not 100% sure this is the best place to do the removal, but it seems to work. This might also require some additional processing to make sure we are removing the right session... ACCESS::session remove } when HTTP_RESPONSE_RELEASE { Remove APM session cookies from the server response HTTP::cookie remove "MRHSession" HTTP::cookie remove "LastMRH_Session" }
As a sidenote, the problem of getting an initial 401 responce, which I mentioned in the question, was APM overriding the pool selects we had in place before APM processing. APM actually doesn't send the session cookies to the server (I verified this using an iRule to count the cookies), so there was no issue on the server side in terms of transparency. We just needed to get rid of the client side cookies and remove the APM session at the right point in time.
It still remains to be seen how efficient this approach is, but at least it works. Production tests in the near future will show that. At least the original iRule could be streamlined a bit, as we know the requests coming in will never contain an APM session cookie, so that check is a waste of time.