Forum Discussion
APM authentication for a sessionless and clientless API
- 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.
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.
How can I use this iRule so that it is only invoked if the HTTP::url starts_with "/api". I don't want this iRule to run unless the HTTP uri starts_with "/api"
Recent Discussions
Related Content
* Getting Started on DevCentral
* Community Guidelines
* Community Terms of Use / EULA
* Community Ranking Explained
* Community Resources
* Contact the DevCentral Team
* Update MFA on account.f5.com