Google reCAPTCHA v2 challenge iRule, integration with BIG-IP virtual server
This iRule integrates a reCAPTCHA v2 (like the one shown below) with your virtual server.
This iRule is similar to the existing reCAPTCHA v1 on devcentral: https://devcentral.f5.com/s/articles/google-recaptcha-challenge-irule
Because reCAPTCHA v2 uses Google API for Token validation, the two iRules are different in structure. Nonetheless this iRule borrows a good bit of code from the v1 iRule linked above.
Before you can use this iRule, you must register and obtain a reCAPTCHA API key pair at this link: reCAPTCHA: Easy on Humans, Hard on Bots
The key pair are entered in the iRule under the RULE_INIT event, (lines 14 and 15)
You may also want to read the Getting Started page. https://developers.google.com/recaptcha/docs/start
In order to connect to the Google API using HTTPS, the iRule uses sideband commands that target an internal virtual server. The name of this server is arbitrary, but in my iRule I use "www.google.com". See line 112. This virtual must be created and assigned a SERVERSSL profile and a pool. The pool only needs one member, but if you specify an FQDN instead of an IP address, all the IP addresses returned by the DNS server will become pool members.
Here is the internal virtual server which is targeted by the sideband commands:
[root@exch:Active:Standalone] config # tmsh list ltm virtual www.google.com
ltm virtual www.google.com {
destination 10.1.10.7:http
ip-protocol tcp
mask 255.255.255.255
pool www.google.com
profiles {
serverssl {
context serverside
}
tcp { }
}
source 0.0.0.0/0
source-address-translation {
type automap
}
translate-address enabled
translate-port enabled
vs-index 15
}
This is the pool of Google servers:
[root@exch:Active:Standalone] config # tmsh list ltm pool www.google.com
ltm pool www.google.com {
members {
www.google.com:https {
fqdn {
autopopulate enabled
name www.google.com
}
state fqdn-up
}
www.google.com-74.125.28.103:https {
address 74.125.28.103
fqdn { name www.google.com }
}
www.google.com-74.125.28.104:https {
address 74.125.28.104
fqdn { name www.google.com }
}
www.google.com-74.125.28.105:https {
address 74.125.28.105
fqdn { name www.google.com }
}
www.google.com-74.125.28.106:https {
address 74.125.28.106
fqdn { name www.google.com }
}
www.google.com-74.125.28.147:https {
address 74.125.28.147
fqdn { name www.google.com }
}
www.google.com-74.125.28.99:https {
address 74.125.28.99
fqdn { name www.google.com }
}
}
}
Additional notes :
- The iRule does not rely on the DNS server. Except for the Google API server pool FQDN.
- The iRule does not rely on tables to keep session information.
- The iRule does not use tables to keep persistence information.
- The iRule uses a cookie (i_am_not_bot) to keep track of users who successfully solved the google captcha. When client connection is SSL/TLS based, this cookie uses the SSL session ID to secure the cookie, as per https://tools.ietf.org/html/rfc6896#section-7.2.1 . By default this is a session cookie which will expire when the browser is restarted. User can specify a re-captcha2-session-Cookie timeout in seconds relative to current time. If not using SSL, cookie expiration can be used as the only defense against cookie hijacking attacks.
- If you want to use this iRule without SSL, You will need to remove the SSL event CLIENTSSL_HANDSHAKE. You should also set the Cookie Timeout variable under the RULE_INIT event.
- If the google API server sideband connection is not successful, you will get a message in /var/log/ltm. In this case, you can troubleshoot the google virtual server separately. If you can browse to the IP address you assigned for this virtual and receive the google search engine page, you are good. Note, this virtual does not need an HTTP profile but, adding one could aid in troubleshooting.
- Set the global variable "static::logging" to level 1, 2 or 3 for the desired level of verbose logging. Do not leave it at level 3 for production.
The iRule:
######################################
# NOTE:
# If not using SSL, the whole CLIENTSSL_HANDSHAKE event should be removed or commented out.
when CLIENTSSL_HANDSHAKE {
#log local0. "[SSL::sessionid]"
set cookie_encr_key [string range [SSL::sessionid] 0 31]
set secure "; secure"
}
######################################
when RULE_INIT {
# set public and private reCAPTCHA keys (obtain from https://www.google.com/recaptcha/admin#list)
set static::recaptcha_public_key "6LccTygTAAAAA*********izaZ-4LpEXTf35ju1x"
set static::recaptcha_private_key "6LccTygTAAAAAA********Gvc_-jLJ_KTtKGvLP3"
# set the recaptcha session cookie timeout in SECONDS. Only needed if using https.
# If not using https, this timeout is only defense against Cookie replay attack.
set static::recaptcha_session_timeout ""
# log level, 0 = silent, 1 = log client interaction, 2 = log all interaction with client and Google
set static::logging 1
# begin - HTML for reCAPTCHA form page
set static::recaptcha_challenge_form {
<html>
<head>
<title>reCAPTCHA demo: Simple page</title>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body>
<form action="?" method="POST">
<div class="g-recaptcha" data-sitekey="}
append static::recaptcha_challenge_form $static::recaptcha_public_key
append static::recaptcha_challenge_form {"></div>
<br/>
<input type="submit" value="Submit">
</form>
</body>
</html>
}
# end - HTML for reCAPTCHA form page
}
when CLIENT_ACCEPTED {
if { $static::logging >= 2 } {
set session_identifier "[IP::client_addr]:[TCP::client_port]/[IP::local_addr]:[TCP::local_port]"
log local0. "New session: $session_identifier"
}
# The following variables will be over-written if using SSL
set secure ""
set cookie_encr_key [string range $static::recaptcha_private_key 8 39]
}
when HTTP_REQUEST priority 10 {
#log local0. "[HTTP::method] - [HTTP::uri] - [HTTP::path]"
# no_bot_cookie is the value that will be stuffed in the i_am_not_a_bot cookie during response
set no_bot_cookie [IP::client_addr]-[TCP::client_port]-[HTTP::uri]
if {[HTTP::cookie exists i_am_not_a_bot] and [AES::decrypt $cookie_encr_key [b64decode [HTTP::cookie i_am_not_a_bot]]] ne "" } {
if { $static::logging >= 1 } {
log local0. "URI: [HTTP::uri] Cookie: [HTTP::cookie exists i_am_not_a_bot] - [AES::decrypt $cookie_encr_key [b64decode [HTTP::cookie i_am_not_a_bot]]]"
log local0. "Request to [HTTP::uri] with valid cookie ALLOWED."
}
} else {
if {[HTTP::method] equals "POST" } {
log local0. "Collecting. [HTTP::header Content-Length]"
HTTP::collect 100
return
} else {
set redirect_uri [HTTP::uri]
set no_bot_cookie [IP::client_addr]-[TCP::client_port]-[HTTP::uri]
HTTP::respond 200 content $static::recaptcha_challenge_form Connection "Keep-Alive"
}
}
}
when HTTP_REQUEST_DATA {
if { $static::logging >= 2 } {
# Body of POST starts with parameter name: g-recaptcha-response
# The 21 is the length of the string: 'g-recaptcha-response='
log local0. "Session $session_identifier : user responded with:"
log local0. "[string range [HTTP::payload] 21 end]"
}
set recaptcha_response_field [string range [HTTP::payload] 21 end]
# assemble body of reCAPTCHA verification POST
set recaptcha_post_data "secret=$static::recaptcha_private_key&"
append recaptcha_post_data "remoteip=[IP::remote_addr]&"
append recaptcha_post_data "response=$recaptcha_response_field"
# calculate Content-length header value
set recaptcha_post_content_length [string length $recaptcha_post_data]
# assemble reCAPTCHA verification POST request
set recaptcha_verify_request "POST /recaptcha/api/siteverify HTTP/1.1\r\n"
append recaptcha_verify_request "Host: www.google.com\r\n"
append recaptcha_verify_request "Accept: */*\r\n"
append recaptcha_verify_request "Content-length: $recaptcha_post_content_length\r\n"
append recaptcha_verify_request "Content-type: application/x-www-form-urlencoded\r\n\r\n"
append recaptcha_verify_request "$recaptcha_post_data"
# Sideband connection must go through internal virtual server with serverside SSL in order
# to connect to a server accepting SSL connections.
# Below is the name of internal virtual server.
# establish connection to Google
set conn_id [connect -timeout 1000 -idle 30 www.google.com]
if { $conn_id != "" } {
# send reCATPCHA verification request to Google
send -timeout 1000 -status send_status $conn_id $recaptcha_verify_request
# receive reCAPTCHA verification response from Google
set recaptcha_verify_response [recv -timeout 1000 -status recv_info $conn_id]
if { $recaptcha_verify_response equals "" } { log local0. "Connection to google via internal virtual not successful." }
if { $static::logging >= 2 } {
log local0. "Received verification response from Google: $recaptcha_verify_response"
}
close $conn_id
# process reCAPTCHA verification response and remove user session from trigger table if successful
if { $recaptcha_verify_response contains "success\": true" } {
set redirect_uri [HTTP::uri]
if { $static::logging >= 1 } {
log local0. "passed captcha. Redirecting to $redirect_uri with new cookie."
}
if { $static::recaptcha_session_timeout != ""} {
#set fmt "%a, %d %b %Y %H:%M:%S %Z"
set fmt "%a, %d %h %Y %T GMT"
set expiry "; expires=[clock format [expr [clock seconds] + $static::recaptcha_session_timeout] -format $fmt -gmt true]"
} else {
set expiry ""
}
set cookie "i_am_not_a_bot=[b64encode [AES::encrypt $cookie_encr_key $no_bot_cookie]]; path= /; HTTPonly $expiry"
HTTP::respond 302 Location $redirect_uri Set-Cookie $cookie
} else {
HTTP::respond 200 content $static::recaptcha_challenge_form
if { $static::logging >= 1 } {
log local0. "failed captcha"
}
}
} else {
log local0. "Could not contact google.com to verify token."
HTTP::respond 500 content "<html>Could not contact goole.com:443</html>" Connection close
}
HTTP::release
}
- Jonathan_GaikwaNimbostratus
This works great, cookie method is much more reliable that the previous table method. Thank you!
- cengizmuratNimbostratus
Hi all,
I deployed in our environment, we are able to send verify request but google always return null values on response. When I manually call https://www.google.com/recaptcha/api/siteverify?secret=your_secret&response=response_string&remoteip=user_ip_address I got success true value.
What would be wrong about that ?
Thanks.
- Hardik_Mehta_32Nimbostratus
Hi all,
I tried this in our live environment,I tried to deployed this on Microsoft OWA but we are getting some problem. We have created OWA through iApps Templates in LTM,now when we are using this Captcha iRule it redirects to Captcha page,after solving it redirects to Login page of OWA,but when user enters credentials it again redirects to Captcha page and loop continuous Please help regarding this
Thanks in advance
- DamP_320463Nimbostratus
Hi, any way to integrate this with APM on BIG-IP v12?
Thanks
- Ryan_378122Nimbostratus
Is there a way to apply this when a specific URL is requested? There are sub paths of the virtual server we need to make sure dont get the captcha, or even if a response contains a certain string. For instance, when you load the page and enter incorrect login information, it returns a 403 redirect and ends in a string indicating the failed login error. We’d like to utilize this to potentially work it into that flow. I’m still very new to F5 so I’m learning a lot on what is and is not possible.
- SalishSeaSecurityAltostratus
I tried this on BigIP 12.1.4. I was testing with recaptcha_session_timeout set, and saw line 136 throwing an error. Adding curly braces around this expression resolved the issue:
{ [clock seconds] + $static::recaptcha_session_timeout }
HTH anyone else using this script.
With this one correction, works as expected. With the cookie in play, and conditional statements at the top of the HTTP_REQUEST section, I anticipate we can fine-tune access control over individual portions of the web site. Thank you for providing this.