Skip to content

Conversation

@Pyodin
Copy link

@Pyodin Pyodin commented Jan 10, 2026

This change improves compatibility with OpenID Connect providers by treating ID token claims as the primary identity source and
falling back to userinfo when needed.

The existing OAuth2 flow is preserved and remains backward-compatible.

Summary by CodeRabbit

  • New Features

    • Added OpenID Connect (OIDC) support alongside OAuth2: discovery, ID token validation, and prioritized ID-token-based identity and claim extraction.
  • Bug Fixes

    • Tightened login/auth-source handling to respect form actions and missing-provider cases; improved error messages and audit-friendly logs.
  • Documentation

    • Expanded OAuth2/OIDC docs with discovery examples, username-resolution order, claim evaluation/fallback, PKCE guidance, and legacy OAuth2 examples.
  • Tests

    • New data-driven OAuth2/OIDC tests, PKCE verification, centralized test-state management, and lifecycle helpers.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Walkthrough

Adds OpenID Connect (OIDC) discovery and ID-token aware flows to OAuth2 authentication: ID token validation and claim extraction, OIDC-first username/email resolution with userinfo fallback, guarded auth-source updates, expanded docs and examples, and data-driven tests (including PKCE) with centralized test helpers.

Changes

Cohort / File(s) Summary
Documentation
docs/en_US/oauth2.rst
Broadened OAuth2 docs to include OIDC discovery (OAUTH2_SERVER_METADATA_URL), automatic discovery of token/authorization endpoints, ID token validation and claim extraction, updated OAUTH2_USERINFO_ENDPOINT, OAUTH2_SCOPE guidance (openid), username-resolution order, OAUTH2_ADDITIONAL_CLAIMS behavior, OIDC and legacy OAuth2 examples.
Core Authentication (OIDC-aware)
web/pgadmin/authenticate/oauth2.py
Added OIDC-aware initialization and utilities: self.oauth2_current_client init; new methods _is_oidc_provider(), _get_id_token_claims(), _resolve_username(); reworked login(), get_user_profile() and authenticate() to prefer ID token claims with userinfo fallback, safer token/config access, improved error messages and provider-selection handling.
Auth Source Management
web/pgadmin/authenticate/__init__.py
Made AuthSourceManager.update_auth_sources() conditional: only mutates auth sources for POST requests and when specific login form actions (internal_button, oauth2_button) are present; removed previous unconditional mutation logic.
Browser Test Base
web/pgadmin/browser/tests/__init__.py
Added no-op setUp() and tearDown() lifecycle methods to BrowserGenerateTestCase.
OAuth2/OIDC Test Suite
web/pgadmin/browser/tests/test_oauth2_with_mocking.py
Converted tests to data-driven scenarios covering multiple OIDC variants and PKCE; added centralized state reset and session assertion helpers (_reset_oauth2_state(), _assert_oauth2_session_*()), modular test routines for external auth, success/failure flows, OIDC userinfo skip/call cases, and expanded provider fixtures (oidc-basic, oidc-username-claim, oidc-additional-claims).

Sequence Diagrams

sequenceDiagram
    participant User as User
    participant PgAdmin as pgAdmin
    participant OIDCProv as OIDC Provider
    participant Userinfo as Userinfo Endpoint
    participant DB as User Database

    User->>PgAdmin: Click OAuth2/OIDC Login
    PgAdmin->>OIDCProv: Fetch discovery metadata (server_metadata_url)
    OIDCProv-->>PgAdmin: Return token_endpoint & auth_endpoint
    PgAdmin->>OIDCProv: Redirect to authorization endpoint
    OIDCProv->>OIDCProv: User authenticates
    OIDCProv-->>PgAdmin: Return auth code (and possibly ID token)
    PgAdmin->>OIDCProv: Exchange code for tokens (validate ID token if present)
    OIDCProv-->>PgAdmin: Return access_token and ID token with claims
    PgAdmin->>PgAdmin: Extract claims from ID token
    alt ID token lacks required claims
        PgAdmin->>Userinfo: Fetch user profile using access_token
        Userinfo-->>PgAdmin: Return profile data
    end
    PgAdmin->>DB: Resolve/create user with resolved username/claims
    DB-->>PgAdmin: User record
    PgAdmin-->>User: Login successful, redirect
Loading
sequenceDiagram
    participant User as User
    participant PgAdmin as pgAdmin
    participant OAuth2Prov as OAuth2 Provider
    participant Userinfo as Userinfo Endpoint
    participant DB as User Database

    User->>PgAdmin: Click OAuth2 Login
    PgAdmin->>OAuth2Prov: Redirect to configured authorization endpoint
    OAuth2Prov->>OAuth2Prov: User authenticates
    OAuth2Prov-->>PgAdmin: Return auth code
    PgAdmin->>OAuth2Prov: Exchange code for access_token
    OAuth2Prov-->>PgAdmin: Return access_token
    PgAdmin->>Userinfo: Fetch user profile (userinfo used for non-OIDC)
    Userinfo-->>PgAdmin: Return profile data
    PgAdmin->>PgAdmin: Extract username from profile (OAUTH2_USERNAME_CLAIM or fallback)
    PgAdmin->>DB: Resolve/create user
    DB-->>PgAdmin: User record
    PgAdmin-->>User: Login successful, redirect
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Enhance OAUTH2 and OIDC authentication support' clearly and directly describes the main objective of this pull request, which adds comprehensive OIDC support alongside existing OAuth2 functionality.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 2

🤖 Fix all issues with AI agents
In @web/pgadmin/authenticate/oauth2.py:
- Around line 331-333: Replace the incorrect use of
current_app.logger.exception(error_msg) with current_app.logger.error(error_msg)
so you log the message without implying an exception/stacktrace; keep the
subsequent return False, gettext(error_msg) unchanged and ensure the same
error_msg variable is used when calling gettext.
- Around line 305-313: The code uses current_app.logger.exception() when no
exception is present (in the OIDC/ non-OIDC profile checks), which incorrectly
emits a traceback; change those calls to current_app.logger.error() (or
logger.warning() if preferred) so you log the error message without an exception
context, keeping the same error_msg variable and return behavior in the
functions that use _is_oidc_provider() and profile_dict checks.
🧹 Nitpick comments (1)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py (1)

277-288: Consider catching a more specific exception type.

The broad except Exception catch works for the test's purpose (verifying external redirect handling), but if the test framework raises a specific exception type for external redirects, catching that would be more precise and avoid masking unexpected errors.

📜 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 c87d5f8.

📒 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/authenticate/__init__.py (1)
web/pgadmin/tools/user_management/__init__.py (1)
  • auth_sources (499-507)
🪛 Ruff (0.14.10)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py

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

Replace setattr with assignment

(B010)


284-284: Do not catch blind exception: Exception

(BLE001)


301-301: Unused function argument: self

(ARG001)


334-334: Unused function argument: self

(ARG001)

web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


288-288: Unused method argument: form

(ARG002)

🔇 Additional comments (14)
web/pgadmin/authenticate/__init__.py (1)

229-250: Well-structured guard logic for auth source mutation.

The refactored update_auth_sources method correctly prevents incorrect authentication source selection during login. The early return for non-POST requests and explicit button checks ensure that internal login attempts don't accidentally trigger OAuth2 flows.

One consideration: if a POST request is made without either internal_button or oauth2_button (e.g., a malformed request), the method returns without modifying auth_sources, which preserves the full configured list. This seems like reasonable default behavior.

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

106-109: Good defensive initialization.

Initializing oauth2_current_client to None prevents potential AttributeError when methods like get_friendly_name() or _is_oidc_provider() are called before authenticate() sets the client.


162-185: LGTM - Clean OIDC detection helpers.

The _is_oidc_provider() method correctly uses the presence of OAUTH2_SERVER_METADATA_URL as the OIDC indicator. The _get_id_token_claims() safely retrieves claims with appropriate fallbacks to empty dict.


200-286: Well-structured username resolution with clear fallback hierarchy.

The _resolve_username() method implements a sensible resolution order:

  1. Configured OAUTH2_USERNAME_CLAIM (from ID token, then profile)
  2. OIDC standard claims: email → preferred_username → sub
  3. Non-OIDC: email required

The logging at each decision point provides good observability for debugging authentication issues.


416-463: Good optimization to skip userinfo endpoint for OIDC providers.

The logic correctly:

  1. Checks if ID token contains sufficient claims (email, preferred_username, or sub)
  2. Returns ID token claims directly if sufficient, avoiding an extra network call
  3. Falls back to userinfo endpoint when needed
  4. Handles edge cases with appropriate logging

This aligns well with the OIDC specification where the ID token is the primary identity source.


465-477: Safer provider selection with explicit error handling.

Using request.form.get() instead of direct dictionary access prevents BadRequestKeyError and provides a clear error message when no OAuth2 provider is selected.

docs/en_US/oauth2.rst (2)

14-37: Excellent documentation of OAuth2 vs OIDC differences.

The explanation clearly differentiates between OAuth2 and OIDC, and the note about OAUTH2_SERVER_METADATA_URL triggering OIDC mode is helpful for users configuring their providers.


109-210: Comprehensive configuration examples.

The documentation provides clear examples for:

  • OIDC with discovery (recommended approach)
  • Username resolution order
  • Additional claims authorization
  • Legacy OAuth2 without OIDC

This covers the main use cases and helps users migrate to OIDC where appropriate.

web/pgadmin/browser/tests/__init__.py (1)

13-24: Placeholder test class with explicit lifecycle methods.

The added setUp() and tearDown() methods are no-op implementations, which is appropriate for a smoke test that ensures the test package is discoverable. The comment clearly explains the purpose.

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

25-91: Good data-driven test scenarios.

The refactored scenarios provide comprehensive coverage for:

  • External redirect flow
  • Successful and failed logins
  • PKCE workflow
  • OIDC ID token claims usage
  • Userinfo endpoint skip/call behavior

This approach makes it easy to add new scenarios without duplicating test logic.


237-253: Proper test isolation with state reset.

The _reset_oauth2_state() method ensures clean state for each test by clearing:

  • AuthSourceRegistry._objects singleton cache
  • Per-app auth sources cache
  • OAuth2Authentication class-level caches

Using setattr for current_app (line 248) is appropriate since current_app is a Flask LocalProxy object.


290-356: Clean test orchestrators for success and failure scenarios.

The _test_oauth2_login_success and _test_oauth2_login_failure methods properly:

  • Mock authenticate() to set the current client
  • Mock get_user_profile() to return test data and set up session state
  • Assert expected session state after login

The self parameter in inner functions (lines 301, 334) is required since they're patching instance methods.


358-390: PKCE parameter verification test.

The test correctly verifies that PKCE configuration parameters are passed through to the OAuth client registration, including code_challenge_method, response_type, and default client_kwargs.


392-435: Good coverage for userinfo endpoint skip optimization.

These tests validate the OIDC optimization:

  • _test_oidc_get_user_profile_skip_userinfo: Confirms userinfo endpoint is skipped when ID token contains sufficient claims
  • _test_oidc_get_user_profile_calls_userinfo: Confirms userinfo endpoint is called as fallback when ID token lacks claims

The use of AssertionError in the mock and explicit assert_not_called() / assert_called_once() provides clear test assertions.

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: 4

🤖 Fix all issues with AI agents
In @web/pgadmin/authenticate/oauth2.py:
- Around line 292-297: The code logs full user data (profile, profile_dict,
id_token_claims) which can expose PII; update the debug logging in the OAuth2
handling code to avoid printing full structures and instead log only
non-sensitive metadata (e.g., presence/absence and keys) for variables
profile_dict and id_token_claims and when calling self._get_id_token_claims()
use current_app.logger.debug to output minimal info such as "profile_dict keys"
or "empty" and "id_token_claims keys" or "empty" rather than the full contents;
ensure you update the statements that call current_app.logger.debug for
profile/profile_dict/id_token_claims accordingly.
- Around line 230-284: The debug logs in the username/email extraction flow of
the OAuth2 handler leak PII (actual usernames, emails and sub values); update
the logging in the method that inspects
id_token_claims/profile_dict/username_claim (the block that calls
self._is_oidc_provider(), and references id_token_claims, profile_dict,
username_claim, and email) to avoid printing raw values—log only which claim was
used (e.g., "Using email from ID token as username" or "Found username claim
'preferred_username' in ID token") or log a redacted/masked version of the value
instead of the full identifier; keep existing log levels and messages otherwise.
- Around line 385-391: The audit message currently logs PII by including full
profile and id_token_claims; change the logic that builds audit_msg (the
f-string using username, additional_claims, profile, id_token_claims) to omit
claim values and instead log only non-sensitive metadata such as the username,
the additional_claims checked, and either the keys or counts of profile and
id_token_claims (e.g., profile.keys() or len(profile), id_token_claims.keys() or
len(id_token_claims)); update the call to current_app.logger.warning to use the
sanitized audit_msg so no full claim values are written to logs.
- Around line 360-378: The code passes the raw profile (which may be a list)
into __is_any_claim_valid and debug logs, causing AttributeError when
identity.get is called; change those usages to use the dict returned by
get_profile_dict (profile_dict) instead of profile: call
__is_any_claim_valid(profile_dict, additional_claims) and log profile_dict in
the current_app.logger.debug lines so __is_any_claim_valid and the debug output
always receive the dict representation produced by
get_profile_dict/get_user_profile.
📜 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 c87d5f8 and 631c8de.

📒 Files selected for processing (1)
  • web/pgadmin/authenticate/oauth2.py
🧰 Additional context used
🧬 Code graph analysis (1)
web/pgadmin/authenticate/oauth2.py (1)
web/pgadmin/authenticate/__init__.py (2)
  • login (94-100)
  • login (309-323)
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


288-288: Unused method argument: form

(ARG002)

🔇 Additional comments (6)
web/pgadmin/authenticate/oauth2.py (6)

106-109: LGTM!

Good defensive initialization. Setting oauth2_current_client to None prevents AttributeError in edge cases where the property might be accessed before authenticate() is called.


153-157: LGTM!

Safe handling of missing provider with sensible fallback to the OAUTH2 constant.


162-171: LGTM!

Clean OIDC provider detection based on the presence of OAUTH2_SERVER_METADATA_URL.


173-185: LGTM!

Defensive extraction of ID token claims with proper fallbacks.


416-455: LGTM!

Good optimization to skip the userinfo endpoint call when the ID token already contains sufficient claims. The differentiated logging (warning for OIDC, error for non-OIDC) appropriately reflects the severity of missing endpoint configuration in each context.


465-477: LGTM!

Using request.form.get() instead of direct index access prevents BadRequestKeyError when the OAuth2 button isn't present in the form. The explicit error message when no provider is selected improves the user experience.

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: 3

🤖 Fix all issues with AI agents
In @web/pgadmin/authenticate/oauth2.py:
- Around line 288-345: The login() method can call get_user_profile() when
self.oauth2_current_client is None, causing an uncaught exception; add an early
defensive check at the start of login() that verifies self.oauth2_current_client
is set, and if not, log a clear error via current_app.logger.error and return
False, gettext("No OAuth2 provider available" or similar) to fail gracefully;
place this guard before calling get_user_profile() and other provider-dependent
methods (e.g., _get_id_token_claims(), _is_oidc_provider(), _resolve_username())
so the code never looks up the client with a None key.
🧹 Nitpick comments (3)
web/pgadmin/authenticate/oauth2.py (3)

153-158: get_friendly_name() behavior is reasonable, but the return value is effectively the provider key.
If the intent was a “display name”, consider a separate config key (e.g., OAUTH2_DISPLAY_NAME) rather than returning OAUTH2_NAME (which is also used as the registry key).


200-287: Username/email resolution misses a key OIDC edge case: email may exist only in the ID token.
When profile_dict is empty (or when you intentionally skip userinfo), email can stay None even if id_token_claims['email'] is present—this can lead to creating a user without an email or failing downstream assumptions.

Proposed patch (also pull email from ID token)
@@
-        email = profile_dict[email_key[0]] if email_key else None
+        email = profile_dict[email_key[0]] if email_key else None
+        # If userinfo is skipped/unavailable, email may only exist in the ID token.
+        if not email and isinstance(id_token_claims, dict):
+            email = id_token_claims.get('email')

485-490: Prefer using the passed form arg (and silence Ruff warnings) instead of request.form directly.
This improves testability and resolves Ruff ARG002 (same for validate() and login()).

Proposed patch
@@
-    def authenticate(self, form):
+    def authenticate(self, form):
@@
-        self.oauth2_current_client = request.form.get('oauth2_button')
+        self.oauth2_current_client = (form or request.form).get('oauth2_button')
📜 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 631c8de and cb6a273.

📒 Files selected for processing (1)
  • web/pgadmin/authenticate/oauth2.py
🧰 Additional context used
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


288-288: Unused method argument: form

(ARG002)

🔇 Additional comments (1)
web/pgadmin/authenticate/oauth2.py (1)

106-110: Good: initialize self.oauth2_current_client to avoid edge-case AttributeError.
This makes get_friendly_name()/OIDC helpers safer in tests and unusual flows.

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

🧹 Nitpick comments (1)
web/pgadmin/authenticate/oauth2.py (1)

456-486: Consider initializing needs_userinfo before the conditional block for robustness.

The variable needs_userinfo is defined only inside the if has_sufficient_claims: block (line 466), and then referenced in the condition on line 477. While this works correctly due to short-circuit evaluation, it's fragile—future modifications adding an early return before line 466 could introduce a NameError.

♻️ Suggested improvement
+            needs_userinfo = False
             if has_sufficient_claims:
                 provider = self.oauth2_config.get(
                     self.oauth2_current_client, {}
                 )
                 username_claim = provider.get('OAUTH2_USERNAME_CLAIM')
                 additional_claims = provider.get('OAUTH2_ADDITIONAL_CLAIMS')

                 # If custom username claim or additional authorization
                 #  claims are configured, they may exist only in userinfo;
                 # don't skip userinfo unless ID token has them.
-                needs_userinfo = False
                 if username_claim and username_claim not in id_token_claims:
                     needs_userinfo = True
📜 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 cb6a273 and 99272d5.

📒 Files selected for processing (1)
  • web/pgadmin/authenticate/oauth2.py
🧰 Additional context used
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


288-288: Unused method argument: form

(ARG002)

🔇 Additional comments (7)
web/pgadmin/authenticate/oauth2.py (7)

107-109: LGTM!

Good defensive initialization to prevent AttributeError when methods like _is_oidc_provider() or get_friendly_name() are called before authenticate() sets the provider.


162-171: LGTM!

Clean helper method that correctly identifies OIDC providers by checking for the presence of OAUTH2_SERVER_METADATA_URL. The null-safe access pattern handles the case when no provider is selected.


173-185: LGTM!

The method safely extracts ID token claims with appropriate fallbacks. The docstring correctly documents Authlib's behavior of storing parsed ID token claims in the userinfo key of the token response.


200-286: Well-structured username resolution with clear fallback logic.

The method properly implements OIDC-aware username resolution with a sensible claim priority hierarchy. The documented resolution order (custom claim → email → preferred_username → sub) aligns with OIDC best practices.


288-349: LGTM!

The login flow properly handles both OIDC and non-OIDC providers with appropriate validation and informative error messages. The debug logging of claim keys (not values) is helpful for troubleshooting without exposing sensitive data.


359-417: LGTM!

The two-tier validation approach for OIDC providers (ID token claims first, then userinfo fallback) correctly prioritizes the ID token as the authoritative identity source while maintaining backward compatibility. The audit logging provides good visibility without exposing sensitive claim values.


511-516: LGTM!

Good defensive change using request.form.get() to avoid BadRequestKeyError when the OAuth2 button value is missing, with a clear user-facing error message.

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

🧹 Nitpick comments (1)
web/pgadmin/authenticate/oauth2.py (1)

195-203: Narrow the exception handling.

Catching bare Exception masks different failure modes (network errors, JWT validation failures, parsing errors). Consider catching specific Authlib exceptions or at minimum logging the exception type.

♻️ Suggested refinement
         try:
             claims = client.parse_id_token(token)
             if isinstance(claims, dict):
                 return claims
-        except Exception as e:
+        except (ValueError, KeyError, AttributeError) as e:
             current_app.logger.warning(
-                f'Failed to parse ID token via authlib: {e}'
+                f'Failed to parse ID token via authlib: {type(e).__name__}: {e}'
             )

Note: Verify which specific exceptions Authlib raises for ID token parsing failures and adjust accordingly.

📜 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 99272d5 and 4874818.

📒 Files selected for processing (2)
  • web/pgadmin/authenticate/oauth2.py
  • web/pgadmin/browser/tests/test_oauth2_with_mocking.py
🧰 Additional context used
🧬 Code graph analysis (2)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py (1)
web/pgadmin/authenticate/oauth2.py (4)
  • authenticate (527-539)
  • OAuth2Authentication (96-583)
  • login (307-447)
  • get_user_profile (449-525)
web/pgadmin/authenticate/oauth2.py (1)
web/pgadmin/authenticate/__init__.py (1)
  • validate (273-283)
🪛 Ruff (0.14.10)
web/pgadmin/browser/tests/test_oauth2_with_mocking.py

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

Replace setattr with assignment

(B010)


284-284: Do not catch blind exception: Exception

(BLE001)


301-301: Unused function argument: self

(ARG001)


341-341: Unused function argument: self

(ARG001)

web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


199-199: Do not catch blind exception: Exception

(BLE001)


307-307: Unused method argument: form

(ARG002)

🔇 Additional comments (15)
web/pgadmin/authenticate/oauth2.py (8)

107-109: LGTM: Good defensive initialization.

This safeguard prevents AttributeError in edge cases where oauth2_current_client might be accessed before being set during authenticate().


153-157: LGTM: Safe fallback for missing provider.

The defensive check ensures get_friendly_name() won't fail when called before a provider is selected, returning a sensible default.


162-171: LGTM: Clear OIDC provider detection.

The method correctly identifies OIDC providers by checking for the presence of OAUTH2_SERVER_METADATA_URL configuration.


219-305: LGTM: Well-structured username resolution with clear precedence.

The method implements a sensible fallback hierarchy:

  1. Custom OAUTH2_USERNAME_CLAIM (if configured)
  2. For OIDC: email → preferred_username → sub
  3. For OAuth2: email only

The logging at each decision point aids debugging.


308-348: LGTM: Robust validation distinguishes OIDC from OAuth2 requirements.

The logic correctly requires either ID token claims OR profile for OIDC providers (allowing ID-token-only authentication), while non-OIDC providers must have profile data.


378-413: LGTM: OIDC-aware additional claims validation.

For OIDC providers, the code correctly checks ID token claims first (since they're cryptographically signed and trusted), then falls back to userinfo profile. For non-OIDC providers, it only validates against profile. This aligns with OIDC best practices.


463-502: LGTM: Smart optimization to skip userinfo endpoint when ID token is sufficient.

The logic correctly determines when the userinfo endpoint can be skipped by checking:

  1. ID token has standard identity claims
  2. Custom OAUTH2_USERNAME_CLAIM (if configured) is present in ID token
  3. All OAUTH2_ADDITIONAL_CLAIMS keys are present in ID token

This optimization reduces latency and external calls while maintaining correctness.


530-532: LGTM: Safe form access prevents KeyError.

Using request.form.get('oauth2_button') instead of direct dictionary access prevents BadRequestKeyError when OAuth2 isn't the selected authentication method, improving user experience with a clearer error message.

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

25-92: LGTM: Comprehensive OIDC test scenario coverage.

The expanded scenarios test critical OIDC flows:

  • ID token claims as primary identity source
  • Fallback to userinfo profile
  • Custom username claim precedence
  • Additional claims authorization (success & failure paths)
  • Userinfo endpoint optimization (skip vs. call)

The data-driven approach with the kind field makes the test matrix clear and maintainable.


160-202: LGTM: Well-structured OIDC provider configurations for testing.

The test configurations cover key OIDC scenarios:

  • oidc-basic: Standard OIDC with server metadata URL
  • oidc-username-claim: Custom username claim mapping
  • oidc-additional-claims: Authorization via ID token claims

These configurations enable thorough testing of the new OIDC-aware authentication logic.


237-253: LGTM: Centralized state management ensures test isolation.

The _reset_oauth2_state() helper correctly clears:

  1. AuthSourceRegistry singleton
  2. Per-app cached auth sources
  3. OAuth2 class-level caches (oauth2_clients, oauth2_config)

This prevents test pollution from shared state.


290-330: LGTM: Realistic mocking of OIDC ID token flow.

The _test_oauth2_login_success method properly simulates OIDC authentication by:

  1. Setting up session with OAuth2 token including id_token
  2. Mocking parse_id_token() to return test claims
  3. Verifying successful login via session assertions

The pattern correctly separates concerns between authenticate phase and profile retrieval.


406-428: LGTM: Validates userinfo endpoint optimization.

The test ensures that when ID token contains sufficient claims (email and sub), the userinfo endpoint is not called. The use of side_effect=AssertionError combined with assert_not_called() provides strong verification of the optimization.


430-455: LGTM: Validates userinfo endpoint fallback.

The test correctly verifies that when the ID token lacks standard claims (empty dict from parse_id_token), the implementation falls back to calling the userinfo endpoint. This ensures the optimization doesn't break standard OAuth2 flows.


379-404: LGTM: PKCE validation confirms correct client registration.

The test verifies that PKCE-configured providers pass the correct client_kwargs to OAuth client registration:

  • code_challenge_method: 'S256'
  • response_type: 'code'
  • Default kwargs (scope, verify) are preserved

This ensures PKCE configuration is properly propagated.

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

🧹 Nitpick comments (3)
web/pgadmin/authenticate/oauth2.py (1)

455-501: Consider simplifying the has_sufficient_claims check.

The any() with a list works but could be slightly more efficient as a generator:

has_sufficient_claims = any(
    claim in id_token_claims
    for claim in ('email', 'preferred_username', 'sub')
)

However, the current implementation is clear and the performance difference is negligible for 3 elements.

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

237-253: Test isolation approach is acceptable but consider documenting the fragility.

Directly modifying AuthSourceRegistry._objects and class-level caches is a common test pattern but creates coupling to implementation details. Consider adding a comment noting that this may need updates if the singleton implementation changes.

The setattr on line 248 is flagged by static analysis but is reasonable here since the attribute name is dynamic based on the conditional check.


284-288: Consider catching a more specific exception type.

The broad except Exception is flagged by static analysis. If the test framework raises a specific exception for external redirects, catching that would be cleaner. If not, consider adding a comment explaining why the broad catch is intentional.

             except Exception as e:
+                # Test framework raises a generic exception for external redirect
                 self.assertEqual(
                     'Following external redirects is not supported.',
                     str(e)
                 )
📜 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 4874818 and df0f20f.

📒 Files selected for processing (2)
  • web/pgadmin/authenticate/oauth2.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 (3)
  • authenticate (526-538)
  • login (302-442)
  • get_user_profile (444-524)
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


302-302: Unused method argument: form

(ARG002)

web/pgadmin/browser/tests/test_oauth2_with_mocking.py

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

Replace setattr with assignment

(B010)


284-284: Do not catch blind exception: Exception

(BLE001)


301-301: Unused function argument: self

(ARG001)


336-336: Unused function argument: self

(ARG001)

🔇 Additional comments (13)
web/pgadmin/authenticate/oauth2.py (7)

106-109: LGTM!

Good defensive initialization of oauth2_current_client to None to prevent AttributeError in edge cases where methods are called before authenticate() sets the provider.


153-157: LGTM!

Safe fallback to OAUTH2 when the provider is not found prevents potential errors in edge cases.


162-171: LGTM!

Using OAUTH2_SERVER_METADATA_URL presence as the OIDC indicator is a reasonable approach since OIDC discovery relies on the well-known metadata endpoint.


173-199: LGTM!

Solid defensive programming with type checks and early returns. The approach of extracting claims from the userinfo key in the token response aligns with Authlib's behavior for OIDC flows.


214-300: LGTM!

Well-structured username resolution with clear priority ordering:

  1. Custom OAUTH2_USERNAME_CLAIM (from ID token, then profile)
  2. OIDC standard claims hierarchy (email → preferred_username → sub)
  3. Profile email fallback for non-OIDC

The logging at each decision point aids debugging without exposing sensitive claim values.


302-442: LGTM!

The login flow correctly handles:

  • Missing provider with early exit
  • OIDC vs non-OIDC validation paths
  • Claims validation with appropriate fallback
  • Comprehensive audit logging with claim keys (not values) for security

The error messages provide actionable guidance for administrators.


526-531: LGTM!

Using form.get('oauth2_button') instead of direct dictionary access prevents BadRequestKeyError when the OAuth2 button wasn't clicked, providing a graceful error message instead.

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

25-92: LGTM!

The data-driven approach with the kind field provides clear test organization and good coverage of OAuth2 and OIDC scenarios, including edge cases for claims handling and userinfo fallbacks.


99-203: LGTM!

Comprehensive test configuration with multiple providers covering OAuth2 and OIDC scenarios. The new OIDC providers correctly include OAUTH2_SERVER_METADATA_URL to trigger OIDC-specific behavior.


290-325: LGTM!

The mock functions correctly simulate the OAuth2 flow, and the session injection of id_token_claims via userinfo key matches how Authlib returns OIDC token data. The self parameter in the nested functions is required since they're patched as instance methods.


327-360: LGTM!

The failure test correctly verifies that when additional claims don't match (e.g., group-b instead of group-a), the session is not logged in.


362-394: LGTM!

Good verification that PKCE configuration parameters are correctly propagated to the OAuth client registration.


396-444: LGTM!

These tests effectively validate the userinfo optimization:

  • When ID token has sufficient claims, userinfo endpoint is skipped
  • When ID token lacks claims, userinfo endpoint is called

The use of side_effect=AssertionError as a failsafe combined with assert_not_called() provides double verification.

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: 1

🤖 Fix all issues with AI agents
In @web/pgadmin/authenticate/oauth2.py:
- Around line 173-199: _get_id_token_claims currently looks for a non-existent
'userinfo' key in the session token and thus always returns empty; instead, read
session['oauth2_token']['id_token'] and parse it using Authlib's parse_id_token
on the appropriate client
(self.oauth2_clients[self.oauth2_current_client].parse_id_token) or a safe JWT
decode, returning the resulting dict when successful and falling back to {} on
exceptions; ensure you still check token is a dict and handle missing id_token
gracefully.
🧹 Nitpick comments (1)
web/pgadmin/authenticate/oauth2.py (1)

276-281: Consider checking preferred_username in profile as fallback.

The preferred_username claim is only checked in id_token_claims. If the ID token is minimal but the userinfo endpoint returns preferred_username, it won't be found. For consistency with the email fallback pattern (lines 271-274), consider also checking profile_dict:

Suggested enhancement
             # Priority 2: preferred_username
             if 'preferred_username' in id_token_claims:
                 username = id_token_claims['preferred_username']
                 current_app.logger.debug(
                     'Using preferred_username from ID token')
                 return username, email
+            elif 'preferred_username' in profile_dict:
+                username = profile_dict['preferred_username']
+                current_app.logger.debug(
+                    'Using preferred_username from profile')
+                return username, email
📜 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 df0f20f and fccb31e.

📒 Files selected for processing (1)
  • web/pgadmin/authenticate/oauth2.py
🧰 Additional context used
🪛 Ruff (0.14.10)
web/pgadmin/authenticate/oauth2.py

159-159: Unused method argument: form

(ARG002)


302-302: Unused method argument: form

(ARG002)

🔇 Additional comments (10)
web/pgadmin/authenticate/oauth2.py (10)

107-109: Good defensive initialization.

Initializing oauth2_current_client to None in __init__ prevents potential AttributeError when methods like _is_oidc_provider() or get_friendly_name() are called before authenticate() sets the client.


142-148: Good use of .get() for optional endpoint URLs.

This correctly handles OIDC providers that use OAUTH2_SERVER_METADATA_URL for endpoint discovery, where explicit URLs may not be configured.


153-157: LGTM.

Safe fallback to OAUTH2 when provider configuration is missing.


162-172: Clean OIDC provider detection.

Using the presence of OAUTH2_SERVER_METADATA_URL as the OIDC indicator is a reasonable heuristic since OIDC providers typically support discovery.


302-306: Good guard against missing provider.

The static analysis hint about unused form parameter is a false positive—this method signature is defined by the BaseAuthentication interface and must match even if the OAuth2 flow doesn't use form data.


311-327: Good security practice in logging.

Logging only the claim keys (not values) provides useful debugging information without exposing sensitive user data.


373-408: Well-structured claims validation with appropriate fallback.

The validation correctly tries ID token claims first, then falls back to userinfo profile for OIDC providers. This ensures authorization works even when claims are split between sources.


455-501: Good optimization to skip userinfo endpoint when ID token is sufficient.

The logic correctly determines whether the userinfo call can be skipped by checking:

  1. Basic claims presence (email, preferred_username, sub)
  2. Custom username claim availability
  3. Additional authorization claims availability

This reduces latency for properly configured OIDC providers.


505-516: Appropriate error handling for missing userinfo endpoint.

The differentiated logging between OIDC and non-OIDC providers helps with debugging—OIDC providers should have provided claims in the ID token, while non-OIDC providers require explicit configuration.


526-531: Good defensive change to handle missing form data.

Using request.form.get() prevents BadRequestKeyError and provides a user-friendly error message when no OAuth2 provider is selected.

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.

2 participants