Skip to content

Commit 13ade4c

Browse files
authored
Enable the PKCE workflow for OAuth 2 authentication. pgadmin-org#8941
1 parent 1195f14 commit 13ade4c

3 files changed

Lines changed: 91 additions & 11 deletions

File tree

docs/en_US/oauth2.rst

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ and modify the values for the following parameters:
4848
Useful for checking AzureAD_ *wids* or *groups*, GitLab_ *owner*, *maintainer* and *reporter* claims."
4949
"OAUTH2_SSL_CERT_VERIFICATION", "Set this variable to False to disable SSL certificate verification for OAuth2 provider.
5050
This may need to set False, in case of self-signed certificates."
51+
"OAUTH2_CHALLENGE_METHOD", "Enable PKCE workflow. PKCE method name, only *S256* is supported"
52+
"OAUTH2_RESPONSE_TYPE", "Enable PKCE workflow. Mandatory with OAUTH2_CHALLENGE_METHOD, must be set to *code*"
5153

5254
Redirect URL
5355
============
@@ -65,12 +67,19 @@ the PostgreSQL server password.
6567
To accomplish this, set the configuration parameter MASTER_PASSWORD to *True*, so upon setting the master password,
6668
it will be used as an encryption key while storing the password. If it is False, the server password can not be stored.
6769

68-
6970
Login Page
70-
============
71+
==========
7172

7273
After configuration, on restart, you can see the login page with the Oauth2 login button(s).
7374

7475
.. image:: images/oauth2_login.png
7576
:alt: Oauth2 login
7677
:align: center
78+
79+
PKCE Workflow
80+
=============
81+
82+
Ref: https://oauth.net/2/pkce
83+
84+
To enable PKCE workflow, set the configuration parameters OAUTH2_CHALLENGE_METHOD to *S256* and OAUTH2_RESPONSE_TYPE to *code*.
85+
Both parameters are mandatory to enable PKCE workflow.

web/pgadmin/authenticate/oauth2.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,26 @@ def __init__(self):
109109
OAuth2Authentication.oauth2_config[
110110
oauth2_config['OAUTH2_NAME']] = oauth2_config
111111

112+
# Build client_kwargs with defaults
113+
client_kwargs = {
114+
'scope': oauth2_config.get(
115+
'OAUTH2_SCOPE', 'email profile'),
116+
'verify': oauth2_config.get(
117+
'OAUTH2_SSL_CERT_VERIFICATION', True)
118+
}
119+
120+
# Override with PKCE parameters if provided
121+
if 'OAUTH2_CHALLENGE_METHOD' in oauth2_config and \
122+
'OAUTH2_RESPONSE_TYPE' in oauth2_config:
123+
# Merge PKCE kwargs with defaults
124+
pkce_kwargs = {
125+
'code_challenge_method': oauth2_config[
126+
'OAUTH2_CHALLENGE_METHOD'],
127+
'response_type': oauth2_config[
128+
'OAUTH2_RESPONSE_TYPE']
129+
}
130+
client_kwargs.update(pkce_kwargs)
131+
112132
OAuth2Authentication.oauth2_clients[
113133
oauth2_config['OAUTH2_NAME']
114134
] = OAuth2Authentication.oauth_obj.register(
@@ -118,10 +138,7 @@ def __init__(self):
118138
access_token_url=oauth2_config['OAUTH2_TOKEN_URL'],
119139
authorize_url=oauth2_config['OAUTH2_AUTHORIZATION_URL'],
120140
api_base_url=oauth2_config['OAUTH2_API_BASE_URL'],
121-
client_kwargs={'scope': oauth2_config.get(
122-
'OAUTH2_SCOPE', 'email profile'),
123-
'verify': oauth2_config.get(
124-
'OAUTH2_SSL_CERT_VERIFICATION', True)},
141+
client_kwargs=client_kwargs,
125142
server_metadata_url=oauth2_config.get(
126143
'OAUTH2_SERVER_METADATA_URL', None)
127144
)

web/pgadmin/browser/tests/test_oauth2_with_mocking.py

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pgadmin.authenticate.registry import AuthSourceRegistry
1414
from unittest.mock import patch, MagicMock
1515
from pgadmin.authenticate import AuthSourceManager
16-
from pgadmin.utils.constants import OAUTH2, LDAP, INTERNAL
16+
from pgadmin.utils.constants import OAUTH2, INTERNAL
1717

1818

1919
class Oauth2LoginMockTestCase(BaseTestGenerator):
@@ -33,18 +33,23 @@ class Oauth2LoginMockTestCase(BaseTestGenerator):
3333
oauth2_provider='github',
3434
flag=2
3535
)),
36-
('Oauth2 Authentication', dict(
36+
('Oauth2 Additional Claims Authentication', dict(
3737
auth_source=['oauth2'],
3838
oauth2_provider='auth-with-additional-claim-check',
3939
flag=3
4040
)),
41+
('Oauth2 PKCE Support', dict(
42+
auth_source=['oauth2'],
43+
oauth2_provider='keycloak-pkce',
44+
flag=4
45+
)),
4146
]
4247

4348
@classmethod
4449
def setUpClass(cls):
4550
"""
4651
We need to logout the test client as we are testing
47-
spnego/kerberos login scenarios.
52+
OAuth2 login scenarios.
4853
"""
4954
cls.tester.logout()
5055

@@ -63,7 +68,7 @@ def setUp(self):
6368
'https://github.com/login/oauth/authorize',
6469
'OAUTH2_API_BASE_URL': 'https://api.github.com/',
6570
'OAUTH2_USERINFO_ENDPOINT': 'user',
66-
'OAUTH2_SCOPE': 'email profile',
71+
'OAUTH2_SCOPE': 'openid email profile',
6772
'OAUTH2_ICON': 'fa-github',
6873
'OAUTH2_BUTTON_COLOR': '#3253a8',
6974
},
@@ -82,9 +87,30 @@ def setUp(self):
8287
'OAUTH2_ICON': 'briefcase',
8388
'OAUTH2_BUTTON_COLOR': '#0000ff',
8489
'OAUTH2_ADDITIONAL_CLAIMS': {
85-
'groups': ['123','456'],
90+
'groups': ['123', '456'],
8691
'wids': ['789']
8792
}
93+
},
94+
{
95+
'OAUTH2_NAME': 'keycloak-pkce',
96+
'OAUTH2_DISPLAY_NAME': 'Keycloak with PKCE',
97+
'OAUTH2_CLIENT_ID': 'testclientid',
98+
'OAUTH2_CLIENT_SECRET': 'testclientsec',
99+
'OAUTH2_TOKEN_URL':
100+
'https://keycloak.org/auth/realms/TEST-REALM/protocol/'
101+
'openid-connect/token',
102+
'OAUTH2_AUTHORIZATION_URL':
103+
'https://keycloak.org/auth/realms/TEST-REALM/protocol/'
104+
'openid-connect/auth',
105+
'OAUTH2_API_BASE_URL':
106+
'https://keycloak.org/auth/realms/TEST-REALM',
107+
'OAUTH2_USERINFO_ENDPOINT': 'user',
108+
'OAUTH2_SCOPE': 'openid email profile',
109+
'OAUTH2_SSL_CERT_VERIFICATION': True,
110+
'OAUTH2_ICON': 'fa-black-tie',
111+
'OAUTH2_BUTTON_COLOR': '#3253a8',
112+
'OAUTH2_CHALLENGE_METHOD': 'S256',
113+
'OAUTH2_RESPONSE_TYPE': 'code',
88114
}
89115
]
90116

@@ -101,6 +127,8 @@ def runTest(self):
101127
self.test_oauth2_authentication()
102128
elif self.flag == 3:
103129
self.test_oauth2_authentication_with_additional_claims_success()
130+
elif self.flag == 4:
131+
self.test_oauth2_authentication_with_pkce()
104132

105133
def test_external_authentication(self):
106134
"""
@@ -184,6 +212,32 @@ def test_oauth2_authentication_with_additional_claims_success(self):
184212
respdata = 'Gravatar image for %s' % profile['email']
185213
self.assertTrue(respdata in res.data.decode('utf8'))
186214

215+
def test_oauth2_authentication_with_pkce(self):
216+
"""
217+
Ensure that when PKCE parameters are configured, they are passed
218+
to the OAuth client registration as part of client_kwargs, and that
219+
the default client_kwargs is correctly included.
220+
"""
221+
222+
with patch('pgadmin.authenticate.oauth2.OAuth.register') as \
223+
mock_register:
224+
from pgadmin.authenticate.oauth2 import OAuth2Authentication
225+
226+
OAuth2Authentication()
227+
228+
args, kwargs = mock_register.call_args
229+
client_kwargs = kwargs.get('client_kwargs', {})
230+
231+
# Check that PKCE and default client_kwargs are included
232+
self.assertEqual(
233+
client_kwargs.get('code_challenge_method'), 'S256')
234+
self.assertEqual(
235+
client_kwargs.get('response_type'), 'code')
236+
self.assertEqual(
237+
client_kwargs.get('scope'), 'openid email profile')
238+
self.assertEqual(
239+
client_kwargs.get('verify'), 'true')
240+
187241
def mock_user_profile_with_additional_claims(self):
188242
profile = {'email': 'oauth2@gmail.com', 'wids': ['789']}
189243

0 commit comments

Comments
 (0)