Real world walkthrough of building an OAuth Webex integration

Justin Dupree

Thursday, September 6th 2018

This walkthrough goes into a deep dive explanation of OAuth, what it is used for, and how to work with it. This will help when creating Webex Teams Integrations, which exclusively use OAuth for authorization.

What is OAuth?

OAuth is a framework that allows a user to authorize a product/app to programmatically gain access to data stored within a different product/app. A common example of this would be using your Google or Facebook credentials to log into a third-party site. Instead of creating a new set of credentials for the third-party site, the OAuth process issues a token that represents the user and allows access to the data after the user logs in. The user can then revoke access as needed by revoking the token, vs. removing an entire set of user credentials.

Why OAuth is Important: Security Issues and User Limitations

Before OAuth, if users (also known as resource owners) wanted to allow another integration or application to access their data or resources from another account, they would have to share their credentials with the other third-party application. However, this process created security issues, particularly related to password storage. Sites would either store user passwords in clear text or as an encrypted hash - even when encrypted, there are several free sites that can be used to decrypt hashed strings, and there is little way to know what security measures the third-party site might be using to protect the credentials as a whole.

The other issue was with individual user limitations. Before OAuth, revoking access to one individual end user wasn't possible without revoking access to all users working with the same credentials to access the resources. This meant resource owners couldn't properly restrict access on a case by case basis—if you had the credentials, you had access.

OAuth solved these issues. With OAuth, third-party applications are given an access token which is used to obtain information about the specific service or resource; this token represents the user without exposing their actual credentials. The process of retrieving this access token is called the grant flow.

Integrations and OAuth in Webex Teams

Integrations request permission to invoke Webex Teams APIs on behalf of a user. An example of an integration would be a connection between Webex Teams and a Box folder with the Box integration. Any changes (such as uploading new files or making comments on files) would invoke the Webex Teams API to post a message in a space on behalf of the user that authorized the integration. This differs from a Webex Teams bot, which does not have access to individual user data and exists as its own entity.

Helpful Definitions

Before moving on, knowing the definitions listed below will be helpful, as they may be referenced throughout the rest of this post. For more information, you can also visit our Authentication documentation.

The Four OAuth Roles (as described in RFC6749):

Resource Owner — An entity capable of granting access to a protected resource. When the resource owner is a person, it is typically referred to as an end user.

Resource Server — The server hosting the protected resources, capable of accepting and responding to protected resource requests using Access Tokens.

Client — An application making protected resource requests on behalf of the resource owner and with its authorization. The term "client" does not imply any particular implementation characteristics (e.g., whether the application executes on a server, a desktop, or other devices).

Authorization Server — The server issuing Access Tokens to the client after successfully authenticating the Resource Owner and obtaining authorization.

Client ID — The Authorization Server (in this case, the Webex Teams API) provides the Client Identifier for the integration. The Client ID however, is distinct from the Client Secret (mentioned next) because it is exposed to the Resource Owner (i.e. the user of the integration) and shouldn't be used by itself for client authentication. The Client ID is unique to the Authorization Server.
Example: a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d

Client Secret — The Authorization Server (again, the Webex Teams API in this case) also provides the Client Secret for the integration, which authorizes the Client that is requesting an Access Token. In this way, OAuth 2 uses the Client Secret to verify that a Client should have access to the Resource Server that's waiting for an Access Token, so it can make requests on the user's behalf.
Example: f883984e7a2ba716179f9a5e9cf33abb1b1b83f067a8b0c9c4fa90dae256998c7

Access Token — An object that contains security information such as identity and privileges/scopes for the user account being authorized. Access Tokens for the Webex Teams API are valid for 14 days before expiring automatically.

Refresh Token — A Refresh Token is used to acquire a new Access Token after the original token generated by the Grant Flow expires or is about to expire. The Refresh Token should be stored securely by the application, and is valid for 90 days unless used, at which point the timer will reset (making this type of token effectively perpetual). This enables you to renew the Access Token for a user before it expires (at the previously mentioned 14 days), allowing a user to essentially remain authenticated forever.

Scopes — Scopes determine what resources the Access Token has access to. A full list of scopes available in Webex Teams can be found in our Authentication documentation .

State — This is a unique identifier that can optionally be defined when creating the integration and can be used to verify the authorization request when it's sent to the Redirect URI. Basically, this helps check whether the request containing the Authorization Code is coming from the correct Client or not.

Redirect URI — This represents the endpoint where your users are sent to after they authorize the application. This is typically a valid HTTP endpoint which supports TLS (recommended), so the Authorization Code or Access Tokens can be transmitted securely to avoid attacks.
Example: https://lockbot.cisco.sparkbot.io/oauth

Bear in mind, for native and mobile apps, the Redirect URI scheme may be different. For example: mydemoapp://oauth

The following link includes more information regarding the Redirect URI: https://www.oauth.com/oauth2-servers/redirect-uris/

OAuth Grant Types

As noted above, in order to access the Resource Owner's protected resources, the Client needs to get a credential representing the Resource Owner's authorization and then obtain an Access Token. There are four grant types in OAuth 2.0:

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

Webex Teams currently only supports Authorization Code, which is a grant flow used mostly with web applications to get access to an API.

Setting up the Redirect URI

We'll now get into the actual steps to create the integration. First step: your Redirect URI. As also previously mentioned above, the Redirect URI scheme could be different depending on the nature of the application, whether it's a web based app, a native app, or a mobile app. The Redirect URI acts as the callback entry point for the app and could either be an HTTP redirect URI or custom URI scheme. For example: https://mydemoapp.com:10023/register or mydemoapp://register

We'll focus on how to set up an HTTP Redirect URI for this walkthrough. The Redirect URI is required to use TLS 1.1 or higher for any HTTP URI, in order to protect the endpoint. This prevents any interception during the authorization process. For development purposes, on a Mac you can locally launch a simple light weight server from Terminal with the following command (on Windows, you'd need to install Python first):

python -m SimpleHTTPServer

You should see the following response if everything works correctly:

This is a simple built-in web server from Python that listens on default port 8000, so http://0.0.0.0:8000 or http://localhost:8000 will be your redirect URI.

If you use this method, you will need to use a tunneling proxy such as ngrok to expose the endpoint to the public internet. Many users find this convenient for initial development, but once you're ready to run this in production, you'll want to get an actual hosted site. Common options many developers use include AWS or Heroku, but you're not limited to any specific hosting option as long as your URL is accessible to the open Internet (i.e. not locked down behind a firewall).

Creating an Integration through the Webex for Developers Portal

The next step is to create your integration through the Webex for Developers Portal, which is very easy. Simply log into your Webex account at https://developer.webex.com, click on your avatar at the top right, then click "My Webex Teams Apps". This will take you to the "My Apps" page. Click the blue plus at the top right and then choose "Create an Integration". You should now be on the "New Integration" page. Choose a unique name, a contact email (for Cisco internal support use only, it won't be referenced anywhere publicly), an icon, and the general description or purpose of the integration.

Once these are completed, provide a Redirect URI. You can enter more than one by separating them with a comma, which allows you to redirect your application to different endpoints using a single integration. This could be useful if you have multiple data centers that all perform the same tasks, such as when you have failover setup.

Next, choose which scopes your integration needs access to, and then lastly submit the app by clicking on the "Create Integration" button in blue at the bottom.

Once it has been submitted, be sure to save and store the Client Secret provided (located below the Scopes section of the page) before directing away from this page. If this is lost, you will need to regenerate a new one.

The OAuth Authorization URL shown at the bottom of the integration details page provide the following, in this order:

  • client_id
  • response-type
  • redirect_uri
  • scopes
  • state

We will reference what to do with that Authorization URL in the next section.

Authorization Code Grant Flow

Retrieving the Authorization Code

After your Redirect URI is set up and your Webex Teams integration is created, the next step is to get your Authorization Code. The Client (your app) will ask the Authorization Server (Webex Teams) for an Authorization Code, which will later be exchanged for an Access Token. To do so, you will need to send your prospective users to https://api.ciscospark.com/v1/authorize with the following parameters included, all in one form-encoded URL:

  • response_type — Must be set to 'code' (string)
  • client_id — The Client Identifier (ID), issued when registering the app
  • redirect_uri — Must match one of the URIs provided when the application was created
  • scope — A space-separated list of scopes being requested by the application. This should match exactly with the scopes that were selected when the application was created (see our Authentication guide for full list)
  • state — A unique string used to verify authenticity of the incoming request to the defined redirect URI (optional)

Using a Cisco-built and publicly available integration called LockBot as an example, when someone goes to its page on the Webex App Hub and clicks "Visit Site To Connect", they're directed to https://lockbot.cisco.sparkbot.io/request/.

The Grant button on that page goes to a URL that looks like this (note all the parameters mentioned above are included):

https://api.ciscospark.com/v1/authorize?client_id=a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d&response_type=code&redirect_uri=https%3A%2F%2Flockbot.cisco.sparkbot.io%2Foauth&scope=spark%3Amessages_write%20spark%3Arooms_read%20spark%3Amemberships_read%20spark%3Arooms_write%20spark%3Apeople_read%20spark%3Akms%20spark%3Amemberships_write&state=lockbot

With your integration, this URL should match the OAuth Authorization URL shown on the integration detail page in "My Apps" when you created your Webex Teams integration.

When a user clicks on that link, it will send them to the standard Webex login page. Once they sign in, they would see the following authorization prompt for LockBot:

Once they click Accept, they've granted LockBot access to their Webex Teams data, but LockBot doesn't yet have a functional token to use. Instead, Webex Teams sends an Authorization Code to the previously defined Redirect URI (in LockBot's case, https://lockbot.cisco.sparkbot.io/oauth) with the code and state, like the following:

https://lockbot.cisco.sparkbot.io/oauth?code=Njc5NmI0YTU4OTc1ZDRhNDczMDcxZDE4ZDI4ZmQzMjMzNjJiZDczMWQzMTgwMGI0O&state=lockbot

Looking at that URL, the code in the query string and state "lockbot" previously passed in the request are both present. This code will need to be exchanged for an actual token, which we go through in a later step.

Next as a quick sidebar, we'll show how to create a real simple Grant Flow Landing Page that mimics what a user would see with LockBot; feel free to use it as a baseline for your own landing page.

Creating a Grant Flow Landing Page

Below is a very simple HTML example showing how to build an interface with a Grant button, allowing the user to grant the integration access to their Webex Teams data. The main piece to note is the URL associated with the Grant button itself, which sends the user into the Grant Flow process:

This is the HTML used to create the above LockBot Grant page.

<!DOCTYPE html>
<html>
  <head>
    <title>LockBot Permissions Request</title>
    <meta charset='utf-8'>
    <link rel='stylesheet' type='text/css' href="/static/css/index.css">
    <script src='http://code.jquery.com/jquery-1.4.2.js'></script>
  </head>
  <body>
    <section class='container'>
      <div class='center content'>
        <div class='spacer'></div>
        <img src= "/static/css/lock-bot.png"/>
        <div class='spacer'></div>
        <div class='center'>
          <h1>Grant Lockbot Access To Webex Teams</h1>
        </div>
        <div class='spacer-small'></div>
        <div class='center'>
          <a href="https://api.ciscospark.com/v1/authorize?client_id=a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d&response_type=code&redirect_uri=https%3A%2F%2Flockbot.cisco.sparkbot.io%2Foauth&scope=spark%3Amessages_write%20spark%3Arooms_read%20spark%3Amemberships_read%20spark%3Arooms_write%20spark%3Apeople_read%20spark%3Akms%20spark%3Amemberships_write&state=lockbot">
            <div class='button' style='width:512px;'>GRANT</div>
          </a>
        </div>
      </div>
    </section>
  </body>
</html>

Getting the Access Token

Once your user has moved through the Grant Flow process and you have the Authorization Code, next you'll need to exchange that code for the actual Access Token your app can use to invoke the Webex Teams APIs on behalf of your user. To do this, the app will need to perform an HTTP POST to the following URL with a standard set of OAuth parameters (this endpoint will only accept an x-www-form-urlencoded body): https://api.ciscospark.com/v1/access_token

The required parameters are:

  • grant-type — This should be set to "authorization_code"
  • client_id — Issued when creating your integration
  • client_secret — Remember this guy? You kept it safe somewhere when creating your integration.
  • code — The Authorization Code from the previous step
  • redirect_uri — Must match the one used in the previous step

An example might look like this:

https://api.ciscospark.com/v1/access_token?grant-type=authorization_code&client_id=a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d&client_secret=f883984e7a2ba716179f9a5e9cf33abb1b1b83f067a8b0c9c4fa90dae256998c7&code=Njc5NmI0YTU4OTc1ZDRhNDczMDcxZDE4ZDI4ZmQzMjMzNjJiZDczMWQzMTgwMGI0O&redirect_uri=https%3A%2F%2Flockbot.cisco.sparkbot.io%2Foauth

Webex Teams will then respond with JSON containing an Access Token that's valid for 14 days, and a Refresh Token that expires in 90 days, as shown in the example below:

{
    "access_token":"a375503d74185e03b67322149dceffbe3ce130133bbf4d6282341ae73a31b17a2",
    "expires_in":1209600, //seconds
    "refresh_token":"a434252a53e6f97483df14d4097ba956a24cdf56a6673014a1647ef6eadf4cc50",
    "refresh_token_expires_in":7776000 //seconds
}

The Refresh Token is used to get a new Access Token before it expires, so you can keep your application live without prompting the user to go through the grant process again. Every time the Refresh Token is used to get a new Access Token, the expiration timer will reset.

However, if you allow the Access Token to expire, using it to make a request from the API will result in an "Invalid Token Error" like the following example:

If the Access Token expires, you need to use the Refresh Token to generate a new Access Token. To refresh an Access token, it's recommended to catch token errors in your application, and when they occur, refresh the token and try again. If the refresh fails, this might mean your Refresh Token is older than 90 days and has expired, or that the user or an admin has otherwise revoked their tokens. When this happens, direct the user to the Webex Teams login process again and get a new set of tokens for them. This is considered a better practice than using a timer to refresh tokens before they expire.

Getting a new Access Token using the Refresh Token is done much like getting the initial Access Token but with different parameters:

  • grant-type — This should be set to "refresh_token"
  • client_id — Same client_id you received when you created your integration
  • client_secret — Same secret you used to get the Access Token originally
  • refresh_token: — This is the Refresh Token returned when you got the initial Access Token

An example might look like:

https://api.ciscospark.com/v1/access_token?grant-type=refresh_token&client_id=a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d&client_secret=f883984e7a2ba716179f9a5e9cf33abb1b1b83f067a8b0c9c4fa90dae256998c7&refresh_token=a434252a53e6f97483df14d4097ba956a24cdf56a6673014a1647ef6eadf4cc50

Once this is done, a new Access Token will be returned in the response and can be used for the next 14 days.

Example response:

{
    "access_token": "82d19946849bac67373eb401824b3afcb919544dcda8314220a867a6e561b9e43",
    "expires_in": 1209599, //seconds
    "refresh_token": "a434252a53e6f97483df14d4097ba956a24cdf56a6673014a1647ef6eadf4cc50",
    "refresh_token_expires_in": 7775975 //seconds
}

Example: Retrieving the Authorization Code via Python

Provided below is a snippet in Python, which shows how to retrieve the authorization code through the back-end once the request is redirected back to the server endpoint or Redirect URI. For this example, we're using the Flask web framework. Full working code is available at: https://github.com/webex/Spark-API-Demos/tree/master/OAuthDemo

def oauth():
    """Retrieves oauth code to generate tokens for users"""

    if "code" in request.args and state == "YOUR_STATE_STRING":
        state = request.args.get("state") #Captures value of the state.
        code = request.args.get("code") #Captures value of the code.
        print "OAuth code:", code
        print "OAuth state:", state
        access_token, refresh_token = get_tokens(code) #As you can see, get_tokens() uses the code and returns access and refresh tokens.

        #Now, let's do something with the generated token: Get the user's info: PersonId, Email Address and DisplayName.
        personID, emailID, displayName = get_oauthuser_info(access_token)
        print "personID:", personID
        print "email ID:", emailID
        print "display Name", displayName
        return render_template("granted.html")
    else:
        return render_template("index.html")
                  

Example: Retrieving an Access Token Via Python

The following example snippet is part of the same app as the last example, but shows how to retrieve an Access Token instead of the Authorization Code:

import requests
import json

clientID = "APP_CLIENTID"
secretID = "APP_SECRETID"
redirectURI = "APP_REDIRECTURI" #This could different if you publicly expose this endpoint.

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

get_tokens("123YRDDBSSDDS...")

You can get the complete demo code at: https://github.com/webex/Spark-API-Demos/tree/master/OAuthDemo

Example: Retrieving an Access Token via Postman

While this isn't likely to be anything you'll use in production, sometimes having a GUI REST Client example makes things easier to understand.

  1. Retrieve your Authorization Code from the original Grant Flow and save it - note the authorization code is only valid for one request and will become invalid once the request to /access_token is made.

    Example: code=Njc5NmI0YTU4OTc1ZDRhNDczMDcxZDE4ZDI4ZmQzMjMzNjJiZDczMWQzMTgwMGI0O&state=<insert chosen state>

  2. In Postman, make a POST request using the following information:

    Endpoint: https://api.ciscospark.com/v1/access_token

    Headers:

    Accept: application/json

    Content-type application/json

    Body:

    {
      "grant_type":"authorization_code",
      "client_id":"a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d",
      "client_secret":"f883984e7a2ba716179f9a5e9cf33abb1b1b83f067a8b0c9c4fa90dae256998c7",
      "code":"Njc5NmI0YTU4OTc1ZDRhNDczMDcxZDE4ZDI4ZmQzMjMzNjJiZDczMWQzMTgwMGI0O",
      "redirect_uri":"https://lockbot.cisco.sparkbot.io"
    }

    Once you have performed the POST request, the response body should include both the Access Token and the Refresh Token.

  3. In order to generate a new access_token using the refresh_token, you can do a POST to the same endpoint and with the same header mentioned above, but with a slightly different body

    {
      "grant_type":"refresh_token",
      "client_id":"a3ae1859d31806348e9fa80e20e5c71d3f1496ca61879bde4ef39739897e4788d",
      "client_secret":"f883984e7a2ba716179f9a5e9cf33abb1b1b83f067a8b0c9c4fa90dae256998c7",
      "refresh_token":"a434252a53e6f97483df14d4097ba956a24cdf56a6673014a1647ef6eadf4cc50"
    }

Using OAuth is a secure, though slightly complex way to allow access to Webex Teams API resources. Hopefully this detailed walkthrough helps make sense of the steps necessary to get from the creation of your Webex Teams application on the developer portal to the retrieval of a valid Access Token for your application to use. If you have any additional questions, you can reach out to the Webex Developer Support team via email or live chat 24/7/365.

Colby Alladaye, Arielle Johnson, and Jeff Marshall also contributed to this post.