Demystifying iControl REST Part 6: Token-Based Authentication
iControl REST. It’s iControl SOAP’s baby, brother, introduced back in TMOS version 11.4 as an early access feature but released fully in version 11.5.
Several articles on basic usage have been written on iControl REST so the intent here isn’t basic use, but rather to demystify some of the finer details of using the API. This article will cover the details on how to retrieve and use an authentication token from the BIG-IP using iControl REST and the python programming language. This token is used in place of basic authentication on API calls, which is a requirement for external authentication. Note that for configuration changes, version 12.0 or higher is required as earlier versions will trigger an un-authorized error.
The tacacs config in this article is dependent on a version that I am no longer able to get installed on a modern linux flavor. Instead, try this Dockerized tacacs+ server for your testing.
The Fine Print
The details of the token provider are here in the wiki. We’ll focus on a provider not listed there: tmos. This provider instructs the API interface to use the provider that is configured in tmos. For this article, I’ve configured a tacacs server and the BIG-IP with custom remote roles as shown below to show BIG-IP version 12’s iControl REST support for remote authentication and authorization. Details for how this configuration works can be found in the tacacs+ article I wrote a while back.
BIG-IP tacacs+ configuration
auth remote-role { role-info { adm { attribute F5-LTM-User-Info-1=adm console %F5-LTM-User-Console line-order 1 role %F5-LTM-User-Role user-partition %F5-LTM-User-Partition } mgr { attribute F5-LTM-User-Info-1=mgr console %F5-LTM-User-Console line-order 2 role %F5-LTM-User-Role user-partition %F5-LTM-User-Partition } } } auth remote-user { } auth source { type tacacs } auth tacacs system-auth { debug enabled protocol ip secret $M$Zq$T2SNeIqxi29CAfShLLqw8Q== servers { 172.16.44.20 } service ppp }
Tacacs+ Server configuration
id = tac_plus { debug = PACKET AUTHEN AUTHOR access log = /var/log/access.log accounting log = /var/log/acct.log host = world { address = ::/0 prompt = "\nAuthorized Access Only!\nTACACS+ Login\n" key = devcentral } group = adm { service = ppp { protocol = ip { set F5-LTM-User-Info-1 = adm set F5-LTM-User-Console = 1 set F5-LTM-User-Role = 0 set F5-LTM-User-Partition = all } } } group = mgr { service = ppp { protocol = ip { set F5-LTM-User-Info-1 = mgr set F5-LTM-User-Console = 1 set F5-LTM-User-Role = 100 set F5-LTM-User-Partition = all } } } user = user_admin { password = clear letmein00 member = adm } user = user_mgr { password = clear letmein00 member = mgr } }
Basic Requirements
Before we look at code, however, let’s take a look at the json payload requirements, followed by response data from a query using Chrome’s Advanced REST Client plugin. First, since we are sending json payload, we need to add the Content-Type: application/json header to the query. The payload we are sending with the post looks like this:
{ "username": "remote_auth_user", "password": "remote_auth_password", "loginProviderName": "tmos" }
You submit the same remote authentication credentials in the initial basic authentication as well, no need to have access to the default admin account credentials. A successful query for a token returns data like this:
{ username: "user_admin" loginReference: { link: "https://localhost/mgmt/cm/system/authn/providers/tmos/1f44a60e-11a7-3c51-a49f-82983026b41b/login" }- token: { uuid: "4d1bd79f-dca7-406b-8627-3ad262628f31" name: "5C0F982A0BF37CBE5DE2CB8313102A494A4759E5704371B77D7E35ADBE4AAC33184EB3C5117D94FAFA054B7DB7F02539F6550F8D4FA25C4BFF1145287E93F70D" token: "5C0F982A0BF37CBE5DE2CB8313102A494A4759E5704371B77D7E35ADBE4AAC33184EB3C5117D94FAFA054B7DB7F02539F6550F8D4FA25C4BFF1145287E93F70D" userName: "user_admin" user: { link: "https://localhost/mgmt/cm/system/authn/providers/tmos/1f44a60e-11a7-3c51-a49f-82983026b41b/users/34ba3932-bfa3-4738-9d55-c81a1c783619" }- groupReferences: [1] 0: { link: "https://localhost/mgmt/cm/system/authn/providers/tmos/1f44a60e-11a7-3c51-a49f-82983026b41b/user-groups/21232f29-7a57-35a7-8389-4a0e4a801fc3" }- - timeout: 1200 startTime: "2015-11-17T19:38:50.415-0800" address: "172.16.44.1" partition: "[All]" generation: 1 lastUpdateMicros: 1447817930414518 expirationMicros: 1447819130415000 kind: "shared:authz:tokens:authtokenitemstate" selfLink: "https://localhost/mgmt/shared/authz/tokens/4d1bd79f-dca7-406b-8627-3ad262628f31" }- generation: 0 lastUpdateMicros: 0 }
Among many other fields, you can see the token field with a very long hexadecimal token. That’s what we need to authenticate future API calls.
Requesting the token programmatically
In order to request the token, you first have to supply basic auth credentials like normal. This is currently required to access the /mgmt/shared/authn/login API location. The basic workflow is as follows (with line numbers from the code below in parentheses):
- Make a POST request to BIG-IP with basic authentication header and json payload with username, password, and the login provider (9-16, 41-47)
- Remove the basic authentication (49)
- Add the token from the post response to the X-F5-Auth-Token header (50)
- Continue further requests like normal. In this example, we’ll create a pool to verify read/write privileges. (1-6, 52-53)
And here’s the code (in python) to make that happen:
def create_pool(bigip, url, pool): payload = {} payload['name'] = pool pool_config = bigip.post(url, json.dumps(payload)).json() return pool_config def get_token(bigip, url, creds): payload = {} payload['username'] = creds[0] payload['password'] = creds[1] payload['loginProviderName'] = 'tmos' token = bigip.post(url, json.dumps(payload)).json()['token']['token'] return token if __name__ == "__main__": import os, requests, json, argparse, getpass requests.packages.urllib3.disable_warnings() parser = argparse.ArgumentParser(description='Remote Authentication Test - Create Pool') parser.add_argument("host", help='BIG-IP IP or Hostname', ) parser.add_argument("username", help='BIG-IP Username') parser.add_argument("poolname", help='Key/Cert file names (include the path.)') args = vars(parser.parse_args()) hostname = args['host'] username = args['username'] poolname = args['poolname'] print "%s, enter your password: " % args['username'], password = getpass.getpass() url_base = 'https://%s/mgmt' % hostname url_auth = '%s/shared/authn/login' % url_base url_pool = '%s/tm/ltm/pool' % url_base b = requests.session() b.headers.update({'Content-Type':'application/json'}) b.auth = (username, password) b.verify = False token = get_token(b, url_auth, (username, password)) print '\nToken: %s\n' % token b.auth = None b.headers.update({'X-F5-Auth-Token': token}) response = create_pool(b, url_pool, poolname) print '\nNew Pool: %s\n' % response
Running this script from the command line, we get the following response:
FLD-ML-RAHM:scripts rahm$ python remoteauth.py 172.16.44.15 user_admin myNewestPool1 Password: user_admin, enter your password: Token: 2C61FE257C7A8B6E49C74864240E8C3D3592FDE9DA3007618CE11915F1183BF9FBAF00D09F61DE15FCE9CAB2DC2ACC165CBA3721362014807A9BF4DEA90BB09F New Pool: {u'generation': 453, u'minActiveMembers': 0, u'ipTosToServer': u'pass-through', u'loadBalancingMode': u'round-robin', u'allowNat': u'yes', u'queueDepthLimit': 0, u'membersReference': {u'isSubcollection': True, u'link': u'https://localhost/mgmt/tm/ltm/pool/~Common~myNewestPool1/members?ver=12.0.0'}, u'minUpMembers': 0, u'slowRampTime': 10, u'minUpMembersAction': u'failover', u'minUpMembersChecking': u'disabled', u'queueTimeLimit': 0, u'linkQosToServer': u'pass-through', u'queueOnConnectionLimit': u'disabled', u'fullPath': u'myNewestPool1', u'kind': u'tm:ltm:pool:poolstate', u'name': u'myNewestPool1', u'allowSnat': u'yes', u'ipTosToClient': u'pass-through', u'reselectTries': 0, u'selfLink': u'https://localhost/mgmt/tm/ltm/pool/myNewestPool1?ver=12.0.0', u'serviceDownAction': u'none', u'ignorePersistedWeight': u'disabled', u'linkQosToClient': u'pass-through'}
You can test this out in the Chrome Advanced Rest Client plugin, or from the command line with curl or any other language supporting REST clients as well, I just use python for the examples well, because I like it. I hope you all are digging into iControl REST! What questions do you have? What else would you like clarity on? Drop a comment below.
- Casey_RobertsonNimbostratusThanks for the article. Been working a lot with the REST API lately. We use it to automate node activities during code deployments and Microsoft update patching (Windows web servers). Running into a number of issues though. My patch process kicks off a number of Powershell jobs - ~25 or as high as 60. All of these jobs have code to get an F5 token, check if the server is a node, if so, disable it, drain connections and then re-enabled when done. Randomly I'm getting response timeouts and it's really impactful. I'm going to have to code around it by doing retries or aborting but we have tested and at the same time, we'll see slowness in the F5 management GUI. Any tips on optimizing or giving more resources and/or troubleshooting the REST endpoint? We are running 11.6. Thanks!
- JRahmAdminHi Casey, that's a good question. Do you know if the timeouts are occurring on token acquisition or in the use of the token? If the former, you might share the token across jobs (assuming credentials are the same.) Any helpful errors in /var/log/restjavad.0.log?
- Casey_RobertsonNimbostratusHi Jason, Thanks for the quick reply! I'm not 100% sure yet on exactly which web request causes this… This KB article seems to describe exactly what we see. It kind of makes sense because in the last few months we have begun to use the REST API heavily. https://support.f5.com/kb/en-us/solutions/public/16000/700/sol16751.html
- JRahmAdminYou can increase memory by re-provisioning management from small to medium or large.
- Casey_RobertsonNimbostratusJason - where do you change that memory setting? Also - I'm trying to code in a function to grab all the tokens currently on the F5.... I can get a token and write it to variable...but then running this code returns nothing: function Get-AllF5Tokens { [CmdletBinding()] param( [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] $F5LoadBalancer, [Parameter(Mandatory=$True,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)] $token ) $method = "GET" $uri = "https://$F5LoadBalancer/mgmt/shared/authz/tokens" Invoke-F5RestMethod -URL $uri -method $method -token $token | Out-Null
- Casey_RobertsonNimbostratusNevermind on the powershell - I forgot about the Out-Null. But still would like to hear about the memory piece.
- JRahmAdmini'm sure there's a tmsh method for changing the memory, but I was referencing the GUI System->Provisioning section. Increasing the management provisioning setting from small to medium or large will increase the memory allocated to that function.
- ZdendaCirrusHi, firstly thanks for whole series, this has been helping me a lot. Regarding this token auth stuff, is the code you wrote supposed to be used by python 3.x? I use Python 2.7, but have issues to run your script even without any changes and I get the error like bellow (hopefully it will not be reformated) Traceback (most recent call last): File "auth_test.py", line 46, in token = get_token(b, url_auth, (username, password)) File "auth_test.py", line 15, in get_token token = bigip.post(url, json.dumps(payload)).json()['token']['token'] KeyError: 'token'
- ZdendaCirrusSorry, it got reformatted, but hopefully you can see the point. My problem is probably related to this line ---> token = bigip.post(url, json.dumps(payload)).json()['token']['token']. Thank you.
- JRahmAdminLikely you are getting a 401 instead of the expected payload with a 200