F5 BIG-IP Access Policy Manager (APM) - Google Authenticator and Microsoft Authenticator

Introduction

In our walkthrough we are refreshing an existing time-based one-time password (TOTP) deployment Two-Factor Authentication With Google Authenticator And APM

In our walkthrough we are following the below assumptions,

  • Secret key is generated outside of F5 and saved to Active Directory (AD) user attribute.
  • F5 APM should be able to query AD user attribute (for example, in our case it's called serialNumber).
  • We have two separate portals,
    • One portal for Token generation and QR scanning.
    • One portal for Application access.

Lab guide

Phase 1: Token Generation

  1. User logs in to the token generation portal and authenticates with AD credentials.
  2. F5 APM authenticates the user with AD and query the attribute for the secret key.
  3. F5 APM presnets the QR code for the user to be scanned whether by Google or Microsoft Authenticators.

Phase 2: Token verification

  1. User Access the application and authenticates with AD credentials.
  2. Once user is successfully authenticated, the user is prompted to provide the one-time password (OTP).
  3. Once F5 verifies the provided OTP, the user is allowed to access the application.

Verification iRule

when ACCESS_POLICY_AGENT_EVENT {

    if { [ACCESS::policy agent_id] eq "ga_code_verify" } {
        ### Google Authenticator verification settings ###

        # lock the user out after x attempts for a period of x seconds
        set static::lockout_attempts 3
        set static::lockout_period 30

        # logon page session variable name for code attempt form field
        set static::ga_code_form_field "ga_code_attempt"

        # key (shared secret) storage method: ldap, ad, or datagroup
        set static::ga_key_storage "ad"

        # LDAP attribute for key if storing in LDAP (optional)
        set static::ga_key_ldap_attr "google_auth_key"

        # Active Directory attribute for key if storing in AD (optional)
        set static::ga_key_ad_attr [ACCESS::session data get "session.ad.last.attr.serialNumber"]

        # datagroup name if storing key in a datagroup (optional)
        set static::ga_key_dg "google_auth_keys"


        #####################################
        ### DO NOT MODIFY BELOW THIS LINE ###
        #####################################

        # set lockout table
        set static::lockout_state_table "[virtual name]_lockout_status"

        # set variables from APM logon page
        set username [ACCESS::session data get session.logon.last.username]
        set ga_code_attempt [ACCESS::session data get session.logon.last.$static::ga_code_form_field] 

        # retrieve key from specified storage
        set ga_key ""

        switch $static::ga_key_storage {
            ldap {
                set ga_key [ACCESS::session data get session.ldap.last.attr.$static::ga_key_ldap_attr]
            }
            ad {
                set ga_key [ACCESS::session data get "session.ad.last.attr.serialNumber"]
            }
            datagroup {
                set ga_key [class lookup $username $static::ga_key_dg]
            }
        }

        # increment the number of login attempts for the user
        set prev_attempts [table incr -notouch -subtable $static::lockout_state_table $username]
        table timeout -subtable $static::lockout_state_table $username $static::lockout_period

        # verification result value: 
        # 0 = successful
        # 1 = failed
        # 2 = no key found
        # 3 = invalid key length
        # 4 = user locked out

        # make sure that the user isn't locked out before calculating GA code
        if { $prev_attempts <= $static::lockout_attempts } {

            # check that a valid key was retrieved, then proceed
#Update the key length based on the organization requirements
            if { [string length $ga_key] == 16 } {
                # begin - Base32 decode to binary

                # Base32 alphabet (see RFC 4648)
                array set static::b32_alphabet {
                    A 0  B 1  C 2  D 3
                    E 4  F 5  G 6  H 7
                    I 8  J 9  K 10 L 11
                    M 12 N 13 O 14 P 15
                    Q 16 R 17 S 18 T 19
                    U 20 V 21 W 22 X 23
                    Y 24 Z 25 2 26 3 27
                    4 28 5 29 6 30 7 31
                }

                set ga_key [string toupper $ga_key]
                set l [string length $ga_key]
                set n 0
                set j 0
                set ga_key_bin ""

                for { set i 0 } { $i < $l } { incr i } {
                    set n [expr $n << 5]
                    set n [expr $n + $static::b32_alphabet([string index $ga_key $i])]
                    set j [incr j 5]

                    if { $j >= 8 } {
                        set j [incr j -8]
                        append ga_key_bin [format %c [expr ($n & (0xFF << $j)) >> $j]]
                    }
                }

                # end - Base32 decode to binary

                # begin - HMAC-SHA1 calculation of Google Auth token

                set time [binary format W* [expr [clock seconds] / 30]]

                set ipad ""
                set opad ""

                for { set j 0 } { $j < [string length $ga_key_bin] } { incr j } {
                    binary scan $ga_key_bin @${j}H2 k
                    set o [expr 0x$k ^ 0x5C]
                    set i [expr 0x$k ^ 0x36]
                    append ipad [format %c $i]
                    append opad [format %c $o]
                }

                while { $j < 64 } {
                    append ipad 6
                    append opad \\
                    incr j
                }

                binary scan [sha1 $opad[sha1 ${ipad}${time}]] H* token

                # end - HMAC-SHA1 calculation of Google Auth hex token

                # begin - extract code from Google Auth hex token

                set offset [expr ([scan [string index $token end] %x] & 0x0F) << 1]
                set ga_code [expr (0x[string range $token $offset [expr $offset + 7]] & 0x7FFFFFFF) % 1000000]
                set ga_code [format %06d $ga_code]

                # end - extract code from Google Auth hex token

                if { $ga_code_attempt eq $ga_code } {
                    # code verification successful
                    set ga_result 0
                } else {
                    # code verification failed
                    set ga_result 1
                }
            } elseif { [string length $ga_key] > 0 } {
                # invalid key length, greater than 0, but not length not equal to 16 chars
                set ga_result 3
            } else {
                # could not retrieve user's key
                set ga_result 2
            }
        } else {
            # user locked out due to too many failed attempts
            set ga_result 4
        }

        # set code verification result in session variable
        ACCESS::session data set session.custom.ga_result $ga_result
    }
}

Related Content

Note, Scan the Article photo for more BIG-IP Access Policy Manager (APM) info.

 

 

Published May 08, 2023
Version 1.0
  • HiVladimir_Akhmarov  , Yes I saw your project prior to working on this article. it looks great (Y)

    In this article the main approach is to work with customers who rely on local secret generations and assign them to the users AD accounts, that's why F5 APM query that attribute from AD and verify the token based on it.

    Thanks for your comment ^^

  • liborj's avatar
    liborj
    Icon for Nimbostratus rankNimbostratus

    hello Vladimir_Akhmarov does your solution work with the F5 management console login?

    we are looking for some kind of MFA but struggling to get a straight direction from F5. We only need that for the admins to login to the F5 management UI. One time password or code would work too.

    thank you for sharing

    Libor

     

  • dupapa's avatar
    dupapa
    Icon for Nimbostratus rankNimbostratus

    Hello mmahdy!

    Thanks for your sharing of those technical details!

    I have some questions realted to your technical details:

    1. is the ga_code_submit another Logon page or even External Logon page
    2. if the TOTP validation fails, the APM evaluation process will terminate with Deny, which is ok for demo purpose 🙂 However, if I have to allow max three TOTP attempts, how can we realize it without restarting a brand new APM evaluation process?

    that is, when the times of failuare is less than 3, the APM evaluation process should flow to the ga_code_submit again rather than the Deny ending.

    for instance, the built-in OTP Verify with VPE supports max 3 attempts without terminating the ongoing APM evaluation process.

     

  • dupapa Thanks a lot for your comment, regarding your questions,

    1- Yes, Another logon page.

    2- For this to be acheived, we can put the validation within a macro and allow for multiple repeats, I will try this out in the lab and update the article with the results.

    The OTP generate and verify relies on token being generated at F5, in some environments, there's a need for secret key used for generation and validation to be external to F5, from there the need for such approach was made. So, it's almost like we are creating a custom OTP validate plugin, will try out the macro approach and let you know.

    Thanks again, 

    Mohamed

  • dupapa's avatar
    dupapa
    Icon for Nimbostratus rankNimbostratus

    momahdy I'm looking forward to your new updatings. Although I tried to put the TOTP onboarding and  verification in a Macro as I did for the built-in OTP Verify (F5 Help Desk suggested it to me last year), my TOTP didn't work with a invalid TOTP code, while it works well when the TOTP code is valid.  I've actually registered a support case with F5, https://my.f5.com/manage/s/case/500Hs000021KOOAIA4/max-logon-attempts-allowed-with-my-own-timebased-onetimepassword-totp It'll be great if you could help me with it