Skip to content

fix: don't hard-fail on an undecryptable saved password#10174

Open
kundansable wants to merge 1 commit into
pgadmin-org:masterfrom
kundansable:fix-10140-oidc-decrypt-nonfatal
Open

fix: don't hard-fail on an undecryptable saved password#10174
kundansable wants to merge 1 commit into
pgadmin-org:masterfrom
kundansable:fix-10140-oidc-decrypt-nonfatal

Conversation

@kundansable

@kundansable kundansable commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #10140 — some OIDC/OAuth2 users intermittently get a hard Failed to decrypt the saved password error on every connection attempt, and the account stays broken even after being deleted and recreated via OIDC.

Root Cause

Connection._decode_password() calls decrypt() (AES-CFB8, unauthenticated) on the stored password ciphertext and lets any UnicodeDecodeError from a failed decrypt propagate as a hard error. For OIDC/OAuth2 logins, the crypt key used to encrypt a saved password can differ between sessions; if it does, the ciphertext saved under a previous key can never be decoded correctly under the current key, and every connection attempt hits this same decode failure — surfacing the scary, unrecoverable-looking "Failed to decrypt the saved password" error. Deleting and recreating the pgAdmin user doesn't help because the problem is in the stored ciphertext for the server record, not the user record.

Fix

Catch the decode failure in _decode_password(), log a warning, and return an empty/no-password result instead of propagating the exception — i.e. treat an undecryptable saved password the same as "no saved password was ever set." connect() then falls through to its normal password-prompt path, so the user is prompted for and can re-enter the correct password, making the account recoverable instead of permanently stuck. OIDC/OAuth2 crypt-key derivation itself is untouched by this change.

Test Steps

(Exact repro requires an OIDC/OAuth2 setup where the crypt key can rotate between sessions; a close approximation can be done by directly corrupting a stored password's ciphertext in the config DB for a test server.)

  1. Set up a server with a saved password (Save Password enabled).
  2. Simulate a crypt-key change or corrupt the stored password ciphertext for that server record so it no longer decrypts under the current key.
  3. Attempt to connect to that server.
  4. Before the fix: connection attempt fails immediately with "Failed to decrypt the saved password", with no path to recovery.
  5. After the fix: the bad saved password is silently discarded; pgAdmin instead prompts for the password normally, and entering the correct password connects successfully.
  6. Regression: confirm a server with a valid, correctly-encrypted saved password still connects automatically without any prompt (no over-broad fallback).

Summary by CodeRabbit

  • Bug Fixes
    • Improved connection handling when a saved password cannot be decrypted.
    • Users are now prompted to enter their password again instead of experiencing an immediate connection failure.

Connection._decode_password() let UnicodeDecodeError from decrypt()
propagate as a hard error. For OIDC/OAuth2 users whose crypt key can
differ between sessions, a password saved under a previous key can
never be decoded, and every connection attempt surfaced "Failed to
decrypt the saved password" — even after deleting and recreating the
pgAdmin user, since the corrupted ciphertext is what's stored, not
anything tied to the user record.

Catch the decode failure, log a warning, and treat it as "no saved
password" instead of propagating the error. connect() then falls
through to the normal password prompt, so the account is recoverable
instead of permanently stuck.

Fixes pgadmin-org#10140
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The psycopg3 connection driver now ignores saved passwords that cannot be decrypted, logs a warning, and returns a non-fatal result so the user can enter a password again instead of failing authentication.

Changes

Saved password recovery

Layer / File(s) Summary
Non-fatal decryption fallback
web/pgadmin/utils/driver/psycopg3/connection.py
_decode_password logs a warning and returns (False, '', None) when saved-password decryption fails, without stopping the SSH tunnel or returning a fatal connection error.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: handling undecryptable saved passwords without failing the connection.
Linked Issues check ✅ Passed The change satisfies issue #10140 by ignoring an undecryptable saved password and allowing the user to retry with a fresh prompt.
Out of Scope Changes check ✅ Passed The diff is narrowly scoped to decryption failure handling and logging, with no unrelated behavioral changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
web/pgadmin/utils/driver/psycopg3/connection.py (2)

261-274: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the recovery contract.

Test invalid ciphertext, valid encrypted passwords, passexec fallback, and an SSH-tunnel server with save_password=True; assert that the invalid credential is not reused and the response prompts for a password.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/pgadmin/utils/driver/psycopg3/connection.py` around lines 261 - 274,
Extend the connection tests covering the saved-password handling around the
decryption failure path to cover invalid ciphertext, valid encrypted passwords,
passexec fallback, and an SSH-tunnel server configured with save_password=True.
Assert that invalid credentials are discarded rather than reused and that the
resulting response prompts the user for a password, while preserving successful
reuse for valid encrypted passwords and existing passexec behavior.

255-274: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Narrow the exception scope.

except Exception converts unrelated crypto-helper or programming failures into “bad saved password,” logs them as a warning, and continues with None. Catch the concrete invalid-ciphertext/decryption exceptions plus UnicodeDecodeError, after verifying the repository’s decrypt() contract, and let unexpected failures surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/pgadmin/utils/driver/psycopg3/connection.py` around lines 255 - 274, In
the password-decryption block of the connection logic, replace the broad `except
Exception` with the concrete invalid-ciphertext/decryption exceptions raised by
`decrypt()` plus `UnicodeDecodeError`, verifying the helper’s contract first.
Preserve the existing warning and `(False, '', None)` fallback only for those
expected bad-password cases, while allowing unrelated failures to propagate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/pgadmin/utils/driver/psycopg3/connection.py`:
- Around line 261-274: Update the invalid saved-password handling in the
connection credential flow to clear the cached ciphertext held by self.password
and return an explicit invalid-password state from the decryption helper. Ensure
connect() and the downstream get_response_for_password/SSH prompt flow consume
that state so passexec fallback is not skipped and the UI reliably prompts for a
new password.

---

Nitpick comments:
In `@web/pgadmin/utils/driver/psycopg3/connection.py`:
- Around line 261-274: Extend the connection tests covering the saved-password
handling around the decryption failure path to cover invalid ciphertext, valid
encrypted passwords, passexec fallback, and an SSH-tunnel server configured with
save_password=True. Assert that invalid credentials are discarded rather than
reused and that the resulting response prompts the user for a password, while
preserving successful reuse for valid encrypted passwords and existing passexec
behavior.
- Around line 255-274: In the password-decryption block of the connection logic,
replace the broad `except Exception` with the concrete
invalid-ciphertext/decryption exceptions raised by `decrypt()` plus
`UnicodeDecodeError`, verifying the helper’s contract first. Preserve the
existing warning and `(False, '', None)` fallback only for those expected
bad-password cases, while allowing unrelated failures to propagate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b6638f40-221e-49e2-a5e5-2c2ff1ab7df6

📥 Commits

Reviewing files that changed from the base of the PR and between b15c745 and 63c71ba.

📒 Files selected for processing (1)
  • web/pgadmin/utils/driver/psycopg3/connection.py

Comment on lines +261 to +274
# The saved password could not be decrypted. This happens
# when the stored ciphertext was encrypted with a different
# key (e.g. OIDC/OAuth2 logins where the derived encryption
# key changed between sessions), leaving un-decodable bytes
# (typically a "'utf-8' codec can't decode byte 0x.." error).
# Instead of failing every connection attempt permanently,
# discard the bad saved password and continue so the user is
# prompted for the password again.
current_app.logger.warning(
'Ignoring the saved password as it could not be '
'decrypted. The user will be prompted for the password. '
'Error: {0}'.format(str(e))
)
return False, '', None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Propagate the invalid-saved-password state to the prompt flow.

Returning (False, '', None) at Line 274 does not clear the ciphertext or identify that the saved credential was unusable. connect() has already stored it in self.password, and the truthy encpass skips the passexec fallback at Lines 323-334. The supplied downstream handler also passes not server.save_password to get_response_for_password; for this stale record that is false, so the SSH path can return prompt_password=False. Clear the cached ciphertext and propagate an explicit invalid-password state so the UI reliably re-prompts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/pgadmin/utils/driver/psycopg3/connection.py` around lines 261 - 274,
Update the invalid saved-password handling in the connection credential flow to
clear the cached ciphertext held by self.password and return an explicit
invalid-password state from the decryption helper. Ensure connect() and the
downstream get_response_for_password/SSH prompt flow consume that state so
passexec fallback is not skipped and the UI reliably prompts for a new password.

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.

OIDC users intermittently fail with "Failed to decrypt the saved password" and cannot be recovered by recreating the user

1 participant