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.