Skip to content

Commit 9c42b6b

Browse files
committed
Separate token decryption errors from token renewal errors
1 parent db5bdfc commit 9c42b6b

File tree

1 file changed

+32
-30
lines changed

1 file changed

+32
-30
lines changed

emailproxy.py

+32-30
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
__author__ = 'Simon Robinson'
77
__copyright__ = 'Copyright (c) 2023 Simon Robinson'
88
__license__ = 'Apache 2.0'
9-
__version__ = '2023-07-12' # ISO 8601 (YYYY-MM-DD)
9+
__version__ = '2023-07-13' # ISO 8601 (YYYY-MM-DD)
1010

1111
import abc
1212
import argparse
@@ -539,8 +539,11 @@ def _save_cache(cache_store_identifier, output_config_parser):
539539

540540

541541
class OAuth2Helper:
542+
class TokenRefreshError(Exception):
543+
pass
544+
542545
@staticmethod
543-
def get_oauth2_credentials(username, password, recurse_retries=True):
546+
def get_oauth2_credentials(username, password, reload_remote_accounts=True):
544547
"""Using the given username (i.e., email address) and password, reads account details from AppConfig and
545548
handles OAuth 2.0 token request and renewal, saving the updated details back to AppConfig (or removing them
546549
if invalid). Returns either (True, '[OAuth2 string for authentication]') or (False, '[Error message]')"""
@@ -602,9 +605,9 @@ def get_account_with_catch_all_fallback(option):
602605
refresh_token = config.get(username, 'refresh_token', fallback=None)
603606

604607
# try reloading remotely cached tokens if possible
605-
if not access_token and CACHE_STORE != CONFIG_FILE_PATH and recurse_retries:
608+
if not access_token and CACHE_STORE != CONFIG_FILE_PATH and reload_remote_accounts:
606609
AppConfig.reload()
607-
return OAuth2Helper.get_oauth2_credentials(username, password, recurse_retries=False)
610+
return OAuth2Helper.get_oauth2_credentials(username, password, reload_remote_accounts=False)
608611

609612
# we hash locally-stored tokens with the given password
610613
if not token_salt:
@@ -697,35 +700,34 @@ def get_account_with_catch_all_fallback(option):
697700
oauth2_string = OAuth2Helper.construct_oauth2_string(username, access_token)
698701
return True, oauth2_string
699702

700-
except InvalidToken as e:
701-
# we always remove the access token - we can easily request another using the refresh token
703+
except OAuth2Helper.TokenRefreshError as e:
704+
# always clear access tokens - can easily request another via the refresh token (with no user interaction)
702705
has_access_token = True if config.get(username, 'access_token', fallback=None) else False
703706
config.remove_option(username, 'access_token')
704707
config.remove_option(username, 'access_token_expiry')
705-
if has_access_token:
706-
AppConfig.save()
707708

708-
# if invalid details are the reason for failure we remove our cached version and re-authenticate - this can
709-
# be disabled by a configuration setting, but note that we always remove credentials on 400 Bad Request
710-
if e.args == (400, APP_PACKAGE) or AppConfig.globals().getboolean('delete_account_token_on_password_error',
711-
fallback=True):
712-
# try authentication again with no cached details - note that if we have just removed an invalid access
713-
# token this will trigger an unnecessary reload from the cache store, but it is worth doing this to
714-
# avoid an unnecessary re-authentication request
715-
recurse_retries = True
716-
717-
# if this is already a second attempt, remove the refresh token as well, and force re-authentication
718-
if not has_access_token:
719-
config.remove_option(username, 'token_salt')
720-
config.remove_option(username, 'refresh_token')
721-
AppConfig.save()
722-
else:
723-
recurse_retries = has_access_token # no need to recurse if we are trying the same credentials again
709+
if not has_access_token:
710+
# if this is already a second failure, remove the refresh token as well, and force re-authentication
711+
config.remove_option(username, 'token_salt')
712+
config.remove_option(username, 'refresh_token')
713+
714+
AppConfig.save()
715+
716+
Log.info('Retrying login due to exception while refreshing OAuth 2.0 tokens for', username,
717+
'(attempt %d):' % (1 if has_access_token else 2), Log.error_string(e))
718+
return OAuth2Helper.get_oauth2_credentials(username, password, reload_remote_accounts=False)
719+
720+
except InvalidToken as e:
721+
if AppConfig.globals().getboolean('delete_account_token_on_password_error', fallback=True):
722+
config.remove_option(username, 'access_token')
723+
config.remove_option(username, 'access_token_expiry')
724+
config.remove_option(username, 'token_salt')
725+
config.remove_option(username, 'refresh_token')
726+
AppConfig.save()
724727

725-
if recurse_retries:
726-
Log.info('Retrying login due to exception while requesting OAuth 2.0 credentials for %s:' % username,
727-
Log.error_string(e))
728-
return OAuth2Helper.get_oauth2_credentials(username, password, recurse_retries=has_access_token)
728+
Log.info('Retrying login due to exception while decrypting OAuth 2.0 credentials for', username,
729+
'(invalid password):', Log.error_string(e))
730+
return OAuth2Helper.get_oauth2_credentials(username, password, reload_remote_accounts=False)
729731

730732
Log.error('Invalid password to decrypt', username, 'credentials - aborting login:', Log.error_string(e))
731733
return False, '%s: Login failed - the password for account %s is incorrect' % (APP_NAME, username)
@@ -930,8 +932,8 @@ def refresh_oauth2_access_token(token_url, client_id, client_secret, refresh_tok
930932
except urllib.error.HTTPError as e:
931933
e.message = json.loads(e.read())
932934
Log.debug('Error refreshing access token - received invalid response:', e.message)
933-
if e.code == 400: # 400 Bad Request typically means re-authentication is required (refresh token expired)
934-
raise InvalidToken(e.code, APP_PACKAGE) from e
935+
if e.code == 400: # 400 Bad Request typically means re-authentication is required (token expired)
936+
raise OAuth2Helper.TokenRefreshError from e
935937
raise e
936938

937939
@staticmethod

0 commit comments

Comments
 (0)