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:
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. And therefore, this brings me to:
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. Create a new app and then select integration. Fill out all information and then click add integration.
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. 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: 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: 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:
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.
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.