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