APM session caching for Web API's
Problem this snippet solves:
Background.
Originally the web was personal. You logged on to a site with your browser, worked and quit. A session could last many hours.
Currently, we are using the Web for remote calls, B2B processes and distributed systems. These are normally a single POST with basic authentication. This is inefficient when a session must be created for each transaction.
The Problem.
You can add authentication to a web call by using APM. This authenticates you by creating a Session and setting a cookie. This is done by returning a 302 to /my.policy
But what if you are using a client that isn't a browser? It can't follow redirects and also it can't log out of the session. The client can use a header "clientless-mode: 1" that enable sessions to start without the 302. But there is a session created for every API call, which is a huge overhead and the sessions continue until they time out.
The Solution.
We need to reuse existing sessions (session caching) There was an iRule published to do this. Sorry I haven't been able to find the author. If it's you, please contact me for credit. I have expanded on it.
How to use this snippet:
Add this iRule to the APM VIP
Code :
# Updated 2017-11-22 Extra information printed on a non 2XX response # Session caching iRule for nonbrowser webcalls. # John Huttley 2018 based on others work. no copyright asserted. # This is intended for clients that connect to a server providing an API. # in normal mode this would cause a new session to be created and then left to expire (the client can't call hangup) # This is a lot of work. # The solution is to make a key for each client. The uniqueness of the key controls how many sessions are running. # It would be perfectly possible to set the key to "FredandWilma" and have all clients use the same session. # The session has nothing to do with the web API uri, unless we make it that way. # 1. Client connects with Basic Auth matching the AP Authentication, we get the user name and password. # 2. Use the Client IP, the HTTP::path username and password to generate a key, user_key # 3: All existing MRHSession Cookies are removed from the request. # 4. Use the user_key to see if a session already exists. # yes, session exists: A New MRHSession cookie is inserted into the request -> APM. The client isn't involved. # no, no session found, add the clientless-mode header -> APM # Say Again, we never read any MRHSession cookies, even the ones we set ourselves. We don't care what the client does with them. # ===>>We delete client all MRHSession cookies. <<=== # All state is retrieved from APM from the user_key hash. # The request should not return a 302 redirect to /my.policy. If it does, clientless mode isn't working. Print the headers in any non 2XX response status. when RULE_INIT { set static::missing_auth_header "" set static::ACCESS_LOG_PREFIX "01490000:7:" } when HTTP_REQUEST { log -noname local0. "" # log -noname local0. "Irule Started" #Save request headers, used if the response is not a 2XX array set request_headers {} foreach aHeader [HTTP::header names] { set request_headers($aHeader) [HTTP::header values $aHeader] } # Only allow HTTP Basic Authentication. set auth_info_b64enc "" set http_hdr_auth [HTTP::header Authorization] regexp -nocase {Basic (.*)} $http_hdr_auth match auth_info_b64enc if { $auth_info_b64enc == "" } { set http_hdr_auth "" } if { $http_hdr_auth == "" } { log -noname accesscontrol.local1.debug "$static::ACCESS_LOG_PREFIX Empty/invalid HTTP Basic Authorization header" HTTP::respond 401 content $static::missing_auth_header Connection close return } set username [ HTTP::username ] set password [ HTTP::password ] # log -noname local0. "user: $username Pass: $password" # Building MD5 hash data. Here starts with password to avoid storing raw password data. set hash_data $password # Append source IP address append hash_data [ IP::remote_addr ] # Append URI append hash_data [ HTTP::path ] # Calculate MD5 hash and hex encode it. binary scan [md5 $hash_data ] H* user_hash # Finalize the user key with adding username. This is also for readability. set user_key {} append user_key $username "." $user_hash unset user_hash hash_data set f_insert_clientless_mode 1 # remove all existing MRHSession Cookies. We generate them new and /never/ need them from the client. foreach acookie [HTTP::cookie names] { if { $acookie == "MRHSession" } { # log -noname local0. "removed MRHSession Cookie: [HTTP::cookie value $acookie]" HTTP::cookie remove $acookie } } # Given the user_key (hash) get the list of hashed session_ids # ACCESS::user getsid soap:Client Missing Authorization header #Returns the list of created external SIDs which is associated with the specified key ##### This function is badly named and the description is wrong. ACCESS::user getsid returns a list of SID /hashes/ which then need to be processed by ACCESS::user getkey set sid_hash_list [ ACCESS::user getsid $user_key ] # log -noname local0. "sid_hash_list: $sid_hash_list " # We just need one session... example of one sid_hash: d89a62f4985b724a61e1fc9965e202b5 if { [ llength $sid_hash_list ] != 0 } { #ACCESS::user getkey #Returns the original SID for specified hash of SID set sid [ ACCESS::user getkey [ lindex $sid_hash_list 0 ] ] if { $sid == "" } { # log -noname local0. "sid_hash gave empty sid. bad, Goes to clientless mode." } if { $sid!= "" } { # log -noname local0. "Session ID - $sid" HTTP::cookie insert name MRHSession value $sid set f_insert_clientless_mode 0 } } unset sid_hash_list if { $f_insert_clientless_mode == 1 } { HTTP::header insert clientless-mode 1 log -noname local0. "Started Clientless Mode" } unset f_insert_clientless_mode } when ACCESS_SESSION_STARTED { # log -noname local0. "Access Session Started" if { ! [ info exists user_key ] } { # Something horribly wrong log local0. "ERROR: user_key not set." return } ACCESS::session data set session.user.uuid $user_key ACCESS::session data set session.logon.last.username $username ACCESS::session data set -secure session.logon.last.password $password unset username password } when ACCESS_POLICY_COMPLETED { # log -noname local0. "Access Session Completed" if { ! [ info exists user_key ] } { # Something horribly wrong log local0. "ERROR: user_key not set." return } set policy_result [ACCESS::policy result] # log -noname local0. "Access Session Result: $policy_result" switch $policy_result { "allow" { } "deny" { ACCESS::respond 403 content " " Connection close ACCESS::session remove } } unset user_key policy_result } when HTTP_RESPONSE { if {! [HTTP::status] starts_with "2" } { log -noname local0. "" log -noname local0. "WARNING: Got a [HTTP::status] Response, Clientless-mode not working. SID: $sid, " log -noname local0. "===Request Headers BEGIN===" foreach {header value} [array get request_headers] { log -noname local0. "$header $value" } log -noname local0. "===Request Headers END===" log -noname local0. "===Response Headers BEGIN===" foreach aHeader [HTTP::header names] { log -noname local0. "$aHeader: [HTTP::header value $aHeader]" } log -noname local0. "===Response Headers END===" } } soap:Client The requested operation was denied. Please consult with your administrator.Your support ID is: [ ACCESS::session data get session.user.sessionid ]
Tested this on version:
12.0