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)
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()
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()
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
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
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
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
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)
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]
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]