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:ClientMissing 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
Published Apr 09, 2018
Version 1.0

Was this article helpful?

No CommentsBe the first to comment