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.0JRahm
Admin
Christ Follower, Husband, Father, Technologist. I love community and I especially love THIS community. My background is networking, but I've dabbled in all the F5 iStuff, I'm a recovering Perl guy, and am very much a python enthusiast. Learning alongside all of you in this accelerating industry toward modern apps and architectures.JRahm
Admin
Christ Follower, Husband, Father, Technologist. I love community and I especially love THIS community. My background is networking, but I've dabbled in all the F5 iStuff, I'm a recovering Perl guy, and am very much a python enthusiast. Learning alongside all of you in this accelerating industry toward modern apps and architectures.No CommentsBe the first to comment