Skip to content

Commit

Permalink
Separate token decryption errors from token renewal errors
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrob committed Jul 15, 2023
1 parent db5bdfc commit 9c42b6b
Showing 1 changed file with 32 additions and 30 deletions.
62 changes: 32 additions & 30 deletions emailproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
__author__ = 'Simon Robinson'
__copyright__ = 'Copyright (c) 2023 Simon Robinson'
__license__ = 'Apache 2.0'
__version__ = '2023-07-12' # ISO 8601 (YYYY-MM-DD)
__version__ = '2023-07-13' # ISO 8601 (YYYY-MM-DD)

import abc
import argparse
Expand Down Expand Up @@ -539,8 +539,11 @@ def _save_cache(cache_store_identifier, output_config_parser):


class OAuth2Helper:
class TokenRefreshError(Exception):
pass

@staticmethod
def get_oauth2_credentials(username, password, recurse_retries=True):
def get_oauth2_credentials(username, password, reload_remote_accounts=True):
"""Using the given username (i.e., email address) and password, reads account details from AppConfig and
handles OAuth 2.0 token request and renewal, saving the updated details back to AppConfig (or removing them
if invalid). Returns either (True, '[OAuth2 string for authentication]') or (False, '[Error message]')"""
Expand Down Expand Up @@ -602,9 +605,9 @@ def get_account_with_catch_all_fallback(option):
refresh_token = config.get(username, 'refresh_token', fallback=None)

# try reloading remotely cached tokens if possible
if not access_token and CACHE_STORE != CONFIG_FILE_PATH and recurse_retries:
if not access_token and CACHE_STORE != CONFIG_FILE_PATH and reload_remote_accounts:
AppConfig.reload()
return OAuth2Helper.get_oauth2_credentials(username, password, recurse_retries=False)
return OAuth2Helper.get_oauth2_credentials(username, password, reload_remote_accounts=False)

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

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

# if invalid details are the reason for failure we remove our cached version and re-authenticate - this can
# be disabled by a configuration setting, but note that we always remove credentials on 400 Bad Request
if e.args == (400, APP_PACKAGE) or AppConfig.globals().getboolean('delete_account_token_on_password_error',
fallback=True):
# try authentication again with no cached details - note that if we have just removed an invalid access
# token this will trigger an unnecessary reload from the cache store, but it is worth doing this to
# avoid an unnecessary re-authentication request
recurse_retries = True

# if this is already a second attempt, remove the refresh token as well, and force re-authentication
if not has_access_token:
config.remove_option(username, 'token_salt')
config.remove_option(username, 'refresh_token')
AppConfig.save()
else:
recurse_retries = has_access_token # no need to recurse if we are trying the same credentials again
if not has_access_token:
# if this is already a second failure, remove the refresh token as well, and force re-authentication
config.remove_option(username, 'token_salt')
config.remove_option(username, 'refresh_token')

AppConfig.save()

Log.info('Retrying login due to exception while refreshing OAuth 2.0 tokens for', username,
'(attempt %d):' % (1 if has_access_token else 2), Log.error_string(e))
return OAuth2Helper.get_oauth2_credentials(username, password, reload_remote_accounts=False)

except InvalidToken as e:
if AppConfig.globals().getboolean('delete_account_token_on_password_error', fallback=True):
config.remove_option(username, 'access_token')
config.remove_option(username, 'access_token_expiry')
config.remove_option(username, 'token_salt')
config.remove_option(username, 'refresh_token')
AppConfig.save()

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

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

@staticmethod
Expand Down

0 comments on commit 9c42b6b

Please sign in to comment.