From 97b4b94ddbe7eba1e225331d861b133c05abfc06 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 14 Jun 2019 22:14:34 +1000 Subject: [PATCH 1/7] fixed yubikey auth flow - added functions to handle custom field object - handle challange id value being single - various base64 decoding + base64 url encoding now required by google on post payload --- aws_google_auth/google.py | 62 +++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index f885c6b..7f7832b 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -7,6 +7,7 @@ import json import logging import os +import re import sys import requests @@ -132,6 +133,39 @@ def parse_error_message(sess): else: return error.text + @staticmethod + def find_key_handle(input, challengeTxt): + typeOfInput = type(input) + if typeOfInput == dict: # parse down a dict + for item in input: + return Google.find_key_handle(input[item], challengeTxt) + elif typeOfInput == list: # looks like we've hit an array - iterate it + array = list(filter(None, input)) # remove any None type objects from the array + for item in array: + typeValue = type(item) + if typeValue == list: # another array - recursive call + return Google.find_key_handle(item, challengeTxt) + elif typeValue == unicode: # found a string value - maybe what we're looking for + try: # keyHandle string will be base64 encoded - + # if its not an exception is thrown and we continue as its not the string we're after + base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item)) + if base64UrlEncoded != challengeTxt: # make sure its not the challengeTxt - if it not return it + return base64UrlEncoded + except: + pass + else: # ints bools etc we don't care + continue + + @staticmethod + def find_app_id(inputString): + try: + searchResult = re.search('"appid":"[a-z://.-_]+"',inputString).group() + searchObject = json.loads('{' + searchResult + '}') + return str(searchObject['appid']) + except: + logging.exception('Was unable to find appid value in googles SAML page') + sys.exit(1) + def do_login(self): self.session = requests.Session() self.session.headers['User-Agent'] = "AWS Sign-in/{} (Cevo aws-google-auth)".format(self.version) @@ -357,26 +391,32 @@ def handle_captcha(self, sess, payload): def handle_sk(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] - challenges_txt = response_page.find('input', { 'name': "id-challenge" }).get('value') - challenges = json.loads(challenges_txt) facet_url = urllib_parse.urlparse(challenge_url) facet = facet_url.scheme + "://" + facet_url.netloc - app_id = challenges["appId"] + + keyHandleJSField = response_page.find('div',{'jsname': 'C0oDBd'}).get('data-challenge-ui') + startJSONPosition = keyHandleJSField.find('{') + endJSONPosition = keyHandleJSField.rfind('}') + keyHandleJsonPayload = json.loads(keyHandleJSField[startJSONPosition:endJSONPosition+1]) + + keyHandle = self.find_key_handle(keyHandleJsonPayload, base64.urlsafe_b64encode(base64.b64decode(challenges_txt))) + appId = self.find_app_id(str(keyHandleJsonPayload)) + u2f_challenges = [] - for c in challenges["challenges"]: - c["appId"] = app_id - u2f_challenges.append(c) + u2f_challenges.append({'version': 'U2F_V2', 'challenge': base64.urlsafe_b64encode(base64.b64decode(challenges_txt)), 'appId': appId, 'keyHandle': keyHandle}) # Prompt the user up to attempts_remaining times to insert their U2F device. attempts_remaining = 5 auth_response = None while True: try: - auth_response = json.dumps(u2f.u2f_auth(u2f_challenges, facet)) + auth_response_dict = u2f.u2f_auth(u2f_challenges, facet) + #auth_response_dict['sessionId'] = '' + auth_response = json.dumps(auth_response_dict) break except RuntimeWarning: logging.error("No U2F device found. %d attempts remaining", @@ -403,8 +443,7 @@ def handle_sk(self, sess): response_page.find('input', { 'name': 'challengeType' }).get('value'), - 'continue': - response_page.find('input', { + 'continue': response_page.find('input', { 'name': 'continue' }).get('value'), 'scc': @@ -419,10 +458,7 @@ def handle_sk(self, sess): response_page.find('input', { 'name': 'checkedDomains' }).get('value'), - 'pstMsg': - response_page.find('input', { - 'name': 'pstMsg' - }).get('value'), + 'pstMsg': '1', 'TL': response_page.find('input', { 'name': 'TL' From 956bff21a6e1b0122a59d17bc6ba565b30ab2bf5 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 14 Jun 2019 22:23:28 +1000 Subject: [PATCH 2/7] removed unused code --- aws_google_auth/google.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index 7f7832b..5d5fedc 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -415,7 +415,6 @@ def handle_sk(self, sess): while True: try: auth_response_dict = u2f.u2f_auth(u2f_challenges, facet) - #auth_response_dict['sessionId'] = '' auth_response = json.dumps(auth_response_dict) break except RuntimeWarning: From 1d0c98f99d97ceb3b93d7c4543e5e4be01480fff Mon Sep 17 00:00:00 2001 From: David Date: Sat, 15 Jun 2019 06:37:56 +1000 Subject: [PATCH 3/7] fixed lint items from build --- aws_google_auth/google.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index 5d5fedc..0f70766 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -136,30 +136,30 @@ def parse_error_message(sess): @staticmethod def find_key_handle(input, challengeTxt): typeOfInput = type(input) - if typeOfInput == dict: # parse down a dict + if typeOfInput == dict: # parse down a dict for item in input: return Google.find_key_handle(input[item], challengeTxt) - elif typeOfInput == list: # looks like we've hit an array - iterate it - array = list(filter(None, input)) # remove any None type objects from the array + elif typeOfInput == list: # looks like we've hit an array - iterate it + array = list(filter(None, input)) # remove any None type objects from the array for item in array: typeValue = type(item) - if typeValue == list: # another array - recursive call + if typeValue == list: # another array - recursive call return Google.find_key_handle(item, challengeTxt) - elif typeValue == unicode: # found a string value - maybe what we're looking for - try: # keyHandle string will be base64 encoded - + elif typeValue == unicode: # found a string value - maybe what we're looking for + try: # keyHandle string will be base64 encoded - # if its not an exception is thrown and we continue as its not the string we're after base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item)) - if base64UrlEncoded != challengeTxt: # make sure its not the challengeTxt - if it not return it + if base64UrlEncoded != challengeTxt: # make sure its not the challengeTxt - if it not return it return base64UrlEncoded except: pass - else: # ints bools etc we don't care + else: # ints bools etc we don't care continue @staticmethod def find_app_id(inputString): try: - searchResult = re.search('"appid":"[a-z://.-_]+"',inputString).group() + searchResult = re.search('"appid":"[a-z://.-_]+"', inputString).group() searchObject = json.loads('{' + searchResult + '}') return str(searchObject['appid']) except: @@ -398,10 +398,10 @@ def handle_sk(self, sess): facet_url = urllib_parse.urlparse(challenge_url) facet = facet_url.scheme + "://" + facet_url.netloc - keyHandleJSField = response_page.find('div',{'jsname': 'C0oDBd'}).get('data-challenge-ui') + keyHandleJSField = response_page.find('div', {'jsname': 'C0oDBd'}).get('data-challenge-ui') startJSONPosition = keyHandleJSField.find('{') endJSONPosition = keyHandleJSField.rfind('}') - keyHandleJsonPayload = json.loads(keyHandleJSField[startJSONPosition:endJSONPosition+1]) + keyHandleJsonPayload = json.loads(keyHandleJSField[startJSONPosition:endJSONPosition + 1]) keyHandle = self.find_key_handle(keyHandleJsonPayload, base64.urlsafe_b64encode(base64.b64decode(challenges_txt))) appId = self.find_app_id(str(keyHandleJsonPayload)) From 368c44ad9eb2ee2b874013b031fdf708dc7b3258 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 15 Jun 2019 07:29:47 +1000 Subject: [PATCH 4/7] made python 3x compatible --- aws_google_auth/google.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index 0f70766..f0de0e5 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -145,7 +145,9 @@ def find_key_handle(input, challengeTxt): typeValue = type(item) if typeValue == list: # another array - recursive call return Google.find_key_handle(item, challengeTxt) - elif typeValue == unicode: # found a string value - maybe what we're looking for + elif typeValue == int or typeValue == bool: # ints bools etc we don't care + continue + else: # we went a string or unicode here (python 3.x lost unicode global) try: # keyHandle string will be base64 encoded - # if its not an exception is thrown and we continue as its not the string we're after base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item)) @@ -153,9 +155,6 @@ def find_key_handle(input, challengeTxt): return base64UrlEncoded except: pass - else: # ints bools etc we don't care - continue - @staticmethod def find_app_id(inputString): try: From c86ba41323cac9d8d839e040bb1623c59b4ab73c Mon Sep 17 00:00:00 2001 From: David Date: Sat, 15 Jun 2019 07:35:56 +1000 Subject: [PATCH 5/7] fixed linting again --- aws_google_auth/google.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index f0de0e5..6d29199 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -147,7 +147,7 @@ def find_key_handle(input, challengeTxt): return Google.find_key_handle(item, challengeTxt) elif typeValue == int or typeValue == bool: # ints bools etc we don't care continue - else: # we went a string or unicode here (python 3.x lost unicode global) + else: # we went a string or unicode here (python 3.x lost unicode global) try: # keyHandle string will be base64 encoded - # if its not an exception is thrown and we continue as its not the string we're after base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item)) @@ -155,6 +155,7 @@ def find_key_handle(input, challengeTxt): return base64UrlEncoded except: pass + @staticmethod def find_app_id(inputString): try: From 1c34a190f96673e29c93d80d1896fe2b3dca9d87 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 21 Jun 2019 08:55:07 +1000 Subject: [PATCH 6/7] remove padding if exists - fixes random login error --- aws_google_auth/google.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index 6d29199..b7436c3 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -406,8 +406,11 @@ def handle_sk(self, sess): keyHandle = self.find_key_handle(keyHandleJsonPayload, base64.urlsafe_b64encode(base64.b64decode(challenges_txt))) appId = self.find_app_id(str(keyHandleJsonPayload)) + # txt sent for signing needs to be base64 url encode + # we also have to remove any base64 padding because including including it will prevent google accepting the auth response + challenges_txt_encode_pad_removed = base64.urlsafe_b64encode(base64.b64decode(challenges_txt)).strip('=') u2f_challenges = [] - u2f_challenges.append({'version': 'U2F_V2', 'challenge': base64.urlsafe_b64encode(base64.b64decode(challenges_txt)), 'appId': appId, 'keyHandle': keyHandle}) + u2f_challenges.append({'version': 'U2F_V2', 'challenge': challenges_txt_encode_pad_removed, 'appId': appId, 'keyHandle': keyHandle}) # Prompt the user up to attempts_remaining times to insert their U2F device. attempts_remaining = 5 From aa4ab7615544663ed15de33a9d482d1bfc5689e8 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 28 Jun 2019 17:48:22 +1000 Subject: [PATCH 7/7] Python 3.x compatible! --- aws_google_auth/google.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/aws_google_auth/google.py b/aws_google_auth/google.py index b7436c3..305121a 100644 --- a/aws_google_auth/google.py +++ b/aws_google_auth/google.py @@ -138,7 +138,10 @@ def find_key_handle(input, challengeTxt): typeOfInput = type(input) if typeOfInput == dict: # parse down a dict for item in input: - return Google.find_key_handle(input[item], challengeTxt) + rvalue = Google.find_key_handle(input[item], challengeTxt) + if rvalue is not None: + return rvalue + elif typeOfInput == list: # looks like we've hit an array - iterate it array = list(filter(None, input)) # remove any None type objects from the array for item in array: @@ -408,9 +411,10 @@ def handle_sk(self, sess): # txt sent for signing needs to be base64 url encode # we also have to remove any base64 padding because including including it will prevent google accepting the auth response - challenges_txt_encode_pad_removed = base64.urlsafe_b64encode(base64.b64decode(challenges_txt)).strip('=') + challenges_txt_encode_pad_removed = base64.urlsafe_b64encode(base64.b64decode(challenges_txt)).strip('='.encode()) + u2f_challenges = [] - u2f_challenges.append({'version': 'U2F_V2', 'challenge': challenges_txt_encode_pad_removed, 'appId': appId, 'keyHandle': keyHandle}) + u2f_challenges.append({'version': 'U2F_V2', 'challenge': challenges_txt_encode_pad_removed.decode(), 'appId': appId, 'keyHandle': keyHandle.decode()}) # Prompt the user up to attempts_remaining times to insert their U2F device. attempts_remaining = 5