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:

  1. [root@exch:Active:Standalone] config # tmsh list ltm virtual www.google.com  
  2. ltm virtual www.google.com {  
  3.     destination 10.1.10.7:http  
  4.     ip-protocol tcp  
  5.     mask 255.255.255.255  
  6.     pool www.google.com  
  7.     profiles {  
  8.         serverssl {  
  9.             context serverside  
  10.         }  
  11.         tcp { }  
  12.     }  
  13.     source 0.0.0.0/0  
  14.     source-address-translation {  
  15.         type automap  
  16.     }  
  17.     translate-address enabled  
  18.     translate-port enabled  
  19.     vs-index 15  
  20. }

This is the pool of Google servers:

  1. [root@exch:Active:Standalone] config # tmsh list ltm pool www.google.com  
  2. ltm pool www.google.com {  
  3.     members {  
  4.         www.google.com:https {  
  5.             fqdn {  
  6.                 autopopulate enabled  
  7.                 name www.google.com  
  8.             }  
  9.             state fqdn-up  
  10.         }  
  11.         www.google.com-74.125.28.103:https {  
  12.             address 74.125.28.103  
  13.             fqdn { name www.google.com }  
  14.         }  
  15.         www.google.com-74.125.28.104:https {  
  16.             address 74.125.28.104  
  17.             fqdn { name www.google.com }  
  18.         }  
  19.         www.google.com-74.125.28.105:https {  
  20.             address 74.125.28.105  
  21.             fqdn { name www.google.com }  
  22.         }  
  23.         www.google.com-74.125.28.106:https {  
  24.             address 74.125.28.106  
  25.             fqdn { name www.google.com }  
  26.         }  
  27.         www.google.com-74.125.28.147:https {  
  28.             address 74.125.28.147  
  29.             fqdn { name www.google.com }  
  30.         }  
  31.         www.google.com-74.125.28.99:https {  
  32.             address 74.125.28.99  
  33.             fqdn { name www.google.com }  
  34.         }  
  35.     }  
  36. }  

Additional notes :

  1. The iRule does not rely on the DNS server.  Except for the Google API server pool FQDN.
  2. The iRule does not rely on tables to keep session information.
  3. The iRule does not use tables to keep persistence information.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

  1. ######################################  
  2. # NOTE:  
  3. # If not using SSL, the whole CLIENTSSL_HANDSHAKE event should be removed or commented out.  
  4.   
  5. when CLIENTSSL_HANDSHAKE {  
  6.     #log local0. "[SSL::sessionid]"  
  7.     set cookie_encr_key [string range [SSL::sessionid] 0 31]  
  8.     set secure "; secure"  
  9. }  
  10. ######################################  
  11.   
  12. when RULE_INIT {  
  13.   # set public and private reCAPTCHA keys (obtain from https://www.google.com/recaptcha/admin#list)  
  14.   set static::recaptcha_public_key "6LccTygTAAAAA*********izaZ-4LpEXTf35ju1x"  
  15.   set static::recaptcha_private_key "6LccTygTAAAAAA********Gvc_-jLJ_KTtKGvLP3"  
  16.   
  17.   # set the recaptcha session cookie timeout in SECONDS.  Only needed if using https.  
  18.   # If not using https, this timeout is only defense against Cookie replay attack.  
  19.   set static::recaptcha_session_timeout ""  
  20.   
  21.   # log level, 0 = silent, 1 = log client interaction, 2 = log all interaction with client and Google  
  22.   set static::logging 1  
  23.   # begin - HTML for reCAPTCHA form page  
  24.   set static::recaptcha_challenge_form {  
  25.   <html>  
  26.   <head>  
  27.     <title>reCAPTCHA demo: Simple page</title>  
  28.      <script src="https://www.google.com/recaptcha/api.js" async defer></script>  
  29.   </head>  
  30.   <body>  
  31.     <form action="?" method="POST">  
  32.       <div class="g-recaptcha" data-sitekey="}
  33.  
  34.    append static::recaptcha_challenge_form $static::recaptcha_public_key
  35.   append static::recaptcha_challenge_form {"></div>  
  36.       <br/>  
  37.       <input type="submit" value="Submit">  
  38.     </form>  
  39.   </body>  
  40. </html>  
  41.   }  
  42.   # end - HTML for reCAPTCHA form page  
  43. }  
  44.   
  45.   
  46.   
  47. when CLIENT_ACCEPTED {  
  48.     if { $static::logging >= 2 } {  
  49.         set session_identifier "[IP::client_addr]:[TCP::client_port]/[IP::local_addr]:[TCP::local_port]"  
  50.         log local0. "New session: $session_identifier"  
  51.     }  
  52.     # The following variables will be over-written if using SSL  
  53.     set secure ""  
  54.     set cookie_encr_key [string range $static::recaptcha_private_key 8 39]  
  55. }  
  56.   
  57. when HTTP_REQUEST priority 10 {  
  58.   
  59.     #log local0. "[HTTP::method] - [HTTP::uri] - [HTTP::path]"  
  60.     # no_bot_cookie is the value that will be stuffed in the i_am_not_a_bot cookie during response  
  61.     set no_bot_cookie [IP::client_addr]-[TCP::client_port]-[HTTP::uri]  
  62.   
  63.     if {[HTTP::cookie exists i_am_not_a_bot] and [AES::decrypt $cookie_encr_key [b64decode [HTTP::cookie i_am_not_a_bot]]] ne "" } {  
  64.   
  65.             if { $static::logging >= 1 } {  
  66.                 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]]]"  
  67.                 log local0. "Request to [HTTP::uri] with valid cookie ALLOWED."  
  68.             }  
  69.   
  70.     } else {  
  71.   
  72.             if {[HTTP::method] equals "POST" } {  
  73.                 log local0. "Collecting. [HTTP::header Content-Length]"  
  74.                 HTTP::collect 100  
  75.                 return  
  76.             } else {  
  77.                 set redirect_uri [HTTP::uri]  
  78.                 set no_bot_cookie [IP::client_addr]-[TCP::client_port]-[HTTP::uri]  
  79.                 HTTP::respond 200 content $static::recaptcha_challenge_form Connection "Keep-Alive"  
  80.             }  
  81.     }  
  82. }  
  83.   
  84.   
  85. when HTTP_REQUEST_DATA {  
  86.     if { $static::logging >= 2 } {  
  87.         # Body of POST starts with parameter name: g-recaptcha-response  
  88.         # The 21 is the length of the string: 'g-recaptcha-response='  
  89.         log local0. "Session $session_identifier : user responded with:"  
  90.         log local0. "[string range [HTTP::payload] 21 end]"  
  91.     }  
  92.     set recaptcha_response_field [string range [HTTP::payload] 21 end]  
  93.   
  94.     # assemble body of reCAPTCHA verification POST  
  95.     set recaptcha_post_data "secret=$static::recaptcha_private_key&"  
  96.     append recaptcha_post_data "remoteip=[IP::remote_addr]&"  
  97.     append recaptcha_post_data "response=$recaptcha_response_field"  
  98.     # calculate Content-length header value  
  99.     set recaptcha_post_content_length [string length $recaptcha_post_data]  
  100.     # assemble reCAPTCHA verification POST request  
  101.     set recaptcha_verify_request "POST /recaptcha/api/siteverify HTTP/1.1\r\n"  
  102.     append recaptcha_verify_request "Host: www.google.com\r\n"  
  103.     append recaptcha_verify_request "Accept: */*\r\n"  
  104.     append recaptcha_verify_request "Content-length: $recaptcha_post_content_length\r\n"  
  105.     append recaptcha_verify_request "Content-type: application/x-www-form-urlencoded\r\n\r\n"  
  106.     append recaptcha_verify_request "$recaptcha_post_data"  
  107.     # Sideband connection must go through internal virtual server with serverside SSL in order  
  108.     # to connect to a server accepting SSL connections.  
  109.     # Below is the name of internal virtual server.  
  110.     
  111.     # establish connection to Google  
  112.     set conn_id [connect -timeout 1000 -idle 30 www.google.com]  
  113.   
  114.     if { $conn_id != "" } {  
  115.         # send reCATPCHA verification request to Google  
  116.         send -timeout 1000 -status send_status $conn_id $recaptcha_verify_request  
  117.   
  118.         # receive reCAPTCHA verification response from Google  
  119.         set recaptcha_verify_response [recv -timeout 1000 -status recv_info $conn_id]  
  120.         if { $recaptcha_verify_response equals "" } { log local0. "Connection to google via internal virtual not successful." }  
  121.         if { $static::logging >= 2 } {  
  122.             log local0. "Received verification response from Google:  $recaptcha_verify_response"  
  123.         }  
  124.   
  125.         close $conn_id  
  126.         # process reCAPTCHA verification response and remove user session from trigger table if successful  
  127.         if { $recaptcha_verify_response contains "success\": true" } {  
  128.             set redirect_uri [HTTP::uri]  
  129.             if { $static::logging >= 1 } {  
  130.                 log local0. "passed captcha.  Redirecting to $redirect_uri with new cookie."  
  131.             }  
  132.   
  133.             if { $static::recaptcha_session_timeout != ""} {  
  134.                 #set fmt "%a, %d %b %Y %H:%M:%S %Z"  
  135.                 set fmt "%a, %d %h %Y %T GMT"  
  136.                 set expiry "; expires=[clock format [expr [clock seconds] + $static::recaptcha_session_timeout] -format $fmt -gmt true]"  
  137.             } else {  
  138.                 set expiry ""  
  139.             }  
  140.   
  141.             set cookie "i_am_not_a_bot=[b64encode [AES::encrypt $cookie_encr_key $no_bot_cookie]]; path= /; HTTPonly  $expiry"  
  142.             HTTP::respond 302 Location $redirect_uri Set-Cookie $cookie  
  143.   
  144.          } else {  
  145.             HTTP::respond 200 content $static::recaptcha_challenge_form  
  146.             if { $static::logging >= 1 } {  
  147.                 log local0. "failed captcha"  
  148.             }  
  149.          }  
  150.     } else {  
  151.         log local0. "Could not contact google.com to verify token."  
  152.         HTTP::respond 500 content "<html>Could not contact goole.com:443</html>" Connection close  
  153.     }  
  154.     HTTP::release  
  155.   
  156. }  
Updated Jun 06, 2023
Version 3.0
  • This works great, cookie method is much more reliable that the previous table method. Thank you!

     

  • 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

     

  • 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.

     

  • 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.