Skip to content

Commit

Permalink
Merge branch 'main' into gcloud-service-account
Browse files Browse the repository at this point in the history
  • Loading branch information
simonrob committed Dec 20, 2023
2 parents 7f86f2d + 8b2c0ad commit 10dc3fc
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 34 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ The [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/b
- Gmail / Google Workspace: register a [Google API desktop app client](https://developers.google.com/identity/protocols/oauth2/native-app)
- AOL and Yahoo Mail (and subproviders such as AT&T) are not currently allowing new client registrations with the OAuth email scope – the only option here is to reuse the credentials from an existing client that does have this permission.

The proxy also supports the [client credentials grant](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow) and [resource owner password credentials grant](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc) OAuth 2.0 flows if needed.
Please note that currently only Office 365 is known to support these methods.
In addition, when using the client credentials grant flow, Office 365 only supports IMAP/POP, [_not_ SMTP](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#use-client-credentials-grant-flow-to-authenticate-imap-and-pop-connections) (use [smtp2graph](https://github.com/EvanTrow/smtp2graph) instead here).
The proxy supports [Google Cloud service accounts](https://cloud.google.com/iam/docs/service-account-overview) for access to Google Workspace Gmail.
It also supports the [client credentials grant (CCG)](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow) and [resource owner password credentials grant (ROPCG)](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth-ropc) OAuth 2.0 flows.
Please note that currently only Office 365 is known to support the CCG and ROPCG methods.
See the [sample configuration file](https://github.com/simonrob/email-oauth2-proxy/blob/main/emailproxy.config) for further details.


Expand Down
10 changes: 10 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Security Policy

## Supported Versions

Using the [latest release of the proxy](https://github.com/simonrob/email-oauth2-proxy/releases/latest) is always recommended.
Any security issues found in prior versions should be reported, but may only be fixed if they affect the latest version.

## Reporting a Vulnerability

Report vulnerabilities or security issues using [GitHub's security tools](https://github.com/simonrob/email-oauth2-proxy/security).
97 changes: 76 additions & 21 deletions emailproxy.config
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,36 @@ documentation = Accounts are specified using your email address as the section h
- The proxy supports the client credentials grant (CCG) and resource owner password credentials grant (ROPCG) OAuth
2.0 flows (both currently only known to be available for Office 365). To use either of these flows, add an account
entry as normal, but do not add a `permission_url` value (it does not apply, and its absence signals to the proxy to
use the appropriate token retrieval mechanism). An example is given at the end of the sample account entries below.
The CCG flow is the default in this mode; to specify the ROPCG flow add `oauth2_flow = password` in your account
configuration, and use a normal `oauth2_scope` value rather than the CCG-specific `.default`. Please note also that
Office 365's CCG flow does not support SMTP, and by default it has essentially no local access control when using
IMAP/POP in this mode (no user consent is involved, so the proxy cannot validate passwords). Because of this, you
are encouraged to enable the proxy's secret encryption option - see `encrypt_client_secret_on_first_use` at the end
of this file. In addition, if you are using the proxy in an environment where there is any possibility of malicious
access attempts before the first valid login then pre-encrypting account entries is highly recommended - see the
example setup script at https://github.com/simonrob/email-oauth2-proxy/issues/61#issuecomment-1259110336.
use the appropriate token retrieval mechanism). For CCG, set `oauth2_scope = https://outlook.office365.com/.default`
and `oauth2_flow = client_credentials`. For ROPG, set `oauth2_flow = password` (and use a standard scope value). An
example is given for both methods towards the end of the sample account entries below.

- WARNING: Please note that by default the CCG flow has essentially no local access control when creating new
accounts (no user consent is involved, so the proxy cannot validate login attempts unless an account entry
already exists its configuration file). Using the CCG flow with the proxy in a publicly-accessible context is
not advised. This is especially important when using the proxy's catch-all feature (which is likely to be the
case given the typical use-cases for the CCG flow). Because of this, you are highly encouraged to enable the
proxy's secret encryption option - see `encrypt_client_secret_on_first_use` at the end of this file. In
addition, if you are using the proxy in an environment where there is any possibility of malicious access
attempts before the first valid login, pre-encrypting account entries is highly recommended. See the example
script at https://github.com/simonrob/email-oauth2-proxy/issues/61#issuecomment-1259110336.

Gmail customisation:
- The proxy supports the use of service accounts with Gmail for Google Workspace (note: normal Gmail accounts do not
support this method). To use this option, add an account entry as normal, but do not add a `permission_url` value
(it does not apply, and its absence signals to the proxy to use the appropriate token retrieval mechanism). Set
`oauth2_flow = service_account`. The service account key itself can either be referenced in an external file, or
pasted directly into the account entry. For the file approach, set `client_id = file` and `client_secret` to the
full path to the JSON key file. To include the key directly, set `client_id = key`, then paste the full contents of
your service account's JSON key as the value for `client_secret`, making sure all lines are indented by at least one
space (so that the proxy can tell they are all part of one value). An example is given for both methods towards the
end of the sample account entries below.

- WARNING: Please note that the same potential security issues outlined above with O365's CCG flow also apply to
the service account method: there is essentially no local access control when creating new accounts. Using a
service account with the proxy in a publicly-accessible context is not advised. You are highly encouraged to
enable the proxy's secret encryption option (see `encrypt_client_secret_on_first_use` at the end of this file)
and consider pre-encrypting account entries.

Advanced account configuration:
- For most configurations the default `redirect_uri` value of `http://localhost` is correct, unless you have
Expand Down Expand Up @@ -181,7 +202,7 @@ redirect_uri = http://localhost
client_id = *** your client id here ***
client_secret = *** your client secret here ***

[ccg.or.ropcg.[email protected]]
[[email protected]]
documentation = *** note: this is an advanced O365 account example; in most cases you want the version above instead ***
token_url = https://login.microsoftonline.com/*** your tenant id here ***/oauth2/v2.0/token
oauth2_scope = https://outlook.office365.com/.default
Expand All @@ -190,6 +211,34 @@ redirect_uri = http://localhost
client_id = *** your client id here ***
client_secret = *** your client secret here ***

[[email protected]]
documentation = *** note: this is an advanced O365 account example; in most cases you want the version above instead ***
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 ***

[[email protected]]
documentation = *** note: this is an advanced Google account example; in most cases you want the version above instead ***
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 ***

[[email protected]]
documentation = *** note: this is an advanced Google account example; in most cases you want the version above instead ***
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 ***

[Advanced proxy configuration]
documentation = The parameters below control advanced options for the proxy. In most cases you will not need to modify
the values in this section. If any of these values are not found, the proxy will assume the default value, which
Expand All @@ -204,19 +253,23 @@ documentation = The parameters below control advanced options for the proxy. In
be laborious or need administrator intervention, this can potentially result in a denial-of-service issue, whether
malicious or not. It can also be the source of confusion if using a client (such as Firefox) that stores a separate
password per protocol for each account, but does not make this clear when changing account passwords. Set this
option to False and the proxy will instead return an error when an incorrect password is provided.
option to False and the proxy will instead return an error when an incorrect password is provided. For accounts
using the O365 CCG flow or a Google Cloud service account this option will be overridden and always set to False.

- encrypt_client_secret_on_first_use (default = False): The proxy encrypts sensitive configuration values (e.g.,
cached access tokens) using the password that is given when accessing an account via IMAP/POP/SMTP. It does not do
this for values that are not sensitive. In the most common operation mode (i.e., interactively authorising account
access), the `client_secret` value falls into this category - it is not actually secret, and there is no real need
to prevent access to it. However, when using the Office 365 client credentials grant flow there is no user involved,
and possession of the secret grants full access to an account. If you use this method and it is possible that others
may gain access to the proxy's configuration file, set `encrypt_client_secret_on_first_use` to True and the proxy
will replace the `client_secret` value with a new property `client_secret_encrypted` at the next token refresh. Note
that this option is not compatible with `allow_catch_all_accounts` unless all accounts use the same login password.
In addition, if you are using the proxy's `--cache-store` parameter you will need to manually remove unencrypted
secrets from this configuration file after the encrypted secret has been created (i.e., it will not be automatic).
to prevent access to it. However, when using the client credentials grant (CCG) flow or a service account, there is
no user involved, and possession of the secret grants full access to an account. If you use either of these methods
and it is possible that others may gain access to the proxy's configuration file; or, you are using catch-all
accounts (see below) and others may attempt to log in with accounts that the secret has access to but that you have
not yet set up with the proxy, set `encrypt_client_secret_on_first_use` to True and the proxy will replace the
`client_secret` value with a new property `client_secret_encrypted` at the next token refresh. Note that this option
is not fully compatible with `allow_catch_all_accounts` unless all accounts use the same login password, or you
undertake some additional manual setup configuration (see below for further details). In particular, if you are
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).

- 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
Expand All @@ -225,9 +278,11 @@ documentation = The parameters below control advanced options for the proxy. In
example, add a section [@domain.com] with all of the standard required account values, and the proxy will intercept
authentication requests for all usernames at `domain.com`. Whenever a previously unseen account attempts to connect,
account authorisation will take place as normal, and the proxy will automatically create a new account-level section
that does not need to be configured manually. Any account-level configuration will override domain-level values.
If needed, the global catch-all section [@] can also be used. Please note that this option is not compatible with
`encrypt_client_secret_on_first_use` unless all IMAP/POP/SMTP accounts at the same domain use the same password.
that does not need to be configured manually. Any account-level configuration will override domain-level values
(except for account access and refresh tokens). If needed, the global catch-all section [@] can also be used. Please
note that this option is not fully compatible with `encrypt_client_secret_on_first_use` unless all IMAP/POP/SMTP
accounts at the same domain use the same password, or you undertake additional manual configuraton steps - see the
discussion at https://github.com/simonrob/email-oauth2-proxy/issues/214#issuecomment-1861593781 for details.

[emailproxy]
delete_account_token_on_password_error = True
Expand Down
30 changes: 20 additions & 10 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-11-19' # ISO 8601 (YYYY-MM-DD)
__version__ = '2023-12-20' # ISO 8601 (YYYY-MM-DD)
__package_version__ = '.'.join([str(int(i)) for i in __version__.split('-')]) # for pyproject.toml usage only

import abc
Expand Down Expand Up @@ -340,10 +340,10 @@ def save(store_id, config_dict):


class AWSSecretsManagerCacheStore(CacheStore):
# noinspection PyGlobalUndefined,PyPackageRequirements
@staticmethod
def _get_boto3_client(store_id):
try:
# noinspection PyGlobalUndefined
global boto3, botocore
import boto3
import botocore.exceptions
Expand Down Expand Up @@ -756,7 +756,11 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
try:
# if both secret values are present we use the unencrypted version (as it may have been user-edited)
if client_secret_encrypted and not client_secret:
client_secret = cryptographer.decrypt(client_secret_encrypted)
try:
client_secret = cryptographer.decrypt(client_secret_encrypted)
except InvalidToken as e: # needed to avoid looping as we don't remove secrets on decryption failure
Log.error('Invalid password to decrypt', username, 'secret - aborting login:', Log.error_string(e))
return False, '%s: Login failed - the password for account %s is incorrect' % (APP_NAME, username)

if access_token or refresh_token: # if possible, refresh the existing token(s)
if not access_token or access_token_expiry - current_time < TOKEN_EXPIRY_MARGIN:
Expand Down Expand Up @@ -799,8 +803,11 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
return False, '%s: Login failed for account %s: %s' % (APP_NAME, username, auth_result)

if not oauth2_flow:
# default to ROPCG if not set (CCG is `client_credentials`; service account is `service_account`)
oauth2_flow = 'password'
Log.error('No `oauth2_flow` value specified for', username, '- aborting login')
return (False, '%s: Incomplete config file entry found for account %s - please make sure an '
'`oauth2_flow` value is specified when using a method that does not require a '
'`permission_url`' % (APP_NAME, username))

response = OAuth2Helper.get_oauth2_authorisation_tokens(token_url, redirect_uri, client_id,
client_secret, auth_result, oauth2_scope,
oauth2_flow, username, password)
Expand Down Expand Up @@ -1063,15 +1070,16 @@ def get_service_account_authorisation_token(key_type, key_path_or_contents, oaut
import google.oauth2.service_account
import google.auth.transport.requests
except ModuleNotFoundError:
error_message = 'Unable to load Google Auth SDK - please install the `requests` and `google-auth` ' \
'modules: `python -m pip install requests google-auth`'
raise Exception(error_message)
raise Exception('Unable to load Google Auth SDK - please install the `requests` and `google-auth` modules: '
'`python -m pip install requests google-auth`')

if key_type == 'file':
with open(key_path_or_contents) as key_file:
service_account = json.load(key_file)
else:
elif key_type == 'key':
service_account = json.loads(key_path_or_contents)
else:
raise Exception('Service account key type not specified - `client_id` must be set to `file` or `key`')

credentials = google.oauth2.service_account.Credentials.from_service_account_info(service_account)
credentials = credentials.with_scopes(oauth2_scope.split(' '))
Expand Down Expand Up @@ -2312,7 +2320,9 @@ class App:
"""Manage the menu bar icon, server loading, authorisation and notifications, and start the main proxy thread"""

def __init__(self, args=None):
global CONFIG_FILE_PATH, CACHE_STORE, EXITING, prompt_toolkit
# noinspection PyGlobalUndefined
global prompt_toolkit
global CONFIG_FILE_PATH, CACHE_STORE, EXITING
EXITING = False # needed to allow restarting when imported from parent scripts (or an interpreter)

parser = argparse.ArgumentParser(description='%s: transparently add OAuth 2.0 support to IMAP/POP/SMTP client '
Expand Down

0 comments on commit 10dc3fc

Please sign in to comment.