Yubikey One-Time-Password Authentication with APM

yubikey_4Well my Yubikey 4 arrived today so I had a chance to play around with their one-time-password capabilities – read about their U2F and APM capabilities here. The primary benefit about OTP over U2F is it’s supported across almost every major browser and OS.  This makes the Yubikey 4 a little more palatable for enterprises – note the Yubikey 4 supports both OTP and U2F.

Jason Rahm posted an article on DevCentral regarding 2FA using Yubikey, YubiCloud and BIG-IP LTM  back in 2013.  I’ve adapted this iRules to use APM Agent Events so we can leverage Yubikeys for 2FA in APM.  For more information on Yubikey OTP clients check out the Getting Started Writing Clients page.

Configuration

  1. To configure this you’ll need to add the iRule below to your BIG-IP and XXXXXX with your YubiCloud client ID and Secret Key.
  2. Add a data group (yubikey_users) and populate it with username:serial pairs
  3. add an iRule event to your APM VPE
    1. set the name to OTP Valid
    2. set the ID to “otp_verify”
    3. add a branch rule
      1. name it Yes
      2. add an advanced expression of:

expr { [mcget {session.custom.otp_valid} ] == 1}


### 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 ACCESS_POLICY_AGENT_EVENT {
if { [ACCESS::policy agent_id] eq "otp_verify" } {
# edit these variables
set yubico_client_id "XXXXXX"
set yubico_secret_key "XXXXXX"
# do not edit anything below this line
set nonce [call randomNumberGenerator 25]
set yubico_server [RESOLV::lookup @8.8.8.8 -a "api2.yubico.com"]
if {$yubico_server == ""} {
log local0.error "could not resolve Yubico server"
return
}
ACCESS::session data set "session.custom.otp_valid" 0
ACCESS::session data set "session.custom.is_provisioned" 0
set auth_user [ACCESS::session data get "session.logon.last.username"]
# remove the last 32 characters to reveal the serial number
set yubikey_serial [string trimleft [class lookup $auth_user yubikey_users] 0]
# make sure a yubikey is provisioned for this user
if { $yubikey_serial eq ""} {
log local0.error "no yubikey assigned to $auth_user"
return
} else {
ACCESS::session data set "session.custom.is_provisioned" 1
# extract the Yubikey serial
if { [string is integer -strict $yubikey_serial] } {
# convert to modnex
set yubikey_serial [split [format %012x $yubikey_serial] ""]
set yubikey_modhex ""
#array set 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 }
array set 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}
foreach index $yubikey_serial {
append yubikey_modhex $modhex_alphabet($index)
}
}
}
# do we have an OTP? if so try and verify it
set auth_otp [ACCESS::session data get session.logon.last.otp]
if {$yubikey_modhex equals [string range $auth_otp 0 11]} {
# build GET request to yubico
set params "id=$yubico_client_id&nonce=$nonce&otp=$auth_otp"
set signature [string map { "+" "%2B" } [b64encode [CRYPTO::sign -alg hmac-sha1 -key [b64decode $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 otp_r [getfield [getfield $yubico_response "\r\n" 10] "=" 2]
set nonce_r [getfield [getfield $yubico_response "\r\n" 11] "=" 2]
set status [getfield [getfield $yubico_response "\r\n" 14] "=" 2]
if { ($auth_otp eq $otp_r) && ($nonce eq $nonce_r) } {
ACCESS::session data set "session.custom.otp_valid" 1
}
} else {
log local0.error "error with yubikey serial for $auth_user"
}
}
}

view raw

apm_yubikey_otp

hosted with ❤ by GitHub

Conclusion

No too difficult.  Some ways that we could extend this code would be to try multiple cloud instances (api1.yubico.com-api5.yubico.com) and provide a self enrollment page if the user’s serial number is not in our data group – I’m writing an example of this with Google Authenticator and iRules LX so stay tuned.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.