From c85188d01ca253d9d6262dbbbacf4c6480389021 Mon Sep 17 00:00:00 2001
From: Ajit Awekar <ajit.awekar@enterprisedb.com>
Date: Tue, 16 Jun 2026 14:12:16 +0530
Subject: [PATCH 1/3] Add continuous credential validation framework

Introduce a mechanism that periodically re-validates the credentials of an
active session and terminates the session if they are no longer valid.  A
per-backend timer (CREDENTIAL_VALIDATION_TIMEOUT) fires at a configurable
interval; the pending flag is acted on at the next command boundary in the
main loop, where the check runs inside a short-lived transaction (reusing the
session's open transaction if one exists, so a long-running transaction block
is still validated at each command boundary).

This commit adds the framework and the baseline, auth-method-independent
check: the session role must still exist and must not have passed its
rolvaliduntil expiration.  Method-specific validators plug in via
RegisterCredentialValidator() and are added in following commits.

Two GUCs control the feature: credential_validation_enabled (default off) and
credential_validation_interval (5..3600 seconds, default 60).
---
 doc/src/sgml/config.sgml                      |  45 +++
 src/backend/libpq/Makefile                    |   2 +
 src/backend/libpq/auth-validate-methods.c     |  75 +++++
 src/backend/libpq/auth-validate.c             | 227 +++++++++++++++
 src/backend/libpq/meson.build                 |   2 +
 src/backend/tcop/postgres.c                   |  27 ++
 src/backend/utils/init/globals.c              |   1 +
 src/backend/utils/init/postinit.c             |  18 ++
 src/backend/utils/misc/guc_parameters.dat     |  16 ++
 src/backend/utils/misc/guc_tables.c           |   1 +
 src/backend/utils/misc/postgresql.conf.sample |   6 +
 src/include/libpq/auth-validate-methods.h     |  28 ++
 src/include/libpq/auth-validate.h             |  54 ++++
 src/include/miscadmin.h                       |   1 +
 src/include/utils/timeout.h                   |   1 +
 src/test/authentication/meson.build           |   1 +
 .../t/008_continuous_validation.pl            | 263 ++++++++++++++++++
 17 files changed, 768 insertions(+)
 create mode 100644 src/backend/libpq/auth-validate-methods.c
 create mode 100644 src/backend/libpq/auth-validate.c
 create mode 100644 src/include/libpq/auth-validate-methods.h
 create mode 100644 src/include/libpq/auth-validate.h
 create mode 100755 src/test/authentication/t/008_continuous_validation.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fa566c9e553..e1a1eec2538 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1124,6 +1124,51 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-credential-validation-enabled" xreflabel="credential_validation_enabled">
+      <term><varname>credential_validation_enabled</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>credential_validation_enabled</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+
+      <listitem>
+       <para>
+        When enabled, each backend periodically re-validates the credentials of
+        its active session and terminates the session if they are no longer
+        valid.  The baseline check verifies that the authenticated role still
+        exists and has not passed its <literal>VALID UNTIL</literal> expiration;
+        depending on the authentication method, an additional method-specific
+        check is applied, such as expiration of an <productname>OAuth</productname>
+        bearer token or of the client certificate.  The default is
+        <literal>off</literal>.
+       </para>
+       <para>
+        The re-validation period is controlled by
+        <xref linkend="guc-credential-validation-interval"/>.  Validation is
+        performed at command boundaries, so a session is never interrupted in
+        the middle of a running statement.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-credential-validation-interval" xreflabel="credential_validation_interval">
+      <term><varname>credential_validation_interval</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>credential_validation_interval</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+
+      <listitem>
+       <para>
+        Sets the interval between the periodic credential re-validations that are
+        performed when <xref linkend="guc-credential-validation-enabled"/> is
+        enabled.  If this value is specified without units, it is taken as
+        seconds.  The valid range is from 5 seconds to 3600 seconds (one hour),
+        and the default is 60 seconds.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile
index 98eb2a8242d..32e4c7280e5 100644
--- a/src/backend/libpq/Makefile
+++ b/src/backend/libpq/Makefile
@@ -18,6 +18,8 @@ OBJS = \
 	auth-oauth.o \
 	auth-sasl.o \
 	auth-scram.o \
+	auth-validate-methods.o \
+	auth-validate.o \
 	auth.o \
 	be-fsstubs.o \
 	be-secure-common.o \
diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c
new file mode 100644
index 00000000000..f371a36906a
--- /dev/null
+++ b/src/backend/libpq/auth-validate-methods.c
@@ -0,0 +1,75 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate-methods.c
+ *	  Implementation of authentication credential validation methods
+ *
+ * This module implements the credential validators.  The baseline role-level
+ * check (rolvaliduntil / role existence) implemented here is applied to every
+ * authenticated session, regardless of authentication method.  Method-specific
+ * validators are registered with the framework via
+ * RegisterCredentialValidator().
+ *
+ * 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-methods.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/pg_authid.h"
+#include "libpq/auth-validate-methods.h"
+#include "miscadmin.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+/*
+ * Initialize validation methods
+ */
+void
+InitializeValidationMethods(void)
+{
+	/* No method-specific validators are registered yet. */
+}
+
+/*
+ * Baseline role-level credential check, applied to every authenticated
+ * session regardless of authentication method.
+ *
+ * Checks pg_authid.rolvaliduntil for the session role; this is role-level and
+ * auth-method-independent, so it governs password, certificate, OAuth, etc.
+ * sessions alike.  Also treats a role that no longer exists as invalid.
+ *
+ * Returns true if the role is still valid, false if it has expired or has
+ * been dropped.
+ */
+bool
+ValidateRoleValidity(void)
+{
+	HeapTuple	tuple;
+	Datum		datum;
+	bool		isnull;
+	TimestampTz valid_until;
+	bool		result;
+
+	tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(GetSessionUserId()));
+
+	if (!HeapTupleIsValid(tuple))
+		return false;			/* role no longer exists */
+
+	datum = SysCacheGetAttr(AUTHOID, tuple,
+							Anum_pg_authid_rolvaliduntil,
+							&isnull);
+	if (!isnull)
+	{
+		valid_until = DatumGetTimestampTz(datum);
+		result = (valid_until >= GetCurrentTimestamp());
+	}
+	else
+		result = true;			/* no expiration set */
+
+	ReleaseSysCache(tuple);
+	return result;
+}
diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c
new file mode 100644
index 00000000000..fd31be1ca99
--- /dev/null
+++ b/src/backend/libpq/auth-validate.c
@@ -0,0 +1,227 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate.c
+ *	  Implementation of authentication credential validation
+ *
+ * This module provides a mechanism for validating credentials during
+ * an active PostgreSQL session.
+ *
+ * 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 "access/xact.h"
+#include "access/xlog.h"
+#include "libpq/auth-validate-methods.h"
+#include "libpq/auth-validate.h"
+#include "libpq/auth.h"
+#include "libpq/libpq-be.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "utils/timeout.h"
+
+/* GUC variables */
+bool		credential_validation_enabled;
+int			credential_validation_interval;
+
+
+/* Registered credential validators */
+static CredentialValidationCallback validators[CVT_COUNT];
+
+
+/*
+ * Convert UserAuth enum to CredentialValidationType for validator selection
+ */
+static CredentialValidationType
+UserAuthToValidationType(UserAuth auth_method)
+{
+	switch (auth_method)
+	{
+		case uaOAuth:
+			return CVT_OAUTH;
+		case uaCert:
+			return CVT_CERT;
+		default:
+			/*
+			 * No method-specific validator for other auth methods.  Password
+			 * methods (password/md5/scram) fall here intentionally: their only
+			 * credential check is role-level (rolvaliduntil), which is handled
+			 * for every session by the baseline ValidateRoleValidity().
+			 */
+			return CVT_COUNT;	/* Invalid value */
+	}
+}
+
+/*
+ * ProcessCredentialValidation
+ *
+ * Called from the main command loop when a credential validation cycle is
+ * due.  Runs a full validity check and terminates the session with FATAL if
+ * the credentials have expired.
+ */
+void
+ProcessCredentialValidation(void)
+{
+	bool		valid;
+	bool		own_xact = false;
+
+	if (ClientAuthInProgress || IsInitProcessingMode() || IsBootstrapProcessingMode())
+		return;
+
+	if (!credential_validation_enabled || MyClientConnectionInfo.authn_id == NULL)
+		return;
+
+	/*
+	 * The validators read the system catalogs, which requires a live,
+	 * non-aborted transaction.  In an aborted transaction block catalog
+	 * access is not possible, so skip this cycle and retry at the next
+	 * interval.
+	 */
+	if (IsAbortedTransactionBlockState())
+		return;
+
+	/*
+	 * Between commands there is no transaction, so start a short-lived one of
+	 * our own.  Inside an open transaction block (or a multi-message
+	 * extended-query sequence) reuse the existing transaction, so that
+	 * validation still happens at each command boundary within the block; but
+	 * do not commit it, since it belongs to the user.
+	 */
+	if (!IsTransactionState())
+	{
+		StartTransactionCommand();
+		own_xact = true;
+	}
+
+	valid = CheckCredentialValidity();
+
+	if (own_xact)
+		CommitTransactionCommand();
+
+	if (!valid)
+		ereport(FATAL,
+				(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+				 errmsg("session credentials have expired"),
+				 errhint("Please reconnect to establish a new authenticated session.")));
+}
+
+/*
+ * InitializeCredentialValidation
+ *
+ * Called from InitPostgres after authentication completes.  Registers all
+ * method-specific validation callbacks.
+ */
+void
+InitializeCredentialValidation(void)
+{
+	int			i;
+
+	/* Initialize validator callbacks to NULL */
+	for (i = 0; i < CVT_COUNT; i++)
+		validators[i] = NULL;
+
+	/* Register all method-specific validation callbacks */
+	InitializeValidationMethods();
+}
+
+/*
+ * Enable or re-enable the credential validation timeout timer.
+ * Called at session startup and after each validation or error recovery.
+ */
+void
+EnableCredentialValidationTimeout(void)
+{
+	int			interval_ms;
+
+	/* Only enable if credential validation is configured */
+	if (!credential_validation_enabled)
+		return;
+
+	/* Skip for non-client backends */
+	if (!IsExternalConnectionBackend(MyBackendType))
+		return;
+
+	/* Convert interval from seconds to milliseconds */
+	interval_ms = credential_validation_interval * 1000;
+
+	enable_timeout_after(CREDENTIAL_VALIDATION_TIMEOUT, interval_ms);
+
+	elog(DEBUG1, "credential validation timeout enabled, interval=%d s", credential_validation_interval);
+}
+
+/*
+ * Register a validator callback for a specific authentication method
+ */
+void
+RegisterCredentialValidator(CredentialValidationType method_type, CredentialValidationCallback validator)
+{
+	if (method_type < 0 || method_type >= CVT_COUNT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid validation method type: %d", method_type)));
+
+	validators[method_type] = validator;
+}
+
+/*
+ * Check credential validity for the current session.
+ *
+ * Returns true if the credentials are still valid, false if they have expired.
+ * Must be called within a transaction (the validators read the catalogs).
+ */
+bool
+CheckCredentialValidity(void)
+{
+	CredentialValidationCallback validator = NULL;
+	CredentialValidationType validation_type;
+	bool		result;
+
+	/*
+	 * Skip validation (treat as valid) for any process that does not have a
+	 * client session with credentials to validate:
+	 * - during shutdown or recovery
+	 * - non-client backends (autovacuum, background workers, etc.)
+	 * - while authentication is still in progress
+	 */
+	if (proc_exit_inprogress ||
+		RecoveryInProgress() ||
+		!IsExternalConnectionBackend(MyBackendType) ||
+		AmAutoVacuumLauncherProcess() ||
+		AmAutoVacuumWorkerProcess() ||
+		AmBackgroundWorkerProcess() ||
+		ClientAuthInProgress)
+		return true;
+
+	/* Without an authenticated session there is nothing to validate. */
+	if (MyClientConnectionInfo.authn_id == NULL)
+		return true;
+
+	elog(DEBUG1, "credential validation: checking auth_method=%d",
+		 (int) MyClientConnectionInfo.auth_method);
+
+	/*
+	 * Role-level validity (rolvaliduntil / role existence) is a baseline that
+	 * applies to every authenticated session, regardless of auth method.
+	 */
+	result = ValidateRoleValidity();
+
+	/*
+	 * Additionally run the method-specific validator if one is registered for
+	 * this auth method (e.g. OAuth token expiry, client certificate expiry).
+	 */
+	validation_type = UserAuthToValidationType(MyClientConnectionInfo.auth_method);
+	if (validation_type < CVT_COUNT)
+		validator = validators[validation_type];
+
+	if (result && validator != NULL)
+		result = validator();
+
+	return result;
+}
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index 8571f652844..2e69685672b 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -4,6 +4,8 @@ backend_sources += files(
   'auth-oauth.c',
   'auth-sasl.c',
   'auth-scram.c',
+  'auth-validate-methods.c',
+  'auth-validate.c',
   'auth.c',
   'be-fsstubs.c',
   'be-secure-common.c',
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index dbef734a93f..ced544db2f7 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -45,6 +45,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"
@@ -1443,6 +1444,7 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	 */
 	start_xact_command();
 
+
 	/*
 	 * Switch to appropriate context for constructing parsetrees.
 	 *
@@ -4681,6 +4683,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_IN_TRANSACTION_SESSION_TIMEOUT,
 										 IdleInTransactionSessionTimeout);
 				}
+
+				/* Re-enable credential validation timer if needed */
+				if (credential_validation_enabled &&
+					!get_timeout_active(CREDENTIAL_VALIDATION_TIMEOUT))
+					EnableCredentialValidationTimeout();
 			}
 			else
 			{
@@ -4733,6 +4740,11 @@ PostgresMain(const char *dbname, const char *username)
 					enable_timeout_after(IDLE_SESSION_TIMEOUT,
 										 IdleSessionTimeout);
 				}
+
+				/* Re-enable credential validation timer if needed */
+				if (credential_validation_enabled &&
+					!get_timeout_active(CREDENTIAL_VALIDATION_TIMEOUT))
+					EnableCredentialValidationTimeout();
 			}
 
 			/* Report any recently-changed GUC options */
@@ -4835,6 +4847,21 @@ PostgresMain(const char *dbname, const char *username)
 		if (ignore_till_sync && firstchar != EOF)
 			continue;
 
+		/*
+		 * If a credential validation cycle came due while we were processing
+		 * the previous command or waiting for input, run it now -- a single
+		 * check-point that covers every command type below.  This is a safe
+		 * spot: we are between commands and hold no locks.  Skip it while
+		 * still starting up (e.g. during replication command setup), matching
+		 * the IsNormalProcessingMode() guard used elsewhere.
+		 */
+		if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		{
+			CredentialValidationTimeoutPending = false;
+			ProcessCredentialValidation();	/* may FATAL if credentials expired */
+			EnableCredentialValidationTimeout();
+		}
+
 		switch (firstchar)
 		{
 			case PqMsg_Query:
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index bbd28d14d99..5b9df8fd3f1 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 CredentialValidationTimeoutPending = 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 3d8c9bdebd5..c4f59b2e9a0 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"
@@ -96,6 +97,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);
@@ -805,6 +807,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);
 	}
 
 	/*
@@ -1268,6 +1272,12 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	/* Initialize this backend's session state. */
 	InitializeSession();
 
+	/* Initialize credential validation system */
+	InitializeCredentialValidation();
+
+	/* Enable credential validation timeout if configured */
+	EnableCredentialValidationTimeout();
+
 	/*
 	 * If this is an interactive session, load any libraries that should be
 	 * preloaded at backend start.  Since those are determined by GUCs, this
@@ -1474,6 +1484,14 @@ IdleStatsUpdateTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+static void
+CredentialValidationTimeoutHandler(void)
+{
+	CredentialValidationTimeoutPending = true;
+	InterruptPending = true;
+	SetLatch(MyLatch);
+}
+
 static void
 ClientCheckTimeoutHandler(void)
 {
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index afaa058b046..2d6fe7be45a 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -570,6 +570,22 @@
   assign_hook => 'assign_createrole_self_grant',
 },
 
+{ name => 'credential_validation_enabled', type => 'bool', context => 'PGC_SUSET', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Enables periodic re-validation of session credentials.',
+  long_desc => 'When enabled, each backend periodically re-checks that the authenticated role has not expired and that any method-specific credential (OAuth token, client certificate) is still valid.',
+  variable => 'credential_validation_enabled',
+  boot_val => 'false',
+},
+
+{ name => 'credential_validation_interval', type => 'int', context => 'PGC_SUSET', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the interval in seconds between credential re-validation checks.',
+  flags => 'GUC_UNIT_S',
+  variable => 'credential_validation_interval',
+  boot_val => '60',
+  min => '5',
+  max => '3600',
+},
+
 { name => 'cursor_tuple_fraction', type => 'real', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Sets the planner\'s estimate of the fraction of a cursor\'s rows that will be retrieved.',
   flags => 'GUC_EXPLAIN',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 290ccbc543e..fa7509c558a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -52,6 +52,7 @@
 #include "common/scram-common.h"
 #include "jit/jit.h"
 #include "libpq/auth.h"
+#include "libpq/auth-validate.h"
 #include "libpq/libpq.h"
 #include "libpq/oauth.h"
 #include "libpq/scram.h"
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index ac38cddaaf9..04697ada6aa 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -921,6 +921,12 @@
 #include_if_exists = '...'              # include file only if it exists
 #include = '...'                        # include file
 
+#------------------------------------------------------------------------------
+# CREDENTIAL VALIDATION
+#------------------------------------------------------------------------------
+
+#credential_validation_enabled = off	# re-validate session credentials periodically
+#credential_validation_interval = 60	# revalidation interval in seconds (5-3600)
 
 #------------------------------------------------------------------------------
 # CUSTOMIZED OPTIONS
diff --git a/src/include/libpq/auth-validate-methods.h b/src/include/libpq/auth-validate-methods.h
new file mode 100644
index 00000000000..9a03583d6e1
--- /dev/null
+++ b/src/include/libpq/auth-validate-methods.h
@@ -0,0 +1,28 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate-methods.h
+ *	  Interface for authentication credential validation methods
+ *
+ * This file provides declarations for various credential validation methods
+ * used with the credential validation system.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/libpq/auth-validate-methods.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef AUTH_VALIDATE_METHODS_H
+#define AUTH_VALIDATE_METHODS_H
+
+/* Initialize all validation methods */
+extern void InitializeValidationMethods(void);
+
+/*
+ * Baseline role-level validity check (rolvaliduntil / role existence),
+ * applied to every authenticated session regardless of auth method.
+ */
+extern bool ValidateRoleValidity(void);
+
+#endif							/* AUTH_VALIDATE_METHODS_H */
diff --git a/src/include/libpq/auth-validate.h b/src/include/libpq/auth-validate.h
new file mode 100644
index 00000000000..b0b5a1144a7
--- /dev/null
+++ b/src/include/libpq/auth-validate.h
@@ -0,0 +1,54 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate.h
+ *	  Interface for authentication credential validation
+ *
+ * This file provides a common interface for validating credentials
+ * during an active PostgreSQL session.
+ *
+ * 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
+
+/* Define credential validation method types as an enum */
+typedef enum CredentialValidationType
+{
+	CVT_OAUTH = 0,				/* OAuth bearer token authentication */
+	CVT_CERT,					/* TLS client certificate authentication */
+	CVT_COUNT					/* Total number of credential validation types */
+} CredentialValidationType;
+
+/* Process credential validation */
+extern void ProcessCredentialValidation(void);
+
+/* GUC variables */
+extern PGDLLIMPORT bool credential_validation_enabled;
+extern PGDLLIMPORT int credential_validation_interval;
+
+/* Common credential validation callback prototype */
+typedef bool (*CredentialValidationCallback) (void);
+
+/* Initialize credential validation system */
+extern void InitializeCredentialValidation(void);
+
+/* Register a validation callback for a specific authentication method */
+extern void RegisterCredentialValidator(CredentialValidationType method_type,
+										CredentialValidationCallback validator);
+
+/*
+ * Check credential validity for the current session.  Returns true if the
+ * credentials are still valid, false if they have expired.  Must be called
+ * within a transaction, since the validators read the system catalogs.
+ */
+extern bool CheckCredentialValidity(void);
+
+/* Enable credential validation timeout timer */
+extern void EnableCredentialValidationTimeout(void);
+
+#endif							/* AUTH_VALIDATE_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 7170a4bff98..19d7d89df42 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -101,6 +101,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 CredentialValidationTimeoutPending;
 
 /* 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 */
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index 282a5054e2c..bfb8350a3f8 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -16,6 +16,7 @@ tests += {
       't/005_sspi.pl',
       't/006_login_trigger.pl',
       't/007_pre_auth.pl',
+      't/008_continuous_validation.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/008_continuous_validation.pl b/src/test/authentication/t/008_continuous_validation.pl
new file mode 100755
index 00000000000..8bb7c4848bc
--- /dev/null
+++ b/src/test/authentication/t/008_continuous_validation.pl
@@ -0,0 +1,263 @@
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$use_unix_sockets)
+{
+    plan skip_all => "authentication tests cannot run without Unix-domain sockets";
+}
+
+# Helper to reset pg_hba.conf with specific auth method for test users
+sub reset_pg_hba
+{
+    my ($node, $hba_method, @users) = @_;
+
+    unlink($node->data_dir . '/pg_hba.conf');
+    # Each specified user uses the given method
+    foreach my $user (@users)
+    {
+        $node->append_conf('pg_hba.conf', "local all $user $hba_method\n");
+    }
+    # Others use trust
+    $node->append_conf('pg_hba.conf', "local all all trust\n");
+    $node->reload;
+}
+
+# 1. Initialize and start the PostgreSQL cluster
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Enable credential validation with short interval (5 seconds minimum)
+$node->append_conf('postgresql.conf', "credential_validation_enabled = on\n");
+$node->append_conf('postgresql.conf', "credential_validation_interval = 5\n");
+
+$node->start;
+
+# Configure password auth for user1 and user2 (must be BEFORE "all all trust")
+reset_pg_hba($node, 'md5', 'user1', 'user2');
+
+# Create test users with passwords
+$node->safe_psql('postgres', "CREATE USER user1 LOGIN PASSWORD 'secret';");
+$node->safe_psql('postgres', "CREATE USER user2 LOGIN PASSWORD 'secret2';");
+
+#############################################################################
+# Test 1: VALID UNTIL expiration
+#############################################################################
+note "=== Test 1: VALID UNTIL expiration ===";
+
+$ENV{PGPASSWORD} = 'secret';
+my $session1 = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user1']
+);
+
+# Verify user1 can execute a query normally
+my ($stdout, $ret) = $session1->query('SELECT 1 AS success;');
+like($stdout, qr/1/, 'user1 can execute queries initially');
+is($ret, 0, 'no errors during initial query for user1');
+
+# Admin alters the VALID UNTIL date to the past
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2025-11-02 16:59:37+05:30';");
+
+# Wait for the credential validation timeout to fire
+note "Waiting 7 seconds for credential validation timeout to fire...";
+sleep(7);
+
+# User1 attempts to execute another query - should be terminated
+eval {
+    ($stdout, $ret) = $session1->query('SELECT 2 AS failure_expected;');
+};
+
+# Check the server log for the expected FATAL error
+my $log_contents = slurp_file($node->logfile);
+like(
+    $log_contents,
+    qr/FATAL:.*session credentials have expired/,
+    'Test 1: server log shows session terminated due to expired credentials'
+);
+
+eval { $session1->quit; };
+
+#############################################################################
+# Test 2: User dropped while session is active
+#############################################################################
+note "=== Test 2: User dropped while session is active ===";
+
+$ENV{PGPASSWORD} = 'secret2';
+my $session2 = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user2']
+);
+
+# Verify user2 can execute a query normally
+($stdout, $ret) = $session2->query('SELECT 1 AS success;');
+like($stdout, qr/1/, 'user2 can execute queries initially');
+is($ret, 0, 'no errors during initial query for user2');
+
+# Admin drops user2 while the session is still active
+$node->safe_psql('postgres', "DROP USER user2;");
+
+# Wait for the credential validation timeout to fire
+note "Waiting 7 seconds for credential validation timeout to fire...";
+sleep(7);
+
+# User2 attempts to execute another query - should be terminated
+eval {
+    ($stdout, $ret) = $session2->query('SELECT 2 AS failure_expected;');
+};
+
+# Check the server log for the expected FATAL error (user no longer exists)
+$log_contents = slurp_file($node->logfile);
+like(
+    $log_contents,
+    qr/FATAL:.*session credentials have expired/,
+    'Test 2: server log shows session terminated after user was dropped'
+);
+
+eval { $session2->quit; };
+
+#############################################################################
+# Test 3: VALID UNTIL extended keeps session alive (positive test)
+#############################################################################
+note "=== Test 3: VALID UNTIL extended keeps session alive ===";
+
+# Reset user1 for this test (user1 still exists from Test 1, just expired)
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';");
+reset_pg_hba($node, 'md5', 'user1');
+
+$ENV{PGPASSWORD} = 'secret';
+my $session3 = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user1']
+);
+
+# Set VALID UNTIL to far future
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2099-12-31 23:59:59';");
+
+# Wait for validation cycle
+note "Waiting 7 seconds for credential validation timeout to fire...";
+sleep(7);
+
+# Session should still be alive
+($stdout, $ret) = $session3->query('SELECT 1 AS still_alive;');
+like($stdout, qr/1/, 'Test 3: session remains alive with valid VALID UNTIL');
+is($ret, 0, 'Test 3: no errors when VALID UNTIL is in the future');
+
+eval { $session3->quit; };
+
+#############################################################################
+# Test 4: Multiple sessions terminated when user expires
+#############################################################################
+note "=== Test 4: Multiple sessions terminated when user expires ===";
+
+# Reset user1
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';");
+
+$ENV{PGPASSWORD} = 'secret';
+my $session4a = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user1']
+);
+my $session4b = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user1']
+);
+
+# Verify both sessions work
+($stdout, $ret) = $session4a->query('SELECT 1;');
+like($stdout, qr/1/, 'session4a works initially');
+($stdout, $ret) = $session4b->query('SELECT 1;');
+like($stdout, qr/1/, 'session4b works initially');
+
+# Expire user1
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2020-01-01';");
+
+note "Waiting 7 seconds for credential validation timeout to fire...";
+sleep(7);
+
+# Both sessions should fail
+eval { $session4a->query('SELECT 2;'); };
+eval { $session4b->query('SELECT 2;'); };
+
+$log_contents = slurp_file($node->logfile);
+# Count occurrences of the termination message
+my @matches = ($log_contents =~ /FATAL:.*session credentials have expired/g);
+cmp_ok(scalar(@matches), '>=', 3, 'Test 4: multiple sessions terminated for same user');
+
+eval { $session4a->quit; };
+eval { $session4b->quit; };
+
+#############################################################################
+# Test 5: Trust auth sessions are not affected
+#############################################################################
+note "=== Test 5: Trust auth sessions are not affected ===";
+
+# Create user3 with trust auth (no password validation registered)
+$node->safe_psql('postgres', "CREATE USER user3 LOGIN;");
+reset_pg_hba($node, 'trust', 'user3');
+
+delete $ENV{PGPASSWORD};
+my $session5 = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user3']
+);
+
+# Set expired VALID UNTIL (but trust auth has no validator)
+$node->safe_psql('postgres', "ALTER USER user3 VALID UNTIL '2020-01-01';");
+
+note "Waiting 7 seconds for credential validation timeout to fire...";
+sleep(7);
+
+# Session should still work - trust has no registered validator
+($stdout, $ret) = $session5->query('SELECT 1 AS trust_still_works;');
+like($stdout, qr/1/, 'Test 5: trust auth session not terminated (no validator)');
+
+eval { $session5->quit; };
+
+#############################################################################
+# Test 6: Credential validation disabled
+#############################################################################
+note "=== Test 6: Credential validation disabled ===";
+
+# Disable credential validation
+$node->safe_psql('postgres', "ALTER SYSTEM SET credential_validation_enabled = off;");
+$node->reload;
+
+# Reset user1
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL 'infinity';");
+reset_pg_hba($node, 'md5', 'user1');
+
+$ENV{PGPASSWORD} = 'secret';
+my $session6 = $node->background_psql(
+    'postgres',
+    on_error_stop => 0,
+    extra_params  => ['-U', 'user1']
+);
+
+# Expire user1
+$node->safe_psql('postgres', "ALTER USER user1 VALID UNTIL '2020-01-01';");
+
+note "Waiting 7 seconds...";
+sleep(7);
+
+# Session should still work since validation is disabled
+($stdout, $ret) = $session6->query('SELECT 1 AS validation_disabled;');
+like($stdout, qr/1/, 'Test 6: session survives when validation is disabled');
+
+eval { $session6->quit; };
+
+# Re-enable for any subsequent tests
+$node->safe_psql('postgres', "ALTER SYSTEM SET credential_validation_enabled = on;");
+$node->reload;
+
+# Clean up
+$node->stop;
+done_testing();
-- 
2.52.0

