2fa
2 TopicsSend an One Time Password (OTP) via the MessageBird SMS gateway
Problem this snippet solves: This snippet makes it possible to send an One Time Password (OTP) via the MessageBird SMS gateway. This snippet uses iRuleLX and the node.js messagebird package to interact with the MessageBird API. How to use this snippet: Prepare the BIG-IP Provision the BIG-IP with iRuleLX. Create LX Workspace: messagebird Add iRule: messagebird_irule Add Extension: messagebird_extension Add LX Plugin: messagebird_plugin -> From Workspace: messagebird Install the node.js messagebird module # cd /var/ilx/workspaces/Common/messagebird/extensions/messagebird_extension # npm install messagebird --save messagebird@2.1.4 node_modules/messagebird # irule To make it works, you need to install the irule on the Virtual Server that publish your application with APM authentication. access profile If you already have an existing access profile, you will need to modify it and include some additionnal configuration in your VPE. If you have no access profile, you can starts building your own based on the description we provide below. Configuring the Visual Policy Editor The printscreen below is a minimal Visual Policy Editor used to make MessageBird OTP Authentication works properly : For a larger version of this image please download here. Irule Event – MessageBird This is an irule event with the ID set to ‘MessageBird’. This will trigger the messagebird_irule to come into action. MessageBird Status This is an empty action with two branches. The branch named "successful" contains the following expression : expr { [mcget {session.custom.messagebird.status}] contains "successful" } Message Box This is a message box that will inform the user that there was a failure sending the One Time Password. messagebird_irule ### ### Name : messagebird_irule ### Author : Niels van Sluis, <niels@van-sluis.nl> ### Version: 20180721.001 ### Date : 2018-07-21 ### when ACCESS_POLICY_AGENT_EVENT { if { [ACCESS::policy agent_id ] eq "MessageBird" } { # Set MessageBird access key set accessKey "<ACCESS_KEY>" # Set user-friendly message which will be send prior to the OTP itself set message "Your OTP is: " # Set username used for logging purposes only set username "[ACCESS::session data get session.logon.last.username]" # Set OTP generated by BIG-IP APM which will be added to the SMS message. set generatedOTP "[ACCESS::session data get session.otp.assigned.val]" # Set phonenumber to send the SMS to set telephoneNumber "[ACCESS::session data get session.ad.last.attr.telephoneNumber]" # Set the sender of the message. This can be a telephone number (including country code) or an alphanumeric string. # In case of an alphanumeric string, the maximum length is 11 characters. set sender "MessageBird" if {[info exists username] && ($username eq "")} { log local0. "Error: username variable is empty; no OTP sent." return } if {[info exists generatedOTP] && ($generatedOTP eq "")} { log local0. "Error: generatedOTP variable is empty; no OTP sent for user $username." return } if {([info exists telephoneNumber] && $telephoneNumber eq "")} { log local0. "Error: telephoneNumber variable is empty; no OTP sent for user $username." return } set rpc_handle [ ILX::init messagebird_plugin messagebird_extension ] if {[ catch { ILX::call $rpc_handle sendOTP $generatedOTP $telephoneNumber $sender $message $accessKey } result ] } { log local0. "sendOTP failed for telephoneNumber: $telephoneNumber, ILX failure: $result" return } log local0. "MessageBird status for user $username ($telephoneNumber): $result" ACCESS::session data set session.custom.messagebird.status $result } } Code : /** *** Name : messagebird_extension *** Author : Niels van Sluis, *** Version: 20180721.001 *** Date : 2018-07-21 **/ // Import the f5-nodejs module. var f5 = require('f5-nodejs'); // Create a new rpc server for listening to TCL iRule calls. var ilx = new f5.ILXServer(); ilx.addMethod('sendOTP', function(req, res) { var generatedOTP = req.params()[0]; var telephoneNumber = req.params()[1]; var sender = req.params()[2]; var message = req.params()[3]; var accessKey = req.params()[4]; var params = { 'originator': sender, 'recipients': [ telephoneNumber ], 'body': message + generatedOTP }; var messagebird = require('messagebird')(accessKey); messagebird.messages.create(params, function (err, response) { if (err) { //console.log(err); return res.reply('failed'); } //console.log(response); return res.reply('successful'); }); }); // Start listening for ILX::call and ILX::notify events. ilx.listen(); Tested this on version: 13.02.4KViews0likes3CommentsRadius External Monitor (Python)
Problem this snippet solves: Note: This script works and is usable however currently the only issue is that for some unknown reason it fails authentication on valid credentials, but since it still gets a valid response from the Radius server it will still mark the pool member as up. This only occurs via a configured monitor using either variable or arguments, running the script via command line with the same arguments works. This python script is an external monitor for radius that checks for any radius response in order to mark a pool member as up. If the connection times out the script does not output any response and as such will result in the member being marked as down. The script strips of routing domain tags and the IPv6 padding of the Node IPs. How to use this snippet: Installation: Import as an External Monitor Program File and name it however you want. Get and copy python modules Six (six py) and Py-Radius (radius py) then copy them to a directory with suitable permissions such as the /config/eav directory. If you're running python 2.6, which you probably will, then the radius module will need to be modified as per Code Mods section in the code below. Monitor Configuration: Set up a monitor with at the very least a valid SECRET, if you're looking to simply test whether the Radius server is alive and responding then the user account does not need to be valid. Valid Environment Variables are as follows: DEBUG = 0, 1, 2 (Default: 0) MOD_PATH = Directory containing the Python modules (Default: /config/eav) SECRET = Radius server secret (Default: NoSecret) USER = User account (Default: radius_monitor) PASSWD = Account Password (Default: NoPassword) Notes: The configured environment variables can overridden using Arguments with TESTME as the first argument followed by the variables you want to override in the same format as above, eg. KEY=Value. This is usedful for troubleshooting and can be used to override the F5 supplied NODE_IP and NODE_PORT variables. Log file location: /var/log/monitors Debug level 2 outputs environment variables, command line arguments and radius module into the log file. Code : #!/usr/bin/env python # -*- coding: utf-8 -*- # # Filename : radius_mon.py # Author : ATennent # Version : 1.1 # Date : 2018/09/14 # Python ver: 2.6+ # F5 version: 12.0+ # ========== Installation # Import this script via GUI: # System > File Management > External Monitor Program File List > Import... # Name it however you want. # Get, modify and copy the following modules: # ========== Required modules # -- six -- # https://pypi.org/project/six/ # Copy six.py into /config/eav # # -- py-radius -- # https://pypi.org/project/py-radius/ # Copy radius.py into /config/eav # If running python2.6 modify radius.py as per "Code mods -- python.py --" section below # ========== Notes # NB: Any and all outputs from EAV scripts equate to a POSITIVE result by the F5 monitor # so to make the result of the script to be a NEGATIVE result, ie. a DOWN state, we need # to ensure that the script does not output anything when the attempt results in a time out. # ========== Environment Variables # NODE_IP - Supplied by F5 monitor # NODE_PORT - Supplied by F5 monitor # MOD_PATH - Path to location of Python modules six.py and radius.py, default: /config/eav # SECRET - Radius server secret # USER - Username for test account # PASSWD - Password for user account # DEBUG - Enable/Disable Debugging # ========== Additional Environment Variables (supplied at execution) # MON_TMPL_NAME - Monitor Name, eg. '/Common/radius_monitor' # RUN_I - Run file, eg. '/Common/radius_mon.py' # SECURITY_FIPS140_COMPLIANCE - 'true'/'false' # PATH - Colon delimited and not editable. # ARGS_I - Copy of command line Arguments # NODE_NAME - Node Name, eg, '/Common/SNGPCTXWEB01' # MON_INST_LOG_NAME - Pool member's monitor log, eg. '/var/log/monitors/Common_radius_monitor-Common_MyNode-1812.log' # SECURITY_COMMONCRITERIA - 'true'/'false' # ========== Code mods -- python.py -- # Dictionary comprehension fixes: # https://stackoverflow.com/questions/1747817/create-a-dictionary-with-list-comprehension-in-python#1747827 # LINE:193; replace with #ATTR_NAMES = dict((v.lower(), k) for (k, v) in ATTRS.items()) # # LINE:100; replace with #CODE_NAMES = dict((v.lower(), k) for (k, v) in CODES.items()) # # Logger.NullHandler fix: # https://stackoverflow.com/questions/33175763/how-to-use-logging-nullhandler-in-python-2-6#34939479 # LINE:64-65; replace with #LOGGER = logging.getLogger(__name__) #try: # Python 2.7+ # LOGGER.addHandler(logging.NullHandler()) #except AttributeError: # class NullHandler(logging.Handler): # def emit(self, record): # pass # LOGGER.addHandler(NullHandler()) # ========== Imports/Modules from sys import path from sys import argv from sys import stdout from os import environ import logging.handlers import re if environ.get('MOD_PATH'): path.append(environ.get('MOD_PATH')) else: path.append('/config/eav') import radius # ========== Dictionary Defaults opts_dict = {'NODE_IP': '', 'NODE_PORT': '', 'SECRET': 'NoSecret', 'USER': 'radius_monitor', 'PASSWD': 'NoPassword', 'DEBUG': '0', } # ========== TEST w/ command line try: if argv[3] == 'TESTME': environ['NODE_IP'] = argv[1] environ['NODE_PORT'] = argv[2] for cmd_opt in argv[3:]: if 'DEBUG' in cmd_opt: environ['DEBUG'] = cmd_opt.split('=')[1] elif 'SECRET' in cmd_opt: environ['SECRET'] = cmd_opt.split('=')[1] elif 'USER' in cmd_opt: environ['USER'] = cmd_opt.split('=')[1] elif 'PASSWD' in cmd_opt: environ['PASSWD'] = cmd_opt.split('=')[1] else: continue except: pass # ========== Logging Config try: if int(environ.get('DEBUG')) == 0: log_file = '/dev/null' elif int(environ.get('DEBUG')) <=2: log_file = environ.get('MON_INST_LOG_NAME') else: log_file = '/dev/null' except: # DEBUG not supplied as ENV variable environ['DEBUG'] = opts_dict['DEBUG'] log_file = '/dev/null' if int(environ.get('DEBUG')) == 2: logging.basicConfig( filename=log_file, level=logging.DEBUG, format='%(asctime)s %(name)s:%(levelname)s %(message)s') else: logging.basicConfig( filename=log_file, level=logging.INFO, format='%(asctime)s %(name)s:%(levelname)s %(message)s') log = logging.getLogger('radius_mon') syslog_handler = logging.handlers.SysLogHandler(address='/dev/log', facility=0) log.addHandler(syslog_handler) if int(environ.get('DEBUG')) == 0: log.setLevel(logging.INFO) opts_dict['DEBUG'] = environ.get('DEBUG') elif int(environ.get('DEBUG')) == 1: log.setLevel(logging.DEBUG) opts_dict['DEBUG'] = environ.get('DEBUG') elif int(environ.get('DEBUG')) == 2: log.setLevel(logging.DEBUG) opts_dict['DEBUG'] = environ.get('DEBUG') log.debug('VERBOSE Debug Enabled, CMD line and ENV variables will be dumped to file.') else: log.error('Bad DEBUG value!') # ========== CMD Line and ENV Dump if int(opts_dict['DEBUG']) == 2: log.debug('=========== CMD ARGS') log.debug(argv[0:]) log.debug('=========== ENV VARS') log.debug(environ) # ========== Update Dictionary if environ.get('NODE_IP'): opts_dict['NODE_IP'] = environ.get('NODE_IP') log.debug('Set NODE_IP, %s', opts_dict['NODE_IP']) else: log.error('NODE_IP not supplied as ENV variable') log.error('Exiting..') exit(0) if environ.get('NODE_PORT'): opts_dict['NODE_PORT'] = environ.get('NODE_PORT') log.debug('Set NODE_PORT, %s', opts_dict['NODE_PORT']) else: log.error('NODE_PORT not supplied as ENV variable') log.error('Exiting..') exit(0) if environ.get('SECRET'): opts_dict['SECRET'] = environ.get('SECRET') log.debug('Set SECRET, %s', opts_dict['SECRET']) else: log.debug('SECRET not supplied as ENV variable') if environ.get('USER'): opts_dict['USER'] = environ.get('USER') log.debug('Set USER, %s', opts_dict['USER']) else: log.debug('USER not supplied as ENV variable') if environ.get('PASSWD'): opts_dict['PASSWD'] = environ.get('PASSWD') log.debug('Set PASSWD, %s', opts_dict['PASSWD']) else: log.debug('PASSWD not supplied as ENV variable') # ========== Clean up NODE_IP for IPv4 if '::ffff:' in opts_dict['NODE_IP']: opts_dict['NODE_IP'] = re.sub(r'::ffff:','', opts_dict['NODE_IP']) log.debug('Stripped IPv6 notation from IPv4 address') # ========== Strip routing domain from NODE_IP if '%' in opts_dict['NODE_IP']: opts_dict['NODE_IP'] = re.sub(r'%\d{1,4}$', '', opts_dict['NODE_IP']) log.debug('Stripped routing domain from IP address') # ========== Dictionary debug if int(opts_dict['DEBUG']) == 2: log.debug('----- KEY, VALUE pairs:') for key in opts_dict.keys(): log.debug('%s, %s', key, opts_dict[key]) log.debug('-----') # ========== Main r = radius.Radius(opts_dict['SECRET'], host=opts_dict['NODE_IP'], port=int(opts_dict['NODE_PORT'])) try: if r.authenticate(opts_dict['USER'], opts_dict['PASSWD']): log.debug('Service UP, Authentication attempt Successful') stdout.write('UP') else: log.debug('Service UP, Authentication attempt Failure') stdout.write('UP') except radius.ChallengeResponse: log.debug('Service UP, Authentication attempt Challenge Response') stdout.write('UP') except (radius.NoResponse, radius.SocketError): # This includes bad SECRET keys, the radius RFC states that Invalid responses from the radius # server are to be silently dropped which, from a client perspective, will result ina timeout. # This cannot be changed without modifying radius.py to instead raise an Exception. log.debug('No Response from %s:%s', opts_dict['NODE_IP'], int(opts_dict['NODE_PORT'])) exit(0) except Exception as msg: log.error('Exception due to %s, marking as DOWN', msg) exit(0) Tested this on version: 12.01.1KViews1like0Comments