public inbox for [email protected]  
help / color / mirror / Atom feed
Re: [OAuth2] Infrastructure for tracking token expiry time
11+ messages / 4 participants
[nested] [flat]

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-17 06:59  Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Zsolt Parragi @ 2026-02-17 06:59 UTC (permalink / raw)
  To: Ajit Awekar <[email protected]>; +Cc: VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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






^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-17 10:47  VASUKI M <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: VASUKI M @ 2026-02-17 10:47 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Ajit Awekar <[email protected]>; PostgreSQL Hackers <[email protected]>

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


^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-18 08:38  Ajit Awekar <[email protected]>
  parent: VASUKI M <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Ajit Awekar @ 2026-02-18 08:38 UTC (permalink / raw)
  To: VASUKI M <[email protected]>; +Cc: Zsolt Parragi <[email protected]>; PostgreSQL Hackers <[email protected]>

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


On Tue, 17 Feb 2026 at 16:17, VASUKI M <[email protected]> wrote:

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


Attachments:

  [application/octet-stream] password_expiry_oauth_V2.patch (13.8K, 3-password_expiry_oauth_V2.patch)
  download | inline diff:
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
index 11365048951..6e684e9bd0d 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -684,6 +684,13 @@ validate(Port *port, const char *auth)
 		goto cleanup;
 	}
 
+	/*
+	 * Store the validator's expiration callback and timestamp in the Port
+	 * structure to allow for session-wide validity enforcement.
+	 */
+	port->expired_cb = ret->expired_cb;
+	port->expiry = ret->expiry;
+
 	if (port->hba->oauth_skip_usermap)
 	{
 		/*
diff --git a/src/backend/libpq/pqcomm.c b/src/backend/libpq/pqcomm.c
index 6570f27297b..9f1be07bf69 100644
--- a/src/backend/libpq/pqcomm.c
+++ b/src/backend/libpq/pqcomm.c
@@ -319,6 +319,12 @@ pq_init(ClientSocket *client_sock)
 	Assert(socket_pos == FeBeWaitSetSocketPos);
 	Assert(latch_pos == FeBeWaitSetLatchPos);
 
+	/*
+	 * Initialize OAuth session fields to safe defaults (no expiry/no callback).
+	 */
+	port->expiry = DT_NOBEGIN;
+	port->expired_cb = NULL;
+
 	return port;
 }
 
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21de158adbb..291810f4c59 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -185,7 +185,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 check_oauth_expiry(Port *port);
 
 /* ----------------------------------------------------------------
  *		infrastructure for valgrind debugging
@@ -1049,6 +1049,13 @@ exec_simple_query(const char *query_string)
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/*
 	 * 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 +1437,13 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/*
 	 * Switch to appropriate context for constructing parsetrees.
 	 *
@@ -1705,6 +1719,13 @@ exec_bind_message(StringInfo input_message)
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2217,6 +2238,13 @@ exec_execute_message(const char *portal_name, long max_rows)
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/*
 	 * 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 +2663,13 @@ exec_describe_statement_message(const char *stmt_name)
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2727,6 +2762,13 @@ exec_describe_portal_message(const char *portal_name)
 	 */
 	start_xact_command();
 
+	/*
+	 * 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);
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -5271,3 +5313,19 @@ disable_statement_timeout(void)
 	if (get_timeout_active(STATEMENT_TIMEOUT))
 		disable_timeout(STATEMENT_TIMEOUT, false);
 }
+
+/*
+ * Validates the current OAuth session. If a validator has provided a
+ * callback, execute it. A return value of 'true' triggers a FATAL
+ * error to terminate the session immediately.
+ */
+static void
+check_oauth_expiry(Port *port)
+{
+	if (port->expired_cb != NULL && port->expired_cb(port->expiry))
+	{
+		ereport(FATAL,
+				(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
+				 errmsg("session expired: OAuth token is no longer valid")));
+	}
+}
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..388e3e8d8ba 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -238,6 +238,16 @@ typedef struct Port
 	char	   *raw_buf;
 	ssize_t		raw_buf_consumed,
 				raw_buf_remaining;
+
+	/*
+	 * The expiration time of the authentication credential.
+	 * If not it represents the point in time after which the current session is
+	 * considered invalid.
+	 */
+	TimestampTz expiry;
+
+	/* Callback to verify session validity at runtime */
+	bool (*expired_cb) (TimestampTz);
 } Port;
 
 /*
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
index 4a822e9a1f2..c1d278590e1 100644
--- a/src/include/libpq/oauth.h
+++ b/src/include/libpq/oauth.h
@@ -49,6 +49,22 @@ typedef struct ValidatorModuleResult
 	 * delegation. See the validator module documentation for details.
 	 */
 	char	   *authn_id;
+
+	/*
+	 * 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;
 
 /*
diff --git a/src/test/modules/oauth_validator/t/003_token_expiry.pl b/src/test/modules/oauth_validator/t/003_token_expiry.pl
new file mode 100755
index 00000000000..ec81c248d70
--- /dev/null
+++ b/src/test/modules/oauth_validator/t/003_token_expiry.pl
@@ -0,0 +1,148 @@
+#
+# Test OAuth token expiration implementation
+# This test verifies that when an OAuth token expires or the validator callback
+# indicates it has been revoked, the session is properly terminated.
+#
+
+use strict;
+use warnings;
+use JSON::PP qw(encode_json);
+use MIME::Base64 qw(encode_base64);
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use FindBin;
+use lib $FindBin::RealBin;
+use OAuth::Server;
+
+# Skip tests if environment doesn't support them
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\boauth\b/)
+{
+	plan skip_all =>
+	  'Potentially unsafe test oauth not enabled in PG_TEST_EXTRA';
+}
+
+unless (check_pg_config("#define HAVE_SYS_EVENT_H 1")
+	or check_pg_config("#define HAVE_SYS_EPOLL_H 1"))
+{
+	plan skip_all =>
+	  'OAuth server-side tests are not supported on this platform';
+}
+
+if ($ENV{with_libcurl} ne 'yes')
+{
+	plan skip_all => 'client-side OAuth not supported by this build';
+}
+
+if ($ENV{with_python} ne 'yes')
+{
+	plan skip_all => 'OAuth tests require --with-python to run';
+}
+
+# This test validates that the OAuth token expiration mechanism
+# is properly implemented by examining log entries.
+# Set environment variables for test execution
+# Use the default admin user from the test environment
+# This is typically determined by the PostgreSQL::Test::Cluster module
+
+plan tests => 4;
+
+# Create a PostgreSQL instance for testing
+my $node = PostgreSQL::Test::Cluster->new('oauth_expiry');
+$node->init;
+$node->append_conf('postgresql.conf', "log_connections = on");
+$node->append_conf('postgresql.conf', "log_disconnections = on");
+$node->append_conf('postgresql.conf', "oauth_validator_libraries = 'validator'\n");
+$node->start;
+
+# Create test users
+$node->safe_psql('postgres', 'CREATE USER test;');
+
+# Start the mock OAuth server
+my $webserver = OAuth::Server->new();
+$webserver->run();
+
+END
+{
+	my $exit_code = $?;
+	$webserver->stop() if defined $webserver;
+	$? = $exit_code;
+}
+
+my $port = $webserver->port();
+my $issuer = "http://127.0.0.1:$port";
+
+# Configure HBA for OAuth authentication
+unlink($node->data_dir . '/pg_hba.conf');
+# First, add a specific rule for the test user with OAuth authentication
+$node->append_conf(
+	'pg_hba.conf', qq{
+# OAuth authentication for test user (this must be the first rule)
+local postgres test oauth validator=validator issuer="$issuer" scope="openid postgres"
+});
+# Add a separate trust rule for the admin user (after the OAuth rule)
+$node->append_conf(
+	'pg_hba.conf', qq{
+# Trust authentication for admin access
+local all all trust
+});
+
+$node->reload;
+
+# Get log start position to track new log entries
+my $log_start = $node->wait_for_log(qr/reloading configuration files/);
+
+# Create a background connection for configuration changes
+my $bgconn = $node->background_psql('postgres');
+ok($bgconn, "Background admin connection established");
+
+# Enable OAuth token expiration test mode
+$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.enable_expiry_test TO true");
+$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.token_expires_in TO 2");
+$node->reload;
+$node->wait_for_log(qr/reloading configuration files/, $log_start);
+
+# Update log position after reload
+$log_start = $node->wait_for_log(qr/parameter "oauth_validator.token_expires_in" changed to "2"/);
+
+# Enable OAuth debug mode for connection testing
+# This is required for the test to use OAuth authentication
+$ENV{PGOAUTHDEBUG} = "UNSAFE";
+
+# Make a connection with OAuth auth
+$node->connect_ok(
+	"user=test dbname=postgres oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+	"connect with OAuth token",
+	# Allow any stderr output since OAuth debugging will produce messages
+	expected_stderr => qr/.*/
+);
+
+# Wait for token to expire
+note "Waiting for token to expire (3 seconds)...";
+sleep 3;
+
+# Ensure the OAuth debug environment variable is set before trying the second connection
+$ENV{PGOAUTHDEBUG} = "UNSAFE";
+
+# Try another OAuth connection
+my ($stdout, $stderr) = ('', '');
+$node->psql(
+	'postgres',
+	"SELECT 'This should sleep'; SELECT pg_sleep(3); SELECT 'This should never run';",
+	extra_params => ['--set', "ON_ERROR_STOP=1"],
+	env => { PGOAUTHDEBUG => "UNSAFE" },
+	timeout => $PostgreSQL::Test::Utils::timeout_default,
+	connstr => "user=test oauth_issuer=$issuer oauth_client_id=f02c6361-0635",
+	stdout => \$stdout,
+	stderr => \$stderr
+);
+
+# Look for token expiration errors in the logs
+my $expiry_logged = $node->wait_for_log(qr/session expired: OAuth token is no longer valid/, $log_start);
+ok($expiry_logged, "Token expiration message found in server logs");
+
+# Clean up
+$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.enable_expiry_test");
+$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.token_expires_in");
+
+$node->stop;
diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c
index 0b983a9dc8f..36468312b82 100644
--- a/src/test/modules/oauth_validator/validator.c
+++ b/src/test/modules/oauth_validator/validator.c
@@ -18,6 +18,8 @@
 #include "miscadmin.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
+#include "utils/timestamp.h"
+#include <limits.h>
 
 PG_MODULE_MAGIC;
 
@@ -40,6 +42,8 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 /* GUCs */
 static char *authn_id = NULL;
 static bool authorize_tokens = true;
+static bool enable_expiry_test = false;
+static int token_expires_in = 0;
 
 /*---
  * Extension entry point. Sets up GUCs for use by tests:
@@ -72,6 +76,25 @@ _PG_init(void)
 							 0,
 							 NULL, NULL, NULL);
 
+	/* Parameters for token expiration testing */
+	DefineCustomBoolVariable("oauth_validator.enable_expiry_test",
+							 "Enable token expiration testing",
+							 NULL,
+							 &enable_expiry_test,
+							 false,
+							 PGC_SIGHUP,
+							 0,
+							 NULL, NULL, NULL);
+	DefineCustomIntVariable("oauth_validator.token_expires_in",
+							"Token lifetime in seconds for expiry test",
+							NULL,
+							&token_expires_in,
+							0,
+							0, INT_MAX,
+							PGC_SIGHUP,
+							0,
+							NULL, NULL, NULL);
+
 	MarkGUCPrefixReserved("oauth_validator");
 }
 
@@ -114,6 +137,15 @@ validator_shutdown(ValidatorModuleState *state)
 			 state->private_data);
 }
 
+/*
+ * Test callback function for token expiration checking
+ */
+static bool
+test_token_expired_callback(TimestampTz expiry)
+{
+	return (TimestampTzPlusMilliseconds(expiry, 0) < GetCurrentTimestamp());
+}
+
 /*
  * Validator implementation. Logs the incoming data and authorizes the token by
  * default; the behavior can be modified via the module's GUC settings.
@@ -139,5 +171,26 @@ validate_token(const ValidatorModuleState *state,
 	else
 		res->authn_id = pstrdup(role);
 
+	/* Set up expiration data if testing is enabled */
+	if (enable_expiry_test)
+	{
+		TimestampTz now = GetCurrentTimestamp();
+
+		/* Set the callback if enabled */
+		res->expired_cb = test_token_expired_callback;
+
+		/* Add token_expires_in seconds to current time for expiry */
+		if (token_expires_in > 0)
+		{
+			res->expiry = TimestampTzPlusSeconds(now, token_expires_in);
+		}
+		else
+		{
+			/* Use a far future time */
+			res->expiry = DT_NOEND;
+		}
+
+	}
+
 	return true;
 }


^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-18 10:34  Daniel Gustafsson <[email protected]>
  parent: Ajit Awekar <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Daniel Gustafsson @ 2026-02-18 10:34 UTC (permalink / raw)
  To: Ajit Awekar <[email protected]>; +Cc: VASUKI M <[email protected]>; Zsolt Parragi <[email protected]>; PostgreSQL Hackers <[email protected]>

> On 18 Feb 2026, at 09:38, Ajit Awekar <[email protected]> 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







^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-18 12:04  Zsolt Parragi <[email protected]>
  parent: Daniel Gustafsson <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Zsolt Parragi @ 2026-02-18 12:04 UTC (permalink / raw)
  To: Daniel Gustafsson <[email protected]>; +Cc: Ajit Awekar <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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

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

> 2. Terminating sessions with expired/revoked tokens before executing new
> commands.

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

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%40mai...
[2] : https://www.postgresql.org/message-id/flat/DDPQ1RV5FE9U.I2WW34NGRD8Z%40jeltef.nl






^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-18 16:30  Daniel Gustafsson <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Daniel Gustafsson @ 2026-02-18 16:30 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Ajit Awekar <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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

>> 2. Terminating sessions with expired/revoked tokens before executing new
>> commands.
> 
>> Token expiration is IMHO not a use case for a FATAL error, if we want to
>> terminate a connection we can do it in a more graceful way.
> 
> 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.

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







^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-18 16:58  Zsolt Parragi <[email protected]>
  parent: Daniel Gustafsson <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Zsolt Parragi @ 2026-02-18 16:58 UTC (permalink / raw)
  To: Daniel Gustafsson <[email protected]>; +Cc: Ajit Awekar <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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






^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-02-20 09:42  Ajit Awekar <[email protected]>
  parent: Zsolt Parragi <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Ajit Awekar @ 2026-02-20 09:42 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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


Attachments:

  [text/x-patch] credential_validation_framework_poc.patch (19.3K, 3-credential_validation_framework_poc.patch)
  download | inline diff:
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 */


^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-03-16 13:57  Ajit Awekar <[email protected]>
  parent: Ajit Awekar <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Ajit Awekar @ 2026-03-16 13:57 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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 protected]> wrote:

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


Attachments:

  [application/octet-stream] credential_validation_V1.patch (23.2K, 3-credential_validation_V1.patch)
  download | inline diff:
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-oauth.c b/src/backend/libpq/auth-oauth.c
index 11365048951..c6a7840cb7a 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -892,3 +892,18 @@ done:
 
 	return (*err_msg == NULL);
 }
+
+/*
+ * Check if an OAuth token has expired.
+ * This is called from credential validation to check token validity.
+ */
+bool
+CheckOAuthValidatorExpiration(void)
+{
+	/* Delegate to validator's expire_cb if available */
+	if (ValidatorCallbacks != NULL && ValidatorCallbacks->expire_cb != NULL)
+		return ValidatorCallbacks->expire_cb(validator_module_state);
+
+	/* No expire_cb, assume valid */
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c
new file mode 100644
index 00000000000..d7e1506c2a1
--- /dev/null
+++ b/src/backend/libpq/auth-validate-methods.c
@@ -0,0 +1,140 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate-methods.c
+ *      Implementation of authentication credential validation methods
+ *
+ * This module provides credential validation methods for various authentication
+ * types during active PostgreSQL sessions. It includes validation for password
+ * expiry, OAuth token expiry, and can be extended to other authentication
+ * mechanisms.
+ *
+ * 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 "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/pg_authid.h"
+#include "catalog/catalog.h"
+#include "libpq/auth-validate.h"
+#include "libpq/libpq-be.h"
+#include "libpq/oauth.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+/* Function declarations for internal use */
+static bool validate_password_credentials(void);
+static bool validate_oauth_credentials(void);
+
+/* Function prototypes */
+void InitializeValidationMethods(void);
+
+/*
+ * Initialize validation methods
+ */
+void
+InitializeValidationMethods(void)
+{
+	/* Register all the validation methods */
+	RegisterCredentialValidator(CVT_PASSWORD, validate_password_credentials);
+	RegisterCredentialValidator(CVT_OAUTH, validate_oauth_credentials);
+}
+
+/*
+ * Validate password credentials by checking rolvaliduntil
+ * Returns true if credentials are still valid, false if they have expired.
+ */
+static bool
+validate_password_credentials(void)
+{
+	HeapTuple   tuple = NULL;
+	Datum       rolvaliduntil_datum;
+	bool        validuntil_null;
+	TimestampTz valid_until = 0;
+	TimestampTz current_time;
+	Oid         userid;
+	bool        result = false;
+
+	userid = GetSessionUserId();
+
+	/*
+	 * Try to take AccessShareLock on pg_authid to prevent concurrent modifications
+	 * from interfering with our validation. Use conditional acquisition to avoid
+	 * indefinite waiting during credential validation.
+	 */
+	if (!ConditionalLockRelationOid(AuthIdRelationId, AccessShareLock))
+	{
+		/*
+		 * Could not acquire lock immediately, which likely means another session
+		 * is modifying user data. For credential validation, it's better to
+		 * consider credentials valid and retry later than to block indefinitely.
+		 */
+		elog(LOG, "credential validation: could not acquire lock on pg_authid immediately, will retry later");
+		return true; /* Consider valid */
+	}
+
+	PG_TRY();
+	{
+		tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(userid));
+
+		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);
+				current_time = GetCurrentTimestamp();
+
+				result = !(valid_until < current_time);
+			}
+			else
+				result = true;
+
+			ReleaseSysCache(tuple);
+			tuple = NULL;
+		}
+	}
+	PG_CATCH();
+	{
+		if (tuple != NULL)
+			ReleaseSysCache(tuple);
+
+		UnlockRelationOid(AuthIdRelationId, AccessShareLock);
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	/* Release the relation lock */
+	UnlockRelationOid(AuthIdRelationId, AccessShareLock);
+
+	return result;
+}
+
+/*
+ * Check if an OAuth token has expired.
+ *
+ * Returns true if the token is still valid, false if it has expired.
+ *
+ * Calls wrapper CheckOAuthValidatorExpiration() function
+ * to verify that the token hasn't expired.
+ */
+static bool
+validate_oauth_credentials(void)
+{
+	/* Call the validator's expire_cb to check token expiration */
+	if (!CheckOAuthValidatorExpiration())
+		return false;
+
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c
new file mode 100644
index 00000000000..82c475f6df0
--- /dev/null
+++ b/src/backend/libpq/auth-validate.c
@@ -0,0 +1,244 @@
+/*-------------------------------------------------------------------------
+*
+* 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.h"
+#include "libpq/libpq-be.h"
+#include "libpq/auth-validate.h"
+#include "libpq/auth-validate-methods.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "tcop/tcopprot.h"
+#include "utils/elog.h"
+#include "utils/guc.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[CVT_COUNT];
+
+/*
+ * Convert UserAuth enum to CredentialValidationType for validator selection
+ */
+static CredentialValidationType
+UserAuthToValidationType(UserAuth auth_method)
+{
+	switch (auth_method)
+	{
+		case uaPassword:
+		case uaMD5:
+		case uaSCRAM:
+		/* All password-based methods use the password validator */
+			return CVT_PASSWORD;
+		case uaOAuth:
+			return CVT_OAUTH;
+		default:
+			/* No specific validator for other auth methods */
+			return CVT_COUNT;  /* Invalid value */
+	}
+}
+
+/*
+ * Process credential validation
+ */
+void
+ProcessCredentialValidation(void)
+{
+	/* Skip validation during initialization, bootstrap, authentication or connection setup */
+	if (ClientAuthInProgress || IsInitProcessingMode() || IsBootstrapProcessingMode())
+		return;
+
+	/* Check credentials if validation is enabled */
+	if (credential_validation_enabled && MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationStatus status;
+		UserAuth	auth_method = MyClientConnectionInfo.auth_method;
+
+		status = CheckCredentialValidity();
+
+		switch (status)
+		{
+			case CVS_VALID:
+				/* Credentials are valid, continue */
+				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 minutes.",
+							NULL,
+							&credential_validation_interval,
+							1,	/* default: 1 minute */
+							1,	/* min: 1 minute */
+							60,	/* max: 60 minutes */
+							PGC_SUSET,
+							GUC_UNIT_MIN,
+							NULL,
+							NULL,
+							NULL);
+
+	/* Initialize validator callbacks to NULL */
+	for (i = 0; i < CVT_COUNT; i++)
+		validators[i] = NULL;
+
+	/* Initialize and register all validation methods */
+	InitializeValidationMethods();
+}
+
+/*
+ * 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 using the appropriate validator
+ */
+CredentialValidationStatus
+CheckCredentialValidity(void)
+{
+	CredentialValidationCallback validator = NULL;
+	CredentialValidationStatus status;
+
+	/*
+	 * Skip validation for:
+	 * - During shutdown or recovery
+	 * - Non-client backends (any process not serving a client connection)
+	 * - AutoVacuum processes (launcher and workers)
+	 * - Background worker processes
+	 * - Authentication is in progress
+	 */
+	if (proc_exit_inprogress ||
+		RecoveryInProgress() ||
+		!IsExternalConnectionBackend(MyBackendType) ||
+		AmAutoVacuumLauncherProcess() ||
+		AmAutoVacuumWorkerProcess() ||
+		AmBackgroundWorkerProcess() ||
+		ClientAuthInProgress)
+		return CVS_VALID;
+	/*
+	 * Use the session's authentication method from MyClientConnectionInfo
+	 * to select the appropriate validator.
+	 */
+	if (MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationType validation_type;
+
+		validation_type = UserAuthToValidationType(MyClientConnectionInfo.auth_method);
+
+		/*
+		 * If we have a valid validation type, get the corresponding
+		 * validator
+		 */
+		if (validation_type < CVT_COUNT)
+			validator = validators[validation_type];
+
+	}
+
+	/*
+	 * 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;
+
+		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;
+
+		return status;
+	}
+	PG_CATCH();
+	{
+		/* Error during validation */
+		elog(DEBUG1, "credential validation: error during validation");
+
+		FlushErrorState();
+		return CVS_ERROR;
+	}
+	PG_END_TRY();
+}
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index ee337cf42cc..608d9e10eb0 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 d01a09dd0c4..7307660e85a 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"
@@ -97,6 +98,49 @@ bool		Log_disconnections = false;
 
 int			log_statement = LOGSTMT_NONE;
 
+
+
+/*
+ * Function that performs credential validation when needed
+ * Uses a time-based approach to periodically validate credentials
+ * during normal operation, skipping validation in bootstrapping.
+ */
+static void
+CheckAndExecuteCredentialValidation(void)
+{
+	TimestampTz now;
+	TimestampTz diff;
+
+	/* Fast early returns for all cases where we should skip validation */
+	if (IsInitProcessingMode() || IsBootstrapProcessingMode())
+		return;
+
+	/* Get the current time */
+	now = GetCurrentTimestamp();
+
+	/* Use direct timestamp comparison for better performance */
+	if (LastCredentialValidationTime != 0)
+	{
+		int64 interval_us;
+
+		diff = now - LastCredentialValidationTime;
+		interval_us = (int64) credential_validation_interval * 60 * INT64CONST(1000000); /* minutes to microseconds */
+
+		/* Exit early if not enough time has passed */
+		if (diff < interval_us)
+			return;
+	}
+
+	/* Process credential validation */
+	ProcessCredentialValidation();
+
+	/* Update the last validation time */
+	LastCredentialValidationTime = now;
+
+	/* Only log at DEBUG level to reduce noise */
+	elog(DEBUG1, "Credential validation completed successfully");
+}
+
 /* wait N seconds to allow attach from a debugger */
 int			PostAuthDelay = 0;
 
@@ -1049,6 +1093,10 @@ exec_simple_query(const char *query_string)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation using time-based approach */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * 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 +1478,10 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation for extended protocol */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * Switch to appropriate context for constructing parsetrees.
 	 *
@@ -1705,6 +1757,10 @@ exec_bind_message(StringInfo input_message)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation for extended protocol */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2217,6 +2273,10 @@ exec_execute_message(const char *portal_name, long max_rows)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation for extended protocol */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * 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 +2695,10 @@ exec_describe_statement_message(const char *stmt_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation for extended protocol */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2727,6 +2791,10 @@ exec_describe_portal_message(const char *portal_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation for extended protocol */
+	if (credential_validation_enabled && credential_validation_interval > 0 && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..45beb71ef22 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;
+TimestampTz LastCredentialValidationTime = 0;
 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..138fd440600 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"
@@ -1226,6 +1227,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 +1444,7 @@ ClientCheckTimeoutHandler(void)
 	SetLatch(MyLatch);
 }
 
+
 /*
  * Returns true if at least one role is defined in this database cluster.
  */
diff --git a/src/include/libpq/auth-validate-methods.h b/src/include/libpq/auth-validate-methods.h
new file mode 100644
index 00000000000..420183a1c7d
--- /dev/null
+++ b/src/include/libpq/auth-validate-methods.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "utils/timestamp.h"
+
+/* Initialize all validation methods */
+extern void InitializeValidationMethods(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..52b17952744
--- /dev/null
+++ b/src/include/libpq/auth-validate.h
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "libpq/protocol.h"
+#include "postmaster/postmaster.h"
+#include "utils/guc.h"
+#include "utils/timeout.h"
+
+/* Define credential validation method types as an enum */
+typedef enum CredentialValidationType
+{
+	CVT_PASSWORD = 0,          /* All password-based methods (md5, scram, etc) */
+	CVT_OAUTH,                 /* OAuth bearer token 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);
+
+/* Credential validation status */
+typedef enum CredentialValidationStatus
+{
+	CVS_VALID,					/* Credentials are valid */
+	CVS_EXPIRED,				/* Credentials have expired */
+	CVS_ERROR					/* Error during validation */
+} CredentialValidationStatus;
+
+/* 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 */
+extern CredentialValidationStatus CheckCredentialValidity(void);
+
+#endif							/* AUTH_VALIDATE_H */
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
index 4a822e9a1f2..bbb9290626c 100644
--- a/src/include/libpq/oauth.h
+++ b/src/include/libpq/oauth.h
@@ -64,6 +64,7 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
 typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
 									 const char *token, const char *role,
 									 ValidatorModuleResult *result);
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
 
 /*
  * Identifies the compiled ABI version of the validator module. Since the server
@@ -80,6 +81,7 @@ typedef struct OAuthValidatorCallbacks
 	ValidatorStartupCB startup_cb;
 	ValidatorShutdownCB shutdown_cb;
 	ValidatorValidateCB validate_cb;
+	ValidatorExpireCB expire_cb;  /* Optional: Check token expiration */
 } OAuthValidatorCallbacks;
 
 /*
@@ -98,4 +100,8 @@ extern PGDLLIMPORT const pg_be_sasl_mech pg_be_oauth_mech;
  */
 extern bool check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg);
 
+/*
+ * Check OAuth token expiration using validator's expire_cb if available.
+ */
+bool CheckOAuthValidatorExpiration(void);
 #endif							/* PG_OAUTH_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..30c3d40d418 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 TimestampTz LastCredentialValidationTime;
 
 /* these are marked volatile because they are examined by signal handlers: */
 extern PGDLLIMPORT volatile uint32 InterruptHoldoffCount;


^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-03-27 10:59  Ajit Awekar <[email protected]>
  parent: Ajit Awekar <[email protected]>
  0 siblings, 1 reply; 11+ messages in thread

From: Ajit Awekar @ 2026-03-27 10:59 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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 protected]> wrote:

> 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 protected]> wrote:
>
>>
>> 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.
>>>
>>


Attachments:

  [application/octet-stream] Credential_validation.patch (31.8K, 3-Credential_validation.patch)
  download | inline diff:
diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml
index 704089dd7b3..05c626bab93 100644
--- a/doc/src/sgml/oauth-validators.sgml
+++ b/doc/src/sgml/oauth-validators.sgml
@@ -312,6 +312,7 @@ typedef struct OAuthValidatorCallbacks
     ValidatorStartupCB startup_cb;
     ValidatorShutdownCB shutdown_cb;
     ValidatorValidateCB validate_cb;
+    ValidatorExpireCB expire_cb;    /* Optional: check token expiration */
 } OAuthValidatorCallbacks;
 
 typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
@@ -320,6 +321,15 @@ typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
    Only the <function>validate_cb</function> callback is required, the others
    are optional.
   </para>
+  <para>
+   The <literal>magic</literal> field identifies the ABI version of the module.
+   The server supports both <literal>PG_OAUTH_VALIDATOR_MAGIC_V1</literal> (the
+   original version without <function>expire_cb</function>) and
+   <literal>PG_OAUTH_VALIDATOR_MAGIC_V2</literal> (which adds
+   <function>expire_cb</function>).  New modules should use
+   <literal>PG_OAUTH_VALIDATOR_MAGIC</literal>, which always refers to the
+   latest version.
+  </para>
  </sect1>
 
  <sect1 id="oauth-validator-callbacks">
@@ -412,5 +422,27 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
    </para>
   </sect2>
 
+  <sect2 id="oauth-validator-callback-expire">
+   <title>Expire Callback</title>
+   <para>
+    The <function>expire_cb</function> callback is an optional callback that
+    can be used to check whether the OAuth token has expired. This is called
+    during credential validation to verify that the token is still valid.
+<programlisting>
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
+</programlisting>
+    The callback should return <literal>true</literal> if the token is still
+    valid, or <literal>false</literal> if the token has expired. If this
+    callback is not provided (set to NULL), the server assumes the token
+    remains valid.
+   </para>
+   <para>
+    This callback was added in <literal>PG_OAUTH_VALIDATOR_MAGIC_V2</literal>.
+    Modules compiled against the older <literal>PG_OAUTH_VALIDATOR_MAGIC_V1</literal>
+    do not have this field, and the server will not attempt to call it for
+    such modules.
+   </para>
+  </sect2>
+
  </sect1>
 </chapter>
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-oauth.c b/src/backend/libpq/auth-oauth.c
index 11365048951..495234343c2 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -43,6 +43,7 @@ static void shutdown_validator_library(void *arg);
 
 static ValidatorModuleState *validator_module_state;
 static const OAuthValidatorCallbacks *ValidatorCallbacks;
+static int ValidatorABIVersion;		/* tracks V1 vs V2 module ABI */
 
 /* Mechanism declaration */
 const pg_be_sasl_mech pg_be_oauth_mech = {
@@ -767,13 +768,22 @@ load_validator_library(const char *libname)
 	 * Check the magic number, to protect against break-glass scenarios where
 	 * the ABI must change within a major version. load_external_function()
 	 * already checks for compatibility across major versions.
+	 *
+	 * We accept both V1 and V2 magic numbers for backward compatibility.
+	 * V1 modules don't have the expire_cb field, so we track the version
+	 * to avoid accessing non-existent struct members.
 	 */
-	if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC)
+	if (ValidatorCallbacks->magic == PG_OAUTH_VALIDATOR_MAGIC_V2)
+		ValidatorABIVersion = 2;
+	else if (ValidatorCallbacks->magic == PG_OAUTH_VALIDATOR_MAGIC_V1)
+		ValidatorABIVersion = 1;
+	else
 		ereport(ERROR,
 				errmsg("%s module \"%s\": magic number mismatch",
 					   "OAuth validator", libname),
-				errdetail("Server has magic number 0x%08X, module has 0x%08X.",
-						  PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic));
+				errdetail("Server expects magic number 0x%08X or 0x%08X, module has 0x%08X.",
+						  PG_OAUTH_VALIDATOR_MAGIC_V2, PG_OAUTH_VALIDATOR_MAGIC_V1,
+						  ValidatorCallbacks->magic));
 
 	/*
 	 * Make sure all required callbacks are present in the ValidatorCallbacks
@@ -892,3 +902,24 @@ done:
 
 	return (*err_msg == NULL);
 }
+
+/*
+ * Check if an OAuth token has expired.
+ * This is called from credential validation to check token validity.
+ */
+bool
+CheckOAuthValidatorExpiration(void)
+{
+	/*
+	 * Delegate to validator's expire_cb if available.  Only V2+ modules have
+	 * the expire_cb field, so we must check the ABI version before accessing
+	 * it to maintain backward compatibility with V1 modules.
+	 */
+	if (ValidatorCallbacks != NULL &&
+		ValidatorABIVersion >= 2 &&
+		ValidatorCallbacks->expire_cb != NULL)
+		return ValidatorCallbacks->expire_cb(validator_module_state);
+
+	/* V1 module or no expire_cb, assume token is valid */
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c
new file mode 100644
index 00000000000..f6516459624
--- /dev/null
+++ b/src/backend/libpq/auth-validate-methods.c
@@ -0,0 +1,136 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate-methods.c
+ *      Implementation of authentication credential validation methods
+ *
+ * This module provides credential validation methods for various authentication
+ * types during active PostgreSQL sessions. It includes validation for password
+ * expiry, OAuth token expiry, and can be extended to other authentication
+ * mechanisms.
+ *
+ * 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 "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/pg_authid.h"
+#include "catalog/catalog.h"
+#include "libpq/auth-validate.h"
+#include "libpq/libpq-be.h"
+#include "libpq/oauth.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+/* Function declarations for internal use */
+static bool validate_password_credentials(void);
+static bool validate_oauth_credentials(void);
+
+/* Function prototypes */
+void InitializeValidationMethods(void);
+
+/*
+ * Initialize validation methods
+ */
+void
+InitializeValidationMethods(void)
+{
+	/* Register all the validation methods */
+	RegisterCredentialValidator(CVT_PASSWORD, validate_password_credentials);
+	RegisterCredentialValidator(CVT_OAUTH, validate_oauth_credentials);
+}
+
+/*
+ * Validate password credentials by checking rolvaliduntil
+ * Returns true if credentials are still valid, false if they have expired.
+ */
+static bool
+validate_password_credentials(void)
+{
+	HeapTuple   tuple = NULL;
+	Datum       rolvaliduntil_datum;
+	bool        validuntil_null;
+	TimestampTz valid_until = 0;
+	TimestampTz current_time;
+	Oid         userid;
+	bool        result = false;
+
+	userid = GetSessionUserId();
+
+	/*
+	 * Try to take AccessShareLock on pg_authid to prevent concurrent modifications
+	 * from interfering with our validation. Use conditional acquisition to avoid
+	 * indefinite waiting during credential validation.
+	 */
+	if (!ConditionalLockRelationOid(AuthIdRelationId, AccessShareLock))
+	{
+		/*
+		 * Could not acquire lock immediately, which likely means another session
+		 * is modifying user data. For credential validation, it's better to
+		 * consider credentials valid and retry later than to block indefinitely.
+		 */
+		elog(LOG, "credential validation: could not acquire lock on pg_authid immediately, will retry later");
+		return true; /* Consider valid */
+	}
+
+	PG_TRY();
+	{
+		tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(userid));
+
+		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);
+				current_time = GetCurrentTimestamp();
+
+				result = !(valid_until < current_time);
+			}
+			else
+				result = true;
+
+			ReleaseSysCache(tuple);
+			tuple = NULL;
+		}
+	}
+	PG_FINALLY();
+	{
+		if (tuple != NULL)
+			ReleaseSysCache(tuple);
+
+		UnlockRelationOid(AuthIdRelationId, AccessShareLock);
+	}
+	PG_END_TRY();
+
+	return result;
+}
+
+/*
+ * Check if an OAuth token has expired.
+ *
+ * Returns true if the token is still valid, false if it has expired.
+ *
+ * Calls wrapper CheckOAuthValidatorExpiration() function
+ * to verify that the token hasn't expired.
+ */
+static bool
+validate_oauth_credentials(void)
+{
+	/* Call the validator's expire_cb to check token expiration */
+	if (!CheckOAuthValidatorExpiration())
+		return false;
+
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c
new file mode 100644
index 00000000000..92d4683651b
--- /dev/null
+++ b/src/backend/libpq/auth-validate.c
@@ -0,0 +1,260 @@
+/*-------------------------------------------------------------------------
+*
+* 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.h"
+#include "libpq/libpq-be.h"
+#include "libpq/auth-validate.h"
+#include "libpq/auth-validate-methods.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "tcop/tcopprot.h"
+#include "utils/elog.h"
+#include "utils/guc.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[CVT_COUNT];
+
+
+/*
+ * Convert UserAuth enum to CredentialValidationType for validator selection
+ */
+static CredentialValidationType
+UserAuthToValidationType(UserAuth auth_method)
+{
+	switch (auth_method)
+	{
+		case uaPassword:
+		case uaMD5:
+		case uaSCRAM:
+		/* All password-based methods use the password validator */
+			return CVT_PASSWORD;
+		case uaOAuth:
+			return CVT_OAUTH;
+		default:
+			/* No specific validator for other auth methods */
+			return CVT_COUNT;  /* Invalid value */
+	}
+}
+
+/*
+ * Process credential validation
+ */
+void
+ProcessCredentialValidation(void)
+{
+	/* Skip validation during initialization, bootstrap, authentication or connection setup */
+	if (ClientAuthInProgress || IsInitProcessingMode() || IsBootstrapProcessingMode())
+		return;
+
+	/* Check credentials if validation is enabled */
+	if (credential_validation_enabled && MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationStatus status;
+		UserAuth	auth_method = MyClientConnectionInfo.auth_method;
+
+		status = CheckCredentialValidity();
+
+		switch (status)
+		{
+			case CVS_VALID:
+				/* Credentials are valid, continue */
+				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 minutes.",
+							NULL,
+							&credential_validation_interval,
+							1,	/* default: 1 minute */
+							1,	/* min: 1 minute */
+							60,	/* max: 60 minutes */
+							PGC_SUSET,
+							GUC_UNIT_MIN,
+							NULL,
+							NULL,
+							NULL);
+
+	/* Initialize validator callbacks to NULL */
+	for (i = 0; i < CVT_COUNT; i++)
+		validators[i] = NULL;
+
+	/* Initialize and register all validation methods */
+	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 minutes to milliseconds */
+	interval_ms = credential_validation_interval * 60 * 1000;
+
+	enable_timeout_after(CREDENTIAL_VALIDATION_TIMEOUT, interval_ms);
+
+	elog(DEBUG1, "credential validation timeout enabled, interval=%d min", 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 using the appropriate validator
+ */
+CredentialValidationStatus
+CheckCredentialValidity(void)
+{
+	CredentialValidationCallback validator = NULL;
+	CredentialValidationStatus status;
+
+	/*
+	 * Skip validation for:
+	 * - During shutdown or recovery
+	 * - Non-client backends (any process not serving a client connection)
+	 * - AutoVacuum processes (launcher and workers)
+	 * - Background worker processes
+	 * - Authentication is in progress
+	 */
+	if (proc_exit_inprogress ||
+		RecoveryInProgress() ||
+		!IsExternalConnectionBackend(MyBackendType) ||
+		AmAutoVacuumLauncherProcess() ||
+		AmAutoVacuumWorkerProcess() ||
+		AmBackgroundWorkerProcess() ||
+		ClientAuthInProgress)
+		return CVS_VALID;
+	/*
+	 * Use the session's authentication method from MyClientConnectionInfo
+	 * to select the appropriate validator.
+	 */
+	if (MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationType validation_type;
+
+		validation_type = UserAuthToValidationType(MyClientConnectionInfo.auth_method);
+
+		/*
+		 * If we have a valid validation type, get the corresponding
+		 * validator
+		 */
+		if (validation_type < CVT_COUNT)
+			validator = validators[validation_type];
+
+	}
+
+	/*
+	 * 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 */
+	elog(DEBUG1, "credential validation: validating auth_method=%d", (int) MyClientConnectionInfo.auth_method);
+
+	PG_TRY();
+	{
+		bool		result = validator();
+
+		status = result ? CVS_VALID : CVS_EXPIRED;
+	}
+	PG_CATCH();
+	{
+		/* Error during validation */
+		FlushErrorState();
+		status = CVS_ERROR;
+	}
+	PG_END_TRY();
+
+	return status;
+}
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 b3563113219..ce9f9cef5c5 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"
@@ -98,6 +99,25 @@ bool		Log_disconnections = false;
 
 int			log_statement = LOGSTMT_NONE;
 
+
+
+/*
+ * Function that performs credential validation when needed
+ * Uses a timer-based approach to periodically validate credentials
+ * during normal operation, skipping validation in bootstrapping.
+ */
+static void
+CheckAndExecuteCredentialValidation(void)
+{
+	CredentialValidationTimeoutPending = false;
+
+	/* Process credential validation */
+	ProcessCredentialValidation();
+
+	/* Re-enable the timeout for the next validation cycle */
+	EnableCredentialValidationTimeout();
+}
+
 /* wait N seconds to allow attach from a debugger */
 int			PostAuthDelay = 0;
 
@@ -1050,6 +1070,10 @@ exec_simple_query(const char *query_string)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * Zap any pre-existing unnamed statement.  (While not strictly necessary,
 	 * it seems best to define simple-Query mode as if it used the unnamed
@@ -1431,6 +1455,11 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	 */
 	start_xact_command();
 
+
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * Switch to appropriate context for constructing parsetrees.
 	 *
@@ -1706,6 +1735,10 @@ exec_bind_message(StringInfo input_message)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2218,6 +2251,10 @@ exec_execute_message(const char *portal_name, long max_rows)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * If we re-issue an Execute protocol request against an existing portal,
 	 * then we are only fetching more rows rather than completely re-executing
@@ -2636,6 +2673,10 @@ exec_describe_statement_message(const char *stmt_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2728,6 +2769,10 @@ exec_describe_portal_message(const char *portal_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -4635,6 +4680,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
 			{
@@ -4687,6 +4737,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 */
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..aab526a45dd 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 26118661f07..dceb7453013 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"
@@ -90,6 +91,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);
@@ -774,6 +776,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);
 	}
 
 	/*
@@ -1227,6 +1231,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
@@ -1433,6 +1443,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/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e4abe6c0077..05e28143281 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -904,6 +904,12 @@
 #include_if_exists = '...'              # include file only if it exists
 #include = '...'                        # include file
 
+#------------------------------------------------------------------------------
+# CREDENTIAL VALIDATION
+#------------------------------------------------------------------------------
+
+credential_validation.enabled = true    # enable periodic credential validation
+credential_validation.interval = 1      # validation interval in minutes (1-60)
 
 #------------------------------------------------------------------------------
 # 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..420183a1c7d
--- /dev/null
+++ b/src/include/libpq/auth-validate-methods.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "utils/timestamp.h"
+
+/* Initialize all validation methods */
+extern void InitializeValidationMethods(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..ea08c52fdb9
--- /dev/null
+++ b/src/include/libpq/auth-validate.h
@@ -0,0 +1,64 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "libpq/protocol.h"
+#include "postmaster/postmaster.h"
+#include "utils/guc.h"
+#include "utils/timeout.h"
+
+/* Define credential validation method types as an enum */
+typedef enum CredentialValidationType
+{
+	CVT_PASSWORD = 0,          /* All password-based methods (md5, scram, etc) */
+	CVT_OAUTH,                 /* OAuth bearer token 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);
+
+/* Credential validation status */
+typedef enum CredentialValidationStatus
+{
+	CVS_VALID,					/* Credentials are valid */
+	CVS_EXPIRED,				/* Credentials have expired */
+	CVS_ERROR					/* Error during validation */
+} CredentialValidationStatus;
+
+/* 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 */
+extern CredentialValidationStatus CheckCredentialValidity(void);
+
+/* Enable credential validation timeout timer */
+extern void EnableCredentialValidationTimeout(void);
+
+#endif							/* AUTH_VALIDATE_H */
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
index 4a822e9a1f2..2fd30c55814 100644
--- a/src/include/libpq/oauth.h
+++ b/src/include/libpq/oauth.h
@@ -64,6 +64,7 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
 typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
 									 const char *token, const char *role,
 									 ValidatorModuleResult *result);
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
 
 /*
  * Identifies the compiled ABI version of the validator module. Since the server
@@ -71,7 +72,9 @@ typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
  * versions, this is reserved for emergency use within a stable release line.
  * May it never need to change.
  */
-#define PG_OAUTH_VALIDATOR_MAGIC 0x20250220
+#define PG_OAUTH_VALIDATOR_MAGIC_V1 0x20250220
+#define PG_OAUTH_VALIDATOR_MAGIC_V2 0x20260326
+#define PG_OAUTH_VALIDATOR_MAGIC PG_OAUTH_VALIDATOR_MAGIC_V2
 
 typedef struct OAuthValidatorCallbacks
 {
@@ -80,6 +83,7 @@ typedef struct OAuthValidatorCallbacks
 	ValidatorStartupCB startup_cb;
 	ValidatorShutdownCB shutdown_cb;
 	ValidatorValidateCB validate_cb;
+	ValidatorExpireCB expire_cb;  /* Optional: Check token expiration */
 } OAuthValidatorCallbacks;
 
 /*
@@ -98,4 +102,8 @@ extern PGDLLIMPORT const pg_be_sasl_mech pg_be_oauth_mech;
  */
 extern bool check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg);
 
+/*
+ * Check OAuth token expiration using validator's expire_cb if available.
+ */
+bool CheckOAuthValidatorExpiration(void);
 #endif							/* PG_OAUTH_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..42a09e2f299 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 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/modules/oauth_validator/fail_validator.c b/src/test/modules/oauth_validator/fail_validator.c
index 3de0470a541..8754e1e8f85 100644
--- a/src/test/modules/oauth_validator/fail_validator.c
+++ b/src/test/modules/oauth_validator/fail_validator.c
@@ -29,6 +29,7 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 	PG_OAUTH_VALIDATOR_MAGIC,
 
 	.validate_cb = fail_token,
+	.expire_cb = NULL,
 };
 
 const OAuthValidatorCallbacks *
diff --git a/src/test/modules/oauth_validator/magic_validator.c b/src/test/modules/oauth_validator/magic_validator.c
index 550da41d11b..6e4d72fde30 100644
--- a/src/test/modules/oauth_validator/magic_validator.c
+++ b/src/test/modules/oauth_validator/magic_validator.c
@@ -30,6 +30,7 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 	0xdeadbeef,
 
 	.validate_cb = validate_token,
+	.expire_cb = NULL,
 };
 
 const OAuthValidatorCallbacks *
diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c
index 0b983a9dc8f..2784708a784 100644
--- a/src/test/modules/oauth_validator/validator.c
+++ b/src/test/modules/oauth_validator/validator.c
@@ -34,7 +34,8 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 
 	.startup_cb = validator_startup,
 	.shutdown_cb = validator_shutdown,
-	.validate_cb = validate_token
+	.validate_cb = validate_token,
+	.expire_cb = NULL,			/* Optional: not implemented */
 };
 
 /* GUCs */


^ permalink  raw  reply  [nested|flat] 11+ messages in thread

* Re: [OAuth2] Infrastructure for tracking token expiry time
@ 2026-03-31 13:49  Ajit Awekar <[email protected]>
  parent: Ajit Awekar <[email protected]>
  0 siblings, 0 replies; 11+ messages in thread

From: Ajit Awekar @ 2026-03-31 13:49 UTC (permalink / raw)
  To: Zsolt Parragi <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; VASUKI M <[email protected]>; PostgreSQL Hackers <[email protected]>

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 protected]> wrote:

> 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 protected]> wrote:
>
>> 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 protected]> wrote:
>>
>>>
>>> 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.
>>>>
>>>


Attachments:

  [application/octet-stream] Credential_validation_V1.patch (41.3K, 3-Credential_validation_V1.patch)
  download | inline diff:
diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml
index 704089dd7b3..05c626bab93 100644
--- a/doc/src/sgml/oauth-validators.sgml
+++ b/doc/src/sgml/oauth-validators.sgml
@@ -312,6 +312,7 @@ typedef struct OAuthValidatorCallbacks
     ValidatorStartupCB startup_cb;
     ValidatorShutdownCB shutdown_cb;
     ValidatorValidateCB validate_cb;
+    ValidatorExpireCB expire_cb;    /* Optional: check token expiration */
 } OAuthValidatorCallbacks;
 
 typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
@@ -320,6 +321,15 @@ typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
    Only the <function>validate_cb</function> callback is required, the others
    are optional.
   </para>
+  <para>
+   The <literal>magic</literal> field identifies the ABI version of the module.
+   The server supports both <literal>PG_OAUTH_VALIDATOR_MAGIC_V1</literal> (the
+   original version without <function>expire_cb</function>) and
+   <literal>PG_OAUTH_VALIDATOR_MAGIC_V2</literal> (which adds
+   <function>expire_cb</function>).  New modules should use
+   <literal>PG_OAUTH_VALIDATOR_MAGIC</literal>, which always refers to the
+   latest version.
+  </para>
  </sect1>
 
  <sect1 id="oauth-validator-callbacks">
@@ -412,5 +422,27 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
    </para>
   </sect2>
 
+  <sect2 id="oauth-validator-callback-expire">
+   <title>Expire Callback</title>
+   <para>
+    The <function>expire_cb</function> callback is an optional callback that
+    can be used to check whether the OAuth token has expired. This is called
+    during credential validation to verify that the token is still valid.
+<programlisting>
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
+</programlisting>
+    The callback should return <literal>true</literal> if the token is still
+    valid, or <literal>false</literal> if the token has expired. If this
+    callback is not provided (set to NULL), the server assumes the token
+    remains valid.
+   </para>
+   <para>
+    This callback was added in <literal>PG_OAUTH_VALIDATOR_MAGIC_V2</literal>.
+    Modules compiled against the older <literal>PG_OAUTH_VALIDATOR_MAGIC_V1</literal>
+    do not have this field, and the server will not attempt to call it for
+    such modules.
+   </para>
+  </sect2>
+
  </sect1>
 </chapter>
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-oauth.c b/src/backend/libpq/auth-oauth.c
index 11365048951..495234343c2 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -43,6 +43,7 @@ static void shutdown_validator_library(void *arg);
 
 static ValidatorModuleState *validator_module_state;
 static const OAuthValidatorCallbacks *ValidatorCallbacks;
+static int ValidatorABIVersion;		/* tracks V1 vs V2 module ABI */
 
 /* Mechanism declaration */
 const pg_be_sasl_mech pg_be_oauth_mech = {
@@ -767,13 +768,22 @@ load_validator_library(const char *libname)
 	 * Check the magic number, to protect against break-glass scenarios where
 	 * the ABI must change within a major version. load_external_function()
 	 * already checks for compatibility across major versions.
+	 *
+	 * We accept both V1 and V2 magic numbers for backward compatibility.
+	 * V1 modules don't have the expire_cb field, so we track the version
+	 * to avoid accessing non-existent struct members.
 	 */
-	if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC)
+	if (ValidatorCallbacks->magic == PG_OAUTH_VALIDATOR_MAGIC_V2)
+		ValidatorABIVersion = 2;
+	else if (ValidatorCallbacks->magic == PG_OAUTH_VALIDATOR_MAGIC_V1)
+		ValidatorABIVersion = 1;
+	else
 		ereport(ERROR,
 				errmsg("%s module \"%s\": magic number mismatch",
 					   "OAuth validator", libname),
-				errdetail("Server has magic number 0x%08X, module has 0x%08X.",
-						  PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic));
+				errdetail("Server expects magic number 0x%08X or 0x%08X, module has 0x%08X.",
+						  PG_OAUTH_VALIDATOR_MAGIC_V2, PG_OAUTH_VALIDATOR_MAGIC_V1,
+						  ValidatorCallbacks->magic));
 
 	/*
 	 * Make sure all required callbacks are present in the ValidatorCallbacks
@@ -892,3 +902,24 @@ done:
 
 	return (*err_msg == NULL);
 }
+
+/*
+ * Check if an OAuth token has expired.
+ * This is called from credential validation to check token validity.
+ */
+bool
+CheckOAuthValidatorExpiration(void)
+{
+	/*
+	 * Delegate to validator's expire_cb if available.  Only V2+ modules have
+	 * the expire_cb field, so we must check the ABI version before accessing
+	 * it to maintain backward compatibility with V1 modules.
+	 */
+	if (ValidatorCallbacks != NULL &&
+		ValidatorABIVersion >= 2 &&
+		ValidatorCallbacks->expire_cb != NULL)
+		return ValidatorCallbacks->expire_cb(validator_module_state);
+
+	/* V1 module or no expire_cb, assume token is valid */
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate-methods.c b/src/backend/libpq/auth-validate-methods.c
new file mode 100644
index 00000000000..f6516459624
--- /dev/null
+++ b/src/backend/libpq/auth-validate-methods.c
@@ -0,0 +1,136 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-validate-methods.c
+ *      Implementation of authentication credential validation methods
+ *
+ * This module provides credential validation methods for various authentication
+ * types during active PostgreSQL sessions. It includes validation for password
+ * expiry, OAuth token expiry, and can be extended to other authentication
+ * mechanisms.
+ *
+ * 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 "access/htup_details.h"
+#include "access/xact.h"
+#include "catalog/pg_authid.h"
+#include "catalog/catalog.h"
+#include "libpq/auth-validate.h"
+#include "libpq/libpq-be.h"
+#include "libpq/oauth.h"
+#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+
+/* Function declarations for internal use */
+static bool validate_password_credentials(void);
+static bool validate_oauth_credentials(void);
+
+/* Function prototypes */
+void InitializeValidationMethods(void);
+
+/*
+ * Initialize validation methods
+ */
+void
+InitializeValidationMethods(void)
+{
+	/* Register all the validation methods */
+	RegisterCredentialValidator(CVT_PASSWORD, validate_password_credentials);
+	RegisterCredentialValidator(CVT_OAUTH, validate_oauth_credentials);
+}
+
+/*
+ * Validate password credentials by checking rolvaliduntil
+ * Returns true if credentials are still valid, false if they have expired.
+ */
+static bool
+validate_password_credentials(void)
+{
+	HeapTuple   tuple = NULL;
+	Datum       rolvaliduntil_datum;
+	bool        validuntil_null;
+	TimestampTz valid_until = 0;
+	TimestampTz current_time;
+	Oid         userid;
+	bool        result = false;
+
+	userid = GetSessionUserId();
+
+	/*
+	 * Try to take AccessShareLock on pg_authid to prevent concurrent modifications
+	 * from interfering with our validation. Use conditional acquisition to avoid
+	 * indefinite waiting during credential validation.
+	 */
+	if (!ConditionalLockRelationOid(AuthIdRelationId, AccessShareLock))
+	{
+		/*
+		 * Could not acquire lock immediately, which likely means another session
+		 * is modifying user data. For credential validation, it's better to
+		 * consider credentials valid and retry later than to block indefinitely.
+		 */
+		elog(LOG, "credential validation: could not acquire lock on pg_authid immediately, will retry later");
+		return true; /* Consider valid */
+	}
+
+	PG_TRY();
+	{
+		tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(userid));
+
+		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);
+				current_time = GetCurrentTimestamp();
+
+				result = !(valid_until < current_time);
+			}
+			else
+				result = true;
+
+			ReleaseSysCache(tuple);
+			tuple = NULL;
+		}
+	}
+	PG_FINALLY();
+	{
+		if (tuple != NULL)
+			ReleaseSysCache(tuple);
+
+		UnlockRelationOid(AuthIdRelationId, AccessShareLock);
+	}
+	PG_END_TRY();
+
+	return result;
+}
+
+/*
+ * Check if an OAuth token has expired.
+ *
+ * Returns true if the token is still valid, false if it has expired.
+ *
+ * Calls wrapper CheckOAuthValidatorExpiration() function
+ * to verify that the token hasn't expired.
+ */
+static bool
+validate_oauth_credentials(void)
+{
+	/* Call the validator's expire_cb to check token expiration */
+	if (!CheckOAuthValidatorExpiration())
+		return false;
+
+	return true;
+}
diff --git a/src/backend/libpq/auth-validate.c b/src/backend/libpq/auth-validate.c
new file mode 100644
index 00000000000..cc4c7d31753
--- /dev/null
+++ b/src/backend/libpq/auth-validate.c
@@ -0,0 +1,260 @@
+/*-------------------------------------------------------------------------
+*
+* 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.h"
+#include "libpq/libpq-be.h"
+#include "libpq/auth-validate.h"
+#include "libpq/auth-validate-methods.h"
+#include "miscadmin.h"
+#include "postmaster/postmaster.h"
+#include "storage/ipc.h"
+#include "tcop/tcopprot.h"
+#include "utils/elog.h"
+#include "utils/guc.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[CVT_COUNT];
+
+
+/*
+ * Convert UserAuth enum to CredentialValidationType for validator selection
+ */
+static CredentialValidationType
+UserAuthToValidationType(UserAuth auth_method)
+{
+	switch (auth_method)
+	{
+		case uaPassword:
+		case uaMD5:
+		case uaSCRAM:
+		/* All password-based methods use the password validator */
+			return CVT_PASSWORD;
+		case uaOAuth:
+			return CVT_OAUTH;
+		default:
+			/* No specific validator for other auth methods */
+			return CVT_COUNT;  /* Invalid value */
+	}
+}
+
+/*
+ * Process credential validation
+ */
+void
+ProcessCredentialValidation(void)
+{
+	/* Skip validation during initialization, bootstrap, authentication or connection setup */
+	if (ClientAuthInProgress || IsInitProcessingMode() || IsBootstrapProcessingMode())
+		return;
+
+	/* Check credentials if validation is enabled */
+	if (credential_validation_enabled && MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationStatus status;
+		UserAuth	auth_method = MyClientConnectionInfo.auth_method;
+
+		status = CheckCredentialValidity();
+
+		switch (status)
+		{
+			case CVS_VALID:
+				/* Credentials are valid, continue */
+				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,	/* default: 60 seconds */
+							5,	/* min: 5 seconds */
+							3600,	/* max: 3600 seconds (1 hour) */
+							PGC_SUSET,
+							GUC_UNIT_S,
+							NULL,
+							NULL,
+							NULL);
+
+	/* Initialize validator callbacks to NULL */
+	for (i = 0; i < CVT_COUNT; i++)
+		validators[i] = NULL;
+
+	/* Initialize and register all validation methods */
+	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 using the appropriate validator
+ */
+CredentialValidationStatus
+CheckCredentialValidity(void)
+{
+	CredentialValidationCallback validator = NULL;
+	CredentialValidationStatus status;
+
+	/*
+	 * Skip validation for:
+	 * - During shutdown or recovery
+	 * - Non-client backends (any process not serving a client connection)
+	 * - AutoVacuum processes (launcher and workers)
+	 * - Background worker processes
+	 * - Authentication is in progress
+	 */
+	if (proc_exit_inprogress ||
+		RecoveryInProgress() ||
+		!IsExternalConnectionBackend(MyBackendType) ||
+		AmAutoVacuumLauncherProcess() ||
+		AmAutoVacuumWorkerProcess() ||
+		AmBackgroundWorkerProcess() ||
+		ClientAuthInProgress)
+		return CVS_VALID;
+	/*
+	 * Use the session's authentication method from MyClientConnectionInfo
+	 * to select the appropriate validator.
+	 */
+	if (MyClientConnectionInfo.authn_id != NULL)
+	{
+		CredentialValidationType validation_type;
+
+		validation_type = UserAuthToValidationType(MyClientConnectionInfo.auth_method);
+
+		/*
+		 * If we have a valid validation type, get the corresponding
+		 * validator
+		 */
+		if (validation_type < CVT_COUNT)
+			validator = validators[validation_type];
+
+	}
+
+	/*
+	 * 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 */
+	elog(DEBUG1, "credential validation: validating auth_method=%d", (int) MyClientConnectionInfo.auth_method);
+
+	PG_TRY();
+	{
+		bool		result = validator();
+
+		status = result ? CVS_VALID : CVS_EXPIRED;
+	}
+	PG_CATCH();
+	{
+		/* Error during validation */
+		FlushErrorState();
+		status = CVS_ERROR;
+	}
+	PG_END_TRY();
+
+	return status;
+}
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 b3563113219..ce9f9cef5c5 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"
@@ -98,6 +99,25 @@ bool		Log_disconnections = false;
 
 int			log_statement = LOGSTMT_NONE;
 
+
+
+/*
+ * Function that performs credential validation when needed
+ * Uses a timer-based approach to periodically validate credentials
+ * during normal operation, skipping validation in bootstrapping.
+ */
+static void
+CheckAndExecuteCredentialValidation(void)
+{
+	CredentialValidationTimeoutPending = false;
+
+	/* Process credential validation */
+	ProcessCredentialValidation();
+
+	/* Re-enable the timeout for the next validation cycle */
+	EnableCredentialValidationTimeout();
+}
+
 /* wait N seconds to allow attach from a debugger */
 int			PostAuthDelay = 0;
 
@@ -1050,6 +1070,10 @@ exec_simple_query(const char *query_string)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * Zap any pre-existing unnamed statement.  (While not strictly necessary,
 	 * it seems best to define simple-Query mode as if it used the unnamed
@@ -1431,6 +1455,11 @@ exec_parse_message(const char *query_string,	/* string to execute */
 	 */
 	start_xact_command();
 
+
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * Switch to appropriate context for constructing parsetrees.
 	 *
@@ -1706,6 +1735,10 @@ exec_bind_message(StringInfo input_message)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2218,6 +2251,10 @@ exec_execute_message(const char *portal_name, long max_rows)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/*
 	 * If we re-issue an Execute protocol request against an existing portal,
 	 * then we are only fetching more rows rather than completely re-executing
@@ -2636,6 +2673,10 @@ exec_describe_statement_message(const char *stmt_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -2728,6 +2769,10 @@ exec_describe_portal_message(const char *portal_name)
 	 */
 	start_xact_command();
 
+	/* Check and potentially execute credential validation */
+	if (CredentialValidationTimeoutPending && IsNormalProcessingMode())
+		CheckAndExecuteCredentialValidation();
+
 	/* Switch back to message context */
 	MemoryContextSwitchTo(MessageContext);
 
@@ -4635,6 +4680,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
 			{
@@ -4687,6 +4737,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 */
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 36ad708b360..aab526a45dd 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 26118661f07..dceb7453013 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"
@@ -90,6 +91,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);
@@ -774,6 +776,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);
 	}
 
 	/*
@@ -1227,6 +1231,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
@@ -1433,6 +1443,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/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e4abe6c0077..2aef6f67309 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -904,6 +904,12 @@
 #include_if_exists = '...'              # include file only if it exists
 #include = '...'                        # include file
 
+#------------------------------------------------------------------------------
+# CREDENTIAL VALIDATION
+#------------------------------------------------------------------------------
+
+credential_validation.enabled = true  # enable periodic credential validation
+credential_validation.interval = 60    # validation 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..420183a1c7d
--- /dev/null
+++ b/src/include/libpq/auth-validate-methods.h
@@ -0,0 +1,25 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "utils/timestamp.h"
+
+/* Initialize all validation methods */
+extern void InitializeValidationMethods(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..ea08c52fdb9
--- /dev/null
+++ b/src/include/libpq/auth-validate.h
@@ -0,0 +1,64 @@
+/*-------------------------------------------------------------------------
+ *
+ * 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
+
+#include "libpq/libpq-be.h"
+#include "libpq/protocol.h"
+#include "postmaster/postmaster.h"
+#include "utils/guc.h"
+#include "utils/timeout.h"
+
+/* Define credential validation method types as an enum */
+typedef enum CredentialValidationType
+{
+	CVT_PASSWORD = 0,          /* All password-based methods (md5, scram, etc) */
+	CVT_OAUTH,                 /* OAuth bearer token 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);
+
+/* Credential validation status */
+typedef enum CredentialValidationStatus
+{
+	CVS_VALID,					/* Credentials are valid */
+	CVS_EXPIRED,				/* Credentials have expired */
+	CVS_ERROR					/* Error during validation */
+} CredentialValidationStatus;
+
+/* 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 */
+extern CredentialValidationStatus CheckCredentialValidity(void);
+
+/* Enable credential validation timeout timer */
+extern void EnableCredentialValidationTimeout(void);
+
+#endif							/* AUTH_VALIDATE_H */
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
index 4a822e9a1f2..2fd30c55814 100644
--- a/src/include/libpq/oauth.h
+++ b/src/include/libpq/oauth.h
@@ -64,6 +64,7 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
 typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
 									 const char *token, const char *role,
 									 ValidatorModuleResult *result);
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
 
 /*
  * Identifies the compiled ABI version of the validator module. Since the server
@@ -71,7 +72,9 @@ typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
  * versions, this is reserved for emergency use within a stable release line.
  * May it never need to change.
  */
-#define PG_OAUTH_VALIDATOR_MAGIC 0x20250220
+#define PG_OAUTH_VALIDATOR_MAGIC_V1 0x20250220
+#define PG_OAUTH_VALIDATOR_MAGIC_V2 0x20260326
+#define PG_OAUTH_VALIDATOR_MAGIC PG_OAUTH_VALIDATOR_MAGIC_V2
 
 typedef struct OAuthValidatorCallbacks
 {
@@ -80,6 +83,7 @@ typedef struct OAuthValidatorCallbacks
 	ValidatorStartupCB startup_cb;
 	ValidatorShutdownCB shutdown_cb;
 	ValidatorValidateCB validate_cb;
+	ValidatorExpireCB expire_cb;  /* Optional: Check token expiration */
 } OAuthValidatorCallbacks;
 
 /*
@@ -98,4 +102,8 @@ extern PGDLLIMPORT const pg_be_sasl_mech pg_be_oauth_mech;
  */
 extern bool check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg);
 
+/*
+ * Check OAuth token expiration using validator's expire_cb if available.
+ */
+bool CheckOAuthValidatorExpiration(void);
 #endif							/* PG_OAUTH_H */
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..42a09e2f299 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 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..6da639a416f
--- /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();
diff --git a/src/test/modules/oauth_validator/fail_validator.c b/src/test/modules/oauth_validator/fail_validator.c
index 3de0470a541..8754e1e8f85 100644
--- a/src/test/modules/oauth_validator/fail_validator.c
+++ b/src/test/modules/oauth_validator/fail_validator.c
@@ -29,6 +29,7 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 	PG_OAUTH_VALIDATOR_MAGIC,
 
 	.validate_cb = fail_token,
+	.expire_cb = NULL,
 };
 
 const OAuthValidatorCallbacks *
diff --git a/src/test/modules/oauth_validator/magic_validator.c b/src/test/modules/oauth_validator/magic_validator.c
index 550da41d11b..6e4d72fde30 100644
--- a/src/test/modules/oauth_validator/magic_validator.c
+++ b/src/test/modules/oauth_validator/magic_validator.c
@@ -30,6 +30,7 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 	0xdeadbeef,
 
 	.validate_cb = validate_token,
+	.expire_cb = NULL,
 };
 
 const OAuthValidatorCallbacks *
diff --git a/src/test/modules/oauth_validator/validator.c b/src/test/modules/oauth_validator/validator.c
index 0b983a9dc8f..2784708a784 100644
--- a/src/test/modules/oauth_validator/validator.c
+++ b/src/test/modules/oauth_validator/validator.c
@@ -34,7 +34,8 @@ static const OAuthValidatorCallbacks validator_callbacks = {
 
 	.startup_cb = validator_startup,
 	.shutdown_cb = validator_shutdown,
-	.validate_cb = validate_token
+	.validate_cb = validate_token,
+	.expire_cb = NULL,			/* Optional: not implemented */
 };
 
 /* GUCs */


^ permalink  raw  reply  [nested|flat] 11+ messages in thread


end of thread, other threads:[~2026-03-31 13:49 UTC | newest]

Thread overview: 11+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-02-17 06:59 Re: [OAuth2] Infrastructure for tracking token expiry time Zsolt Parragi <[email protected]>
2026-02-17 10:47 ` VASUKI M <[email protected]>
2026-02-18 08:38   ` Ajit Awekar <[email protected]>
2026-02-18 10:34     ` Daniel Gustafsson <[email protected]>
2026-02-18 12:04       ` Zsolt Parragi <[email protected]>
2026-02-18 16:30         ` Daniel Gustafsson <[email protected]>
2026-02-18 16:58           ` Zsolt Parragi <[email protected]>
2026-02-20 09:42             ` Ajit Awekar <[email protected]>
2026-03-16 13:57               ` Ajit Awekar <[email protected]>
2026-03-27 10:59                 ` Ajit Awekar <[email protected]>
2026-03-31 13:49                   ` Ajit Awekar <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox