Skip to content

Conversation

@Pyodin
Copy link

@Pyodin Pyodin commented Jan 13, 2026

This PR builds on top of the OIDC support introduced in #9525 (comment) and is not meant to be merged independently.

This PR adds first-class support for OAuth2 public clients in pgAdmin, enabling secure authentication without storing a client secret.

It allows pgAdmin to authenticate against modern OAuth2 / OIDC providers (such as Microsoft Entra ID) using the Authorization Code flow with PKCE, which is now the recommended approach for browser-based and containerized applications.

Why this change is needed
Many OAuth2 providers (including Entra ID) support public clients that:

  • Do not use a client secret
  • Rely on PKCE to protect the authorization code exchange
  • Are suitable for apps running in containers, Kubernetes, or managed cloud services

Prior to this change, pgAdmin implicitly required a client_secret, preventing:

  • Secretless deployments
  • Secure Kubernetes-native authentication patterns
  • Alignment with modern OAuth2 best practices

Summary by CodeRabbit

  • New Features

    • OIDC support for OAuth2 flows, PKCE enforcement for public clients, and improved username resolution prioritizing ID token claims
  • Documentation

    • Expanded OAuth2/OIDC docs with discovery-based examples, PKCE workflows, public vs confidential client guidance, and configuration samples
  • Bug Fixes

    • Prevented unintended auth-source mutations on non-POST requests; improved error handling and OIDC-aware validation
  • Tests

    • Added comprehensive OAuth2/OIDC tests covering PKCE, public/confidential clients, ID token vs userinfo behaviors

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Walkthrough

Adds OIDC support and PKCE-aware handling to OAuth2 authentication: documentation expanded for OAuth2 vs OIDC, login flow prefers ID token claims with userinfo fallback, public clients must use PKCE, and code/tests updated to validate OIDC behaviors and PKCE registration/flows.

Changes

Cohort / File(s) Summary
Documentation
docs/en_US/oauth2.rst
Title updated to include OIDC; added OAuth2 vs OIDC section; clarified config params (discovery URL, optional client_secret for public clients, username claim rules); added Public vs Confidential clients, PKCE guidance, OIDC examples, and legacy OAuth2 example.
Auth sources handling
web/pgadmin/authenticate/__init__.py
Only mutate auth_sources on POST; detect internal_button and oauth2_button to selectively remove other sources and return early to avoid unintended mutations.
OAuth2 / OIDC logic
web/pgadmin/authenticate/oauth2.py
Added OIDC detection, ID token claim extraction, and OIDC-first username resolution; PKCE/client-secret validation for public vs confidential clients; updated profile retrieval to skip userinfo when ID token suffices; improved error handling, logging, and safe provider lookups.
Test harness lifecycle
web/pgadmin/browser/tests/__init__.py
Added no-op setUp() and tearDown() methods to BrowserGenerateTestCase.
OAuth2/OIDC tests
web/pgadmin/browser/tests/test_oauth2_with_mocking.py
Reworked tests into explicit kinds covering external redirects, login success/failure, PKCE flows, public/confidential registration, invalid public config, and OIDC claim scenarios; added helpers for provider registration, state reset, session assertions, and granular subtests using mocks.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as pgAdmin App
    participant OAuth as OAuth2 Provider
    participant IdP as Identity Provider

    User->>App: Click "Login with OAuth2"
    App->>App: Determine client type and PKCE config
    alt Public client (PKCE)
        App->>App: Generate code_challenge/code_verifier
        App->>OAuth: /authorize (code_challenge, scope=openid?)
    else Confidential client
        App->>OAuth: /authorize (client_id, scope)
    end
    OAuth->>IdP: Request authentication
    IdP->>User: Prompt credentials
    User->>IdP: Submit credentials
    IdP->>OAuth: Issue authorization grant
    OAuth->>App: Redirect with authorization code
    alt Public client (PKCE)
        App->>OAuth: /token (code, code_verifier, no client_secret)
    else Confidential client
        App->>OAuth: /token (code, client_secret)
    end
    OAuth->>App: Respond with access_token (+ id_token if OIDC)
    alt OIDC provider (id_token present)
        App->>App: Extract claims from id_token
        App->>App: Resolve username from id_token (custom claim / email / preferred_username / sub)
        alt Claims insufficient
            App->>OAuth: GET /userinfo
            OAuth->>App: Return userinfo
        end
    else Non-OIDC provider
        App->>OAuth: GET /userinfo
        OAuth->>App: Return userinfo
    end
    App->>App: Validate/create session and authorize
    App->>User: Grant access
Loading
sequenceDiagram
    participant Test as Test Case
    participant Auth as OAuth2 Auth Module
    participant MockReg as Mock OAuth.register

    Test->>Test: _reset_oauth2_state()
    Test->>Test: Configure OAUTH2 sources
    Test->>MockReg: Register providers (github, public-pkce, oidc-*)
    alt PKCE authentication test
        Test->>Auth: authenticate() with PKCE
        Auth->>Auth: Validate PKCE params
        Auth->>MockReg: Exchange code with code_verifier
        MockReg->>Auth: Return tokens + id_token
        Auth->>Test: Return profile (from id_token or userinfo)
    else Public client missing PKCE
        Test->>Auth: register public client without PKCE
        Auth-->>Test: Raise ValueError (PKCE required)
    else OIDC id_token-only test
        Test->>Auth: get_user_profile() with id_token
        Auth->>Auth: Extract claims and skip userinfo
        Auth->>Test: Return profile from id_token
    end
    Test->>Test: Assert session state (logged in / not logged in)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: adding support for OAuth2 public clients using Authorization Code flow with PKCE.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6f5010 and 7417243.

📒 Files selected for processing (1)
  • docs/en_US/oauth2.rst
🔇 Additional comments (5)
docs/en_US/oauth2.rst (5)

8-37: Excellent introduction and OAuth2 vs OIDC explanation!

The documentation clearly distinguishes between OAuth2 and OIDC protocols, accurately explains when ID tokens vs userinfo endpoints are used, and provides helpful guidance on the recommended approach. The note box effectively communicates the key behavioral changes when using OIDC discovery.


44-75: Configuration parameters are well-documented and technically accurate.

The parameter descriptions correctly reflect OIDC support, including:

  • Optional client secret for public clients with proper PKCE enforcement explanation
  • OIDC discovery URL with practical Azure example
  • Clear guidance on when userinfo endpoint can be skipped
  • Accurate username resolution fallback order for OIDC
  • Security-conscious ID token-first checking for additional claims

109-126: Clear and accurate explanation of public vs confidential OAuth client types.

This section correctly:

  • Distinguishes between confidential and public client types
  • Explains when PKCE is mandatory (public clients) vs optional (confidential clients)
  • Accurately describes the unauthenticated token exchange mechanism for public clients per OAuth2 RFC standards

The technical details about Authlib's native behavior and token endpoint authentication method align with OAuth2 best practices.


127-255: Comprehensive and practical OIDC configuration examples!

The examples section effectively demonstrates:

  • OIDC discovery-based configuration (recommended approach)
  • Public client configuration with PKCE (showing OAUTH2_CLIENT_SECRET: None with clear comments)
  • Username resolution fallback order aligned with OIDC standard claims
  • Additional claims authorization with ID token-first checking
  • Legacy OAuth2 configuration as a fallback option

All Python code examples are syntactically correct and include helpful inline comments. The progressive structure (recommended → public client → advanced features → legacy) helps users understand different deployment scenarios.


1-255: Outstanding documentation update that thoroughly addresses the PR objectives!

This documentation successfully introduces public client support with Authorization Code + PKCE, which is the core objective of PR #9534. The documentation:

✅ Clearly explains OAuth2 vs OIDC protocols and when to use each
✅ Documents optional client secret for public clients with mandatory PKCE enforcement
✅ Provides comprehensive OIDC discovery examples (recommended approach)
✅ Demonstrates public client configuration with practical code examples
✅ Explains username resolution and claims handling for OIDC
✅ Maintains legacy OAuth2 documentation for backward compatibility
✅ Addresses security considerations (ID token validation, PKCE requirements)

The documentation aligns perfectly with the PR's goal of enabling secretless deployments and supporting secure Kubernetes-native authentication patterns while following OAuth2 best practices.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/en_US/oauth2.rst (1)

49-54: Minor typo: capitalize "you" at start of sentence.

📝 Suggested fix
-   for this parameter. you can modify the value as follows:
+   for this parameter. You can modify the value as follows:
🧹 Nitpick comments (1)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py (1)

285-301: Test state reset is thorough but relies on internal access.

The _reset_oauth2_state method properly isolates test scenarios by clearing singleton caches. While accessing AuthSourceRegistry._objects directly is fragile, it's acceptable for test infrastructure. Consider adding a comment noting this dependency on internal state.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74c8b2d and a6f5010.

📒 Files selected for processing (5)
  • docs/en_US/oauth2.rst
  • web/pgadmin/authenticate/__init__.py
  • web/pgadmin/authenticate/oauth2.py
  • web/pgadmin/browser/tests/__init__.py
  • web/pgadmin/browser/tests/test_oauth2_with_mocking.py
🧰 Additional context used
🧬 Code graph analysis (1)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py (1)
web/pgadmin/authenticate/oauth2.py (4)
  • authenticate (560-572)
  • OAuth2Authentication (96-616)
  • login (336-476)
  • get_user_profile (478-558)
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

147-153: Avoid specifying long messages outside the exception class

(TRY003)


178-178: Possible hardcoded password assigned to: "token_endpoint_auth_method"

(S105)


193-193: Unused method argument: form

(ARG002)


336-336: Unused method argument: form

(ARG002)

web/pgadmin/browser/tests/test_oauth2_with_mocking.py

122-124: Avoid specifying long messages outside the exception class

(TRY003)


296-296: Do not call setattr with a constant attribute value. It is not any safer than normal property access.

Replace setattr with assignment

(B010)


332-332: Do not catch blind exception: Exception

(BLE001)


349-349: Unused function argument: self

(ARG001)


384-384: Unused function argument: self

(ARG001)

🔇 Additional comments (16)
web/pgadmin/browser/tests/__init__.py (1)

14-22: LGTM!

The no-op setUp and tearDown hooks provide standard test lifecycle scaffolding. The comment clearly documents the purpose as a placeholder for test discovery.

docs/en_US/oauth2.rst (3)

14-37: Excellent documentation for OAuth2 vs OIDC distinction.

The comparison section clearly explains when ID tokens are used vs userinfo endpoints, and the note about OAUTH2_SERVER_METADATA_URL triggering OIDC mode aligns with the implementation in _is_oidc_provider().


109-126: Public vs Confidential clients section is well documented.

The explanation of PKCE requirements for public clients and the note about Authlib's unauthenticated token exchange matches the implementation logic in OAuth2Authentication.__init__.


182-211: Username resolution documentation matches implementation.

The documented priority order (OAUTH2_USERNAME_CLAIM → email → preferred_username → sub) correctly reflects the _resolve_username method logic in oauth2.py.

web/pgadmin/authenticate/oauth2.py (6)

126-153: PKCE validation logic is sound.

The detection (pkce_is_configured) triggers if either PKCE parameter is set, while the public client validation requires both OAUTH2_CHALLENGE_METHOD="S256" and OAUTH2_RESPONSE_TYPE="code". This correctly catches partial PKCE configurations for public clients.

Note: The static analysis hint S105 on line 178 (token_endpoint_auth_method = 'none') is a false positive—'none' is a valid OAuth2 token endpoint authentication method, not a password.


196-234: OIDC detection and claim extraction helpers are well implemented.

The _is_oidc_provider method correctly uses OAUTH2_SERVER_METADATA_URL as the OIDC indicator. The _get_id_token_claims method safely extracts claims from Authlib's token response structure with appropriate defensive checks.


248-334: Username resolution logic is comprehensive and well-documented.

The OIDC-aware fallback chain (custom claim → email → preferred_username → sub) correctly implements the documented behavior. The logging at each resolution step aids debugging. The non-OIDC fallback to email maintains backward compatibility.


407-442: Well-structured claim validation with OIDC priority.

The validation correctly checks ID token claims first for OIDC providers, then falls back to userinfo profile. This aligns with the documented behavior and reduces unnecessary network calls when ID token contains sufficient claims.


489-535: Efficient OIDC profile retrieval with userinfo optimization.

The logic correctly skips the userinfo endpoint call when the ID token contains sufficient claims (email, preferred_username, or sub) and all configured authorization claims. This optimization reduces latency and network overhead for OIDC flows.


560-572: Defensive handling of missing OAuth2 provider selection.

Using request.form.get('oauth2_button') instead of direct dictionary access prevents BadRequestKeyError when the form doesn't contain the OAuth2 button. The explicit error message improves user experience.

web/pgadmin/authenticate/__init__.py (1)

229-250: Auth source filtering correctly respects user's login method choice.

The POST-only guard and explicit button handling ensure authentication sources are properly filtered based on the user's selection:

  • internal_button → filters to internal/LDAP auth
  • oauth2_button → filters to OAuth2 only

This prevents the previous issue where internal login could incorrectly attempt OAuth2 authentication.

web/pgadmin/browser/tests/test_oauth2_with_mocking.py (5)

25-109: Comprehensive test scenario coverage.

The test scenarios thoroughly cover:

  • External authentication redirects
  • Login success/failure paths
  • PKCE registration (both public and confidential clients)
  • OIDC ID token claim handling
  • Username claim precedence
  • Additional claims authorization

This aligns well with the implementation changes in oauth2.py.


314-336: External redirect test correctly verifies expected behavior.

The generic Exception catch at line 332 is intentional—the test validates that the test client correctly raises an error when encountering external redirects. The exception message assertion confirms the expected behavior.


338-408: Login success/failure tests properly mock OAuth2 flow.

The _fake_authenticate and _fake_get_user_profile functions correctly set up the OAuth2 state including ID token claims in the session. The static analysis hint about unused self is a false positive—these functions replace instance methods and require the parameter for the patch to work correctly.


475-503: Public client PKCE validation test ensures fail-fast behavior.

The test correctly verifies that a public client configuration without PKCE raises ValueError during initialization, and that OAuth.register is never called. This ensures misconfigurations are caught early.


505-553: OIDC userinfo optimization tests validate correct behavior.

The tests properly verify:

  1. When ID token contains sufficient claims (email, sub), the userinfo endpoint is not called
  2. When ID token lacks claims, the userinfo endpoint is called

The use of AssertionError as a side effect for the skip test is a clever way to ensure the optimization works correctly.

@Pyodin
Copy link
Author

Pyodin commented Jan 14, 2026

PR reported in #9536

@Pyodin Pyodin closed this Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant