Subject: Re: [OAuth2] Infrastructure for tracking token expiry time

From: Zsolt Parragi <[email-masked]>
Date: Tue, 17 Feb 2026 06:59:02 +0000

> For providers using opaque tokens or introspection APIs where an 'exp' claim might be missing, the API remains compatible by allowing the validator to return DT_NOBEGIN.

I don't think this is a good answer: the OAuth validator API went to
the trouble of introducing a generic infrastructure with the explicit
goal to work any OAuth provider, and now this proposes a change that
limits a new API to some of them, while it wouldn't be more difficult
to propose a generic API that works for everything.

Let me rephrase the question: why is this a better approach than
introducing an additional validator callback method, expired_cb?

* it returns if the current OAuth token is expired or not
* if it's NULL, nothing happens, so there's an easy upgrade path for
validator in PG19
* for JWT validators with a clear expiry date, all they have to do is
to store the expiry date in a global variable and then check if we
passed that time in the new callback
* alternatively, this callback could return the current expected
expiry date, and the calling code could check it, but I think that's
overcomplicating

And in both cases, I think handling of the value/callback should be
part of the patch - only providing an API and then doing nothing with
it would set wrong expectations.

---

From: VASUKI M <[email-masked]>
Date: Tue, 17 Feb 2026 16:17:50 +0530

Hi All,

I see the concern about keeping the validator API generic and not
implicitly favoring JWT-style providers.
The callback-based approach does seem more flexible, especially for opaque
tokens or providers supporting revocation, where validity cannot be
represented as a fixed timestamp.
Perhaps one possible direction could be to support both:

An optional expiry timestamp for simple/static cases.

An optional callback (e.g., expired_cb) for dynamic validation.

This would allow JWT-based validators to remain lightweight while enabling
more complex providers to implement custom revalidation logic.
If enforcement is planned at statement start, integrating the callback
mechanism in the same patch might also clarify the intended semantics.

Best regards,
Vasuki M
C-DAC,Chennai

---

From: Ajit Awekar <[email-masked]>
Date: Wed, 18 Feb 2026 14:08:32 +0530

Hi Vasuki, Zsolt

Thanks a lot for your review comments and reply.I have updated the patch
and below is summary of changes

   1. Adding a check_oauth_expiry() function called during command
   execution to verify token validity
   2. Terminating sessions with expired/revoked tokens before executing new
   commands.
   3. Supporting  callback-based revocation checks

I have added a unit test case to validate that sessions are properly
terminated when their OAuth tokens expire.

Request a review.

Thanks & Best Regards,
Ajit

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 18 Feb 2026 11:34:01 +0100

> On 18 Feb 2026, at 09:38, Ajit Awekar <[email-masked]> wrote:

> I have added a unit test case to validate that sessions are properly terminated when their OAuth tokens expire.

+	/*
+	 * If the current session was authenticated via OAuth, verify that the
+	 * token has not expired or been revoked before executing the query.
+	 */
+	if (MyClientConnectionInfo.auth_method == uaOAuth)
+		check_oauth_expiry(MyProcPort);

This seems too expensive and invasive in these codepaths and also not in line
with how other authentication methods are handled, we for example don't check
if a client certificate has been revoked in a similar manner.  I don't think it
would be bad if we could detect expired credentials mid-flight but I think it
would need to be done smarter than sprinkling auth specific checks like this.

+		ereport(FATAL,
+				(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+				 errmsg("session expired: OAuth token is no longer valid")));

Token expiration is IMHO not a usecase for a FATAL error, if we want to
terminate a connection we can do it in a more graceful way.

+	/*
+	 * Optional callback to check if the session is still valid.
+	 * Returns true if the token is expired/revoked, false otherwise.
+	 * If NULL, the backend assumes the session never expires.
+	 * If provided, the validator can use this to limit session duration based on
+	 * parameter value or based on it's custom logic.
+	 */
+	bool (*expired_cb) (TimestampTz expiry);
+
+	/*
+	 * The expiration time of the token (e.g., from the 'exp' claim) if
+	 * provided. This value is passed as an argument to the expired_cb function
+	 * above to determine if the session should terminate.
+	 */
+	TimestampTz expiry;
 } ValidatorModuleResult;

What's the point of having an expiry timestamp as well as an expiration check
callback?  Wouldn't that open up for the caller to make its own judgment on
expiration which might conflict with expired_cb?  Is token expiration really
always tied to a timestamp for all types of tokens?

Also, why is this defined in ValidatorModuleResult?  If I interpret Zsolt's
comment upthread correctly it was meant to be placed in OAuthValidatorCallbacks
as a new callback - which I agree with would be better.  The question of where
and when to invoke it remains, but whatever we build I think it should be auth
agnostic so that any auth can be hooked into it.

--
Daniel Gustafsson

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 18 Feb 2026 12:04:13 +0000

> agnostic so that any auth can be hooked into it.

+1 Yes, that's what I meant. Also agree with the unnecessary expiry
timestamp with the callback - if we want to store some additional data
for the checks, that should be a void*, but with the single threaded
model there's no real need for it, the validator can store anything it
needs in global variables.

> Token expiration is IMHO not a use case for a FATAL error, if we want to

There are different reasons for token expiration, one is a simple
timeout where all we have to do is communicate to the client that we
need a refresh (gracefully), and the other is where a token is
immediately revoked because of a security incident, in which case
immediate termination is a good practice.

There was an earlier discussion about it in the password expiration
thread[1], and the possible use of the GoAway[2] for it. Jacob
suggested making it user configurable, which seems like a reasonable
way to do it.

I also wanted to work on this patch, but I didn't start working on it
so far, because I wanted to see where those threads go, as the changes
introduced in them could be useful for the oauth token
refresh/disconnection mechanism.

[1] : https://www.postgresql.org/message-id/flat/CAER375OvH3_ONmc-SgUFpA6gv_d6eNj2KdZktzo-f_uqNwwWNw%40mail.gmail.com
[2] : https://www.postgresql.org/message-id/flat/DDPQ1RV5FE9U.I2WW34NGRD8Z%40jeltef.nl

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 18 Feb 2026 17:30:24 +0100

> On 18 Feb 2026, at 13:04, Zsolt Parragi <[email-masked]> wrote:

> immediate termination is a good practice.

I understand these cases and agree that there are different needs for messaging
to the user for these cases, but I still think that neither should overload
what FATAL error means.  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.

--
Daniel Gustafsson

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 18 Feb 2026 16:58:58 +0000

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

---

From: Ajit Awekar <[email-masked]>
Date: Fri, 20 Feb 2026 15:12:10 +0530

Thanks a lot Daniel, Zslot, Vasuki for your review comments.

>The mechanism used is however a secondary discussion,

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

> 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 */

---

From: Ajit Awekar <[email-masked]>
Date: Mon, 16 Mar 2026 19:27:17 +0530

Hi,

Please find the attached first version of the patch providing credential
validation framework.

The credential validation framework provides a mechanism to continuously
validate authentication credentials during an active session. This enables
the server to periodically check credential validity and take appropriate
action when credentials expire or become invalid.

  Currently, Postgres validates credentials only at connection time. Once
authenticated, a session remains active even if:

  - A user's rolvaliduntil expiration time passes
  - An OAuth bearer token expires

* Proposed Solution*
  The patch introduces a credential validation framework that:

  1. Periodically checks credential validity during active sessions
  2. Terminates sessions when credentials expire or become invalid

*Implementation*

  The framework consists of:

  - Core infrastructure (auth-validate.c/h): Manages validation callbacks,
dispatches validation checks based on authentication method
  - Method implementations (auth-validate-methods.c/h): Contains validators
for password-based auth (checks rolvaliduntil in pg_authid) and OAuth
  (delegates to validator's expire_cb)

  Validation is triggered during query execution in both simple and
extended query protocol paths, using a time-based approach to limit
overhead.

*  Configuration*
  Two new GUC parameters:

  credential_validation.enabled = false   # enable/disable validation
  credential_validation.interval = 1      # check interval in minutes (1-60)

* Extensibility*
  New authentication methods can be supported by:
  1. Adding an enum value to CredentialValidationType
  2. Implementing a validation callback
  3. Registering it via RegisterCredentialValidator()

 Should there be per-authentication-method enable/disable settings?

Thanks & Best Regards,
Ajit

On Fri, 20 Feb 2026 at 15:12, Ajit Awekar <[email-masked]> wrote:

> The current implementation primarily supports password-based
> authentication methods, verifying that passwords haven't expired. It can be
> extended to any authentication method.

---

From: Ajit Awekar <[email-masked]>
Date: Fri, 27 Mar 2026 16:29:57 +0530

Hi All,

I'd like to propose a patch that adds a credential validation framework to
PostgreSQL, enabling periodic re-validation of authentication
 credentials during active sessions.

Currently, PostgreSQL validates credentials only at connection time. Once
authenticated, a session remains valid even if the underlying
credentials expire (e.g., a user's rolvaliduntil passes, or an OAuth token
expires). This can be problematic in environments with strict
security requirements where sessions should be terminated when credentials
become invalid.

This patch introduces a timer-based credential validation mechanism that
periodically checks whether a session's authentication credentials are
 still valid. When credentials are found to be expired, the session is
terminated with an appropriate error message

*Key Components  *

  1. Core Framework (auth-validate.c, auth-validate.h)
    - Manages validation callbacks for different authentication methods
    - Provides a timeout-based scheduler using
CREDENTIAL_VALIDATION_TIMEOUT

    - Handles validation status (CVS_VALID, CVS_EXPIRED, CVS_ERROR)
  2. Validation Methods (auth-validate-methods.c)

    - Password validation: checks rolvaliduntil in pg_authid

    - OAuth validation: delegates to the validator module's new expire_cb
callback
  3. GUC Parameters

    - credential_validation.enabled (boolean, default: false)
    - credential_validation.interval (integer, 1-60 minutes, default: 1)

*OAuth Validator Changes  *

  The patch extends the OAuth validator API with an optional expire_cb
callback. To maintain backward compatibility, the magic version has been
  bumped to PG_OAUTH_VALIDATOR_MAGIC_V2. Existing V1 validators continue to
work; the server simply skips expiration checking for them.

*Example Configuration*
  credential_validation.enabled = on
  credential_validation.interval = 5   # minutes

Thanks & Best Regards,
Ajit

On Mon, 16 Mar 2026 at 19:27, Ajit Awekar <[email-masked]> wrote:

>   - Method implementations (auth-validate-methods.c/h): Contains
> validators for password-based auth (checks rolvaliduntil in pg_authid) and
> OAuth

---

From: Ajit Awekar <[email-masked]>
Date: Tue, 31 Mar 2026 19:19:45 +0530

Hi All,

 I've added additional test cases to src/test/authentication/t/
008_continuous_validation.pl to improve coverage of the continuous
credential
  validation feature.

Updated the README with:
  - Fixed interval parameter
  - Added a Testing section documenting all 6 test cases

  - Added instructions for running the tests

Thanks & Best Regards,
Ajit

On Fri, 27 Mar 2026 at 16:29, Ajit Awekar <[email-masked]> wrote:

>   bumped to PG_OAUTH_VALIDATOR_MAGIC_V2. Existing V1 validators continue
> to work; the server simply skips expiration checking for them.
>
>
> *Example Configuration*
>>   - Core infrastructure (auth-validate.c/h): Manages validation
>> callbacks, dispatches validation checks based on authentication method