Use of OAuth2 authentication against Riverbed Appliances

General

 

This document will discuss theory and methods for calling REST APIs employing OAuth2 authentication against Riverbed’s Steelhead and NetProfiler appliances. Steelhead, NetProfiler, and SteelCentral Controller have almost identical implementations of OAuth2. The single difference between these products will be called out below in the code examples.


The examples and descriptions below will run the user through the process of requesting an OAuth2 token from a NetProfiler and then using that token to access an authenticated URL. The attached script then uses the exact same methods to request a token from a Steelhead Appliance. Profiler has the ability to allow users to generate Access Codes. This is demonstrated and the resulting code is used to get a token and access an authorized URL. Lastly some curl examples are shown for both username and password authentication as well as OAuth2 token request and use.

 

All of the python code examples in this text are taken from the attached script (oauth2-test.py)

 

Output from running the script with debug enabled (default). Note that the key file used in this run was generated using the steps below under Basic Requirments. Please see step 11.

dvernon$ ./oauth2-test.py --netpro_host <netprofiler_host> --sh_host <steelhead_host> --auth_code profiler_oath.key

DEBUG: Our token is: ewoJIm5vbmNlIjogIjk0NzliZTZkIiwKCSJhdWQiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyL2FwaS9jb21tb24vMS4wL29hdXRoL3Rva2VuIiwKCSJpc3MiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyIiwKCSJwcm4iOiAiYWRtaW4iLAoJImp0aSI6ICIxMjciLAoJImV4cCI6ICIxNDY4MzQ4Nzc3IiwKCSJpYXQiOiAiMTQ2ODM0NTE3NyIKfQ==

 

DEBUG: There are 1 user(s) on <netprofiler_host>

User admin - Auth Type: Local, Role: Developer

 

DEBUG: There are 1148 Apps defined on <steelhead_host>

DEBUG: There are 1759 Apps defined on <netprofiler_host>

 

dvernon$ cat profiler_oath.key

ewoJIm5vbmNlIjogIjRkMDZkZTg2IiwKCSJhdWQiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyL2FwaS9jb21tb24vMS4wL29hdXRoL3Rva2VuIiwKCSJpc3MiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyIiwKCSJwcm4iOiAiYWRtaW4iLAoJImp0aSI6ICIxIiwKCSJleHAiOiAiMTQ3MDUxMDMyNSIsCgkiaWF0IjogIjE0Njc5MTgzMjUiCn0=

 

Basic Requirements

 

The user should configure a shared OAuth2 key on the NetProfiler and Steelhead systems. The script above and the code examples taken from it below assume a single OAuth2 key works on both systems. Note that the instructions given below were tested a Steelhead at version 9.5. Other versions may differ. NetProfiler's OAuth access has not changed since introduced so these steps should work on any recent version.

  1. On the NetProfiler web interface choose CONFIGURATION:Account Management:OAuth Access
  2. Click the 'Generate new' button.
  3. Enter a text description of the new access code.
  4. Copy the text in the "OAuth Access Code" text block presented in a pop-up.
  5. Choose 'Close'
  6. On the Steelhead web UI choose ADMINISTRATION:REST API Access
  7. Click the + sign next to the text 'Add Access Code'
  8. Select "Import Existing Access Code" and paste the text copied above into the text box.
  9. Click "Add"
  10. Check that "Enable REST API Access" is selected. Select and apply if it is not.
  11. Save the pasted OAuth2 access code text into a file. The default name assumed by the script is 'test_oauth.key'

 

Python Requirements

The sample script uses the requests library. This will have to be installed by the user. 'pip install requests' should work in most environments.

 

The code examples

 

NetProfiler and Steelhead https ssl certificates are self signed by default. IF the user has not replaced these certificates with certificates signed by an ssl certificate authority recognized by clients in the environment then these examples will fail.


The following code snip disables warnings from the requests library. We will disable certificate verification later in the actual calls to the requests library. Both of these steps should be avoided in production code.


requests.packages.urllib3.disable_warnings()

























 

We encode some text a few times so wrapping it in a short function makes for better reading. NOTE: line numbers return to 01 with each code snip. See the attached script if you want to view all of this in context.

 

def encode(s):
  return base64.urlsafe_b64encode(s)



























 

We will need a wrapper for the get() and post() functions in the requests library.

  • The first argument will be the host name or IP address of the host we want to access
  • The second part is the relative URL we want to access. For http://google.com/someplace/or/other the relative url is '/someplace/or/other
  • 'data' is a python object (list or dict) that contains the data we want to post. If this argument is not None then the method performed will be a POST.
  • headers is a dictionary of key value pairs defining any headers you want to include in the GET or POST. Examples include 'Accept' or 'Content-Type'. As with all http headers these are NOT cap sensitive.

 

def do_request(host, url, data=None, headers=None):
    url_start = host_part.format(host_id = host)
    request_url = "{0}{1}".format(url_start, url)
    r = None
    if data is None:
        # no data is a get request.
        # verify is set to false so SSL errors don't cause problems
        # allow_redirects is false because the NetProfiler REST API
        # uses the location header in a redirect to pass back OAuth2
        # key data.
        r = requests.get(request_url,
                         headers=headers,
                         verify=False,
                         allow_redirects=False)
    else:
        r = requests.post(request_url,
                          data=data,
                          headers=headers,
                          verify=False,
                          allow_redirects=False)
    return r







 

Moving into the main() function of the script we start with opening and reading the OAuth2 Access code.

     with open(args.auth_code, 'r') as f:
          file_text = f.readline()
          # strip the text just in case some white space got in there.
          oauth_code = file_text.strip()

























 

Now we get into the part of the script that is actually concerned with formulating an OAuth2 token request.

 

A Riverbed OAuth2 token request requires data in the following format:

  • The data must be URL Encoded and contain a grant type declaration, an assertion, and a state.
    • The grant type is always "access_code".
    • The assertion is made up of the URL safe b64 encoded string "{\"alg\":\"none\"}\n", followed by the text of the OAuth2 access code, and finally the URL safe b64 encoded signature string. Riverbed products don't require token requests to be signed at this point so the signature can by an empty string ('').
    • The state is a hex encoded string. The state data will be echoed back to the user if the token request succeeds. The user may optionally use the echoed value to check that the token request was answered correctly.

 

The following code creates these items

 

     header_encoded = encode("{\"alg\":\"none\"}\n")
     # We don't sign our REST calls so empty string
     signature_encoded = ''
     assertion = '.'.join([header_encoded, oauth_code, signature_encoded])
     grant_type = 'access_code'

     # NetProfiler views the state variable as optional. Against a NetProfiler
     # this can be an empty sting. Steelhead requires this. Best to use it in
     # all cases. This is the only difference between Steelhead and NetProfiler
     # implementations.
     state = hashlib.md5(str(time.time())).hexdigest()
     data = {'grant_type': grant_type,
             'assertion': assertion,
             'state': state}


























With that we have prepped the data described above.


The requests library will automatically encode the data presented to match the headers we present. In this case we are presenting a dictionary object as data. We are also going to send an 'Accept' header or 'application/json'. So requests will implicitly encode the data as JSON for the request. If we wanted to make the encoding explicit we could add a 'Content-Type' header.


Now we use the data and the header to execute the token request. We requested JSON so we can use the built in json attribute to manipulate the data as python objects. The same thing could be accomplished by calling json.loads() on the response objects 'text' attribute.


     oauth_netpro = do_request(args.netpro_host,
                               token_url,
                               data=data,
                               headers={'Accept': 'application/json'})
     oauth_netpro_obj = oauth_netpro.json()























 

The next snip demonstrates how the user could use the 'state' value to check that the OAuth2 token request was valid. If they did not match for some reason then the token should not be trusted.

 

    if oauth_netpro_obj['state'] != state:
        print "Inconsistent state value in OAuth response"
        sys.exit(1)























 

Assuming that the request succeed our token is now text in oauth_netpro_obj['access_token']. We can use this data to formulate an authenticated REST call.

 

NetProfiler has a url in its REST API that will return a list of the systems users. As an example of retrieving an authenticated URL we will use the token we just got to retrieve that list. This request will feature both an authentication header and an 'Accept' content type header. The authentication header is simply "Bearer <token_text>" in a string.


This code formulates those headers

 

     users_url = '/api/profiler/1.5/users'
     auth_hdr_data = 'Bearer {0}'.format(oauth_netpro_obj['access_token'])
     auth_hdr = {'Authorization': auth_hdr_data,
                 'Accept': 'application/json'}






















 

With the header created we can now make the authenticated request. In this case I have printed out the number of users and a list of their username, authentication type, and role.

 

    users_req = do_request(args.netpro_host,
                           users_url,
                           headers=auth_hdr)

    users_obj = users_req.json()
    if args.debug:
        print "DEBUG: There are {0} user(s) on {1}".format(len(users_obj),
                                                           args.netpro_host)
        for user in users_obj:
            print ("User {username} - Auth Type: {authentication_type}, Role: "
                   "{role}".format(username=user['username'],
                                   authentication_type=user['authentication_type'],
                                   role=user['role']))






















 

The next example in the script starting at line 177 shows that the exact same methods used on the NetProfiler can be used to retrieve a list of applications defined on a Steelhead appliance. As stated above, the state variable is mandatory for this request. Otherwise no changes are required.

 

Beginning at line 216 in the script we have an example of calling a NetProfiler REST API URL that causes the NetProfiler to create a new OAuth2 Access Key with a description defined by the user. This REST call is a GET so any data passed to the server must be encoded in the URL. The second unique part of this REST call is that the call returns a redirect and uses the 'Location' header to pass the Access Token back to the user.

 

For this example we have to tell the Requests library functions not to follow redirects. Since redirects are only used for this purpose in Riverbed REST APIs at this time it is safe to always disable redirects as we have done in the do_request() function outlined above.

 

First we define the URLs we will be using.

 

    auth_url_str = ('/api/common/1.0/oauth/authorize?response_type=code&desc='
                    'TestOAuth2Code_{0}'.format(state))
    netpro_apps_url = '/api/profiler/1.5/applications'






















 

The authorize  REST API call only supports username and password or cookie authentication. We will use username and password. This is how we encode that header. Any NetProfiler example using OAuth2 token headers could also be done using this header with just username:password auth. The only difference is the header.


    u_n_p = encode("{user}:{passwd}".format(user=args.user,
                                            passwd=args.passwd))






















 

 

We don't care what format the response comes back in so no JSON header. We are only interested in the headers returned. So note the lack of an 'Accept' header.

    user_auth_hdr = {'Authorization': 'Basic {0}'.format(u_n_p)}

    newkey_resp = do_request(args.netpro_host,
                             auth_url_str,
                             headers=user_auth_hdr)






















 

Because we only want the headers we don't attempt any decode of the content (html) returned. Note that the Request library will automatically encode the header so that it is URL safe. This means that any URL special chars like '=' that are in the Access Code will be changed to their hex equivalent. We can use urllib.unquote() to reverse that.

 

    header_dict = newkey_resp.headers
    local_header = header_dict.get('Location')

    new_auth_code = ''
    # if we got a new access code then the location header will start with
    # '?code=' followed by the access code string.
    if (local_header is not None and
        local_header[:6] == '?code='):
        new_auth_code = local_header[6:].strip()
        new_auth_code = urllib.unquote(new_auth_code)






















 

Now we simply repeat the same set of steps shown above using the new access code to get a token and make an authorized request. You will note that the following code simply repeats the steps taken above but utilizes the new Access Code we requested.

 

    if new_auth_code != '':
        # Get the token. First redo the assertion because we have a new code
        assertion = '.'.join([header_encoded, new_auth_code, signature_encoded])
        # Rebuild the data now that we have a new assertion
        data = {'grant_type': grant_type,
                'assertion': assertion,
                'state': state}
        netpro_newtoken_req = do_request(args.netpro_host,
                                         token_url,
                                         data=data,
                                         headers={'Accept': 'application/json'})
        new_key_obj = netpro_newtoken_req.json()
        # new token so new request headers
        auth_hdr = {'Authorization': 'Bearer {0}'.format(
                                                   new_key_obj['access_token']),
                    'Accept': 'application/json'}
        netpro_applist = do_request(args.netpro_host,
                                         netpro_apps_url,
                                         headers=auth_hdr)

        netpro_applist_obj = netpro_applist.json()


        if args.debug:
            print ("DEBUG: There are {0} Apps defined on"
                   " {1}".format(len(netpro_applist_obj), args.netpro_host))






















 

Using Curl for REST API calls

The following are some examples of making basic authentication and OAuth2 token requests utilizing curl. Username and password authentication are simple as the curl supports these natively. For example, to duplicate the application request above to the NetProfiler the user would do:

 

curl --user admin:admin https://$host/api/profiler/1.5/applications -k

 

Now lets try a token request with curl. As we see from the code above a token request simply requires a grant type, and assertion, and a state value. For curl we will package those up as follows (you may need to scroll to the right in the text box below):


data='grant_type=access_code&assertion=eyJhbGciOiJub25lIn0K.ewoJIm5vbmNlIjogIjRkMDZkZTg2IiwKCSJhdWQiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyL2FwaS9jb21tb24vMS4wL29hdXRoL3Rva2VuIiwKCSJpc3MiOiAiaHR0cHM6Ly9jYXNjYWRlLXByb2ZpbGVyIiwKCSJwcm4iOiAiYWRtaW4iLAoJImp0aSI6ICIxIiwKCSJleHAiOiAiMTQ3MDUxMDMyNSIsCgkiaWF0IjogIjE0Njc5MTgzMjUiCn0=.&state=93d83979050908c4832a7c2b2a74e98a'

 

The token request is a push so we call curl as follows:

 

curl -X POST -d $data https://$host/api/common/1.0/oauth/token -k

 

We will get something like this back:

 

<token_response access_token="ewoJIm5vbmNlIjogIjZhNTFiYjEiLAoJImF1ZCI6ICJodHRwczovL2Nhc2NhZGUtcHJvZmlsZXIvYXBpL2NvbW1vbi8xLjAvb2F1dGgvdG9rZW4iLAoJImlzcyI6ICJodHRwczovL2Nhc2NhZGUtcHJvZmlsZXIiLAoJInBybiI6ICJhZG1pbiIsCgkianRpIjogIjEyMyIsCgkiZXhwIjogIjE0NjgzNDA4MzIiLAoJImlhdCI6ICIxNDY4MzM3MjMyIgp9" expires_in="3600" state="93d83979050908c4832a7c2b2a74e98a" token_type="bearer"/>

 

Now we can take the token we got back and use it to get the same application list we got above. For that we will use the -H option on curl to insert the Authorization header. You may use -H as many times with curl as there are headers you would like to add.


access_token="ewoJIm5vbmNlIjogIjZhNTFiYjEiLAoJImF1ZCI6ICJodHRwczovL2Nhc2NhZGUtcHJvZmlsZXIvYXBpL2NvbW1vbi8xLjAvb2F1dGgvdG9rZW4iLAoJImlzcyI6ICJodHRwczovL2Nhc2NhZGUtcHJvZmlsZXIiLAoJInBybiI6ICJhZG1pbiIsCgkianRpIjogIjEyMyIsCgkiZXhwIjogIjE0NjgzNDA4MzIiLAoJImlhdCI6ICIxNDY4MzM3MjMyIgp9"


curl -H "Authorization: Bearer $access_token" -H "Accept: application/json" https://$host/api/profiler/1.5/applications -k