Example #1
0
 def __init__(self, token, expires_in, token_type, expires_at=None):
     Struct.__init__(self,
                     {k: v
                      for k, v in locals().items() if k != 'self'})
     if expires_at == None:
         issued = datetime.utcnow()
         self.expires_at = issued + timedelta(seconds=self.expires_in)
Example #2
0
    def __init__(self, ib, user_info, client_info, cache_token=True, **kwargs):
        ''' can take an optional scope argument which is passed to the brokers auth_code_grant_flow.
        If scope is not given then the default of the broker is used
        scope is a space separated list of requested scopes:
            spark:people_read          Read your company directory
            spark:rooms_read           List the titles of rooms that you're in
            spark:rooms_write          Manage rooms on your behalf
            spark:memberships_read     List the people in rooms that you're in
            spark:memberships_write    Invite people to rooms on your behalf
            spark:messages_read        Read the content of rooms that you're in
            spark:messages_write       Post and delete messages on your behalf
        '''
        self._ib = ib
        self._user_info = user_info
        self._client_info = client_info
        self.cache_token = cache_token

        refresh_token = None
        if cache_token:
            '''
            check if a cached refresh token exists
            These are saved as <userid>-token.json.
            '''
            cache_file = '{}-refresh.json'.format(user_info['id'])
            try:
                f = open(cache_file, 'r')
            except IOError:
                pass
            else:
                log.info('Reading cached refresh token from file {}'.format(
                    cache_file))
                refresh_token = Struct(json.load(f))
                f.close()
                refresh_token.expires_at = datetime.strptime(
                    refresh_token.expires_at, '%Y-%m-%d %H:%M:%S')
                refresh_token = AuthToken(token=refresh_token.token,
                                          expires_in=refresh_token.expires_in,
                                          token_type=refresh_token.token_type,
                                          expires_at=refresh_token.expires_at)
                log.info(
                    'Refresh token lifetime is {} seconds, expires at {}, lifetime remaining: {:.0%}'
                    .format(refresh_token.expires_in, refresh_token.expires_at,
                            refresh_token.ratio_remaining()))
        # if cache_token:

        # this is the handler to get a new refresh token
        self.get_new_refresh_token = lambda: self.code_grant_flow(**kwargs)

        if refresh_token:
            # exchange refresh token for access token
            self._refresh = refresh_token
            self.refresh_access_token()
        else:
            self.get_new_refresh_token()
Example #3
0
 def __init__(self, ib, user_info, client_info, **kwargs):
     ''' can take an optional scope argument which is passed to the brokers auth_code_grant_flow.
     If scope is not given then the default of the broker is used
     scope is a space separated list of requested scopes:
         spark:people_read          Read your company directory
         spark:rooms_read           List the titles of rooms that you're in
         spark:rooms_write          Manage rooms on your behalf
         spark:memberships_read     List the people in rooms that you're in
         spark:memberships_write    Invite people to rooms on your behalf
         spark:messages_read        Read the content of rooms that you're in
         spark:messages_write       Post and delete messages on your behalf
     '''
     self._ib = ib
     self._user_info = user_info
     self._client_info = client_info
     
     '''
     check if a cached refresh token exists
     These are saved as <userid>-token.json.
     '''
     cache_file = '{}-refresh.json'.format(user_info['id'])
     refresh_token = None
     try:
         f = open(cache_file, 'r')
     except IOError:
         pass
     else:
         log.info('Reading cached refresh token from file {}'.format(cache_file))
         refresh_token = Struct(json.load(f))
         f.close()
         refresh_token.expires_at = datetime.strptime(refresh_token.expires_at, '%Y-%m-%d %H:%M:%S')
         refresh_token = AuthToken(token = refresh_token.token, 
                                   expires_in = refresh_token.expires_in, 
                                   token_type = refresh_token.token_type, 
                                   expires_at = refresh_token.expires_at)
         log.info('Refresh token lifetime is {} seconds, expires at {}, lifetime remaining: {:.0%}'.
                  format(refresh_token.expires_in, refresh_token.expires_at, refresh_token.ratio_remaining()))
     # this is the handler to get a new refresh token
     self.get_new_refresh_token = lambda: self.code_grant_flow(**kwargs)
     
     if refresh_token:
         # exchange refresh token for access token
         self._refresh = refresh_token
         self.refresh_access_token()
     else:
         self.get_new_refresh_token()
Example #4
0
    def auth_code_to_token(self, client_info, code):

        endpoint = self.endpoint('access_token')
        data = Struct()
        data.grant_type = 'authorization_code'
        data.redirect_uri = client_info['redirect_uri']
        data.code = code
        data.client_id = client_info['id']
        data.client_secret = client_info['secret']

        log.debug(
            'Exchanging code for access token. POST to {}'.format(endpoint))
        response = self.session.post(endpoint, data=data.get_dict())
        dump_response(response)
        if response.status_code != 200:
            raise IbError('Unexpected status code on POST(12): {} {}'.format(
                response.status_code, response.reason))

        oauth_token = Struct(response.json())
        return oauth_token
Example #5
0
 def auth_code_to_token(self, client_info, code):
     
     endpoint = self.endpoint('access_token')
     data = Struct() 
     data.grant_type = 'authorization_code'
     data.redirect_uri = client_info['redirect_uri']
     data.code = code
     data.client_id = client_info['id']
     data.client_secret = client_info['secret']
     
     log.debug('Exchanging code for access token. POST to {}'.format(endpoint))
     response = self.session.post(endpoint, data=data.get_dict())
     dump_response(response)
     if response.status_code != 200: raise IbError('Unexpected status code on POST(12): {} {}'.format(response.status_code, response.reason))
     
     oauth_token = Struct(response.json())
     return oauth_token
Example #6
0
    def refresh_token_to_access_token(self, refresh_token, client_info):
        endpoint = self.endpoint('access_token')
        data = Struct()
        data.grant_type = 'refresh_token'
        data.refresh_token = refresh_token.token
        data.client_id = client_info['id']
        data.client_secret = client_info['secret']

        # Sometime we get a 401 and retrying helps to fix that temporary glitch
        for _ in range(5):
            response = self.session.post(endpoint, data=data.get_dict())
            dump_response(response)
            if response.status_code != 401: break
            log.warning('Got 401 on token refresh. Retrying...')
        if response.status_code != 200:
            raise IbError(
                'Unexpected status code on GET(12): {} {}'.format(
                    response.status_code, response.reason),
                response.status_code, response.reason, response.text)
        result = Struct(response.json())
        return result
Example #7
0
 def refresh_token_to_access_token(self, refresh_token, client_info):
     endpoint = self.endpoint('access_token')
     data = Struct()
     data.grant_type = 'refresh_token'
     data.refresh_token = refresh_token.token
     data.client_id = client_info['id']
     data.client_secret = client_info['secret']
     
     # Sometime we get a 401 and retrying helps to fix that temporary glitch
     for _ in range(5):
         response = self.session.post(endpoint, data=data.get_dict())
         dump_response(response)
         if response.status_code != 401: break
         log.warning('Got 401 on token refresh. Retrying...')
     if response.status_code != 200: raise IbError('Unexpected status code on GET(12): {} {}'.format(response.status_code, response.reason), response.status_code, response.reason, response.text)
     result = Struct(response.json())
     return result
Example #8
0
 def __init__(self, token, expires_in, token_type, expires_at = None):
     Struct.__init__(self, {k:v for k,v in locals().items() if k != 'self'})
     if expires_at == None:
         issued = datetime.utcnow()
         self.expires_at = issued + timedelta(seconds=self.expires_in)
Example #9
0
 def auth_code_grant_flow(self, user_info, client_info, scope = 'webexsquare:admin'):
     ''' Executes an OAuth Authorization Code Grant Flow
     Returns an Authorizatioon code
     '''
     assert user_info['email']
     assert user_info['id']
     assert user_info['password']
     assert client_info['id']
     assert client_info['redirect_uri']
     assert client_info['secret']
     
     # we try to use the Authorization Code Grant Flow
     endpoint = self.endpoint('authorize')
     
     # random state
     flow_state = str(uuid.uuid4())
     data = Struct() 
     data.response_type = 'code'
     data.state = flow_state
     data.client_id = client_info['id']
     data.redirect_uri = client_info['redirect_uri']
     data.scope = scope
     log.debug('auth code grant flow: access endpoint {}'.format(endpoint))
     
     response = self.session.get(endpoint, params=data.get_dict())
     dump_response(response)
     if response.status_code !=200: raise FlowError('Unexpected status code on GET(1): {} {}'.format(response.status_code, response.reason)) 
   
     # after a number of redirects this gets us to a page on which we need to enter an email address
     # The title is "Sign In - Cisco WebEx"
     # if we still have a valid session cookie we might actually get to the OAuth2 authorization page directly
     soup = bs4.BeautifulSoup(response.text, 'lxml')
     title = soup.find('title')
     
     if not(title and title.text.strip() in ['Sign In - Cisco WebEx', 'OAuth2 Authorization - Cisco WebEx']):
         raise FlowError('Didn\'t find expected title')
     
     if title and title.text.strip() == 'Sign In - Cisco WebEx':
         # Need to sign in.
        
         log.debug('auth code grant flow: found expected \'Sign In - Cisco WebEx\'')
         '''
         This form is part of the reply:
             <form name="GlobalEmailLookup" id="GlobalEmailLookupForm" method="post" action="/idb/globalLogin">
                 <input type="hidden" id="email" name="email" value=""></input>
                 <input type="hidden" id="isCookie" name="isCookie" value="false"></input>
                 <input type="hidden" name="gotoUrl" value="aHR0cHM6Ly9pZGJyb2tlci53ZWJleC5jb20vaWRiL29hdXRoMi92MS9hdXRob3JpemU/c2NvcGU9c3BhcmslM0FwZW9wbGVfcmVhZCtzcGFyayUzQXJvb21zX3JlYWQrc3BhcmslM0FtZW1iZXJzaGlwc19yZWFkK3NwYXJrJTNBbWVzc2FnZXNfcmVhZCZjbGllbnRfaWQ9Q2U2N2Y5NzE0YTEzN2U2ODg0OGJhNjQ1YzQ4NjBmYThhZWUyYzUwMzFlZTA1YmMyMjE2MzNkMGNlZWRlOWExYjkmcmVkaXJlY3RfdXJpPWh0dHBzJTNBJTJGJTJGb2F1dGgua3JvaG5zLmRlJTJGb2F1dGgyJnN0YXRlPXNvbWVSYW5kb21TdHJpbmcmcmVzcG9uc2VfdHlwZT1jb2Rl" />
                 <input type="hidden" id="encodedParamsString" name="encodedParamsString" value="dHlwZT1sb2dpbg==" />
             </form>
         A POST with the email address to that form is the next step
         '''
         soup = bs4.BeautifulSoup(response.text, 'lxml')
         form = soup.find(id = 'GlobalEmailLookupForm')
         if not form: raise FlowError('Couldn\'t find form \'GlobalEmailLookupForm\' to post user\'s email address')
         
         inputs = form.find_all('input')
         # 1st input is the email address
         inputs[0]['value'] = user_info['email']
         form_data = {i['name'] : i['value'] for i in inputs}
         form_action = urllib.parse.urljoin(response.request.url, form.get('action', urllib.parse.urlparse(response.request.url).path)) 
         log.debug('auth code grant flow: Posting email address {} to form {}'.format(user_info['email'], form_action))
         response = self.session.post(form_action, data = form_data)
         dump_response(response)    
         
         # For CIS users this redirects us to a page with title "Sign In - Cisco WebEx"
         log.debug('auth code grant flow: Checking for title \'Sign In - Cisco WebEx\'')
         soup = bs4.BeautifulSoup(response.text, 'lxml')
         title = soup.find('title')
         if title and title.text.strip() == 'Sign In - Cisco WebEx':
             # Identified the form to directly enter credentials
             dump_response(response)
             # search for the form with name 'Login'
             form = soup.find(lambda tag : tag.name == 'form' and tag.get('name', '') == 'Login')
             inputs = form.find_all('input')
             form_data = {i['name'] : i['value'] for i in inputs}
             form_data['IDToken0'] = ''
             form_data['IDToken1'] = user_info['email']
             form_data['IDToken2'] = user_info['password']
             form_data['IDButton'] = 'Sign In'
             form_action = urllib.parse.urljoin(response.request.url, form.get('action', urllib.parse.urlparse(response.request.url).path)) 
             log.debug('auth code grant flow: Found title \'Sign In - Cisco WebEx\'. Posting credentials to {}'.format(form_action))
             response = self.session.post(form_action, data = form_data)
             dump_response(response)
         else:
             # authentication of a cisco.com SSO enabled user requires multiple steps (SAML 2.0 REDIRECT/POST flow with some javascript ...
             response = self._cisco_sso_user_auth(response, user_info['id'], user_info['password'])
         # if title and title.text.strip() == if title and title.text.strip() == 'Sign In - Cisco WebEx': .. else ..
     # if title and title.text.strip() == 'Sign In - Cisco WebEx':
                 
     # this now is a form where we are requested to grant the requested access
     soup = bs4.BeautifulSoup(response.text, 'lxml')
     form = soup.find('form')
     if not form: raise FlowError('No form found(13)')
     
     # the action tag has a URL to be used for the form action. The full URL uses the same base as the request URL
     form_action = urllib.parse.urljoin(response.url, form.get('action', urllib.parse.urlparse(response.url).path)) 
     
     inputs = form.find_all('input')
     if not inputs: raise FlowError('No input fields found(14)')
     
     # compile the form data
     # the form basically has few hidden fields and the "decision" field needs to be set to "accept"
     form_data = {inp['name'] : inp['value'] for inp in inputs if inp['type'] == 'hidden'}
     form_data['decision'] = 'accept'
     
     # Again post, but no automatic redirects
     log.debug('auth code grant flow: Granting access to client by posting \'accept\' decision')
     response = self.session.post(form_action, data = form_data, allow_redirects=False)
     
     # follow redirects, but stop at client redirect URI; this allows to use non-existing redirect URIs
     response = self._follow_redirects(response, client_info['redirect_uri'])
     if not response: raise FlowError('Failed to get OAuth authorization code')
     if response['state'][0] != flow_state: raise FlowError('State has been tampered with?!. Got ({}), expected ({})'.format(response['state'][0], flow_state))
     return response['code'][0]
Example #10
0
    def auth_code_grant_flow(self,
                             user_info,
                             client_info,
                             scope='webexsquare:admin'):
        ''' Executes an OAuth Authorization Code Grant Flow
        Returns an Authorizatioon code
        '''
        assert user_info['email']
        assert user_info['id']
        assert user_info['password']
        assert client_info['id']
        assert client_info['redirect_uri']
        assert client_info['secret']

        # we try to use the Authorization Code Grant Flow
        endpoint = self.endpoint('authorize')

        # random state
        flow_state = str(uuid.uuid4())
        data = Struct()
        data.response_type = 'code'
        data.state = flow_state
        data.client_id = client_info['id']
        data.redirect_uri = client_info['redirect_uri']
        data.scope = scope
        log.debug('auth code grant flow: access endpoint {}'.format(endpoint))

        response = self.session.get(endpoint, params=data.get_dict())
        dump_response(response)
        if response.status_code != 200:
            raise FlowError('Unexpected status code on GET(1): {} {}'.format(
                response.status_code, response.reason))

        # after a number of redirects this gets us to a page on which we need to enter an email address
        # The title is "Sign In - Cisco WebEx"
        # if we still have a valid session cookie we might actually get to the OAuth2 authorization page directly
        soup = bs4.BeautifulSoup(response.text, 'lxml')
        title = soup.find('title')

        if not (title and title.text.strip() in [
                'Sign In - Cisco WebEx', 'OAuth2 Authorization - Cisco WebEx'
        ]):
            raise FlowError('Didn\'t find expected title')

        if title and title.text.strip() == 'Sign In - Cisco WebEx':
            # Need to sign in.

            log.debug(
                'auth code grant flow: found expected \'Sign In - Cisco WebEx\''
            )
            '''
            This form is part of the reply:
                <form name="GlobalEmailLookup" id="GlobalEmailLookupForm" method="post" action="/idb/globalLogin">
                    <input type="hidden" id="email" name="email" value=""></input>
                    <input type="hidden" id="isCookie" name="isCookie" value="false"></input>
                    <input type="hidden" name="gotoUrl" value="aHR0cHM6Ly9pZGJyb2tlci53ZWJleC5jb20vaWRiL29hdXRoMi92MS9hdXRob3JpemU/c2NvcGU9c3BhcmslM0FwZW9wbGVfcmVhZCtzcGFyayUzQXJvb21zX3JlYWQrc3BhcmslM0FtZW1iZXJzaGlwc19yZWFkK3NwYXJrJTNBbWVzc2FnZXNfcmVhZCZjbGllbnRfaWQ9Q2U2N2Y5NzE0YTEzN2U2ODg0OGJhNjQ1YzQ4NjBmYThhZWUyYzUwMzFlZTA1YmMyMjE2MzNkMGNlZWRlOWExYjkmcmVkaXJlY3RfdXJpPWh0dHBzJTNBJTJGJTJGb2F1dGgua3JvaG5zLmRlJTJGb2F1dGgyJnN0YXRlPXNvbWVSYW5kb21TdHJpbmcmcmVzcG9uc2VfdHlwZT1jb2Rl" />
                    <input type="hidden" id="encodedParamsString" name="encodedParamsString" value="dHlwZT1sb2dpbg==" />
                </form>
            A POST with the email address to that form is the next step
            '''
            soup = bs4.BeautifulSoup(response.text, 'lxml')
            form = soup.find(id='GlobalEmailLookupForm')
            if not form:
                raise FlowError(
                    'Couldn\'t find form \'GlobalEmailLookupForm\' to post user\'s email address'
                )

            inputs = form.find_all('input')
            # 1st input is the email address
            inputs[0]['value'] = user_info['email']
            form_data = {i['name']: i['value'] for i in inputs}
            form_action = urllib.parse.urljoin(
                response.request.url,
                form.get('action',
                         urllib.parse.urlparse(response.request.url).path))
            log.debug(
                'auth code grant flow: Posting email address {} to form {}'.
                format(user_info['email'], form_action))
            response = self.session.post(form_action, data=form_data)
            dump_response(response)

            # For CIS users this redirects us to a page with title "Sign In - Cisco WebEx"
            log.debug(
                'auth code grant flow: Checking for title \'Sign In - Cisco WebEx\''
            )
            soup = bs4.BeautifulSoup(response.text, 'lxml')
            title = soup.find('title')
            if title and title.text.strip() == 'Sign In - Cisco WebEx':
                # Identified the form to directly enter credentials
                dump_response(response)
                # search for the form with name 'Login'
                form = soup.find(lambda tag: tag.name == 'form' and tag.get(
                    'name', '') == 'Login')
                inputs = form.find_all('input')
                form_data = {i['name']: i['value'] for i in inputs}
                form_data['IDToken0'] = ''
                form_data['IDToken1'] = user_info['email']
                form_data['IDToken2'] = user_info['password']
                form_data['IDButton'] = 'Sign In'
                form_action = urllib.parse.urljoin(
                    response.request.url,
                    form.get('action',
                             urllib.parse.urlparse(response.request.url).path))
                log.debug(
                    'auth code grant flow: Found title \'Sign In - Cisco WebEx\'. Posting credentials to {}'
                    .format(form_action))
                response = self.session.post(form_action, data=form_data)
                dump_response(response)
            else:
                # authentication of a cisco.com SSO enabled user requires multiple steps (SAML 2.0 REDIRECT/POST flow with some javascript ...
                response = self._cisco_sso_user_auth(response, user_info['id'],
                                                     user_info['password'])
            # if title and title.text.strip() == if title and title.text.strip() == 'Sign In - Cisco WebEx': .. else ..
        # if title and title.text.strip() == 'Sign In - Cisco WebEx':

        # this now is a form where we are requested to grant the requested access
        soup = bs4.BeautifulSoup(response.text, 'lxml')
        form = soup.find('form')
        if not form: raise FlowError('No form found(13)')

        # the action tag has a URL to be used for the form action. The full URL uses the same base as the request URL
        form_action = urllib.parse.urljoin(
            response.url,
            form.get('action',
                     urllib.parse.urlparse(response.url).path))

        inputs = form.find_all('input')
        if not inputs: raise FlowError('No input fields found(14)')

        # compile the form data
        # the form basically has few hidden fields and the "decision" field needs to be set to "accept"
        form_data = {
            inp['name']: inp['value']
            for inp in inputs if inp['type'] == 'hidden'
        }
        form_data['decision'] = 'accept'

        # Again post, but no automatic redirects
        log.debug(
            'auth code grant flow: Granting access to client by posting \'accept\' decision'
        )
        response = self.session.post(form_action,
                                     data=form_data,
                                     allow_redirects=False)

        # follow redirects, but stop at client redirect URI; this allows to use non-existing redirect URIs
        response = self._follow_redirects(response,
                                          client_info['redirect_uri'])
        if not response:
            raise FlowError('Failed to get OAuth authorization code')
        if response['state'][0] != flow_state:
            raise FlowError(
                'State has been tampered with?!. Got ({}), expected ({})'.
                format(response['state'][0], flow_state))
        return response['code'][0]