For more information regarding the security incident at F5, the actions we are taking to address it, and our ongoing efforts to protect our customers, click here.

Yubikey 2Factor Authentication with BIG-IP LTM

Problem this snippet solves:

This solution using Yubico's YubiKey OTP generator technology along with their YubiCloud service and BIG-IP LTM to perform Two-Factor Authentication for an application. Please reference the tech tip for details.

NOTE: Built on version 11.4, though the only 11.4 dependency is the nonce generator proc that can be rolled into the CLIENT_ACCEPTED event for earlier versions. The non-proc version is of the CLIENT_ACCEPTED event is shown in the tech tip reference above.

How to use this snippet:


Code :

# Required Data-groups

ltm data-group internal authorized_users {
    records {
        jason {
            data /md5sum of password here/
        }
        jrahm {
            data /md5sum of password here/
        }
    }
    type string
}
ltm data-group internal yubikey_users {
    records {
        jrahm {
            data /yubikey serial number here/
        }
    }
    type string
}

# Source Code - NOTE: You'll need your own YubiCloud API Client ID And Secret Key to plug into the static variables

### PROC FOR NONCE GENERATOR ###
proc randomNumberGenerator {length {chars "0123456789"}} {
  set range [expr {[string length $chars]-1}]
  set txt ""
  for {set i 0} {$i < $length} {incr i} {
    set pos [expr {int(rand()*$range)}]
    append txt [string range $chars $pos $pos]
  }
  return $txt
}
when RULE_INIT {
  #for yubico auth
  array set static::modhex_alphabet { 0 c 1 b 2 d 3 e 4 f 5 g 6 h 7 i 8 j 9 k A l B n C r D t E u F v }
  set static::yubico_client_id "xxxxx"
  set static::yubico_secret_key "xxxxx"  
}  
when CLIENT_ACCEPTED {
  set attempts 0
  #for form auth
  set forceauth 1
  set auth_status 2
  set aeskey "AES 128 63544a5e7178677b45366b41405f2dab"
  set ckname BIGIP_AUTH
  #for yubico auth
  set yubico_server [RESOLV::lookup @24.217.0.5 -a "api2.yubico.com"]
  set nonce [call randomNumberGenerator 25]  
}

when HTTP_REQUEST {
  if { not ([HTTP::path] starts_with "/Login_form") } {
    if { [HTTP::cookie exists $ckname] } {
      set cookie_payload [HTTP::cookie value $ckname]
      set decryptedCookie [AES::decrypt $aeskey [b64decode $cookie_payload ]]
      if { not ( $decryptedCookie equals "" ) } {
        # retrieve the auth status from the session table
        set auth_status [session lookup uie $decryptedCookie]
      }
      # If the auth status is 0 then the user is authenticated
      if { $auth_status eq 0 } {
        #Cookie Decrypted & Session Auth valid 
        set forceauth 0
      }
    }
    if {$forceauth eq 1} {
      set orig_uri [HTTP::uri]
      HTTP::redirect "/Login_form?req=$orig_uri"
    }
  } else {
    # If the user is re-directed to the login form then serve the login form from the BigIP
    if { [HTTP::path] starts_with "/Login_form" && [HTTP::method] equals "GET" } {
      # Retrieve the login form from ifile
      HTTP::respond 200 content [ifile get loginformlong] "Content-Type" "text/html"
  return  
    } elseif { [HTTP::path] starts_with "/Login_form" && [HTTP::method] equals "POST" } {
      # Process the login form and auth the user
      HTTP::collect [HTTP::header Content-Length]
    }
  }
}
when HTTP_REQUEST_DATA {
  set namevals [split [HTTP::payload] "&"]
  # Break out the POST data for username and password values
  for {set i 0} {$i < [llength $namevals]} {incr i} {
    set params [split [lindex $namevals $i] "="]
    if { [lindex $params 0] equals "username" } {
      set auth_user [lindex $params 1]
    }
    if { [lindex $params 0] equals "password" } {
      set auth_pw [lindex $params 1]
    }
if { [lindex $params 0] equals "otp" } {
  set auth_otp [lindex $params 1]
} 
  }
  #validate basic authentication (username/password) before trying OTP validation  
  binary scan [md5 $auth_pw] H* password  
  if { $password eq [class lookup $auth_user authorized_users] } {
#start OTP validation here
set yubikey_modhex 1
if { $auth_user ne "" } {
set yubikey_serial [string trimleft [class lookup $auth_user yubikey_users] 0]
if { $yubikey_serial eq "" } {
HTTP::respond 200 content "

No Yubikey on file for username $auth_user. Contact Support.

" return } if { [string is integer -strict $yubikey_serial] } { set yubikey_serial [split [format %012X $yubikey_serial] ""] set yubikey_modhex "" foreach index $yubikey_serial { append yubikey_modhex $static::modhex_alphabet($index) } } } if { $yubikey_modhex equals [string range $auth_otp 0 11] } { ## Build GET request to yubico ## set params "id=$static::yubico_client_id&nonce=$nonce&otp=$auth_otp" set signature [string map { "+" "%2B" } [b64encode [CRYPTO::sign -alg hmac-sha1 -key [b64decode $static::yubico_secret_key] $params]]] set yubico_get_request "GET /wsapi/2.0/verify?$params&h=$signature HTTP/1.1\r\n" append yubico_get_request "Host: api2.yubico.com\r\n" append yubico_get_request "Accept: */*\r\n\r\n" ## Create connection and send request set conn [connect -timeout 1000 -idle 30 $yubico_server:80] send -timeout 1000 -status send_status $conn $yubico_get_request ## Store Response from yubico set yubico_response [recv -timeout 1000 -status recv_info $conn] #set hash [getfield [getfield $yubico_response "\r\n" 9] "=" 2] #set timestamp [getfield [getfield $yubico_response "\r\n" 10] "=" 2] set otp_r [getfield [getfield $yubico_response "\r\n" 11] "=" 2] set nonce_r [getfield [getfield $yubico_response "\r\n" 12] "=" 2] #set sl [getfield [getfield $yubico_response "\r\n" 13] "=" 2] set status [getfield [getfield $yubico_response "\r\n" 14] "=" 2] if { not ( ($status eq "OK") && ($auth_otp eq $otp_r) && ($nonce eq $nonce_r)) } { HTTP::respond 200 content "

Yubico Authentication Failed. Status=$status

" } else { session add uie $auth_user 0 1800 set encrypted_user [b64encode [AES::encrypt $aeskey $auth_user]] set authcookie [format "%s=%s; path=/; " $ckname $encrypted_user] HTTP::respond 302 Location $orig_uri "Set-Cookie" $authcookie } } else { if { $attempts < 3 } { HTTP::redirect "/Login_form?req=$orig_uri" incr attempts } else { HTTP::respond 200 content "You have exceeded the acceptable login attempts. Please try again later." } } } else { if { $attempts < 3 } { HTTP::redirect "/Login_form?req=$orig_uri" incr attempts } else { HTTP::respond 200 content "You have exceeded the acceptable login attempts. Please try again later." } } }
Published Mar 18, 2015
Version 1.0
No CommentsBe the first to comment