OAuth2.0 for Google¶
Contributed by: Adrian Noblett¶
Note: This iRule was developed before BIG-IP APM 13.0 was released
which has native OAuth 2.0 support. For new OAuth 2.0 deployments with
BIG-IP, please use the native functionality in 13.0.
OAuth 2.0 Overview¶
The OAuth 2.0 (OAuth2) authorization protocol enables a third-party
application to obtain limited access to an HTTP service, either on
behalf of a resource owner by orchestrating an approval interaction
between the resource owner and the HTTP service, or by allowing the
third-party application to obtain access on its own behalf. OAuth2 is
not unlike SAML, and at a high level the authentication flow looks
very similar. This iRule adds the hooks necessary for APM to use
Google as an Authorisation Server for integrating APM’s SSO
capabilities for Enterprises who are using Google as a method or
authentication.
This document “APM - Google OAuth 2.0
v1.0”
describes how to use the iRule to integrate APM with Google OAuth.
OAuth2 iRule¶
when RULE_INIT {
# Log debug messages to /var/log/ltm? 0=no, 1=yes, 2=verbose
set static::oauth_debug 100
# The timeout, interval and retries determine how long in total we wait for the sideband server to send
# the response HTTP headers. If the server fails to respond, try increasing the timeout first
# as increasing the retries will eat up more CPU cycles unnecessarily
# Time (in ms) to wait for sideband connections to respond
set static::timeout 3000
# Interval (in ms) to check for sideband data
set static::interval 100
# Number of times to try getting the sideband server response HTTP data
set static::retries 30
# Local virtual server name to connect to for Google Account lookups
# The local virtual server should contain a pool of remote lookup servers
#set static::account_vserver "sideband_google_accounts"
#set static::apis_vserver "sideband_google_apis"
set static::connect_proxy "sideband_proxy_ssl"
# Google OAuth Application Settings
set static::client_id "828399738225-1bnp3tbtkrl6ookrf80agnue6fnrh914.apps.googleusercontent.com"
set static::client_secret "GeQ3zcdgoEGu7-NjdSt8PrCa"
set static::redirect_uri "https://oauth-dev.f5se.com.au/oauth"
set static::redirect_path "/oauth"
}
when HTTP_REQUEST {
set log_prefix "[IP::client_addr]:[TCP::client_port]:\t"
if { [HTTP::uri] equals "/google" } {
ACCESS::session remove
}
if { [HTTP::uri] starts_with $static::redirect_path } {
if {$static::oauth_debug} { log local0. "$log_prefix OAuth uri: [HTTP::uri]" }
set HTTPuri [HTTP::uri]
set OA2code [URI::query $HTTPuri code]
if {$static::oauth_debug} { log local0. "$log_prefix OAuth hit. code=$OA2code" }
set OA2state [URI::query $HTTPuri state]
if {$static::oauth_debug} { log local0. "$log_prefix state=$OA2state" }
if { $OA2code == "" } {
# OA2code blank - must have had an error - ACCESS DENIED
if {$static::oauth_debug} { log local0. "$log_prefix OA2code missing - Transaction unsucessfull - ACCESS DENIED" }
ACCESS::session data set session.oauth.result 0
HTTP::respond 302 location "/my.policy"
return
}
# Test that session.user.sessionid == state
if { !( [ACCESS::session data get session.user.sessionid] == $OA2state ) } {
if {$static::oauth_debug} { log local0. "$log_prefix Session state mismatch - Possible cross-site-request-forgery - ACCESS DENIED" }
# No state match, could be cross-site-request-forgery - ACCESS DENIED
ACCESS::session data set session.oauth.result 0
ACCESS::session data set session.oauth.alert "Session state mismatch - Possible cross-site-request-forgery - ACCESS DENIED"
HTTP::respond 302 location "/my.policy"
return
}
set OA2post "code=$OA2code"
append OA2post "&redirect_uri=$static::redirect_uri"
append OA2post "&client_id=$static::client_id"
append OA2post "&scope="
append OA2post "&client_secret=$static::client_secret"
append OA2post "&grant_type=authorization_code"
set OA2postlen [string length $OA2post]
set OA2head "POST /o/oauth2/token HTTP/1.1\r\n"
append OA2head "Host: accounts.google.com\r\n"
append OA2head "Content-Length: $OA2postlen\r\n"
append OA2head "Content-Type: application/x-www-form-urlencoded\r\n"
append OA2head "User-Agent: f5-apm-oauth\r\n"
set OA2post_REQUEST "$OA2head\r\n$OA2post\r\n\r\n"
if {$static::oauth_debug} { log local0. "$log_prefix Created OA2 POST Request:" }
if {$static::oauth_debug} { log local0. "$log_prefix $OA2post_REQUEST" }
set conn [connect -protocol TCP -timeout $static::timeout -idle 30 -status conn_status $static::connect_proxy]
if {$static::oauth_debug} { log local0. "$log_prefix Connect returns: <$conn> and conn status: <$conn_status>" }
# Check if sideband connection was not established to server
if {$conn eq ""}{
if {$static::oauth_debug} { log local0. "$log_prefix Connection could not be established " }
return
}
# Get the current connection status & send data
set conn_info [connect info -idle -status $conn]
if {$static::oauth_debug} { log local0. "$log_prefix Connect info: <$conn_info>" }
set send_info [send -timeout $static::timeout -status send_status $conn $OA2post_REQUEST]
if {$static::oauth_debug} { log local0. "$log_prefix Sent $send_info bytes, send status: <$send_status>" }
set start [clock clicks -milliseconds]
for {set i 0} {$i <= $static::retries} {incr i}{
# See what data we have received so far on the connection within the interval timeout
set recv_data [recv -peek -status peek_status -timeout $static::interval $conn]
if {$static::oauth_debug > 1} { log local0. "$log_prefix Peek ([string length $recv_data]): $recv_data" }
# Check if we have received the response headers (terminated by two CRLFs)
if {[string match "HTTP/*\r\n\r\n*" $recv_data]}{
if {$static::oauth_debug} { log local0. "$log_prefix Found the end of the HTTP headers" }
# Look for a content-length header in the data we have received so far
if {[string match -nocase "*Content-Length: *" $recv_data]}{
# Calculate the length of the response headers plus the payload by parsing the Content-Length header
# Get the index of the end of the HTTP headers
set header_length [expr {[string first "\r\n\r\n" $recv_data] + 4}]
set payload_length [findstr [string tolower $recv_data] "content-length: " 16 "\r"]
if {$static::oauth_debug} { log local0. "$log_prefix \$header_length: $header_length, \$payload_length: $payload_length" }
# If the payload length is greater than 0
if {$payload_length ne "" and $payload_length > 0}{
if {$static::oauth_debug} { log local0. "$log_prefix Peeking for [expr {$header_length + $payload_length}] bytes to get the payload" }
set recv_data [recv -peek -timeout $static::timeout -status recv_status [expr {$header_length + $payload_length}] $conn]
# Exit the while loop
break
} else {
# Content-Length header is 0 so no payload to wait for.
# Exit the while loop
break
}
} else {
# No Content-Length header so assume there is no payload to wait for.
# Exit the while loop
break
}
}
}
# Get the response
if {$static::oauth_debug} { log local0. "$log_prefix Recv data ([string length $recv_data] bytes) in [expr {[clock clicks -milliseconds] - $start}] ms:\
<$recv_data>, peek status: <$peek_status>" }
close $conn
if {$static::oauth_debug} { log local0. "$log_prefix Closed, conn info: <[connect info -status $conn]>" }
set result [string range $recv_data 0 [string first "\r\n" $recv_data ]]
if {$static::oauth_debug} { log local0. "$log_prefix Access Token Response Code : $result" }
if { !([getfield $result " " 2] == 200) } {
if {$static::oauth_debug} { log local0. "$log_prefix Access Token Request FAILED"}
ACCESS::session data set session.custom.oauth.result 0
HTTP::respond 302 location "/my.policy"
return
}
# Extract Access Token & store it
regsub -- {.*"access_token"\s*:\s*"([^"]*)",.*} $recv_data {\1} access_token
ACCESS::session data set session.oauth.access_token $access_token
ACCESS::session data set session.oauth.token_raw $recv_data
if {$static::oauth_debug} { log local0. "$log_prefix Access Token : $access_token" }
set OA2userinfo "GET /oauth2/v2/userinfo HTTP/1.1\r\n"
append OA2userinfo "Host: www.googleapis.com\r\n"
append OA2userinfo "Content-length: 0\r\n"
append OA2userinfo "Authorization: Bearer $access_token\r\n\r\n"
if {$static::oauth_debug} { log local0. "$log_prefix User Info Query: $OA2userinfo" }
set conn [connect -protocol TCP -timeout $static::timeout -idle 30 -status conn_status $static::connect_proxy]
if {$static::oauth_debug} { log local0. "$log_prefix Connect returns: <$conn> and conn status: <$conn_status>" }
# Check if sideband connection was not established to server
if {$conn eq ""}{
if {$static::oauth_debug} { log local0. "$log_prefix Connection could not be established " }
return
}
# Get the current connection status & send data
set conn_info [connect info -idle -status $conn]
if {$static::oauth_debug} { log local0. "$log_prefix Connect info: <$conn_info>" }
set send_info [send -timeout $static::timeout -status send_status $conn $OA2userinfo]
if {$static::oauth_debug} { log local0. "$log_prefix Sent $send_info bytes, send status: <$send_status>" }
set start [clock clicks -milliseconds]
for {set i 0} {$i <= $static::retries} {incr i}{
# See what data we have received so far on the connection within the interval timeout
set recv_data [recv -peek -status peek_status -timeout $static::interval $conn]
if {$static::oauth_debug > 1} { log local0. "$log_prefix Peek ([string length $recv_data]): $recv_data" }
# Check if we have received the response headers (terminated by two CRLFs)
if {[string match "HTTP/*\r\n\r\n*" $recv_data]}{
if {$static::oauth_debug} { log local0. "$log_prefix Found the end of the HTTP headers" }
# Look for a content-length header in the data we have received so far
if {[string match -nocase "*Content-Length: *" $recv_data]}{
# Calculate the length of the response headers plus the payload by parsing the Content-Length header
# Get the index of the end of the HTTP headers
set header_length [expr {[string first "\r\n\r\n" $recv_data] + 4}]
set payload_length [findstr [string tolower $recv_data] "content-length: " 16 "\r"]
if {$static::oauth_debug} { log local0. "$log_prefix \$header_length: $header_length, \$payload_length: $payload_length" }
# If the payload length is greater than 0
if {$payload_length ne "" and $payload_length > 0}{
if {$static::oauth_debug} { log local0. "$log_prefix Peeking for [expr {$header_length + $payload_length}] bytes to get the payload" }
set recv_data [recv -peek -timeout $static::timeout -status recv_status [expr {$header_length + $payload_length}] $conn]
# Exit the while loop
break
} else {
# Content-Length header is 0 so no payload to wait for.
# Exit the while loop
break
}
} else {
# No Content-Length header so assume there is no payload to wait for.
# Exit the while loop
break
}
}
}
# Get the response
if {$static::oauth_debug} { log local0. "$log_prefix Recv data ([string length $recv_data] bytes) in [expr {[clock clicks -milliseconds] - $start}] ms:\
<$recv_data>, peek status: <$peek_status>" }
close $conn
if {$static::oauth_debug} { log local0. "$log_prefix Closed, conn info: <[connect info -status $conn]>" }
set result [string range $recv_data 0 [string first "\r\n" $recv_data ]]
if {$static::oauth_debug} { log local0. "$log_prefix User Info Response Code : $result" }
if { !([getfield $result " " 2] == 200) } {
if {$static::oauth_debug} { log local0. "$log_prefix User Info Request FAILED"}
ACCESS::session data set session.oauth.result 0
} else {
ACCESS::session data set session.oauth.result 1
}
# Extract OAuth Data & Save
regsub -- {.*"id"\s*:\s*"([^"]*)",.*} $recv_data {\1} id
regsub -- {.*"email"\s*:\s*"([^"]*)",.*} $recv_data {\1} useremail
regsub -- {.*"name"\s*:\s*"([^"]*)",.*} $recv_data {\1} name
regsub -- {.*"given_name"\s*:\s*"([^"]*)",.*} $recv_data {\1} given_name
regsub -- {.*"family_name"\s*:\s*"([^"]*)",.*} $recv_data {\1} family_name
regsub -- {.*"link"\s*:\s*"([^"]*)",.*} $recv_data {\1} link
regsub -- {.*"gender"\s*:\s*"([^"]*)",.*} $recv_data {\1} gender
ACCESS::session data set session.oauth.id $id
ACCESS::session data set session.oauth.email $useremail
ACCESS::session data set session.oauth.name $name
ACCESS::session data set session.oauth.given_name $given_name
ACCESS::session data set session.oauth.family_name $family_name
ACCESS::session data set session.oauth.link $link
ACCESS::session data set session.oauth.gender $gender
ACCESS::session data set session.oauth.user_raw $recv_data
HTTP::respond 302 location "/my.policy"
}
}
HTTP_Host_Connect iRule¶
when RULE_INIT {
set static::dns "8.8.8.8"
}
when HTTP_REQUEST {
set IPlist [RESOLV::lookup @$static::dns -a [HTTP::host]]
if {$IPlist eq ""}{
# Input wasn't an IP address, take some default action?
} else {
# Select the IP
node [lindex $IPlist 0] 443
}
}
The BIG-IP API Reference documentation contains community-contributed content. F5 does not monitor or control community code contributions. We make no guarantees or warranties regarding the available code, and it may contain errors, defects, bugs, inaccuracies, or security vulnerabilities. Your access to and use of any code available in the BIG-IP API reference guides is solely at your own risk.