Forum Discussion

Tero_Toikkanen_'s avatar
Tero_Toikkanen_
Icon for Nimbostratus rankNimbostratus
Oct 18, 2018
Solved

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 c...
  • Tero_Toikkanen_'s avatar
    Nov 13, 2018

    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.