diff --git a/docs/en_US/oauth2.rst b/docs/en_US/oauth2.rst index cc4c0704b..916a3a63c 100644 --- a/docs/en_US/oauth2.rst +++ b/docs/en_US/oauth2.rst @@ -48,6 +48,8 @@ and modify the values for the following parameters: Useful for checking AzureAD_ *wids* or *groups*, GitLab_ *owner*, *maintainer* and *reporter* claims." "OAUTH2_SSL_CERT_VERIFICATION", "Set this variable to False to disable SSL certificate verification for OAuth2 provider. This may need to set False, in case of self-signed certificates." + "OAUTH2_CHALLENGE_METHOD", "Enable PKCE workflow. PKCE method name, only *S256* is supported" + "OAUTH2_RESPONSE_TYPE", "Enable PKCE workflow. Mandatory with OAUTH2_CHALLENGE_METHOD, must be set to *code*" Redirect URL ============ @@ -65,12 +67,19 @@ the PostgreSQL server password. To accomplish this, set the configuration parameter MASTER_PASSWORD to *True*, so upon setting the master password, it will be used as an encryption key while storing the password. If it is False, the server password can not be stored. - Login Page -============ +========== After configuration, on restart, you can see the login page with the Oauth2 login button(s). .. image:: images/oauth2_login.png :alt: Oauth2 login :align: center + +PKCE Workflow +============= + +Ref: https://oauth.net/2/pkce + +To enable PKCE workflow, set the configuration parameters OAUTH2_CHALLENGE_METHOD to *S256* and OAUTH2_RESPONSE_TYPE to *code*. +Both parameters are mandatory to enable PKCE workflow. diff --git a/web/pgadmin/authenticate/oauth2.py b/web/pgadmin/authenticate/oauth2.py index b43491190..c96d2f339 100644 --- a/web/pgadmin/authenticate/oauth2.py +++ b/web/pgadmin/authenticate/oauth2.py @@ -109,6 +109,26 @@ class OAuth2Authentication(BaseAuthentication): OAuth2Authentication.oauth2_config[ oauth2_config['OAUTH2_NAME']] = oauth2_config + # Build client_kwargs with defaults + client_kwargs = { + 'scope': oauth2_config.get( + 'OAUTH2_SCOPE', 'email profile'), + 'verify': oauth2_config.get( + 'OAUTH2_SSL_CERT_VERIFICATION', True) + } + + # Override with PKCE parameters if provided + if 'OAUTH2_CHALLENGE_METHOD' in oauth2_config and \ + 'OAUTH2_RESPONSE_TYPE' in oauth2_config: + # Merge PKCE kwargs with defaults + pkce_kwargs = { + 'code_challenge_method': oauth2_config[ + 'OAUTH2_CHALLENGE_METHOD'], + 'response_type': oauth2_config[ + 'OAUTH2_RESPONSE_TYPE'] + } + client_kwargs.update(pkce_kwargs) + OAuth2Authentication.oauth2_clients[ oauth2_config['OAUTH2_NAME'] ] = OAuth2Authentication.oauth_obj.register( @@ -118,10 +138,7 @@ class OAuth2Authentication(BaseAuthentication): access_token_url=oauth2_config['OAUTH2_TOKEN_URL'], authorize_url=oauth2_config['OAUTH2_AUTHORIZATION_URL'], api_base_url=oauth2_config['OAUTH2_API_BASE_URL'], - client_kwargs={'scope': oauth2_config.get( - 'OAUTH2_SCOPE', 'email profile'), - 'verify': oauth2_config.get( - 'OAUTH2_SSL_CERT_VERIFICATION', True)}, + client_kwargs=client_kwargs, server_metadata_url=oauth2_config.get( 'OAUTH2_SERVER_METADATA_URL', None) ) diff --git a/web/pgadmin/browser/tests/test_oauth2_with_mocking.py b/web/pgadmin/browser/tests/test_oauth2_with_mocking.py index a6f65f238..5d7cb66b6 100644 --- a/web/pgadmin/browser/tests/test_oauth2_with_mocking.py +++ b/web/pgadmin/browser/tests/test_oauth2_with_mocking.py @@ -13,7 +13,7 @@ from regression.python_test_utils import test_utils as utils from pgadmin.authenticate.registry import AuthSourceRegistry from unittest.mock import patch, MagicMock from pgadmin.authenticate import AuthSourceManager -from pgadmin.utils.constants import OAUTH2, LDAP, INTERNAL +from pgadmin.utils.constants import OAUTH2, INTERNAL class Oauth2LoginMockTestCase(BaseTestGenerator): @@ -33,18 +33,23 @@ class Oauth2LoginMockTestCase(BaseTestGenerator): oauth2_provider='github', flag=2 )), - ('Oauth2 Authentication', dict( + ('Oauth2 Additional Claims Authentication', dict( auth_source=['oauth2'], oauth2_provider='auth-with-additional-claim-check', flag=3 )), + ('Oauth2 PKCE Support', dict( + auth_source=['oauth2'], + oauth2_provider='keycloak-pkce', + flag=4 + )), ] @classmethod def setUpClass(cls): """ We need to logout the test client as we are testing - spnego/kerberos login scenarios. + OAuth2 login scenarios. """ cls.tester.logout() @@ -63,7 +68,7 @@ class Oauth2LoginMockTestCase(BaseTestGenerator): 'https://github.com/login/oauth/authorize', 'OAUTH2_API_BASE_URL': 'https://api.github.com/', 'OAUTH2_USERINFO_ENDPOINT': 'user', - 'OAUTH2_SCOPE': 'email profile', + 'OAUTH2_SCOPE': 'openid email profile', 'OAUTH2_ICON': 'fa-github', 'OAUTH2_BUTTON_COLOR': '#3253a8', }, @@ -82,9 +87,30 @@ class Oauth2LoginMockTestCase(BaseTestGenerator): 'OAUTH2_ICON': 'briefcase', 'OAUTH2_BUTTON_COLOR': '#0000ff', 'OAUTH2_ADDITIONAL_CLAIMS': { - 'groups': ['123','456'], + 'groups': ['123', '456'], 'wids': ['789'] } + }, + { + 'OAUTH2_NAME': 'keycloak-pkce', + 'OAUTH2_DISPLAY_NAME': 'Keycloak with PKCE', + 'OAUTH2_CLIENT_ID': 'testclientid', + 'OAUTH2_CLIENT_SECRET': 'testclientsec', + 'OAUTH2_TOKEN_URL': + 'https://keycloak.org/auth/realms/TEST-REALM/protocol/' + 'openid-connect/token', + 'OAUTH2_AUTHORIZATION_URL': + 'https://keycloak.org/auth/realms/TEST-REALM/protocol/' + 'openid-connect/auth', + 'OAUTH2_API_BASE_URL': + 'https://keycloak.org/auth/realms/TEST-REALM', + 'OAUTH2_USERINFO_ENDPOINT': 'user', + 'OAUTH2_SCOPE': 'openid email profile', + 'OAUTH2_SSL_CERT_VERIFICATION': True, + 'OAUTH2_ICON': 'fa-black-tie', + 'OAUTH2_BUTTON_COLOR': '#3253a8', + 'OAUTH2_CHALLENGE_METHOD': 'S256', + 'OAUTH2_RESPONSE_TYPE': 'code', } ] @@ -101,6 +127,8 @@ class Oauth2LoginMockTestCase(BaseTestGenerator): self.test_oauth2_authentication() elif self.flag == 3: self.test_oauth2_authentication_with_additional_claims_success() + elif self.flag == 4: + self.test_oauth2_authentication_with_pkce() def test_external_authentication(self): """ @@ -184,6 +212,32 @@ class Oauth2LoginMockTestCase(BaseTestGenerator): respdata = 'Gravatar image for %s' % profile['email'] self.assertTrue(respdata in res.data.decode('utf8')) + def test_oauth2_authentication_with_pkce(self): + """ + Ensure that when PKCE parameters are configured, they are passed + to the OAuth client registration as part of client_kwargs, and that + the default client_kwargs is correctly included. + """ + + with patch('pgadmin.authenticate.oauth2.OAuth.register') as \ + mock_register: + from pgadmin.authenticate.oauth2 import OAuth2Authentication + + OAuth2Authentication() + + args, kwargs = mock_register.call_args + client_kwargs = kwargs.get('client_kwargs', {}) + + # Check that PKCE and default client_kwargs are included + self.assertEqual( + client_kwargs.get('code_challenge_method'), 'S256') + self.assertEqual( + client_kwargs.get('response_type'), 'code') + self.assertEqual( + client_kwargs.get('scope'), 'openid email profile') + self.assertEqual( + client_kwargs.get('verify'), 'true') + def mock_user_profile_with_additional_claims(self): profile = {'email': 'oauth2@gmail.com', 'wids': ['789']}