Skip to content

Commit 3495d14

Browse files
committed
Don't override client secret when using PKCE
1 parent 39a0086 commit 3495d14

File tree

2 files changed

+19
-17
lines changed

2 files changed

+19
-17
lines changed

emailproxy.config

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,9 @@ documentation = Accounts are specified using your email address as the section h
213213
the same address. To avoid clashes, it is recommended that each account has a unique `redirect_uri` (or
214214
`redirect_listen_address`) value, for example by using a different port for each account.
215215

216-
- Some providers require the use of SHA-256 PKCE code challenges instead of client secrets. To use this method, set
217-
`client_secret = pkce` as shown in the Fastmail example below.
216+
- When using the authorisation code OAuth 2.0 flow (which is the proxy's default), some providers require the use of
217+
a SHA-256 Proof Key for Code Exchange (PKCE) challenge instead of (or in addition to) a client secret. To use this
218+
method, set `use_pkce = True` as shown in the Fastmail example below. This value is ignored in all other flows.
218219

219220
Integration with a secrets manager:
220221
- The proxy caches authenticated OAuth 2.0 tokens and associated metadata back into this configuration file by
@@ -249,7 +250,7 @@ permission_url = https://api.fastmail.com/oauth/authorize
249250
token_url = https://api.fastmail.com/oauth/refresh
250251
oauth2_scope = https://www.fastmail.com/dev/protocol-imap https://www.fastmail.com/dev/protocol-pop https://www.fastmail.com/dev/protocol-smtp
251252
client_id = *** your client id here ***
252-
client_secret = pkce
253+
use_pkce = True
253254

254255
[your.email@yahoo.co.uk]
255256
permission_url = https://api.login.yahoo.com/oauth2/request_auth

emailproxy.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
2.0 authentication. Designed for apps/clients that don't support OAuth 2.0 but need to connect to modern servers."""
55

66
__author__ = 'Simon Robinson'
7-
__copyright__ = 'Copyright (c) 2024 Simon Robinson'
7+
__copyright__ = 'Copyright (c) 2025 Simon Robinson'
88
__license__ = 'Apache 2.0'
9-
__package_version__ = '2025.6.24' # for pyproject.toml usage only - needs to be ast.literal_eval() compatible
9+
__package_version__ = '2025.6.25' # for pyproject.toml usage only - needs to be ast.literal_eval() compatible
1010
__version__ = '-'.join('%02d' % int(part) for part in __package_version__.split('.')) # ISO 8601 (YYYY-MM-DD)
1111

1212
import abc
@@ -752,6 +752,7 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
752752
client_secret = AppConfig.get_option_with_catch_all_fallback(config, username, 'client_secret')
753753
client_secret_encrypted = AppConfig.get_option_with_catch_all_fallback(config, username,
754754
'client_secret_encrypted')
755+
use_pkce = AppConfig.get_option_with_catch_all_fallback(config, username, 'use_pkce', fallback=False)
755756
jwt_certificate_path = AppConfig.get_option_with_catch_all_fallback(config, username, 'jwt_certificate_path')
756757
jwt_key_path = AppConfig.get_option_with_catch_all_fallback(config, username, 'jwt_key_path')
757758

@@ -781,13 +782,6 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
781782
access_token_expiry = config.getint(username, 'access_token_expiry', fallback=current_time)
782783
refresh_token = config.get(username, 'refresh_token', fallback=None)
783784

784-
code_verifier = None
785-
code_challenge = None
786-
if client_secret == 'pkce': # special value to enable PKCE code challenge
787-
code_verifier = OAuth2Helper.generate_code_verifier()
788-
code_challenge = OAuth2Helper.generate_code_challenge(code_verifier)
789-
client_secret = None
790-
791785
# try reloading remotely cached tokens if possible
792786
if not access_token and CACHE_STORE != CONFIG_FILE_PATH and reload_remote_accounts:
793787
AppConfig.unload()
@@ -896,10 +890,17 @@ def get_oauth2_credentials(username, password, reload_remote_accounts=True):
896890

897891
if not access_token:
898892
auth_result = None
893+
code_verifier = None
894+
code_challenge = None
895+
899896
if permission_url: # O365 CCG/ROPCG and Google service accounts skip authorisation; no permission_url
900897
if oauth2_flow != 'device': # the device flow is a poll-based method with asynchronous interaction
901898
oauth2_flow = 'authorization_code'
902899

900+
if oauth2_flow == 'authorization_code' and use_pkce:
901+
code_verifier = OAuth2Helper.generate_code_verifier()
902+
code_challenge = OAuth2Helper.generate_code_challenge(code_verifier)
903+
903904
permission_url = OAuth2Helper.construct_oauth2_permission_url(permission_url, redirect_uri,
904905
client_id, oauth2_scope, username,
905906
state, code_challenge)
@@ -1099,7 +1100,7 @@ def construct_oauth2_permission_url(permission_url, redirect_uri, client_id, sco
10991100
'access_type': 'offline', 'login_hint': username}
11001101
if state:
11011102
params['state'] = state
1102-
if code_challenge:
1103+
if code_challenge: # PKCE; see RFC 7636
11031104
params['code_challenge'] = code_challenge
11041105
params['code_challenge_method'] = 'S256'
11051106
if not redirect_uri: # unlike other interactive flows, DAG doesn't involve a (known) final redirect
@@ -1213,13 +1214,13 @@ def get_oauth2_authorisation_tokens(token_url, redirect_uri, client_id, client_s
12131214
params = {'client_id': client_id, 'client_secret': client_secret, 'code': authorisation_result,
12141215
'redirect_uri': redirect_uri, 'grant_type': oauth2_flow}
12151216
expires_in = AUTHENTICATION_TIMEOUT
1217+
1218+
if code_verifier:
1219+
params['code_verifier'] = code_verifier # PKCE; see RFC 7636
1220+
12161221
if not client_secret:
12171222
del params['client_secret'] # client secret can be optional for O365, but we don't want a None entry
12181223

1219-
# the code verifier is only used when we don't have a secret (or, rather, the config file secret is `pkce`)
1220-
if code_verifier:
1221-
params['code_verifier'] = code_verifier
1222-
12231224
# certificate credentials are only used when no client secret is provided
12241225
if jwt_client_assertion:
12251226
params['client_assertion_type'] = 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'

0 commit comments

Comments
 (0)