Cisco WebEx: Problem generating a new code for a new set of tokens using Python 2.7

2.4k Views Asked by At

Questions at bottom.

Explanation:
We're trying to automate different aspects of the Cisco WebEx API. For instance, getting all devices and device data from the API and then presenting it. The devices could show connectivity status, locations and much more. This is just an example.

In order to get access to the WebEx API, you will need to do the following:

  1. Login to your business account (https://developer.webex.com). enter image description here Then enter your password: enter image description here After this, all logins are redirected through: enter image description here

When you are logged in, you can use the documentation and browse through the API reference. Here you can also try out all the functions and this is using a personal token, which, as far as i know, cannot be exported or seen anywhere on the site. You can copy it and use it, but it expires after 24 hours. I thought maybe you could use Python to log into your account and continously refresh your personal token, but this does not seem like a good solution. enter image description here And therefore, this brings me to:

  1. Create an integration, that will eventually give you the tokens needed for the API access. So here I will show how you can create an integration and thereby get the codes and tokens needed for the API access. enter image description here Create a new app and then select integration. enter image description here enter image description here enter image description here Fill out all information and then click add integration.

  2. Generate a code for requesting a token by accepting the terms and conditions. After adding, you will be taken to the app integration page and here you can see all the information needed to generate a code, which will be used to generate a token set. enter image description here As you can see in the black box, this is the URL you need to copy/paste into a browser and then accept the terms and conditions, before you can generate a code. This is the result of that: enter image description here Checkbox says: "only ask when new permissions are requested" After clicking accept, you are redirected to the URL specified when creating the app integration. This website does not exist, but it will still show the code in the URL: enter image description here Copying the code you can now generate a token set using the following code and information from the app:

Code:

import requests, json

clientID = "C7aa20b0a57cae4127d1e08b15e3a94efacd5c7bb3e1a70ceed4308903a5b807b"
secretID = "b52db3ec6ad8622d0e0e01a0572bac982b214076308ea2e73a566fcf848c9369"
redirectURI = "https://www.your-oauth-website-here.dk/oauth"

def get_tokens(code):
    """Gets access token and refresh token"""
    print("code:", code)
    url = "https://api.ciscospark.com/v1/access_token"
    headers = {'accept':'application/json','content-type':'application/x-www-form-urlencoded'}
    payload = ("grant_type=authorization_code&client_id={0}&client_secret={1}&"
                    "code={2}&redirect_uri={3}").format(clientID, secretID, code, redirectURI)
    req = requests.post(url=url, data=payload, headers=headers)
    results = json.loads(req.text)
    print(results)
    access_token = results["access_token"]
    refresh_token = results["refresh_token"]
    return access_token, refresh_token

test = get_tokens("MWU4YTJmYTgtNjllMy00YjAzLTg0NTQtMTRkYTRiMmIxMzZkNWEzNzRkZDQtYTNk_PF84_33672567-1029-48fb-ba77-7fe2001ee897") # CODE

Result:

code: MWU4YTJmYTgtNjllMy00YjAzLTg0NTQtMTRkYTRiMmIxMzZkNWEzNzRkZDQtYTNk_PF84_33672567-1029-48fb-ba77-7fe2001ee897
{'access_token': 'OTU5MWNhYTItOWNiZC00MWU1LThlZDktNjRlYjI5OGIyYjNmNjI2M2U2MzgtNjAz_PF84_33672567-1029-48fb-ba77-7fe2001ee897', 'expires_in': 1209599, 'refresh_token': 'MGQ4MWRmMzAtYjQyNi00Mzk1LWI0MzAtMmRkMGIzMWQ3ZDVjNzQwZDM3N2YtMWIw_PF84_33672567-1029-48fb-ba77-7fe2001ee897', 'refresh_token_expires_in': 7775999}

If you try to generate a new token pair using the same code, then it displays the following error:

{'message': "POST failed: HTTP/1.1 400 Bad Request (url = https://idbroker.webex.com/idb/oauth2/v1/access_token, request/response TrackingId = ROUTER_5FD9B9DA-FCB3-01BB-03C9-503F01C403C9, error = '(invalid_grant) Authorization code has been used.')", 'errors': [{'description': "POST failed: HTTP/1.1 400 Bad Request (url = https://idbroker.webex.com/idb/oauth2/v1/access_token, request/response TrackingId = ROUTER_5FD9B9DA-FCB3-01BB-03C9-503F01C403C9, error = '(invalid_grant) Authorization code has been used.')"}], 'trackingId': 'ROUTER_5FD9B9DA-FCB3-01BB-03C9-503F01C403C9'}
Traceback (most recent call last):
  File "c:/Python/test_shit.py", line 117, in <module>
    test = get_tokens("MWU4YTJmYTgtNjllMy00YjAzLTg0NTQtMTRkYTRiMmIxMzZkNWEzNzRkZDQtYTNk_PF84_33672567-1029-48fb-ba77-7fe2001ee897") # CODE
  File "c:/Python/test_shit.py", line 113, in get_tokens
    access_token = results["access_token"]
KeyError: 'access_token'

Problem: The tokens validity is set as followed:

  • Token: 14 days expiration.
  • Refresh token: 90 days expiration.

After this period you will have to go through the process of creating a new code and then request a new set of tokens.

I'm struggling to see how I can automate the process of getting a new code, using only Python 2.7.5 and CLI interface on a Red Hat Linux system.

I have tried using the request library to open and click accept, but i've not had any success.

Here's an example of what happends when i try to open the code URL in the black box via requests session:

>>> url = "https://webexapis.com/v1/authorize?client_id=C7aa20b0a57cae4127d1e08b15e3a94efacd5c7bb3e1a70ceed4308903a5b807b&response_type=code&redirect_uri=https%3A%2F%2Fwww.your-oauth-website-here.dk%2Foauth&scope=spark%3Aall%20spark%3Akms&state=set_state_here"
>>> with requests.Session() as s:
...     response = s.get(url)
...
>>> print(response.text)
<!DOCTYPE html>
<html>
<head>
        <meta charset="utf-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>
        Sign In - Webex
</title>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script>
    window.jQuery || document.write("<script src='/idb/js/jquery-3.5.1.min.js'><\/script>");
</script>
<link rel="stylesheet" href="/idb/css/momentum-ui_fbdee616043ab213856f370993c83f01.min.css"/>
<link rel="stylesheet" href="/idb/css/idbstyle_service_default.css" type="text/css" />
<script src="/idb/js/auth_symphony_997017a549f50f21347311e1488607ab.js"></script>
        <link rel="stylesheet" href="/idb/css/idbstyle_symphony_a2736f93e46b8c14f389ccac12436dcb.css" type="text/css" />

<link rel="apple-touch-icon" sizes="180x180" href="/idb/favicons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/idb/favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/idb/favicons/favicon-16x16.png" />
<link rel="manifest" href="/idb/favicons/manifest.json" />
<link rel="mask-icon" href="/idb/favicons/safari-pinned-tab.svg" color="#07C1E4" />
<link rel="shortcut icon" href="/idb/favicons/favicon.ico" />
<meta name="msapplication-config" content="/idb/favicons/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
<meta name="robots" content="noindex, nofollow" />
        <!-- Ref: http://seclab.stanford.edu/websec/framebusting/framebust.pdf -->
<style id="antiClickjack">body{display:none !important;}</style>
<noscript><style>body{display:block !important;}</style></noscript>
<script type="text/javascript">
    if (self === top) {
        var antiClickjack = document.getElementById("antiClickjack");
        antiClickjack.parentNode.removeChild(antiClickjack);
    } else {
        top.location = self.location;
    }
</script>
<script>
        var nameValidated = false;
        var redirectUrl = "https://idbroker.webex.com/idb/IdBGlobalLogin?type=login&goto=https%3A%2F%2Fidbroker.webex.com%2Fidb%2Foauth2%2Fv1%2Fauthorize%3Fclient_id%3DC7aa20b0a57cae4127d1e08b15e3a94efacd5c7bb3e1a70ceed4308903a5b807b%26response_type%3Dcode%26redirect_uri%3Dhttps%253A%252F%252Fwww.your-oauth-website-here.dk%252Foauth%26scope%3Dspark%253Aall%2520spark%253Akms%26state%3Dset_state_here";
        $(document).ready(function() {
                $("#md-form").submit(function(e){
                        e.preventDefault();
                        if(nameValidated) {
                                processForm();
                        }
                });
                var cookieEmail = "";
                if(cookieEmail.length > 0) {
                        $('#GlobalEmailLookupForm').find('input[id="email"]').val(cookieEmail);
                        $('#GlobalEmailLookupForm').find('input[id="isCookie"]').val("true");
                        $('#GlobalEmailLookupForm').submit();
                }
        });
        function validateName(name, nameId) {
        nameId = nameId.replace('IDToken' ,'');
        var divId = 'DivToken' + nameId;
        if(name.length > 0) {
                $.ajax({
                        type: "POST",
                        url: "/idb/validateEmail",
                        data: { user: name, action: 'login', validate: "true" }
                })
                .done(function( responseData ) {
                        if(responseData.status == 'invalid') {
                                document.getElementById('nameContextualError'+nameId).innerHTML = "Enter a valid email address. Example: [email protected]"
                                highlightErrorInputTextbox(divId);
                        } else {
                                nameValidated = true;
                        }
                });
        }
    }
        function processForm() {
                var email = $.trim(document.getElementById('IDToken1').value);
                if(nameValidated) {
                        $('#GlobalEmailLookupForm').find('input[id="email"]').val(email);
                        $('#GlobalEmailLookupForm').submit();
                } else {
                        if(email.length > 0) {
                        $.ajax({
                                type: "POST",
                                url: "/idb/validateEmail",
                                data: { user: email, action: 'login', validate: "true" }
                        })
                        .done(function( responseData ) {
                                if(responseData.status == 'invalid') {
                                        document.getElementById('nameContextualError1').innerHTML = "Enter a valid email address. Example: [email protected]"
                                        highlightErrorInputTextbox('DivToken1');
                                } else {
                                        $('#GlobalEmailLookupForm').find('input[id="email"]').val(email);
                                        $('#GlobalEmailLookupForm').submit();
                                }
                        });
                }
                }
                return false;
        }
</script>
        <style>
                @media (forced-colors: active) {
                        .md-button--blue {
                                border: 1px solid;
                        }
                }
        </style>
</head>
<body id="login" class="md md--sites">
        <div id="globalInfo" style="display:none;">
                <div>
                        It appears that cookies are not enabled on your computer, so some functions will not work. To enable cookies, change the privacy settings in your browser, and then refresh the page.
                </div>
                <a id="close_crossplatform_message" href="javascript:">
                        Close
                </a>
        </div>
        <noscript>
                <div>
                        It appears that JavaScript is not enabled on your computer, so some functions will not work. To enable JavaScript, change the privacy settings in your browser, and then refresh the page.
                </div>
        </noscript>
        <div class="md-panel md-panel--form md-panel--full">
                <div class="md-panel__main">
            <div class="md-panel__image ci__logo"></div>
                        <div class="md-panel__title">
                Enter your email address
            </div>
                        <form class="md-panel__form" id="md-form" novalidate="">
                <div class="md-input-container md-input--filled" id="DivToken1">
                                        <div class="md-input__wrapper">
                                                <input class="md-input" id="IDToken1" data-monitor-id="IDToken1"
                                                        name="IDToken1" value="" autocomplete="email"
                                                        placeholder="Email address"
                                                        alt="Email address"
                                                        onblur="validateName($.trim(this.value), this.id);" maxlength="512"
                                                        type="email" autofocus>
                                        </div>
                    <div class="md-input__messages" id="DivErrorToken1">
                        <div class="message" id="nameContextualError1">
                            Enter the email address for your Webex account.
                        </div>
                    </div>
                </div>
                <div class="md-panel__cta">
                    <button name="btnOK" type="submit" id="IDButton2"
                            class="md-button md-button--blue" onClick="processForm();">
                        Next
                    </button>
                </div>
                        </form>
                </div>
                <form name="GlobalEmailLookup" id="GlobalEmailLookupForm" method="post" action="/idb/globalLogin">
                        <input type="hidden" id="email" name="email" value="" />
                        <input type="hidden" id="isCookie" name="isCookie" value="false" />
                        <input type="hidden" id="ForceAuth" name="ForceAuth" value="false" />
                        <input type="hidden" id="cisService" name="cisService" value="common" />
                                <input type="hidden" name="gotoUrl" value="aHR0cHM6Ly9pZGJyb2tlci53ZWJleC5jb20vaWRiL29hdXRoMi92MS9hdXRob3JpemU/Y2xpZW50X2lkPUM3YWEyMGIwYTU3Y2FlNDEyN2QxZTA4YjE1ZTNhOTRlZmFjZDVjN2JiM2UxYTcwY2VlZDQzMDg5MDNhNWI4MDdiJnJlc3BvbnNlX3R5cGU9Y29kZSZyZWRpcmVjdF91cmk9aHR0cHMlM0ElMkYlMkZ3d3cueW91ci1vYXV0aC13ZWJzaXRlLWhlcmUuZGslMkZvYXV0aCZzY29wZT1zcGFyayUzQWFsbCUyMHNwYXJrJTNBa21zJnN0YXRlPXNldF9zdGF0ZV9oZXJl" />
                        <input type="hidden" id="encodedParamsString" name="encodedParamsString" value="dHlwZT1sb2dpbiY=" />
                </form>
<div id="footer" class="md-panel__footer">
    <img class="footer__logo" src="/idb/images/cisco-webex/lockup/cisco-webex-lockup-blue.svg" alt="Cisco Webex logo"/>
    <div id="footer-links" class="footer__copyright">
        By using Webex you accept the
            <a href="https://www.cisco.com/c/en/us/about/legal/cloud-and-software/cloud-terms.html" target="_blank" rel="noopener">
                Terms of Service
            </a> &
            <a href="https://www.cisco.com/web/siteassets/legal/privacy.html" target="_blank" rel="noopener">
                Privacy Statement
            </a>.
            <a href="https://www.webex.com" target="_blank" rel="noopener">
                Learn more about
            </a> Webex
    </div>
</div>
        </div>
</body>
</html>

I'm just getting the login screen.

Alternatively: Is there a way to sign into the WebEx website using Python CLI?

If I could just log into the Cisco WebEx developer site and then use this session to get a new code, that might work out.

Update: I have managed to get past the "enter email address", but I'm now stuck at enter password, but i continue investigating how I can log into the WebEx website.

with requests.Session() as s:
    login_url = "https://developer.webex.com/login"
    result = s.get(login_url)
    payload = { "email": "[email protected]" }
    print(result.url)
    result = s.post(result.url, data=payload)
    print(result.text)

Any thoughts on the matter will be highly appreciated.

2

There are 2 best solutions below

0
On BEST ANSWER

Turns out I didn't read the documentation properly, sorry for anyone who spent time reading/etc.

The refresh token expiration will also reset, when refreshing the normal token. I didn't know this was the case, but after contacting Cisco Dev support today, they showed me.

The official reply was:

Hi XXX,

Thank you for contacting Webex Developer Support. There is no way to fully automate the OAuth Grant Flow, as the first manual step, where the user authorises the integration to act on their behalf is always mandatory. However once the access token expires or even before that, you can use the refresh token to extend the validity of the access token. With that, the validity of the refresh token will also be renewed, so you can practically keep refreshing your access token indefinitely. The refresh token expiry time is reset at most once a day for performance reason, so the counter will seem to be counting down within the span of one day, but if you were to refresh it the following day, you should see that time/integer go back up.

The code i use to refresh the token:

def refreshCiscoWebExToken(refresh_token):
    """Refresh access token"""
    payload = ( "grant_type=refresh_token&client_id={0}&client_secret={1}&"
                "refresh_token={2}").format(clientID, secretID, refresh_token)
    response = requests.post(url=TokenUrl, data=payload, headers=TokenHeaders)
    if response.status_code == 200:
        results = json.loads(response.text)
        access_token = results["access_token"]
        expires_in = results["expires_in"]
        return(access_token, expires_in)

I hope this can/will help others in search of an answer.

0
On

I was able to solve the problem of retrieving the authorization code using playwright. The script is rather rough around the edges, especially when it deals with the redirect URL at the end of the execution stage, but it gets the job done. Python version is 3.10.

#!/usr/bin/env python
import asyncio
import sys
import uuid
from typing import AnyStr
from urllib.parse import urlparse

from playwright._impl._api_types import TimeoutError
from playwright.sync_api import Page, expect, sync_playwright

webex_client_id: AnyStr = "A141a8326943ba24e991c2a0deba75d56817321e76470082e9e79177ccc43703f"
webex_client_secret: AnyStr = "A6b57ca34710b376c6d9ff1020fe4b86a9ed509bc72952f4a806f158d7fdbca0"
webex_base_path: AnyStr = "https://webexapis.com/v1"
webex_grant_type: AnyStr = "authorization_code"
webex_redirect_uri: AnyStr = "http://localhost:8080/webex"
webex_response_type: AnyStr = "code"
webex_state: AnyStr = f"{uuid.uuid4()}"
username: AnyStr = "[email protected]"
password: AnyStr = "password"


def authorization_uri() -> AnyStr:
    var = f"{webex_base_path}/authorize?"
    var += f"client_id={webex_client_id}"
    var += f"&"
    var += f"response_type={webex_response_type}"
    var += f"&"
    var += f"redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fwebex"
    var += f"&"
    var += f"scope=audit%3Aevents_read%20"
    var += f"spark-admin%3Abroadworks_enterprises_read%20"
    var += f"spark-admin%3Abroadworks_enterprises_write%20"
    var += f"spark-admin%3Abroadworks_subscribers_read%20"
    var += f"spark-admin%3Abroadworks_subscribers_write%20"
    var += f"spark-admin%3Aplaces_read%20"
    var += f"spark-admin%3Aplaces_write%20"
    var += f"spark%3Akms"
    var += f"&"
    var += f"state={webex_state}"
    return var


def locator_landing_page(page: Page):
    return page.locator("div").filter(
        has_text="Welcome to Webex Enter the email address for your Webex account. Sign In Need he").first


def locator_password_page(page: Page):
    return page.locator("div").filter(has_text=f"Welcome {username} 1 Email address {username} 2 Passwo").nth(1)


def run(play):
    firefox = play.firefox
    browser = firefox.launch(headless=True)
    page = browser.new_page()
    try:
        page.goto(f"{authorization_uri()}", timeout=30000)
    except TimeoutError:
        sys.exit()

    initial_locator = locator_landing_page(page)
    expect(initial_locator).to_be_visible()

    # fill and click the username field:
    page.get_by_placeholder("Email address").click()
    page.get_by_placeholder("Email address").fill(username)
    page.get_by_role("button", name="Sign In").click()

    password_locator = locator_password_page(page)
    expect(password_locator).to_be_visible()

    # fill and click the password field:
    page.get_by_placeholder("Password ").fill(password)
    page.get_by_role("button", name="Sign In").click()

    try:
        page.wait_for_load_state('networkidle')
        with page.expect_response('http://localhost:8080/webex?code=**&state=set_state_here', timeout=1000) as response:
            parsed = urlparse(page.url)
            query_elements = parsed.query.split("&")
            code = query_elements[0].split("=")[1]
            print(code.strip())
    except TimeoutError:
        pass
    except asyncio.exceptions.InvalidStateError:
        pass
    finally:
        browser.close()


if __name__ == '__main__':
    with sync_playwright() as playwright:
        run(playwright)