Skip to content

Commit

Permalink
Merge branch 'client_credentials_password'
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrob committed Sep 12, 2024
2 parents 5610650 + 963ec4b commit 3b6f180
Show file tree
Hide file tree
Showing 2 changed files with 24 additions and 12 deletions.
20 changes: 13 additions & 7 deletions emailproxy.config
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ local_address = 127.0.0.1
[Account setup]
documentation = Accounts are specified using your email address as the section heading (e.g., [[email protected]],
etc, below). Account usernames (i.e., email addresses) must be unique - only one entry per account is permitted.
Each account section must provide values for `permission_url`, `token_url`, `oauth2_scope` and `redirect_uri`. If
you are adding an account for a service other than the examples shown below then the provider's documentation should
provide these details.
Each account section must provide values for at least `token_url`, `oauth2_scope` and `client_id`. Depending on the
OAuth 2.0 flow you are using, other values may also be required (see examples below). If you are adding an account
for a service other than the examples shown below then the provider's documentation should provide these details.

You will also need to add your own `client_id` and `client_secret` values as indicated below. These can either be
reused from an existing source (such as another email client that supports OAuth 2.0), or you can register and use
Expand Down Expand Up @@ -228,7 +228,6 @@ documentation = *** note: this is an advanced O365 account example; in most case
token_url = https://login.microsoftonline.com/*** your tenant id here ***/oauth2/v2.0/token
oauth2_scope = https://outlook.office365.com/.default
oauth2_flow = client_credentials
redirect_uri = http://localhost
client_id = *** your client id here ***
client_secret = *** your client secret here ***

Expand All @@ -237,7 +236,6 @@ documentation = *** note: this is an advanced O365 account example; in most case
token_url = https://login.microsoftonline.com/*** your tenant id here ***/oauth2/v2.0/token
oauth2_scope = https://outlook.office365.com/IMAP.AccessAsUser.All https://outlook.office365.com/POP.AccessAsUser.All https://outlook.office365.com/SMTP.Send offline_access
oauth2_flow = password
redirect_uri = http://localhost
client_id = *** your client id here ***
client_secret = *** your client secret here ***

Expand All @@ -246,7 +244,6 @@ documentation = *** note: this is an advanced Google account example; in most ca
token_url = https://oauth2.googleapis.com/token
oauth2_scope = https://mail.google.com/
oauth2_flow = service_account
redirect_uri = http://localhost
client_id = file
client_secret = *** your /path/to/service-account-key.json here ***

Expand All @@ -255,7 +252,6 @@ documentation = *** note: this is an advanced Google account example; in most ca
token_url = https://oauth2.googleapis.com/token
oauth2_scope = https://mail.google.com/
oauth2_flow = service_account
redirect_uri = http://localhost
client_id = key
client_secret = *** your pasted service account JSON key file contents here,
making sure to indent all lines by at least one space ***
Expand Down Expand Up @@ -292,6 +288,15 @@ documentation = The parameters below control advanced options for the proxy. In
using catch-all accounts or the proxy's `--cache-store` parameter you must manually remove unencrypted secrets from
the local configuration file after the encrypted secret has been created (i.e., this will not be automatic).

- use_login_password_as_client_credentials_secret (default = False): When using the O365 client credentials grant
(CCG) flow, rather than encrypting the client secret (see above), the proxy can be instructed to use the given
IMAP/POP/SMTP login password as the client secret. This approach removes the risk of storing the unencrypted client
secret in the proxy's configuration file, and also means there is no risk of unauthorised account access when using
the O365 CCG flow in conjunction with the proxy's catch-all mode (see below). To enable this option, set
`use_login_password_as_client_credentials_secret` to True. Note that if a `client_secret` value is present in your
account's configuration entry, that value will be used instead of the given IMAP/POP/SMTP login password even if
this option is enabled. To avoid this, remove the entire `client_secret` line from the configuration entry.

- allow_catch_all_accounts (default = False): The default behaviour of the proxy is to require a full separate
configuration file entry for each account. However, when proxying multiple accounts from the same domain it can be
cumbersome to have to create multiple near-identical configuration profiles. To simplify this the proxy supports
Expand All @@ -308,4 +313,5 @@ documentation = The parameters below control advanced options for the proxy. In
[emailproxy]
delete_account_token_on_password_error = True
encrypt_client_secret_on_first_use = False
use_login_password_as_client_credentials_secret = False
allow_catch_all_accounts = False
16 changes: 11 additions & 5 deletions emailproxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,13 +725,12 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
jwt_certificate_path = AppConfig.get_option_with_catch_all_fallback(config, username, 'jwt_certificate_path')
jwt_key_path = AppConfig.get_option_with_catch_all_fallback(config, username, 'jwt_key_path')

# note that we don't require permission_url here because it is not needed for the client credentials grant flow,
# and likewise for client_secret here because it can be optional for Office 365 configurations
if not (token_url and oauth2_scope and redirect_uri and client_id):
# because the proxy supports a wide range of OAuth 2.0 flows, in addition to the token_url we only mandate the
# core parameters that are required by all methods: oauth2_scope and client_id
if not (token_url and oauth2_scope and client_id):
Log.error('Proxy config file entry incomplete for account', username, '- aborting login')
return (False, '%s: Incomplete config file entry found for account %s - please make sure all required '
'fields are added (permission_url, token_url, oauth2_scope, redirect_uri, client_id '
'and client_secret)' % (APP_NAME, username))
'fields are added (at least token_url, oauth2_scope and client_id)' % (APP_NAME, username))

# while not technically forbidden (RFC 6749, A.1 and A.2), it is highly unlikely the example value is valid
example_client_value = '*** your'
Expand Down Expand Up @@ -1125,12 +1124,19 @@ def get_oauth2_authorisation_tokens(token_url, redirect_uri, client_id, client_s
params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'
params['client_assertion'] = jwt_client_assertion

# CCG flow can fall back to the login password as the client secret (see GitHub #271 discussion)
elif oauth2_flow == 'client_credentials' and AppConfig.get_global(
'use_login_password_as_client_credentials_secret', fallback=False):
params['client_secret'] = password

if oauth2_flow != 'authorization_code':
del params['code'] # CCG/ROPCG flows have no code, but we need the scope and (for ROPCG) username+password
params['scope'] = oauth2_scope
if oauth2_flow == 'password':
params['username'] = username
params['password'] = password
if not redirect_uri:
del params['redirect_uri'] # redirect_uri is not typically required in non-code flows; remove if empty
try:
response = urllib.request.urlopen(
urllib.request.Request(token_url, data=urllib.parse.urlencode(params).encode('utf-8'),
Expand Down

0 comments on commit 3b6f180

Please sign in to comment.