Thanks a lot Daniel, Zslot, Vasuki for your review comments.
>The mechanism used is however a secondary discussion,
>first thing to get in place is a design for how to handle mid-connection
>credential expiration.
This patch introduces a generic credential validation framework that allows
us to periodically validate authentication credentials during active
database sessions. When enabled, this feature detects expired
credentials and terminates sessions that are no longer valid.
Added GUCs
Credential_validation.enabled = on // Enable or Disable Credential
validation
Credential_validation.interval = 120 //Frequency in seconds of running
credential validation
The callback mechanism works by:
- Defining a CredentialValidationCallback function pointer type
- Maintaining an array of validators indexed by authentication method
- Allowing other auth mechanisms to register validators via
RegisterCredentialValidator()
- Selecting the appropriate validator at runtime based on the session's
authentication method
The current implementation primarily supports password-based authentication
methods, verifying that passwords haven't expired. It can be extended to
any authentication method.
This patch is WIP. I am submitting it now to get early feedback on the
overall design and approach.
Thanks & Best Regards,
Ajit
On Wed, 18 Feb 2026 at 22:29, Zsolt Parragi <[email protected]>
wrote:
> > but I still think that neither should overload
> > what FATAL error means
>
> I see, I misunderstood what you meant by graceful there. In this case,
> this is also a good comment for the password expiration thread,
> currently that also uses FATAL errors for terminating a connection
> when the password expires.
>
> What other option do you see? Something new for this use case like
> GoAway, and clients not understanding it simply get disconnected after
> some grace period? Or using the recently merged connectionWarning to
> send a warning to the client, and disconnect it shortly if it doesn't
> do anything to fix the situation?
>
> When I tested the password expiration patch I noticed that deleted
> users who still have remaining active connections currently get ERRORs
> for every statement that requires permission checks, so in this regard
> using ERROR/FATAL for the situation seemed fine to me - it's similar
> to what already happens in some edge cases with authentication.
>
diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 98eb2a8242d..d263ca8d931 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -18,6 +18,7 @@ OBJS = \
auth-oauth.o \
auth-sasl.o \
auth-scram.o \
+ auth-validate.o \
auth.o \
be-fsstubs.o \
be-secure-common.o \
@@ -25,6 +26,7 @@ OBJS = \
crypt.o \
hba.o \
ifaddr.o \
+ password-validate.o \
pqcomm.o \
pqformat.o \
pqmq.o \
diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c
new file mode 100644
index 00000000000..ed1218377a3
--- /dev/null
+++ b/src/backend/libpq/auth-validate.c
@@ -0,0 +1,296 @@
+/*-------------------------------------------------------------------------
+*
+* auth-validate.c
+* Implementation of authentication credential validation
+*
+* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+* Portions Copyright (c) 1994, Regents of the University of California
+*
+* IDENTIFICATION
+* src/backend/libpq/auth-validate.c
+*
+*-------------------------------------------------------------------------
+*/
+#include "postgres.h"
+
+#include <time.h>
+#include <sys/time.h>
+
+#include "access/xact.h"
+#include "access/xlog.h"
+#include "libpq/auth.h"
+#include "libpq/hba.h"
+#include "libpq/libpq-be.h"
+#include "libpq/auth-validate.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "storage/latch.h"
+#include "tcop/tcopprot.h"
+#include "utils/builtins.h"
+#include "utils/elog.h"
+#include "utils/guc.h"
+#include "utils/ps_status.h"
+#include "utils/snapmgr.h"
+#include "utils/timestamp.h"
+#include "utils/timeout.h"
+
+/* GUC variables */
+bool credential_validation_enabled;
+int credential_validation_interval;
+
+/* Registered credential validators */
+static CredentialValidationCallback validators[AUTH_REQ_LAST];
+
+/*
+ * Convert UserAuth enum to AUTH_REQ_* constant for validator selection
+ */
+static int
+UserAuthToAuthReq(UserAuth auth_method)
+{
+ switch (auth_method)
+ {
+ case uaPassword:
+ case uaMD5:
+ case uaSCRAM:
+ /* All password-based methods use the password validator */
+ return AUTH_REQ_PASSWORD;
+ default:
+ /* No specific validator for other auth methods */
+ return -1;
+ }
+}
+
+/*
+ * Process credential validation
+ */
+void
+ProcessCredentialValidation(void)
+{
+ /* Skip validation during authentication or connection setup */
+ if (ClientAuthInProgress)
+ return;
+
+ /* Check credentials if validation is enabled */
+ if (credential_validation_enabled && MyClientConnectionInfo.authn_id != NULL)
+ {
+ CredentialValidationStatus status;
+ UserAuth auth_method = MyClientConnectionInfo.auth_method;
+
+ elog(DEBUG1, "credential validation: checking credentials for auth_method=%d",
+ (int) auth_method);
+
+ status = CheckCredentialValidity();
+
+ switch (status)
+ {
+ case CVS_VALID:
+ /* Credentials are valid, continue */
+ elog(DEBUG1, "credential validation: credentials valid for auth_method=%d",
+ (int) auth_method);
+ break;
+
+ case CVS_EXPIRED:
+ elog(LOG, "credential validation: credentials expired for auth_method=%d",
+ (int) auth_method);
+ ereport(FATAL,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("session credentials have expired"),
+ errhint("Please reconnect to establish a new authenticated session")));
+ break;
+
+ case CVS_ERROR:
+ elog(LOG, "credential validation: error checking credentials for auth_method=%d",
+ (int) auth_method);
+ ereport(WARNING,
+ (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+ errmsg("error checking credential validity"),
+ errhint("Credential validation will be retried at the next interval")));
+ break;
+ }
+ }
+}
+
+/*
+ * Initialize credential validation system Called from InitPostgres after
+ * authentication completes
+ */
+void
+InitializeCredentialValidation(void)
+{
+ int i;
+
+ /* Define GUC variables */
+ DefineCustomBoolVariable("credential_validation.enabled",
+ "Enable periodic credential validation.",
+ NULL,
+ &credential_validation_enabled,
+ false,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ DefineCustomIntVariable("credential_validation.interval",
+ "Credential validation interval in seconds.",
+ NULL,
+ &credential_validation_interval,
+ 60,
+ 10,
+ 3600,
+ PGC_SUSET,
+ 0,
+ NULL,
+ NULL,
+ NULL);
+
+ /* Initialize validator callbacks to NULL */
+ for (i = 0; i < AUTH_REQ_LAST; i++)
+ validators[i] = NULL;
+
+ /* Register built-in validators */
+ RegisterCredentialValidator(AUTH_REQ_PASSWORD, validate_password_credentials);
+
+ /* Ensure we log the registration of the validator */
+ elog(DEBUG1, "Registered password validator for AUTH_REQ_PASSWORD=%d", AUTH_REQ_PASSWORD);
+
+ /*
+ * Schedule the first credential validation check if enabled. The
+ * timeout is already registered in postinit.c.
+ */
+ if (credential_validation_enabled && credential_validation_interval > 0)
+ {
+ /* Enable periodic checks at the specified interval */
+ enable_timeout_every(CREDENTIAL_VALIDATION_TIMEOUT,
+ GetCurrentTimestamp(),
+ credential_validation_interval * 1000);
+ }
+
+ /* Initialize method-specific validation systems */
+ InitializePasswordValidation();
+}
+
+/*
+ * Register a validator callback for a specific authentication method
+ */
+void
+RegisterCredentialValidator(int authmethod, CredentialValidationCallback validator)
+{
+ if (authmethod < 0 || authmethod >= AUTH_REQ_LAST)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("invalid authentication method code: %d", authmethod)));
+
+ validators[authmethod] = validator;
+}
+
+/*
+ * Check credential validity using the appropriate validator
+ */
+CredentialValidationStatus
+CheckCredentialValidity(void)
+{
+ CredentialValidationCallback validator = NULL;
+ int auth_req_code = -1;
+ bool started_transaction = false;
+ bool need_snapshot = false;
+
+ /*
+ * Skip validation if not in a transaction state or during shutdown or
+ * Recovery
+ */
+ if (proc_exit_inprogress || RecoveryInProgress())
+ return CVS_VALID;
+
+ /*
+ * Use the session's authentication method from MyClientConnectionInfo
+ * to select the appropriate validator.
+ */
+ if (MyClientConnectionInfo.authn_id != NULL)
+ {
+ auth_req_code = UserAuthToAuthReq(MyClientConnectionInfo.auth_method);
+
+ /*
+ * If we have a valid auth_req_code, get the corresponding
+ * validator
+ */
+ if (auth_req_code >= 0 && auth_req_code < AUTH_REQ_LAST)
+ validator = validators[auth_req_code];
+
+ /* Log detailed info at DEBUG1 level for troubleshooting */
+ elog(DEBUG1, "credential validation: auth_method=%d, auth_req_code=%d, validator=%p",
+ (int) MyClientConnectionInfo.auth_method, auth_req_code, validator);
+ }
+ else
+ {
+ elog(DEBUG1, "credential validation: no authn_id available");
+ }
+
+ /*
+ * If no validator found for the current auth method or no
+ * authenticated session, skip validation and consider credentials
+ * valid
+ */
+ if (validator == NULL || !MyClientConnectionInfo.authn_id)
+ return CVS_VALID;
+
+ /* Call the validator and interpret result */
+ PG_TRY();
+ {
+ bool result;
+ CredentialValidationStatus status;
+
+ /* Start a transaction if we're not in one */
+ if (!IsTransactionState())
+ {
+ StartTransactionCommand();
+ started_transaction = true;
+ }
+
+ /* Ensure we have an active snapshot for catalog access */
+ if (!ActiveSnapshotSet())
+ {
+ PushActiveSnapshot(GetTransactionSnapshot());
+ need_snapshot = true;
+ }
+
+ elog(DEBUG1, "credential validation: calling validator for auth_method=%d",
+ (int) MyClientConnectionInfo.auth_method);
+
+ result = validator();
+
+ if (!result)
+ {
+ elog(DEBUG1, "credential validation: credentials expired");
+ status = CVS_EXPIRED; /* Validator reports credentials expired */
+ }
+ else
+ status = CVS_VALID;
+
+ if (need_snapshot)
+ PopActiveSnapshot();
+
+ if (started_transaction)
+ CommitTransactionCommand();
+
+ return status;
+ }
+ PG_CATCH();
+ {
+ if (need_snapshot)
+ PopActiveSnapshot();
+
+ if (started_transaction)
+ CommitTransactionCommand();
+
+ /* Error during validation */
+ elog(DEBUG1, "credential validation: error during validation");
+ FlushErrorState();
+ return CVS_ERROR;
+ }
+ PG_END_TRY();
+
+ /* Validation passed, credentials are valid */
+ return CVS_VALID;
+}
diff --git a/src/backend/libpq/password-validate.c b/src/backend/libpq/password-validate.c
new file mode 100644
index 00000000000..0e04db447ca
--- /dev/null
+++ b/src/backend/libpq/password-validate.c
@@ -0,0 +1,68 @@
+/*-------------------------------------------------------------------------
+*
+* password-validate.c
+* Password validation for PostgreSQL
+*
+* Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+* Portions Copyright (c) 1994, Regents of the University of California
+*
+* IDENTIFICATION
+* src/backend/libpq/password-validate.c
+*
+*-------------------------------------------------------------------------
+*/
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/pg_authid.h"
+#include "libpq/auth-validate.h"
+ /* #include "libpq/libpq-be.h" */
+#include "miscadmin.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+/*
+ * Initialize password validation
+ */
+void
+InitializePasswordValidation(void)
+{
+}
+
+/*
+ * Validate password credentials by checking rolvaliduntil
+ */
+bool
+validate_password_credentials(void)
+{
+ HeapTuple tuple;
+ Datum rolvaliduntil_datum;
+ bool validuntil_null;
+ TimestampTz valid_until = 0;
+
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(GetSessionUserId()));
+
+ if (HeapTupleIsValid(tuple))
+ {
+ /* Get the expiration time column */
+ rolvaliduntil_datum = SysCacheGetAttr(AUTHOID, tuple,
+ Anum_pg_authid_rolvaliduntil,
+ &validuntil_null);
+ if (!validuntil_null)
+ {
+ valid_until = DatumGetTimestampTz(rolvaliduntil_datum);
+
+ if (valid_until < GetCurrentTimestamp())
+ {
+ ReleaseSysCache(tuple);
+ return false;
+ }
+ }
+ ReleaseSysCache(tuple);
+ return true;
+ }
+ else
+ return false; /* If user not found, consider Invalid */
+}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index d01a09dd0c4..d5c6d65063f 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -44,6 +44,7 @@
#include "libpq/libpq.h"
#include "libpq/pqformat.h"
#include "libpq/pqsignal.h"
+#include "libpq/auth-validate.h"
#include "mb/pg_wchar.h"
#include "mb/stringinfo_mb.h"
#include "miscadmin.h"
@@ -185,6 +186,7 @@ static void report_recovery_conflict(RecoveryConflictReason reason);
static void log_disconnections(int code, Datum arg);
static void enable_statement_timeout(void);
static void disable_statement_timeout(void);
+static void ExecuteCredentialValidationCheck(void);
/* ----------------------------------------------------------------
@@ -1049,6 +1051,9 @@ exec_simple_query(const char *query_string)
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/*
* Zap any pre-existing unnamed statement. (While not strictly necessary,
* it seems best to define simple-Query mode as if it used the unnamed
@@ -1430,6 +1435,9 @@ exec_parse_message(const char *query_string, /* string to execute */
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/*
* Switch to appropriate context for constructing parsetrees.
*
@@ -1705,6 +1713,9 @@ exec_bind_message(StringInfo input_message)
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/* Switch back to message context */
MemoryContextSwitchTo(MessageContext);
@@ -2217,6 +2228,9 @@ exec_execute_message(const char *portal_name, long max_rows)
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/*
* If we re-issue an Execute protocol request against an existing portal,
* then we are only fetching more rows rather than completely re-executing
@@ -2635,6 +2649,9 @@ exec_describe_statement_message(const char *stmt_name)
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/* Switch back to message context */
MemoryContextSwitchTo(MessageContext);
@@ -2727,6 +2744,9 @@ exec_describe_portal_message(const char *portal_name)
*/
start_xact_command();
+ if (CredentialValidationPending)
+ ExecuteCredentialValidationCheck();
+
/* Switch back to message context */
MemoryContextSwitchTo(MessageContext);
@@ -5271,3 +5291,33 @@ disable_statement_timeout(void)
if (get_timeout_active(STATEMENT_TIMEOUT))
disable_timeout(STATEMENT_TIMEOUT, false);
}
+
+/*
+ * Process credential validation in exec_simple_query
+ * This is called when CredentialValidationPending flag is set
+ */
+static void
+ExecuteCredentialValidationCheck(void)
+{
+ TimestampTz next_check_time;
+
+ /* Clear the flag immediately */
+ CredentialValidationPending = false;
+
+ /* Check credentials if we're in a transaction */
+ if (IsTransactionState())
+ {
+ ProcessCredentialValidation();
+
+ /* Re-enable the timeout for next check */
+ if (credential_validation_enabled && credential_validation_interval > 0)
+ {
+ /* Calculate next timeout time as current time + full interval */
+ next_check_time = TimestampTzPlusMilliseconds(GetCurrentTimestamp(),
+ credential_validation_interval * 1000);
+
+ /* Use the existing credential validation timeout ID */
+ enable_timeout_at(CREDENTIAL_VALIDATION_TIMEOUT, next_check_time);
+ }
+ }
+}
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..7981056e3e5 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -34,6 +34,7 @@ volatile sig_atomic_t QueryCancelPending = false;
volatile sig_atomic_t ProcDiePending = false;
volatile sig_atomic_t CheckClientConnectionPending = false;
volatile sig_atomic_t ClientConnectionLost = false;
+volatile sig_atomic_t CredentialValidationPending = false;
volatile sig_atomic_t IdleInTransactionSessionTimeoutPending = false;
volatile sig_atomic_t TransactionTimeoutPending = false;
volatile sig_atomic_t IdleSessionTimeoutPending = false;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index b59e08605cc..37f1c073d6c 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -34,6 +34,7 @@
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_tablespace.h"
#include "libpq/auth.h"
+#include "libpq/auth-validate.h"
#include "libpq/libpq-be.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
@@ -89,6 +90,7 @@ static void TransactionTimeoutHandler(void);
static void IdleSessionTimeoutHandler(void);
static void IdleStatsUpdateTimeoutHandler(void);
static void ClientCheckTimeoutHandler(void);
+static void CredentialValidationTimeoutHandler(void);
static bool ThereIsAtLeastOneRole(void);
static void process_startup_options(Port *port, bool am_superuser);
static void process_settings(Oid databaseid, Oid roleid);
@@ -773,6 +775,8 @@ InitPostgres(const char *in_dbname, Oid dboid,
RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
RegisterTimeout(IDLE_STATS_UPDATE_TIMEOUT,
IdleStatsUpdateTimeoutHandler);
+ RegisterTimeout(CREDENTIAL_VALIDATION_TIMEOUT,
+ CredentialValidationTimeoutHandler);
}
/*
@@ -1226,6 +1230,9 @@ InitPostgres(const char *in_dbname, Oid dboid,
/* Initialize this backend's session state. */
InitializeSession();
+ /* Initialize credential validation system */
+ InitializeCredentialValidation();
+
/*
* If this is an interactive session, load any libraries that should be
* preloaded at backend start. Since those are determined by GUCs, this
@@ -1440,6 +1447,12 @@ ClientCheckTimeoutHandler(void)
SetLatch(MyLatch);
}
+static void
+CredentialValidationTimeoutHandler(void)
+{
+ CredentialValidationPending = true;
+}
+
/*
* Returns true if at least one role is defined in this database cluster.
*/
diff --git a/src/include/libpq/auth-validate.h b/src/include/libpq/auth-validate.h
new file mode 100644
index 00000000000..9d41125537c
--- /dev/null
+++ b/src/include/libpq/auth-validate.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate.h
+ * Interface for authentication credential validation
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/libpq/auth-validate.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef AUTH_VALIDATE_H
+#define AUTH_VALIDATE_H
+
+#include "libpq/libpq-be.h"
+#include "libpq/protocol.h"
+#include "postmaster/postmaster.h"
+#include "utils/guc.h"
+#include "utils/timeout.h"
+
+/* Define auth method constants needed for credential validation */
+#define AUTH_REQ_SCRAM_SHA_256 20 /* SCRAM SHA-256 authentication */
+#define AUTH_REQ_LAST 21 /* One past the last auth request code */
+
+/* Process credential validation */
+void ProcessCredentialValidation(void);
+
+/* Method-specific initialization */
+void InitializePasswordValidation(void);
+
+/* GUC variables */
+extern bool credential_validation_enabled;
+extern int credential_validation_interval;
+
+/* Common credential validation callback prototype */
+
+/* Common credential validation callback prototype */
+typedef bool (*CredentialValidationCallback) ();
+
+/* Credential validation status */
+typedef enum
+{
+ CVS_VALID, /* Credentials are valid */
+ CVS_EXPIRED, /* Credentials have expired */
+ CVS_ERROR /* Error during validation */
+}CredentialValidationStatus;
+
+/* Initialize credential validation system */
+void InitializeCredentialValidation(void);
+
+/* Register a validation callback for a specific authentication method */
+void RegisterCredentialValidator(int authmethod,
+ CredentialValidationCallback validator);
+
+/* Check credential validity */
+CredentialValidationStatus CheckCredentialValidity(void);
+
+/* Validation callback for password */
+bool validate_password_credentials(void);
+
+#endif /* AUTH_VALIDATE_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..96085dd7c9b 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -99,6 +99,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending;
extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending;
extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost;
+extern PGDLLIMPORT volatile sig_atomic_t CredentialValidationPending;
/* these are marked volatile because they are examined by signal handlers: */
extern PGDLLIMPORT volatile uint32 InterruptHoldoffCount;
diff --git a/src/include/utils/timeout.h b/src/include/utils/timeout.h
index 0965b590b34..d4673a8a408 100644
--- a/src/include/utils/timeout.h
+++ b/src/include/utils/timeout.h
@@ -36,6 +36,7 @@ typedef enum TimeoutId
IDLE_STATS_UPDATE_TIMEOUT,
CLIENT_CONNECTION_CHECK_TIMEOUT,
STARTUP_PROGRESS_TIMEOUT,
+ CREDENTIAL_VALIDATION_TIMEOUT,
/* First user-definable timeout reason */
USER_TIMEOUT,
/* Maximum number of timeout reasons */