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