APM variable assign examples
Problem this snippet solves: APM variable assign is a powerful tool to manipulate APM variable during policy evaluation supporting tcl code. On Devcentral answers, there are lots of variable assignment done with irule event ACCESS_POLICY_AGENT_EVENT. these snippets show how to do the same as irule without irule event. Note : I wrote most of codes, some others are from threads I found in DevCentral Answers section. How to use this snippet: create a variable assign box in VPE, then Add new entry In left side, let custom variable / unsecure default choice set the new variable name (or name of the variable you want to change the value). you should use bold value above tcl code. for timeout changes, you must use bold value above tcl code. In expression : let custom expression default choice paste provided code Username / Domain management session.logon.last.username extract CN from certificate subject and set it in username variable set subject [split [mcget {session.ssl.cert.subject}] ",="]; foreach {name value} $subject { if {[string trim $name] equals "CN"} { return [string trim $value]; } } session.logon.last.username combine username and domain variables expr{"[mcget{session.logon.last.domain}]\\[mcget{session.logon.last.username}]"} session.logon.last.ntdomain extract NT domain name from logon name if { [mcget {session.logon.last.username}] contains "\\" } { set username [string tolower [mcget {session.logon.last.logonname}]]; return [string range $username 0 [expr {[string first "\\" $username] -1}] ]; } else { return {} } one-line code expr {[set username [string tolower [mcget {session.logon.last.logonname}]]] contains "\\" ? [string range $username 0 [expr {[string first "\\" $username] -1}] ] : "" } session.logon.last.domain static assignment from ntdomain switch[stringtolower[mcget{session.logon.last.ntdomain}]]{ "domain1"{return "domain1.local"} "domain2"{return "domain2.local"} default{return "default.local" } } session.logon.last.username Extract username name from logonname (full username from logon page even if split domain from username is checked) setusername[stringtrim[mcget{session.logon.last.logonname}]]; if{$usernamecontains"\\"}{ return[stringrange$username[expr{[stringfirst"\\"$username]+1}]end]; }else{return$username} session.logon.last.upn Extract UPN value from Certificate X509Extension setextension[stringtolower[mcget{session.ssl.cert.x509extension}]]; return[stringrange$extension[expr{[stringfirst"othername:upn<"$extension]+14}][expr{[stringlast">"$extension]-1}]]; session timeout management session.inactivity_timeout Change inactivity session timeout based on a checkbox on the logon page (logon variable trusted) if { [mcget {session.logon.last.trusted}] == 1 } { return {5400} } else { return {1800} } one-line code (5400 seconds if condition before ? success, 1800 seconds else) expr { [mcget {session.logon.last.trusted}] == 1 ? {5400} : {1800}} session.inactivity_timeout Change inactivity session timeout based on client type (iOS, Android and WindowsPhone : half of inactivity timeout configured in profile parameters) expr { [mcget {session.client.platform}] == "WindowsPhone" || [mcget {session.client.platform}] == "Android" || [mcget {session.client.platform}] == "iOS" ? [mcget {session.inactivity_timeout}]/2 : [mcget {session.inactivity_timeout}] } session.max_session_timeout force to close the session à 17:00 expr { [clock scan "17:00"] - [mcget {session.user.starttime}] } session.max_session_timeout After a AD query which retreive attribute logonHours, force to close the session when user at the end of allowed logon hours set maximumSessionSeconds 604800 if {[set logonHours [mcget {session.ad.last.attr.logonHours}]] != "" && $logonHours != "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"} { #convert string to binary string binary scan [binary format H* $logonHours] b* logon_hours_binary_string # evaluate the number of seconds from last sunday set time_from_sunday [expr {[clock seconds] - [clock scan "last sunday"]}]; # search in string next hours with 0 value set current_index [expr {$time_from_sunday / 3600}]; # convert the index to number of seconds from last sunday if {[set next_denied_index [string first 0 $logon_hours_binary_string$logon_hours_binary_string $current_index]] == $current_index } {return 0} # evaluate number on seconds to disconnect time return [expr { $next_denied_index*3600 - $time_from_sunday}] } else { return $maximumSessionSeconds} Windows Info session.windows_info_os.last.fqdn search and return FQDN hostname in computer names list after windows Info Box foreach x [split [mcget {session.windows_info_os.last.computer}] "|"] { if { $x ends_with ".f5demo.lab" } { return $x } } session.windows_info_os.last.computer_name search FQDN hostname in computer names list after windows Info Box, then return shortname (without domain name) foreach x [split [mcget {session.windows_info_os.last.computer}] "|"] { if { $x ends_with ".f5demo.lab" } { return [lindex [split $x "."] 0] } } Machine cert To allow machine certificate revocation validation, add a variable assign with 2 following variables before OCSP or CRLDP boxes. session.ssl.cert.whole store machine certificate as it was user certificate expr {[mcget {session.check_machinecert.last.cert.cert}]} session.ssl.cert.certissuer store machine certificate issuer as it was user certificate issuer expr {[mcget {session.check_machinecert.last.cert.issuer}]} HTTP auth returned cookie parsing session.custom.http_auth_mycookie extract from HTTP auth cookie list the cookie value of mycookie expr { [lindex [regexp -inline {mycookie=([^;\\\r]*)} [mcget session.http.last.response_cookie]] 1] } replace portal or network access Webtop by full webtop if unsupported resource are assigned Webtop can be: Portal webtop : define an internal web server as home page Network access Webtop : start automatically Network access when connected Full Webtop : display all assigned ressources in one page hosted on the F5. Some customers want to assign different webtop based on assigned ressources. one portal ressource only -> portal webtop one Network access ressource only -> Network Access ressource more than one portal ressource -> Full webtop more than one Network access ressource -> Full webtop RDP, Application tunnel, SAML ressources assigned -> Full Webtop In Advanced ressource assign, the last assigned webtop is applied to the session. If the user is assigned non portal ressource (ex : RDP) and portal webtop, he will not be allowed to connect. session.assigned.webtop this code code is used if portal or network access webtop are assigned and number of resources is supported only with full webtop set fullwt /Common/wt-Full; set wt [mcget {session.assigned.webtop}]; set pa [llength [mcget {session.assigned.resources.pa}]]; set at [llength [mcget {session.assigned.resources.at}]]; set na [llength [mcget {session.assigned.resources.na}]]; set rd [llength [mcget {session.assigned.resources.rd}]]; set saml [llength [mcget {session.assigned.resources.saml}]]; if {$rd || $at || $saml || ([expr { $pa + $na }] > 1)} {set wt $fullwt}; unset fullwt; unset pa; unset at; unset na; unset rd; unset saml; return $wt; one-line code. Don't forget to replace "/Common/wt-Full" with your own webtop full in expression. expr { [llength [concat [mcget {session.assigned.resources.rd}] [mcget {session.assigned.resources.at}] [mcget {session.assigned.resources.atsaml}]]] || [llength [concat [mcget {session.assigned.resources.pa}] [mcget {session.assigned.resources.na}]]] >1 ? "/Common/wt-Full" : [mcget {session.assigned.webtop}]} Same condition for Advanced resource Assign condition. This condition doesn't match with previous rules in the same Advanced resource assign. must be in a dedicated resource assign box. expr { [llength [concat [mcget {session.assigned.resources.rd}] [mcget {session.assigned.resources.at}] [mcget {session.assigned.resources.atsaml}]]] || [llength [concat [mcget {session.assigned.resources.pa}] [mcget {session.assigned.resources.na}]]] >1} For Kerberos SSO when working with Kerberos SSO, 2 variable sources must be set: username : must be equal to user sAMAccountName domain : must be equal to user FQDN domain When working on access policy with multiple SSO method depending on the URI, Host header or some other parameters, you may have conflict on default SSO variables. For example, for Exchange : activesync SSO profile is basic with username format is NTDOMAIN\username Autodiscover SSP profile can be NTLM with username format is username domain format is NTDOMAIN OWA SSO profile can be kerberos with username : must be equal to user sAMAccountName domain : must be equal to user FQDN domain like DOMAIN.LOCAL (different than NT Domain) default SSO variables are : session.sso.token.last.username session.sso.token.last.password session.logon.last.domain to support multiple SSO on the same Access policy, I recommende to set new variables based on previous AD Query session.krbsso.username expr {[mcget {session.ad.last.attr.sAMAccountName}]} session.krbsso.domain expr {[mcget {session.ad.last.actualdomain}]} Code : No code20KViews4likes19CommentsiRule Event Order Flowchart
Problem this snippet solves The original contributions are fromHTTP Event Order -- Access Policy Manager - DevCentral (f5.com) I have updated this code example to use draw.io so that anyone can update diagram for required changes. Attempt to diagram a logic path for iRule event processing visually. There is special emphasis with ACCESS events as their lifecycle is a bit more complicated. These events should cover the most common use cases to customize ACCESS functionality. How to use this snippet This diagram can be updated via the free draw.io at https://app.diagrams.net/ Tested this on version No Version Found6.3KViews4likes4CommentsBypass Azure Login Page by adding a login hint in the SAML Request
Problem this snippet solves: Enhance the login experience between F5 (SAML SP) and Azure (SAML IDP) by injecting the "email address" as a login hint on behalf of the user. This enhances the user experience because it allows to bypass the Azure Login Page and avoids the user to type two times his login/email address. Example of use Your application need to be accessed by both "domain users" and "federated users". Your application is protected by the F5 APM with a "Login Page" that asks for the user "email address". Based on the "email address" value you determine the domain: if the user is a "domain user", you authenticate him on the local directory (AD Auth, LDAP Auth or ...) if the user is a "federated user" (such as xxx@gmail.com), you send him to the Azure IDP that will manage all federated access This snippet is particularly interesting for the "federated user" scenario because: without this code, a "federated user" will need to type his "login" twice. First time on "F5 Login Page" and the second time on "Azure Login Page" with this code, a "federated user" will need to type his "login" only on the F5 Login Page How to use this snippet: Go to "Access > Federation > SAML Service Provider > External IDP Connectors" and edit the "External IdP Connectors" object that match with the Azure IDP app. On the "Single Sign On Service Settings" add at the end of the "Single Sign On Service URL" the following string "?login_hint=" as shown in the picture below. The string "?login_hint=" is added here only to be able to uniquely identify it later by the iRule and replaced it. 3. Finally, apply the iRule below on the VS that has the Access Policy enabled and for which the SAML SP role is attributed and is binded to the Azure IDP application. The iRule will simply catch the "Single Sign On Service URL" and replace it with "?login_hint=xxxx@gmail.com". Code : when CLIENT_ACCEPTED { ACCESS::restrict_irule_events disable } when HTTP_RESPONSE_RELEASE { if { [string tolower [HTTP::header value "Location"]] contains "/saml2/?login_hint="} { set user_login [ACCESS::session data get "session.logon.last.mail"] #log local0. "Before adding the hint [HTTP::header value "Location"]" set locationWithoutHint "?login_hint=" set locationWithHint "?login_hint=$user_login" HTTP::header replace Location [string map -nocase "${locationWithoutHint} ${locationWithHint}" [HTTP::header Location]] #log local0. "After adding the hint [HTTP::header value "Location"]" } } Tested this on version: No Version Found4.8KViews1like5CommentsGoogle Authenticator Token Verification iRule For APM
Problem this snippet solves: This iRule adds token authentication capabilities for Google Authenticator to APM. The implementation is described in George Watkins' article: Two Factor Authentication With Google Authenticator And APM The iRule should be applied to an access policy-enabled virtual server. In order to provide two-factor authentication, a AAA server must be defined to verify user credentials. The users' Google Authenticator secrets can be mapped to individual users using a data group, an LDAP schema attribute, or an Active Directory attribute. The storage method can be defined in the beginning section of the iRule. Here are a list of all the configurable options: lockout_attempts - number of attempts a user is allowed to make prior to being locked out temporarily lockout_period - duration of lockout period ga_code_form_field - name of HTML form field used in the APM logon page, this field is define in the "Logon Page" access policy object ga_key_storage - key storage method for users' Google Authenticator shared keys, valid options include: datagroup, ldap, or ad ga_key_ldap_attr - name of LDAP schema attribute containing users' key ga_key_ad_attr - name of Active Directory schema attribute containing users' key ga_key_dg - data group containing user := key mappings Code : 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 "datagroup" # 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 "google_auth_key" # 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.$static::ga_key_ad_attr] } 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 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 } }4.8KViews2likes5CommentsAPM Sharepoint authentication
Problem this snippet solves: Updated version to support Webdav with windows explorer after Nicolas's comment. APM is a great authentication service but it does it only with forms. The default behavior is to redirect user to /my.policy to process VPE. this redirect is only supported for GET method. Sharepoint provide 3 different access types: browsing web site with a browser Editing documents with Office browser folder with webdav client (or editing documents with libreoffice through webdav protocol) This irule display best authentication method for each of these access types: browsers authenticate with default authentication method (form based authentication) Microsoft office authenticate with Form based authentication (with support of MS-OFBA protocol) Libreoffice and webdav clients authenticate with 401 basic authentication Form based authentication (browser and Microsoft office) is compatible (validated for one customer) with SAML authentication Editing documents is managed with a persistent cookie expiring after 5 minutes. to be shared between IE and Office, it requires : cookie is persistent (expiration date instead of deleted at the end of session) web site defined as "trusted sites" in IE. How to use this snippet: install this irule and enable it on the VS. Code : when RULE_INIT { array set static::MSOFBA { ReqHeader "X-FORMS_BASED_AUTH_REQUIRED" ReqVal "/sp-ofba-form" ReturnHeader "X-FORMS_BASED_AUTH_RETURN_URL" ReturnVal "/sp-ofba-completed" SizeHeader "X-FORMS_BASED_AUTH_DIALOG_SIZE" SizeVal "800x600" } set static::ckname "MRHSession_SP" set static::Basic_Realm_Text "SharePoint Authentication" } when HTTP_REQUEST { set apmsessionid [HTTP::cookie value MRHSession] set persist_cookie [HTTP::cookie value $static::ckname] set clientless_mode 0 set form_mode 0 # Identify User-Agents type if {[HTTP::header exists "X-FORMS_BASED_AUTH_ACCEPTED"] && (([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "t") || ([HTTP::header "X-FORMS_BASED_AUTH_ACCEPTED"] equals "f"))} { set clientless_mode 0; set form_mode 1 } else { switch -glob [string tolower [HTTP::header "User-Agent"]] { "*microsoft-webdav-miniredir*" { set clientless_mode 1 } "*microsoft data access internet publishing provider*" - "*office protocol discovery*" - "*microsoft office*" - "*non-browser*" - "msoffice 12*" { set form_mode 1 } "*mozilla/4.0 (compatible; ms frontpage*" { if { [ string range [getfield [string tolower [HTTP::header "User-Agent"]] "MS FrontPage " 2] 0 1] > 12 } { set form_mode 1 } else { set clientless_mode 1 } } "*mozilla*" - "*opera*" { set clientless_mode 0 } default { set clientless_mode 1 } } } if { $clientless_mode || $form_mode } { if { [HTTP::cookie exists "MRHSession"] } {set apmstatus [ACCESS::session exists -state_allow $apmsessionid]} else {set apmstatus 0} if { !($apmstatus) && [HTTP::cookie exists $static::ckname] } {set apmpersiststatus [ACCESS::session exists -state_allow $persist_cookie]} else {set apmpersiststatus 0} if { ($apmpersiststatus) && !($apmstatus) } { # Add MRHSession cookie for non browser user-agent first request and persistent cookie present if { [catch {HTTP::cookie insert name "MRHSession" value $persist_cookie} ] } {log local0. "[IP::client_addr]:[TCP::client_port] : TCL error on HTTP cookie insert MRHSession : URL : [HTTP::host][HTTP::path] - Headers : [HTTP::request]"} else {return} } } else { return } if { $clientless_mode && !($apmstatus)} { if { !([HTTP::header Authorization] == "") } { set clientless(insert_mode) 1 set clientless(username) [ string tolower [HTTP::username] ] set clientless(password) [HTTP::password] binary scan [md5 "$clientless(password)"] H* clientless(hash) set user_key "$clientless(username).$clientless(hash)" set clientless(cookie_list) [ ACCESS::user getsid $user_key ] if { [ llength $clientless(cookie_list) ] != 0 } { set clientless(cookie) [ ACCESS::user getkey [ lindex $clientless(cookie_list) 0 ] ] if { $clientless(cookie) != "" } { HTTP::cookie insert name MRHSession value $clientless(cookie) set clientless(insert_mode) 0 } } if { $clientless(insert_mode) } { HTTP::header insert "clientless-mode" 1 HTTP::header insert "username" $clientless(username) HTTP::header insert "password" $clientless(password) } unset clientless } else { HTTP::respond 401 WWW-Authenticate "Basic realm=\"$static::Basic_Realm_Text\"" Set-Cookie "MRHSession=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" Connection close return } } elseif {$form_mode && !($apmstatus) && !([HTTP::path] equals $static::MSOFBA(ReqVal))}{ HTTP::respond 403 -version "1.1" noserver \ $static::MSOFBA(ReqHeader) "https://[HTTP::host]$static::MSOFBA(ReqVal)" \ $static::MSOFBA(ReturnHeader) "https://[HTTP::host]$static::MSOFBA(ReturnVal)" \ $static::MSOFBA(SizeHeader) $static::MSOFBA(SizeVal) \ "Connection" "Close" return } } when HTTP_RESPONSE { # Insert persistent cookie for html content type and private session if { [HTTP::header "Content-Type" ] contains "text/html" } { HTTP::cookie remove $static::ckname HTTP::cookie insert name $static::ckname value $apmsessionid path "/" HTTP::cookie expires $static::ckname 120 relative HTTP::cookie secure $static::ckname enable } # Insert session cookie if session was recovered from persistent cookie if { ([info exists "apmpersiststatus"]) && ($apmpersiststatus) } { HTTP::cookie insert name MRHSession value $persist_cookie path "/" HTTP::cookie secure MRHSession enable } } when ACCESS_SESSION_STARTED { if {([info exists "clientless_mode"])} { ACCESS::session data set session.clientless $clientless_mode } if { [ info exists user_key ] } { ACCESS::session data set "session.user.uuid" $user_key } } when ACCESS_POLICY_COMPLETED { if { ([info exists "clientless_mode"]) && ($clientless_mode) && ([ACCESS::policy result] equals "deny") } { ACCESS::respond 401 noserver WWW-Authenticate "Basic realm=$static::Basic_Realm_Text" Connection close ACCESS::session remove } } when ACCESS_ACL_ALLOWED { switch -glob [string tolower [HTTP::path]] { "/sp-ofba-form" { ACCESS::respond 302 noserver Location "https://[HTTP::host]$static::MSOFBA(ReturnVal)" } "/sp-ofba-completed" { ACCESS::respond 200 content { Authenticated Good Work, you are Authenticated } noserver } "*/signout.aspx" { # Disconnect session and redirect to APM logout Page ACCESS::respond 302 noserver Location "/vdesk/hangup.php3" return } "/_layouts/accessdenied.aspx" { # Disconnect session and redirect to APM Logon Page if {[string tolower [URI::query [HTTP::uri] loginasanotheruser]] equals "true" } { ACCESS::session remove ACCESS::respond 302 noserver Location "/" return } } default { # No Actions } } } Tested this on version: 11.54.8KViews1like57CommentsJSON Web Token (JWT) Parser
Problem this snippet solves: This feature is now native in v13.1 and it is strongly recommended you implement the native solution instead. This code is left only as an example for future use cases, it should not be used for JWT handling because there is no signature validation. This code parses a JWT (JSON Web Token) received by a Big-IP acting as an OAuth client and creates session variables for the JSON parameters in the header and payload. Example use cases might include Azure AD B2C or Azure AD Enterprise integration. This iRule does not perform signature validation. Code from the "Parse and Set Session Variables" section down could be easily harvested for other JSON parsing use cases that do not need the JWT decoding. How to use this snippet: Attach this iRule to the virtual server receiving the JWT that is configured for OAuth. Inside the VPE after the OAuth Client agent add an iRule agent with id jwt-parse. This iRule will set several variables including: session.oauth.jwt.last.header session.oauth.jwt.last.payload session.oauth.jwt.last.signature In addition it will create a session variable for each parameter in the header and payload in the following syntax. session.oauth.jwt.header.last.* session.oauth.jwt.payload.last.* You can then call these session variables elsewhere. Code : when ACCESS_POLICY_AGENT_EVENT { if { [ACCESS::policy agent_id] eq "jwt-parse" } { #log local0. "JWT-Parse: Started" #Get the JWT set jwt [ACCESS::session data get -secure session.oauth.client.last.id_token] #log local0. "JWT-Parse: JWT Received - jwt is $jwt" #Separate the header, payload, and signature set jwt_header [getfield $jwt "." 1] ACCESS::session data set session.oauth.jwt.last.header $jwt_header #log local0. "JWT-Parse: Header extracted - jwt_header and session.oauth.jwt.last.header are $jwt_header" set jwt_payload [getfield $jwt "." 2] ACCESS::session data set session.oauth.jwt.last.payload $jwt_payload #log local0. "JWT-Parse: Payload extracted - jwt_payload and session.oauth.jwt.last.payload are $jwt_payload" set jwt_signature [getfield $jwt "." 3] ACCESS::session data set session.oauth.jwt.last.signature $jwt_signature #log local0. "JWT-Parse: Signature extracted - jwt_signature and session.oauth.jwt.last.signature are $jwt_signature" #Base 64 decode the header and payload #Fix encoding issues in header set jwt_header_modified $jwt_header set tail [string length $jwt_header_modified] if {$tail % 4 == 2} { append jwt_header_modified {==} } elseif {$tail % 4 == 3} { append jwt_header_modified {=} } #log local0. "JWT-Parse: Header encoding fixes complete - jwt_header_modified is $jwt_header_modified" #Fix encoding issues in payload set jwt_payload_modified $jwt_payload set tail [string length $jwt_payload_modified] if {$tail % 4 == 2} { append jwt_payload_modified {==} } elseif {$tail % 4 == 3} { append jwt_payload_modified {=} } #log local0. "JWT-Parse: Payload encoding fixes complete - jwt_payload_modified is $jwt_payload_modified" #Base64 decode set jwt_header_modified [b64decode $jwt_header_modified] #log local0. "JWT-Parse: Header Base 64 decoded - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [b64decode $jwt_payload_modified] #log local0. "JWT-Parse: Payload Base 64 decoded - jwt_payload_modified is $jwt_payload_modified" #Parse and Set Session Variables #Remove JSON characters set jwt_header_modified [string map {\{ {} \} {} \[ {} \] {} \" {}} $jwt_header_modified] #log local0. "JWT-Parse: Header JSON Characters removed - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [string map {\{ {} \} {} \[ {} \] {} \" {}} $jwt_payload_modified] #log local0. "JWT-Parse: Payload JSON Characters removed - jwt_payload_modified is $jwt_payload_modified" #Split into field/value pairs set jwt_header_modified [split $jwt_header_modified ,] #log local0. "JWT-Parse: Header Fields split - jwt_header_modified is $jwt_header_modified" set jwt_payload_modified [split $jwt_payload_modified ,] #log local0. "JWT-Parse: Payload Fields split - jwt_payload_modified is $jwt_payload_modified" #Set APM session variables for each header parameter foreach parameter $jwt_header_modified { set variable_name [getfield $parameter ":" 1] set variable_value [getfield $parameter ":" 2] ACCESS::session data set session.oauth.jwt.header.last.$variable_name $variable_value #log local0. "JWT-Parse: Header session variable set - session.oauth.jwt.header.last.$variable_name is $variable_value" } #Set APM session variables for each payload parameter foreach parameter $jwt_payload_modified { set variable_name [getfield $parameter ":" 1] set variable_value [getfield $parameter ":" 2] ACCESS::session data set session.oauth.jwt.payload.last.$variable_name $variable_value #log local0. "JWT-Parse: Payload session variable set - session.oauth.jwt.payload.last.$variable_name is $variable_value" } } } Tested this on version: 13.03.4KViews2likes14CommentsCustomizing APM end user login page with APM Advanced Customization Templates
Problem this snippet solves: Advanced customization for APM allows an administrator broad control over the look and feel of nearly every page that APM issues. This is done through "including" both Cascading Style Sheets (CSS) and segments of HTML that are tailored to your environment. How to use this snippet: These templates are meant to be used in combination with the APM Advanced Customization guide included in the attachment. Tested this on version: 11.63.1KViews2likes1CommentSend OTP via sendgrid(email) API
Problem this snippet solves: This is an example of how to send an OTP generated by the Big-IP via email using the sendgrid api. Preparations Register a free Sendgrid account: https://sendgrid.com/ Generate an API key: https://app.sendgrid.com/settings/api_keys Create an iruleLX workspace or just import it from my github (pay attention to naming conventions): https://github.com/alexnimo/F5/blob/master/iRules_LX/sendgrid/sendgrid_ilx.tgz Configure AD resource Preface Most of the code and examples in this tutorial are just cut and paste from different places with slight modifications. I've decided to try it out after trying the twilio tutorial by Niels van Sluis: https://devcentral.f5.com/codeshare/send-an-one-time-password-otp-via-the-twilio-sms-gateway-1132. Afterward, I wanted to better understand the irule LX architecture and usage, so I watched this great tutorial by Artiom: https://www.youtube.com/watch?v=7yRP2fPCxIs&index=1&list=PLRL802iBI7n-6y8_eNawyeUZ8E0YwWro8 And this is my first attempt to write a tutorial... Configurations In case you're configuring from scratch, you need to install sendgrid plugin, otherwise its already included in the tgz file. To install the plugin: cd /var/ilx/workspaces/Common/ilx_sendgrid/extensions/sendgrid/node_modules npm install --save @sendgrid/mail The index.js includes 2 options to send an email. If you want to use the first option. Simple email, then uncomment before //Use this method to send regular email and comment before //Use this method to send email with dynamic template There are no additional configurations to be done. If you want to send an email with a dynamic template(customized), the second(default) option then first you need to create the template via the sendgrid UI: https://sendgrid.com/dynamic_templates You can make different customizations to your email. I've just used an example template which can be found on their github: https://github.com/sendgrid/email-templates/blob/master/paste-templates/password-reset.html and played with it just to understand how it works. Here is an ugly modified template with the variables that will be presented in the email: https://github.com/alexnimo/F5/blob/master/iRules_LX/sendgrid/otp_template.html More information can be found here: https://sendgrid.com/docs/ui/sending-email/how-to-send-an-email-with-dynamic-transactional-templates/ https://sendgrid.com/docs/ui/sending-email/using-handlebars/ I've generated and used the code from the documentation: https://dynamic-templates.api-docs.io/3.0/mail-send-with-dynamic-transactional-templates/v3-mail-send If the default option is used(dynamic templates) then you must include the template id in the variable template_id Hope I haven't forgot anything.... How to use this snippet: Code variables: var api_key = The api key generated in sendgrid Everything else can be customized. template_id = Template ID created in sendgrid. Code : 'use strict'; // Import the f5-nodejs module. var f5 = require('f5-nodejs'); // Import the sendgrid module. const sgMail = require('@sendgrid/mail'); //Set API key var api_key = "PASTE API KEY HERE" sgMail.setApiKey(api_key); // Create a new rpc server for listening to TCL iRule calls. var ilx = new f5.ILXServer(); //Construct email ilx.addMethod('ilx_sendmail', function(tclArgs, funcResp) { console.log("Method Invoked, Args:", tclArgs.params()); const userEmail = tclArgs.params()[2]; var emailText = 'Dear' + tclArgs.params()[0] + ' Your OTP is: ' + tclArgs.params()[1]; /* //Use this method to send regular email const msg = { to: userEmail, from: 'OTP_Admin@otptest.com', subject: 'Please See your OTP', text: emailText, // html: 'and easy to do anywhere, even with Node.js', }; //Send email sgMail.send(msg, (error, result) => { if (error) { console.log("Error sending email, the issue is:" + error) funcResp.reply(error); } else { return result funcResp.reply("email sent"); } }); */ //Use this method to send email with dynamic template var http = require("https"); var options = { "method": "POST", "hostname": "api.sendgrid.com", "port": null, "path": "/v3/mail/send", "headers": { "authorization": "Bearer " + api_key, "content-type": "application/json" } }; var req = http.request(options, function (res) { var chunks = []; res.on("data", function (chunk) { chunks.push(chunk); }); res.on("end", function () { var body = Buffer.concat(chunks); console.log(body.toString()); }); }); req.write(JSON.stringify({ personalizations: [ { to: [ { email: userEmail , name: tclArgs.params()[0] } ], dynamic_template_data: { user: tclArgs.params()[0], otp: tclArgs.params()[1], partysize: 4, english: true } } ], from: { email: 'OTP_Admin@otptest.com', name: 'OTP Admin' }, reply_to: { email: 'OTP_Admin@otptest.com', name: 'OTP Admin' }, template_id: 'PASTE TEMPLATE ID HERE' })); req.end(); funcResp.reply("email sent"); }); ilx.listen(); Tested this on version: 13.02.8KViews0likes1CommentAdd SameSite attribute to APM Cookies
Problem this snippet solves: This irule add SameSite attribute with value None to APM Cookies. related to Chrome 80 behavior change : Cookies default to SameSite=Lax When you put "SameSite=None", you must have the "Secure" attribute set to the cookie. How to use this snippet: Add the irule to a Virtual Server. Change value of the attribute from None to desired value. Pay attention to possible interferences with other irules applied to Virtual Servers. Code : when RULE_INIT { set static::apm_cookies { "F5_fullWT" "F5_HT_shrinked" "F5_ST" "LastMRH_Session" "MRHSequence" "MRHSession" "MRHSHint" "TIN" "F5_VdiUserClientChoicecitrix" "F5_VdiUserClientChoicevmware_view" } } when CLIENT_ACCEPTED { ACCESS::restrict_irule_events disable } when HTTP_RESPONSE_RELEASE { foreach apm_cookie ${static::apm_cookies} { if { [HTTP::cookie exists ${apm_cookie}] } { HTTP::cookie attribute ${apm_cookie} insert "SameSite" None if { ![HTTP::cookie attribute ${apm_cookie} exists "secure"] } { HTTP::cookie attribute ${apm_cookie} insert "Secure" } } } } Tested this on version: 12.12.8KViews4likes5CommentsAPM Optimisation Script
Problem this snippet solves: With the current Covid-19 lockdown, many workers are now working from home which is putting stress on existing APM VPN devices. This script looks through the config and suggests some changes to be made to reduce CPU usage, based on https://support.f5.com/csp/article/K46161759 Matthieu Dierick has created a YouTube video showing how to use this at https://youtu.be/F0Z1AnM3L54 Let me know if you have any questions or requirements. Source code is held at https://github.com/pwhitef5/apm-vpn-optimisation/tree/master How to use this snippet: Copy the file to the /var/tmp directory as apm-optimisation Give it permissions with `chmod +x /var/tmp/apm-optimisation` Run with `/var/tmp/apm-optimisation`. Output is to stdout Example: [root@apm-1:Active:Standalone] ~ # ./apm-optimisation APM Optimisation Visibility CPU Usage -------------------------------- Current Average Maximum 52% 30% 93% -------------------------------- Compression -------------------------------- Licensed Hardware unlimited None -------------------------------- --- Partition /Common --- Connectivity Profile Compression -------------------------------- Profile Name Status -------------------------------- myConnectivity Disabled myConnectivity2 Disabled -------------------------------- Network Access Profile Compression ----------------------------------------------------------------------------------------------------------- Name | Compression | Split-Tunneling | Client Traffic Classifier | DTLS ----------------------------------------------------------------------------------------------------------- networkAccess | Enabled | Enabled | Disabled | Enabled networkAccess2 | Disabled | Enabled | Disabled | Disabled ----------------------------------------------------------------------------------------------------------- --- Optimisation Suggestions --- - CPU rate is LOW. Go down the Winchester and wait for it all to blow over - Hardware Compression is not included so consider turning off the feature ------- Partition /Common ------- - To turn off compression in the connectivity profile, run the command 'tmsh modify apm profile connectivity /Common/myConnectivity compression disabled' - To turn off compression in the NA profile, run the command 'tmsh modify apm resource network-access /Common/networkAccess compression none' - To turn on Client Traffic Classifier, run the commands below: tmsh create apm resource client-rate-class /Common/rate_class_2M { rate 2000000 } tmsh create apm resource client-rate-class /Common/rate_class_1M { rate 1000000 } tmsh create apm resource client-traffic-classifier /Common/client-traffic-classifier-1 { entries add { entry { client-rate-class rate_class_1M dst-ip any dst-mask any dst-port https src-ip any src-mask any } } } tmsh modify apm resource network-access /Common/networkAccess client-traffic-classifier client-traffic-classifier-1 - Network Access profile /Common/networkAccess is using SNAT automap. Consider using a SNAT pool - To turn on Client Traffic Classifier, run the commands below: tmsh create apm resource client-rate-class /Common/rate_class_2M { rate 2000000 } tmsh create apm resource client-rate-class /Common/rate_class_1M { rate 1000000 } tmsh create apm resource client-traffic-classifier /Common/client-traffic-classifier-1 { entries add { entry { client-rate-class rate_class_1M dst-ip any dst-mask any dst-port https src-ip any src-mask any } } } tmsh modify apm resource network-access /Common/networkAccess2 client-traffic-classifier client-traffic-classifier-1 - To turn on DTLS, create a duplicate virtual server listening on UDP and enabled DTLS in the Network Access List Network Settings ( see https://devcentral.f5.com/s/articles/APM-DTLS-Virtual-Server-iApp ) - Network Access profile /Common/networkAccess2 is using SNAT automap. Consider using a SNAT pool ----------------------------------------------------------------------------------------------------------- Code : #!/bin/bash # Version 5 8/4/2020 P.White # This is a script to check your APM system and give suggestions to reduce CPU usage # Taken from suggestions at https://support.f5.com/csp/article/K46161759 # v2 - small typo fix line 119 create changed to modify # v3 - updated classifier to only include https as it was causing an error # v4 - loops through admin partitions and prints out for each # v5 - added DTLS check and suggestion suggestions="--- Optimisation Suggestions ---\n" getLicensedCompression () { # Show the licensed compression comp=`tmsh -q show sys license detail|grep perf_http_compression|awk '{print $2}'|sed 's/\[\(.*\)\]/\1/g'` if [ x$comp != "x" ];then echo -n "$comp" else echo -n "Error!" fi } getHardwareCompression () { # Show hardware compression hcomp=`tmsh -q show sys license detail|grep "HTTP Hardware Compression"` if [ x$hcomp = "x" ];then # Hardware compression is not enabled echo -n "None" else echo -n "$hcomp" fi } clear echo "APM Optimisation Visibility" # CPU usage cur=`tmsh -q show sys cpu |grep "Utilization"|awk '{print $2}'` avg=`tmsh -q show sys cpu |grep "Utilization"|awk '{print $3}'` max=`tmsh -q show sys cpu |grep "Utilization"|awk '{print $4}'` if [ $avg -gt 90 ];then suggestions+=" - CPU rate is VERY HIGH! Turn off compression, implement split tunneling and consider more processing\n" elif [ $avg -gt 60 ];then suggestions+=" - CPU rate is HIGH! Turn off compression and consider split tunneling for non-internal traffic\n" elif [ $avg -gt 40 ];then suggestions+=" - CPU rate is MEDIUM. Consider turning off compression where required\n" else suggestions+=" - CPU rate is LOW. Go down the Winchester and wait for it all to blow over\n" fi echo echo "CPU Usage" echo "--------------------------------" echo -e "Current\tAverage\tMaximum" echo -e "$cur%\t$avg%\t$max%" echo "--------------------------------" echo # Compression clic=`getLicensedCompression` chw=`getHardwareCompression` if [ $chw = "None" ];then suggestions+=" - Hardware Compression is not included so consider turning off the feature\n" fi echo "Compression" echo "--------------------------------" echo -e "Licensed\tHardware" echo -e "$clic\t$chw" echo "--------------------------------" # loop through adminstrative partitions for partition in `tmsh -q list auth partition one-line|awk '{print $3}'`;do suggestions+="\n------- Partition /$partition -------\n" echo " --- Partition /$partition ---" echo echo "Connectivity Profile Compression" echo "--------------------------------" echo -e "Profile Name\t\tStatus" echo "--------------------------------" for profile in `tmsh -q -c "cd /$partition;list apm profile connectivity one-line"|awk '{print $4}'`;do if [ $profile = "connectivity" ];then continue fi if [ `tmsh -q -c "cd /$partition;list apm profile connectivity $profile one-line"|grep "compress-gzip-level 0"|wc -l` -gt 0 ];then echo -e "$profile\t\tDisabled" else suggestions+=" - To turn off compression in the connectivity profile, run the command 'tmsh modify apm profile connectivity /$partition/$profile compress-gzip-level 0'\n" echo -e "$profile\t\tEnabled" fi done echo "--------------------------------" echo echo "Network Access Profile Compression" echo "-----------------------------------------------------------------------------------------------------------" echo -e " Name\t\t\t| Compression\t| Split-Tunneling\t| Client Traffic Classifier\t| DTLS" echo "-----------------------------------------------------------------------------------------------------------" for profile in `tmsh -q -c "cd /$partition;list apm resource network-access one-line"|awk '{print $4}'`;do # Compression if [ `tmsh -q -c "cd /$partition;list apm resource network-access $profile one-line"|grep "compression gzip"|wc -l` -gt 0 ];then echo -en "$profile\t\t| Enabled" suggestions+=" - To turn off compression in the NA profile, run the command 'tmsh modify apm resource network-access /$partition/$profile compression none'\n" else echo -en "$profile\t\t| Disabled" fi if [ `tmsh -q -c "cd /$partition;list apm resource network-access $profile one-line"|grep "split-tunneling true"|wc -l` -gt 0 ];then echo -en "\t| Enabled" else echo -en "\t| Disabled" suggestions+=" - To turn on split-tunneling, run the command 'tmsh modify apm resource network-access /$partition/$profile split-tunneling true'\n" suggestions+=" - To configure split-tunneling exclude traffic by DNS name, run the command 'tmsh modify apm resource network-access /$partition/$profile address-space-exclude-dns-name add { office.com microsoftonline.com google.com gmail.com facebook.com }'\n" suggestions+=" - To configure split-tunneling exclude traffic by IP address, run the command 'tmsh modify apm resource network-access /$partition/$profile address-space-include-subnet add { { subnet 10.0.0.0/8 } { subnet 172.16.0.0/16 } { subnet 192.168.0.0/16 } }'\n" fi if [ `tmsh -q -c "cd /$partition;list apm resource network-access $profile one-line"|grep "client-traffic-classifier "|wc -l` -gt 0 ];then echo -en "\t\t| Enabled" else echo -en "\t\t| Disabled" suggestions+=" - To turn on Client Traffic Classifier, run the commands below:\n" suggestions+="tmsh create apm resource client-rate-class /$partition/rate_class_2M { rate 2000000 }\n" suggestions+="tmsh create apm resource client-rate-class /$partition/rate_class_1M { rate 1000000 }\n" suggestions+="tmsh create apm resource client-traffic-classifier /$partition/client-traffic-classifier-1 { entries add { entry { client-rate-class rate_class_1M dst-ip any dst-mask any dst-port https src-ip any src-mask any } } }\n" suggestions+="tmsh modify apm resource network-access /$partition/$profile client-traffic-classifier client-traffic-classifier-1\n" fi if [ `tmsh -q -c "cd /$partition;list apm resource network-access $profile one-line"|grep "dtls true"|wc -l` -gt 0 ];then echo -en "\t\t\t| Enabled" else echo -en "\t\t\t| Disabled" suggestions+=" - To turn on DTLS, create a duplicate virtual server listening on UDP and enabled DTLS in the Network Access List Network Settings ( see https://devcentral.f5.com/s/articles/APM-DTLS-Virtual-Server-iApp )\n" fi # Check for SNAT automap if [ `tmsh -q -c "cd /$partition;list apm resource network-access $profile one-line all-properties"|grep "snat automap"|wc -l` -gt 0 ];then suggestions+=" - Network Access profile /$partition/$profile is using SNAT automap. Consider using a SNAT pool\n" fi echo "" done echo "-----------------------------------------------------------------------------------------------------------" # Check VSs for mirroring for vs in `tmsh list ltm virtual one-line|awk '{print $3}'`;do if [ `tmsh -q -c "cd /$partition;list ltm virtual $vs mirror"|grep "mirror enabled"|wc -l` -gt 0 ];then echo echo "WARNING! Virtual Server /$partition/$vs has mirroring enabled\n" echo suggestions+="Consider disabling Connection Mirroring for virtual server /$partition/$vs with the command 'tmsh modify ltm virtual /$partition/$vs mirror disabled'\n" fi done done echo echo -e "$suggestions" echo "-----------------------------------------------------------------------------------------------------------" Tested this on version: 13.02.5KViews6likes5Comments