Skip to content

Commit 557823c

Browse files
author
Paul Bourhis
committed
Enhance OAUTH2 and OIDC authentication support with improved claims handling and configuration options
Change logging level from exception to error for OIDC profile data issues Refactor debug logging in OAuth2 authentication to improve clarity and consistency Add error handling for missing OAuth2 provider and enhance claims processing logic Enhance OIDC ID token handling by implementing JWT parsing and updating tests to mock claims extraction Refactor ID token claims extraction for OIDC providers and update tests to mock userinfo handling Refactor OAuth2 configuration to use get method for optional URLs Enhance OAuth2 documentation and implement PKCE support for public clients in authentication logic Fix typo in OAUTH2 authentication documentation Implement Azure Entra ID Workload Identity authentication support and add corresponding tests
1 parent 7c36eab commit 557823c

File tree

5 files changed

+1340
-225
lines changed

5 files changed

+1340
-225
lines changed

docs/en_US/oauth2.rst

Lines changed: 279 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,40 @@
11
.. _oauth2:
22

3-
*****************************************
4-
`Enabling OAUTH2 Authentication`:index:
5-
*****************************************
3+
*******************************************************
4+
`Enabling OAUTH2 and OIDC Authentication`:index:
5+
*******************************************************
66

77

8-
To enable OAUTH2 authentication for pgAdmin, you must configure the OAUTH2
9-
settings in the *config_local.py* or *config_system.py* file (see the
10-
:ref:`config.py <config_py>` documentation) on the system where pgAdmin is
11-
installed in Server mode. You can copy these settings from *config.py* file
12-
and modify the values for the following parameters:
8+
To enable OAUTH2 or OpenID Connect (OIDC) authentication for pgAdmin, you must
9+
configure the OAUTH2 settings in the *config_local.py* or *config_system.py*
10+
file (see the :ref:`config.py <config_py>` documentation) on the system where
11+
pgAdmin is installed in Server mode. You can copy these settings from *config.py*
12+
file and modify the values for the following parameters.
13+
14+
OAuth2 vs OpenID Connect (OIDC)
15+
================================
16+
17+
pgAdmin supports both OAuth2 and OIDC authentication protocols:
18+
19+
**OAuth2** is an authorization framework that allows third-party applications to
20+
obtain limited access to user accounts. When using OAuth2, pgAdmin must explicitly
21+
call the provider's userinfo endpoint to retrieve user profile information.
22+
23+
**OpenID Connect (OIDC)** is an identity layer built on top of OAuth2 that provides
24+
standardized user authentication and profile information. When using OIDC, user
25+
identity information is included directly in the ID token, which is more efficient
26+
and secure.
27+
28+
.. note::
29+
When **OAUTH2_SERVER_METADATA_URL** is configured, pgAdmin treats the provider
30+
as an OIDC provider and will:
31+
32+
- Use ID token claims for user identity (sub, email, preferred_username)
33+
- Skip the userinfo endpoint call when ID token contains sufficient information
34+
- Validate the ID token automatically using the provider's public keys
35+
36+
This is the **recommended approach** for modern identity providers like
37+
Microsoft Entra ID (Azure AD), Google, Keycloak, Auth0, and Okta.
1338

1439

1540
.. _AzureAD: https://learn.microsoft.com/en-us/security/zero-trust/develop/configure-tokens-group-claims-app-roles
@@ -23,29 +48,28 @@ and modify the values for the following parameters:
2348

2449
"AUTHENTICATION_SOURCES", "The default value for this parameter is *internal*.
2550
To enable OAUTH2 authentication, you must include *oauth2* in the list of values
26-
for this parameter. you can modify the value as follows:
51+
for this parameter. You can modify the value as follows:
2752

2853
* [‘oauth2’, ‘internal’]: pgAdmin will display an additional button for authenticating with oauth2"
2954
"OAUTH2_NAME", "The name of the Oauth2 provider, ex: Google, Github"
3055
"OAUTH2_DISPLAY_NAME", "Oauth2 display name in pgAdmin"
3156
"OAUTH2_CLIENT_ID", "Oauth2 Client ID"
32-
"OAUTH2_CLIENT_SECRET", "Oauth2 Client Secret"
57+
"OAUTH2_CLIENT_SECRET", "Oauth2 Client Secret. **Optional for public clients using Authorization Code + PKCE**. For confidential clients (server-side apps), keep this set. For public clients (no secret), pgAdmin will enforce PKCE and perform an unauthenticated token exchange."
58+
"OAUTH2_CLIENT_AUTH_METHOD", "Client authentication method for the token endpoint. Default behavior uses *OAUTH2_CLIENT_SECRET* (confidential client), or PKCE when no secret is provided (public client). Set to *workload_identity* to authenticate using an Azure Entra ID workload identity (federated credential) without a client secret."
59+
"OAUTH2_WORKLOAD_IDENTITY_TOKEN_FILE", "When **OAUTH2_CLIENT_AUTH_METHOD** is *workload_identity*, path to the projected OIDC token file (Kubernetes service account JWT). This file must exist at pgAdmin startup."
3360
"OAUTH2_TOKEN_URL", "Oauth2 Access Token endpoint"
3461
"OAUTH2_AUTHORIZATION_URL", "Endpoint for user authorization"
35-
"OAUTH2_SERVER_METADATA_URL", "Server metadata url for your OAuth2 provider"
62+
"OAUTH2_SERVER_METADATA_URL", "**OIDC Discovery URL** (recommended for OIDC providers). When set, pgAdmin will use OIDC flow with automatic ID token validation and user claims from the ID token. Example: *https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration*. When using this parameter, OAUTH2_TOKEN_URL and OAUTH2_AUTHORIZATION_URL are optional as they will be discovered automatically."
3663
"OAUTH2_API_BASE_URL", "Oauth2 base URL endpoint to make requests simple, ex: *https://api.github.com/*"
37-
"OAUTH2_USERINFO_ENDPOINT", "User Endpoint, ex: *user* (for github, or *user/emails* if the user's email address is private) and *userinfo* (for google),"
38-
"OAUTH2_SCOPE", "Oauth scope, ex: 'openid email profile'. Note that an 'email' claim is required in the resulting profile."
64+
"OAUTH2_USERINFO_ENDPOINT", "User Endpoint, ex: *user* (for github, or *user/emails* if the user's email address is private) and *userinfo* (for google). **For OIDC providers**, this is optional if the ID token contains sufficient claims (email, preferred_username, or sub)."
65+
"OAUTH2_SCOPE", "Oauth scope, ex: 'openid email profile'. **For OIDC providers**, include 'openid' scope to receive an ID token."
3966
"OAUTH2_ICON", "The Font-awesome icon to be placed on the oauth2 button, ex: fa-github"
4067
"OAUTH2_BUTTON_COLOR", "Oauth2 button color"
41-
"OAUTH2_USERNAME_CLAIM", "The claim which is used for the username. If the value is empty
42-
the email is used as username, but if a value is provided, the claim has to exist. Ex: *oid* (for AzureAD), *email* (for Github)"
68+
"OAUTH2_USERNAME_CLAIM", "The claim which is used for the username. If the value is empty, **for OIDC providers** pgAdmin will use: 1) email, 2) preferred_username, or 3) sub (in that order). **For OAuth2 providers** without OIDC, email is required. Ex: *oid* (for AzureAD), *email* (for Github), *preferred_username* (for Keycloak)"
4369
"OAUTH2_AUTO_CREATE_USER", "Set the value to *True* if you want to automatically
4470
create a pgAdmin user corresponding to a successfully authenticated Oauth2 user.
4571
Please note that password is not stored in the pgAdmin database."
46-
"OAUTH2_ADDITIONAL_CLAIMS", "If a dictionary is provided, pgAdmin will check for a matching key and value on the userinfo endpoint
47-
and in the Id Token. In case there is no match with the provided config, the user will receive an authorization error.
48-
Useful for checking AzureAD_ *wids* or *groups*, GitLab_ *owner*, *maintainer* and *reporter* claims."
72+
"OAUTH2_ADDITIONAL_CLAIMS", "If a dictionary is provided, pgAdmin will check for a matching key and value on the **ID token first** (for OIDC providers), then fall back to the userinfo endpoint response. In case there is no match with the provided config, the user will receive an authorization error. Useful for checking AzureAD_ *wids* or *groups*, GitLab_ *owner*, *maintainer* and *reporter* claims."
4973
"OAUTH2_SSL_CERT_VERIFICATION", "Set this variable to False to disable SSL certificate verification for OAuth2 provider.
5074
This may need to set False, in case of self-signed certificates."
5175
"OAUTH2_CHALLENGE_METHOD", "Enable PKCE workflow. PKCE method name, only *S256* is supported"
@@ -83,3 +107,240 @@ Ref: https://oauth.net/2/pkce
83107

84108
To enable PKCE workflow, set the configuration parameters OAUTH2_CHALLENGE_METHOD to *S256* and OAUTH2_RESPONSE_TYPE to *code*.
85109
Both parameters are mandatory to enable PKCE workflow.
110+
111+
Public vs Confidential OAuth Clients
112+
====================================
113+
114+
OAuth providers support two common client types:
115+
116+
- **Confidential clients** have a client secret and can authenticate to the token endpoint.
117+
- **Public clients** do not have a client secret (or the secret cannot be safely stored).
118+
119+
pgAdmin supports interactive user login for both client types:
120+
121+
- If **OAUTH2_CLIENT_SECRET** is set, pgAdmin treats the provider as a confidential client.
122+
- If **OAUTH2_CLIENT_SECRET** is missing, empty, or set to *None*, pgAdmin treats the provider as a public client and **requires PKCE**.
123+
124+
.. note::
125+
For public clients, pgAdmin uses Authlib's native behavior to perform an **unauthenticated token exchange**
126+
(token endpoint client authentication method: ``none``). This is required for Authorization Code + PKCE
127+
flows where no client secret is available.
128+
129+
Azure Entra ID Workload Identity (AKS) (No Client Secret)
130+
========================================================
131+
132+
pgAdmin can authenticate to Microsoft Entra ID (Azure AD) **without a client secret** using an
133+
AKS Workload Identity projected service account token (OIDC federated credential).
134+
135+
This is a **confidential client** scenario (server-side app), but client authentication to the token
136+
endpoint is performed using a **JWT client assertion**.
137+
138+
Enable workload identity mode
139+
-----------------------------
140+
141+
Set the following parameters in your provider configuration:
142+
143+
.. code-block:: python
144+
145+
OAUTH2_CONFIG = [{
146+
'OAUTH2_NAME': 'entra-workload-identity',
147+
'OAUTH2_DISPLAY_NAME': 'Microsoft Entra ID',
148+
'OAUTH2_CLIENT_ID': '<Application (client) ID>',
149+
'OAUTH2_CLIENT_SECRET': None, # not required
150+
'OAUTH2_CLIENT_AUTH_METHOD': 'workload_identity',
151+
'OAUTH2_WORKLOAD_IDENTITY_TOKEN_FILE':
152+
'/var/run/secrets/azure/tokens/azure-identity-token',
153+
'OAUTH2_SERVER_METADATA_URL':
154+
'https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration',
155+
'OAUTH2_SCOPE': 'openid email profile',
156+
}]
157+
158+
With this configuration:
159+
160+
- pgAdmin will **not** require **OAUTH2_CLIENT_SECRET**.
161+
- pgAdmin will **not** use PKCE for this provider.
162+
- During the token exchange, pgAdmin will send:
163+
164+
- ``client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer``
165+
- ``client_assertion=<projected service account JWT>``
166+
167+
Azure App Registration setup
168+
----------------------------
169+
170+
In Microsoft Entra ID:
171+
172+
- Create an **App registration** for pgAdmin.
173+
- Configure a **Redirect URI** to ``<http/https>://<pgAdmin Server URL>/oauth2/authorize``.
174+
- In **Certificates & secrets**, you do **not** need to create a client secret for workload identity.
175+
176+
Federated credential (workload identity) configuration
177+
------------------------------------------------------
178+
179+
Add a **Federated credential** to the App registration:
180+
181+
- **Issuer**: your AKS cluster OIDC issuer URL.
182+
- **Subject**: ``system:serviceaccount:<namespace>:<serviceaccount-name>``
183+
- **Audience**: typically ``api://AzureADTokenExchange``
184+
185+
AKS ServiceAccount example
186+
--------------------------
187+
188+
Example ServiceAccount for AKS Workload Identity:
189+
190+
.. code-block:: yaml
191+
192+
apiVersion: v1
193+
kind: ServiceAccount
194+
metadata:
195+
name: pgadmin
196+
namespace: pgadmin
197+
annotations:
198+
azure.workload.identity/client-id: "<Application (client) ID>"
199+
---
200+
apiVersion: apps/v1
201+
kind: Deployment
202+
metadata:
203+
name: pgadmin
204+
namespace: pgadmin
205+
spec:
206+
template:
207+
metadata:
208+
labels:
209+
azure.workload.identity/use: "true"
210+
spec:
211+
serviceAccountName: pgadmin
212+
213+
.. note::
214+
The projected token file path can vary by cluster configuration.
215+
In many AKS setups it is provided via the ``AZURE_FEDERATED_TOKEN_FILE`` environment
216+
variable and mounted under ``/var/run/secrets/azure/tokens/``.
217+
218+
OIDC Configuration Examples
219+
============================
220+
221+
Using OIDC with Discovery Metadata (Recommended)
222+
-------------------------------------------------
223+
224+
When using OIDC providers, configure the **OAUTH2_SERVER_METADATA_URL** parameter
225+
to enable automatic discovery and ID token validation:
226+
227+
.. code-block:: python
228+
229+
OAUTH2_CONFIG = [{
230+
'OAUTH2_NAME': 'my-oidc-provider',
231+
'OAUTH2_DISPLAY_NAME': 'My OIDC Provider',
232+
'OAUTH2_CLIENT_ID': 'your-client-id',
233+
'OAUTH2_CLIENT_SECRET': 'your-client-secret',
234+
'OAUTH2_SERVER_METADATA_URL': 'https://provider.example.com/.well-known/openid-configuration',
235+
'OAUTH2_SCOPE': 'openid email profile',
236+
# OAUTH2_USERINFO_ENDPOINT is optional when using OIDC
237+
# Token and authorization URLs are discovered automatically
238+
}]
239+
240+
With this configuration:
241+
242+
- pgAdmin will use the OIDC discovery endpoint to automatically find token and authorization URLs
243+
- User identity will be extracted from ID token claims (sub, email, preferred_username)
244+
- The userinfo endpoint will only be called as a fallback if ID token lacks required claims
245+
- ID token will be automatically validated using the provider's public keys
246+
247+
Using OIDC as a Public Client (No Client Secret) with PKCE
248+
-----------------------------------------------------------
249+
250+
If your OAuth/OIDC application is configured as a **public client** (no client secret), pgAdmin can still perform
251+
interactive user login using Authorization Code + PKCE.
252+
253+
.. code-block:: python
254+
255+
OAUTH2_CONFIG = [{
256+
'OAUTH2_NAME': 'my-oidc-public',
257+
'OAUTH2_DISPLAY_NAME': 'My OIDC Provider (Public Client)',
258+
'OAUTH2_CLIENT_ID': 'your-client-id',
259+
# Public client: omit OAUTH2_CLIENT_SECRET or set it to None/empty.
260+
'OAUTH2_CLIENT_SECRET': None,
261+
'OAUTH2_SERVER_METADATA_URL': 'https://provider.example.com/.well-known/openid-configuration',
262+
'OAUTH2_SCOPE': 'openid email profile',
263+
# PKCE is mandatory for public clients
264+
'OAUTH2_CHALLENGE_METHOD': 'S256',
265+
'OAUTH2_RESPONSE_TYPE': 'code',
266+
}]
267+
268+
With this configuration:
269+
270+
- pgAdmin enforces PKCE (challenge method + response type)
271+
- The token exchange is performed without a client secret
272+
273+
Username Resolution for OIDC
274+
-----------------------------
275+
276+
When **OAUTH2_SERVER_METADATA_URL** is configured (OIDC mode), pgAdmin will
277+
resolve the username in the following order:
278+
279+
1. **OAUTH2_USERNAME_CLAIM** (if configured) - checks ID token first, then userinfo
280+
2. **email** claim from ID token or userinfo endpoint
281+
3. **preferred_username** claim from ID token (standard OIDC claim)
282+
4. **sub** claim from ID token (always present in OIDC, used as last resort)
283+
284+
Example with custom username claim:
285+
286+
.. code-block:: python
287+
288+
OAUTH2_CONFIG = [{
289+
# ... other config ...
290+
'OAUTH2_USERNAME_CLAIM': 'preferred_username',
291+
# pgAdmin will use 'preferred_username' from ID token for the username
292+
}]
293+
294+
Example without custom claim (uses automatic fallback):
295+
296+
.. code-block:: python
297+
298+
OAUTH2_CONFIG = [{
299+
# ... other config ...
300+
# No OAUTH2_USERNAME_CLAIM specified
301+
# pgAdmin will try: email -> preferred_username -> sub
302+
}]
303+
304+
Additional Claims Authorization with OIDC
305+
------------------------------------------
306+
307+
When using **OAUTH2_ADDITIONAL_CLAIMS** with OIDC providers, pgAdmin will:
308+
309+
1. Check the ID token claims first (more secure, no additional network call)
310+
2. Fall back to userinfo endpoint response if needed
311+
312+
Example:
313+
314+
.. code-block:: python
315+
316+
OAUTH2_CONFIG = [{
317+
# ... other config ...
318+
'OAUTH2_ADDITIONAL_CLAIMS': {
319+
'groups': ['admin-group', 'pgadmin-users'],
320+
'roles': ['database-admin']
321+
},
322+
# pgAdmin will check these claims in ID token first,
323+
# then userinfo endpoint if not found
324+
}]
325+
326+
Legacy OAuth2 Configuration (Without OIDC)
327+
-------------------------------------------
328+
329+
For providers that don't support OIDC discovery, configure all endpoints manually:
330+
331+
.. code-block:: python
332+
333+
OAUTH2_CONFIG = [{
334+
'OAUTH2_NAME': 'github',
335+
'OAUTH2_DISPLAY_NAME': 'GitHub',
336+
'OAUTH2_CLIENT_ID': 'your-client-id',
337+
'OAUTH2_CLIENT_SECRET': 'your-client-secret',
338+
'OAUTH2_TOKEN_URL': 'https://github.com/login/oauth/access_token',
339+
'OAUTH2_AUTHORIZATION_URL': 'https://github.com/login/oauth/authorize',
340+
'OAUTH2_API_BASE_URL': 'https://api.github.com/',
341+
'OAUTH2_USERINFO_ENDPOINT': 'user',
342+
'OAUTH2_SCOPE': 'user:email',
343+
# No OAUTH2_SERVER_METADATA_URL - pure OAuth2 mode
344+
}]
345+
346+
In this mode, user identity is retrieved only from the userinfo endpoint.

web/pgadmin/authenticate/__init__.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,15 +227,27 @@ def as_dict(self):
227227
return res
228228

229229
def update_auth_sources(self):
230-
for auth_src in [KERBEROS, OAUTH2]:
231-
if auth_src in self.auth_sources:
232-
if 'internal_button' in request.form:
230+
# Only mutate the ordered list of auth sources when a user explicitly
231+
# selected an auth method on the login form.
232+
#
233+
# Without this guard, a plain internal login POST (email/password) can
234+
# incorrectly drop INTERNAL/LDAP and try OAUTH2 first, which then fails
235+
# because no oauth2 provider button was provided.
236+
if request.method != 'POST':
237+
return
238+
239+
if 'internal_button' in request.form:
240+
for auth_src in [KERBEROS, OAUTH2]:
241+
if auth_src in self.auth_sources:
233242
self.auth_sources.remove(auth_src)
234-
else:
235-
if INTERNAL in self.auth_sources:
236-
self.auth_sources.remove(INTERNAL)
237-
if LDAP in self.auth_sources:
238-
self.auth_sources.remove(LDAP)
243+
return
244+
245+
if 'oauth2_button' in request.form:
246+
if INTERNAL in self.auth_sources:
247+
self.auth_sources.remove(INTERNAL)
248+
if LDAP in self.auth_sources:
249+
self.auth_sources.remove(LDAP)
250+
return
239251

240252
def set_current_source(self, source):
241253
self.current_source = source

0 commit comments

Comments
 (0)