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 "soap:Client Missing Authorization 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
#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 "
soap:Client
The requested operation was denied. Please consult with your administrator.Your support ID is: [ ACCESS::session data get session.user.sessionid ]
" 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==="
}
} Tested this on version:
12.0