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 validate_cb callback is required, the others
are optional.
+
+ The magic field identifies the ABI version of the module.
+ The server supports both PG_OAUTH_VALIDATOR_MAGIC_V1 (the
+ original version without expire_cb) and
+ PG_OAUTH_VALIDATOR_MAGIC_V2 (which adds
+ expire_cb). New modules should use
+ PG_OAUTH_VALIDATOR_MAGIC, which always refers to the
+ latest version.
+
@@ -412,5 +422,27 @@ typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
+
+ Expire Callback
+
+ The expire_cb 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.
+
+typedef bool (*ValidatorExpireCB) (const ValidatorModuleState *state);
+
+ The callback should return true if the token is still
+ valid, or false if the token has expired. If this
+ callback is not provided (set to NULL), the server assumes the token
+ remains valid.
+
+
+ This callback was added in PG_OAUTH_VALIDATOR_MAGIC_V2.
+ Modules compiled against the older PG_OAUTH_VALIDATOR_MAGIC_V1
+ do not have this field, and the server will not attempt to call it for
+ such modules.
+
+
+
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 */