Pasword expiration warning

Started by Gilles Daroldabout 4 years ago28 messages
#1Gilles Darold
gilles@migops.com

Hi all,

Now that the security policy is getting stronger, it is not uncommon to
create users with a password expiration date (VALID UNTIL). The problem
is that the user is only aware that his password has expired when he can
no longer log in unless the application with which he is connecting
notifies him beforehand.

I'm wondering if we might be interested in having this feature in psql?
For example for a user whose password expires in 3 days:

gilles=# CREATE ROLE foo LOGIN PASSWORD 'foo' VALID UNTIL '2021-11-22';
CREATE ROLE
gilles=# \c - foo
Password for user foo:
psql (15devel, server 14.1 (Ubuntu 14.1-2.pgdg20.04+1))
** Warning: your password expires in 3 days **
You are now connected to database "gilles" as user "foo".

My idea is to add a psql variable that can be defined in psqlrc to
specify the number of days before the user password expires to start
printing a warning. The warning message is only diplayed in interactive
mode Example:

$ cat /etc/postgresql-common/psqlrc
\set PASSWORD_EXPIRE_WARNING 7

Default value is 0 like today no warning at all.

Of course any other client application have to write his own beforehand
expiration notice but with psql we don't have it for the moment. If
there is interest for this psql feature I can post the patch.

--
Gilles Darold

#2Dinesh Chemuduru
dinesh.kumar@migops.com
In reply to: Gilles Darold (#1)
Re: Pasword expiration warning

On Fri, 19 Nov 2021 at 20:19, Gilles Darold <gilles@migops.com> wrote:

Hi all,

Now that the security policy is getting stronger, it is not uncommon to
create users with a password expiration date (VALID UNTIL). The problem
is that the user is only aware that his password has expired when he can no
longer log in unless the application with which he is connecting notifies
him beforehand.

I'm wondering if we might be interested in having this feature in psql? For
example for a user whose password expires in 3 days:

gilles=# CREATE ROLE foo LOGIN PASSWORD 'foo' VALID UNTIL '2021-11-22';
CREATE ROLE
gilles=# \c - foo
Password for user foo:
psql (15devel, server 14.1 (Ubuntu 14.1-2.pgdg20.04+1))
** Warning: your password expires in 3 days **
You are now connected to database "gilles" as user "foo".

My idea is to add a psql variable that can be defined in psqlrc to specify
the number of days before the user password expires to start printing a
warning. The warning message is only diplayed in interactive mode Example:

$ cat /etc/postgresql-common/psqlrc
\set PASSWORD_EXPIRE_WARNING 7

+1

It is useful to notify the users about their near account expiration,
and we are doing that at client level.

Default value is 0 like today no warning at all.

Show quoted text

Of course any other client application have to write his own beforehand expiration
notice but with psql we don't have it for the moment. If there is interest
for this psql feature I can post the patch.

--
Gilles Darold

#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Gilles Darold (#1)
Re: Pasword expiration warning

Gilles Darold <gilles@migops.com> writes:

Now that the security policy is getting stronger, it is not uncommon to
create users with a password expiration date (VALID UNTIL).

TBH, I thought people were starting to realize that forced password
rotations are a net security negative. It's true that a lot of
places haven't gotten the word yet.

I'm wondering if we might be interested in having this feature in psql?

This proposal kind of seems like a hack, because
(1) not everybody uses psql
(2) psql can't really tell whether rolvaliduntil is relevant.
(It can see whether the server demanded a password, but
maybe that went to LDAP or some other auth method.)

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

Default value is 0 like today no warning at all.

Off-by-default is pretty much guaranteed to not help most people.

regards, tom lane

#4Gilles Darold
gilles@migops.com
In reply to: Tom Lane (#3)
Re: Pasword expiration warning

Le 19/11/2021 à 16:55, Tom Lane a écrit :

Gilles Darold <gilles@migops.com> writes:

Now that the security policy is getting stronger, it is not uncommon to
create users with a password expiration date (VALID UNTIL).

TBH, I thought people were starting to realize that forced password
rotations are a net security negative. It's true that a lot of
places haven't gotten the word yet.

I'm wondering if we might be interested in having this feature in psql?

This proposal kind of seems like a hack, because
(1) not everybody uses psql

Yes, for me it's a comfort feature. When a user connect to a PG backend
using an account that have expired you have no information that the
problem is a password expiration. The message returned to the user is
just: "FATAL: password authentication failed for user "foo".  We had to
verify in the log file that the problem is related to "DETAIL:  User
"foo" has an expired password.".  If the user was warned beforehand to
change the password it will probably saves me some time.

(2) psql can't really tell whether rolvaliduntil is relevant.
(It can see whether the server demanded a password, but
maybe that went to LDAP or some other auth method.)

I agree, I hope that in case of external authentication rolvaliduntil is
not set and in this case I guess that there is other notification
channels to inform the user that his password will expire. Otherwise yes
the warning message could be a false positive but the rolvaliduntil can
be changed to infinity to fix this case.

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I think that this is the responsibility of the client to display a
warning when the password is about to expire, the backend could help the
application by sending a NOTICE but the application will still have to
report the notice. I mean that it can continue to do all the work to
verify that the password is about to expire.

Default value is 0 like today no warning at all.

Off-by-default is pretty much guaranteed to not help most people.

Right, I was thinking of backward compatibility but this does not apply
here. So default to 7 days will be better.

To sum up as I said on top this is just a comfort notification dedicated
to psql and for local pg account to avoid looking at log file for
forgetting users.

--
Gilles Darold

#5Bossart, Nathan
bossartn@amazon.com
In reply to: Gilles Darold (#4)
Re: Pasword expiration warning

On 11/19/21, 7:56 AM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

Nathan

#6Michael Paquier
michael@paquier.xyz
In reply to: Bossart, Nathan (#5)
Re: Pasword expiration warning

On Sat, Nov 20, 2021 at 12:17:53AM +0000, Bossart, Nathan wrote:

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

ClientAuthentication_hook is called before the user is informed of the
authentication result, FWIW, so that does not seem wise.
--
Michael

#7Andrew Dunstan
andrew@dunslane.net
In reply to: Bossart, Nathan (#5)
Re: Pasword expiration warning

On 11/19/21 19:17, Bossart, Nathan wrote:

On 11/19/21, 7:56 AM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

+1 for a server side solution. The people most likely to benefit from
this are the people least likely to be using psql IMNSHO.

cheers

andrew

--
Andrew Dunstan
EDB: https://www.enterprisedb.com

#8Gilles Darold
gilles@migops.com
In reply to: Andrew Dunstan (#7)
Re: Pasword expiration warning

Le 20/11/2021 à 14:48, Andrew Dunstan a écrit :

On 11/19/21 19:17, Bossart, Nathan wrote:

On 11/19/21, 7:56 AM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

+1 for a server side solution. The people most likely to benefit from
this are the people least likely to be using psql IMNSHO.

Ok, I can try to implement something at server side using a NOTICE message.

--
Gilles Darold

#9Gilles Darold
gilles@darold.net
In reply to: Gilles Darold (#8)
1 attachment(s)
Re: Pasword expiration warning

Le 21/11/2021 à 10:49, Gilles Darold a écrit :

Le 20/11/2021 à 14:48, Andrew Dunstan a écrit :

On 11/19/21 19:17, Bossart, Nathan wrote:

On 11/19/21, 7:56 AM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

+1 for a server side solution. The people most likely to benefit from
this are the people least likely to be using psql IMNSHO.

Ok, I can try to implement something at server side using a NOTICE message.

Hi,

Sorry to resurrect this old thread, but I had completely forgotten about
it. If there's still interest in this feature, then please find in
attachment a patch to emit a warning to the client and into the logs
when the password will expire within 7 days by default. A GUC,
password_expire_warning, allow to change the number of days before
sending the message or to disable this feature with setting value 0.

I have chosen to add a new field, const char *warning_message, to struct
ClientConnectionInfo so that it can be used to send other messages to
the client at end of connection ( src/backend/utils/init/postinit.c:
InitPostgres() ). Not sure sure that this is the best way to do that but
as it is a message dedicated to the connection I've though it could be
the right place. If we don't expect other warning message sent to the
client at connection time, just using an integer for the number of days
remaining will be enough. We could use notice but it is not logged by
default and also I think that warning is the good level for this message.

Output at psql connection:

        $ /usr/local/pgsql/bin/psql -h localhost -U test -d postgres
        Password for user test:
        WARNING:  your password will expire in 4 days
        psql (19devel)
        Type "help" for help.

        postgres=>

Output in the log:

        2026-01-05 23:23:13.763 CET [136001] WARNING:  your password
will expire in 4 days

Using a script:

        $ perl test_conn.pl
        WARNING:  your password will expire in 3 days

The message can be handled by any client application to warn the user if
required.

Thanks in advance for your feedback and suggestion for a better
implementation.

Best regards,

--
Gilles Darold
http://hexacluster.ai/

Attachments:

password_expire_warning-v1.patchtext/x-patch; charset=UTF-8; name=password_expire_warning-v1.patchDownload
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..e93a9059f58 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 7;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -80,6 +84,22 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;
 	}
 
+	/*
+	 * Password OK, but check if rolvaliduntil is less than GUC
+	 * password_expire_warning days to send a warning to the client
+	 */
+	if (!isnull && password_expire_warning > 0)
+	{
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+		result /= 86400; /* in days */
+
+		if ((int) result <= password_expire_warning)
+			MyClientConnectionInfo.warning_message =
+					psprintf("your password will expire in %d days", (int) result);
+	}
+
 	return shadow_pass;
 }
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..356c8ffe215 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1232,6 +1232,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
+
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+        if (MyClientConnectionInfo.warning_message)
+                ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
 }
 
 /*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..b7c42da28da 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '7',
+  min => '0',
+  max => '30',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..e92f4f151c9 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4ed61921ccd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
#10Japin Li
japinli@hotmail.com
In reply to: Gilles Darold (#9)
1 attachment(s)
Re: Pasword expiration warning

Hi, Gilles Darold

On Tue, 06 Jan 2026 at 15:43, Gilles Darold <gilles@darold.net> wrote:

Le 21/11/2021 à 10:49, Gilles Darold a écrit :

Le 20/11/2021 à 14:48, Andrew Dunstan a écrit :

On 11/19/21 19:17, Bossart, Nathan wrote:

On 11/19/21, 7:56 AM, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:

That leads me to wonder about server-side solutions. It's easy
enough for the server to see that it's used a password with an
expiration N days away, but how could that be reported to the
client? The only idea that comes to mind that doesn't seem like
a protocol break is to issue a NOTICE message, which doesn't
seem like it squares with your desire to only do this interactively.
(Although I'm not sure I believe that's a great idea. If your
application breaks at 2AM because its password expired, you
won't be any happier than if your interactive sessions start to
fail. Maybe a message that would leave a trail in the server log
would be best after all.)

I bet it's possible to use the ClientAuthentication_hook for this. In
any case, I agree that it probably belongs server-side so that other
clients can benefit from this.

+1 for a server side solution. The people most likely to benefit from
this are the people least likely to be using psql IMNSHO.

Ok, I can try to implement something at server side using a NOTICE message.

Hi,

Sorry to resurrect this old thread, but I had completely forgotten
about it. If there's still interest in this feature, then please find
in attachment a patch to emit a warning to the client and into the
logs when the password will expire within 7 days by default. A GUC,
password_expire_warning, allow to change the number of days before
sending the message or to disable this feature with setting value 0.

I have chosen to add a new field, const char *warning_message, to
struct ClientConnectionInfo so that it can be used to send other
messages to the client at end of connection (
src/backend/utils/init/postinit.c: InitPostgres() ). Not sure sure
that this is the best way to do that but as it is a message dedicated
to the connection I've though it could be the right place. If we don't
expect other warning message sent to the client at connection time,
just using an integer for the number of days remaining will be
enough. We could use notice but it is not logged by default and also I
think that warning is the good level for this message.

Output at psql connection:

        $ /usr/local/pgsql/bin/psql -h localhost -U test -d postgres
        Password for user test:
        WARNING:  your password will expire in 4 days
        psql (19devel)
        Type "help" for help.

        postgres=>

Output in the log:

        2026-01-05 23:23:13.763 CET [136001] WARNING:  your password
will expire in 4 days

Using a script:

        $ perl test_conn.pl
        WARNING:  your password will expire in 3 days

The message can be handled by any client application to warn the user
if required.

Thanks in advance for your feedback and suggestion for a better
implementation.

Thanks for updating the patch.

1.
$ git apply ~/password_expire_warning-v1.patch
/home/japin/password_expire_warning-v1.patch:71: indent with spaces.
if (MyClientConnectionInfo.warning_message)
/home/japin/password_expire_warning-v1.patch:72: indent with spaces.
ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
warning: 2 lines add whitespace errors.

2.
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '7',
+  min => '0',
+  max => '30',
+},
+

The GUC_UNIT_S flag specifies that the unit is seconds, meaning the default
value of 7 corresponds to 7 seconds rather than 7 days. For example:

# ALTER SYSTEM SET password_expire_warning TO 60;
ERROR: 60 s is outside the valid range for parameter "password_expire_warning" (0 s .. 30 s)

3.
I tested the patch and only received an empty WARNING message. After some
analysis, I found that the warning_message buffer is likely freed after
transaction commit.

Here's a new patch that resolves the issues mentioned earlier.

Furthermore, if the remaining time until expiration is less than one day,
should we display it in minutes (or hours) instead of 0 days?

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

Attachments:

v2-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-diffDownload
From a9f38f5c03cd3ccf492848a47b690b14e882dff6 Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Wed, 7 Jan 2026 12:58:41 +0800
Subject: [PATCH v2] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 src/backend/libpq/crypt.c                 | 20 ++++++++++++++++++++
 src/backend/utils/init/miscinit.c         |  1 +
 src/backend/utils/init/postinit.c         |  7 +++++++
 src/backend/utils/misc/guc_parameters.dat |  9 +++++++++
 src/include/libpq/crypt.h                 |  3 +++
 src/include/libpq/libpq-be.h              |  9 +++++++++
 6 files changed, 49 insertions(+)

diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..315c559e4ac 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -80,6 +84,22 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;
 	}
 
+	/*
+	 * Password OK, but check if rolvaliduntil is less than GUC
+	 * password_expire_warning days to send a warning to the client
+	 */
+	if (!isnull && password_expire_warning > 0)
+	{
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+		result /= 86400; /* in days */
+
+		if ((int) result <= password_expire_warning)
+			MyClientConnectionInfo.warning_message =
+					psprintf("your password will expire in %d days", (int) result);
+	}
+
 	return shadow_pass;
 }
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..1f9929e7eaf 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..e92f4f151c9 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4ed61921ccd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#11songjinzhou
tsinghualucky912@foxmail.com
In reply to: Japin Li (#10)
1 attachment(s)
Re: Pasword expiration warning

Hello fellow hackers, I've refined the print information based on the japin code above, but otherwise remained unchanged. The result is as follows:

[postgres@localhost:~/test/bin]$ ./psql -p 5432 -U test_user2 -d postgres
Password for user test_user2:
2026-01-07 00:28:25.999 PST [82198] WARNING:  your password will expire in 7 hours and 31 minutes
WARNING:  your password will expire in 7 hours and 31 minutes
psql (19devel)
Type "help" for help.

postgres=> \q
[postgres@localhost:~/test/bin]$ ./psql -p 5432 -U test_user3 -d postgres
Password for user test_user3:
2026-01-07 00:28:33.998 PST [82282] WARNING:  your password will expire in 2 days and 7 hours
WARNING:  your password will expire in 2 days and 7 hours
psql (19devel)
Type "help" for help.

postgres=> \q
[postgres@localhost:~/test/bin]$

Thanks

songjinzhou
tsinghualucky912@foxmail.com

Attachments:

v3-0001-Add-password_expire_warning-GUC-to-warn-clients.patchapplication/octet-stream; charset=utf-8; name=v3-0001-Add-password_expire_warning-GUC-to-warn-clients.patchDownload
From 908591a520c72288f8abb594c62da2cc95a7443f Mon Sep 17 00:00:00 2001
From: songjinzhou <tsinghualucky912@foxmail.com>
Date: Wed, 7 Jan 2026 00:14:43 -0800
Subject: [PATCH] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 src/backend/libpq/crypt.c                 | 60 +++++++++++++++++++++++
 src/backend/utils/init/miscinit.c         |  1 +
 src/backend/utils/init/postinit.c         |  7 +++
 src/backend/utils/misc/guc_parameters.dat |  9 ++++
 src/include/libpq/crypt.h                 |  3 ++
 src/include/libpq/libpq-be.h              |  9 ++++
 6 files changed, 89 insertions(+)

diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d4..1022f6cc8c 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -80,6 +84,62 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;
 	}
 
+	/*
+	 * Password OK, but check if rolvaliduntil is less than GUC
+	 * password_expire_warning days to send a warning to the client
+	 */
+	if (!isnull && password_expire_warning > 0)
+	{
+		float8          result;
+		int64           remaining_seconds;
+
+		remaining_seconds = (vuntil - GetCurrentTimestamp()) / 1000000; /* in seconds */
+		result = (float8)remaining_seconds / 86400; /* in days */
+
+		if ((int) result <= password_expire_warning)
+		{
+			if (remaining_seconds >= 86400) /* 1 day or more */
+			{
+				int days = (int)result;
+				int hours = (int)((remaining_seconds % 86400) / 3600);
+				if (hours > 0)
+					MyClientConnectionInfo.warning_message =
+							psprintf("your password will expire in %d day%s and %d hour%s",
+									days, (days != 1) ? "s" : "",
+									hours, (hours != 1) ? "s" : "");
+				else
+					MyClientConnectionInfo.warning_message =
+							psprintf("your password will expire in %d day%s",
+									days, (days != 1) ? "s" : "");
+			}
+			else if (remaining_seconds >= 3600) /* 1 hour or more but less than 1 day */
+			{
+				int hours = (int)(remaining_seconds / 3600);
+				int minutes = (int)((remaining_seconds % 3600) / 60);
+				if (minutes > 0)
+					MyClientConnectionInfo.warning_message =
+							psprintf("your password will expire in %d hour%s and %d minute%s",
+									hours, (hours != 1) ? "s" : "",
+									minutes, (minutes != 1) ? "s" : "");
+				else
+					MyClientConnectionInfo.warning_message =
+							psprintf("your password will expire in %d hour%s",
+									hours, (hours != 1) ? "s" : "");
+			}
+			else /* less than 1 hour */
+			{
+				int minutes = (int)(remaining_seconds / 60);
+				int seconds = (int)(remaining_seconds % 60);
+				if (minutes == 0 && seconds > 0)
+					minutes = 1; /* show at least 1 minute if there's any time left */
+
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d minute%s",
+								minutes, (minutes != 1) ? "s" : "");
+			}
+		}
+	}
+
 	return shadow_pass;
 }
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374f..24737c95c2 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d..d0d5c87d4e 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b12556..1f9929e7ea 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e109..e92f4f151c 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4f..4ed61921cc 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#12Gilles Darold
gilles@darold.net
In reply to: Japin Li (#10)
1 attachment(s)
Re: Pasword expiration warning

Le 07/01/2026 à 06:12, Japin Li a écrit :

Thanks for updating the patch.

1.
$ git apply ~/password_expire_warning-v1.patch
/home/japin/password_expire_warning-v1.patch:71: indent with spaces.
if (MyClientConnectionInfo.warning_message)
/home/japin/password_expire_warning-v1.patch:72: indent with spaces.
ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
warning: 2 lines add whitespace errors.

2.
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '7',
+  min => '0',
+  max => '30',
+},
+

The GUC_UNIT_S flag specifies that the unit is seconds, meaning the default
value of 7 corresponds to 7 seconds rather than 7 days. For example:

# ALTER SYSTEM SET password_expire_warning TO 60;
ERROR: 60 s is outside the valid range for parameter "password_expire_warning" (0 s .. 30 s)

3.
I tested the patch and only received an empty WARNING message. After some
analysis, I found that the warning_message buffer is likely freed after
transaction commit.

Here's a new patch that resolves the issues mentioned earlier.

Furthermore, if the remaining time until expiration is less than one day,
should we display it in minutes (or hours) instead of 0 days?

Thanks for the patch review and improvement.

I missed the GUC_UNIT_S c/p but we can use second as unit. I have fixed
your patch update because in this case the result variable must not be
turned into days but kept in seconds to be compared to the GUC value.

I have also added the missing GUC in the sample configuration file.

I'm not in favor of a high granularity in time display, number of days
for me is enough. I the user have the chance to see the 0 day remaining
he knows that he must fix that immediately. But why not, it depends of a
consensus.

Thanks.

--
Gilles Darold
http://hexaculter.ai/

Attachments:

v4-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-patch; charset=UTF-8; name=v4-0001-Add-password_expire_warning-GUC-to-warn-clients.patchDownload
From 8eed415101585e98b1b2cdbb3714a707302f8d7c Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Wed, 7 Jan 2026 15:36:46 +0100
Subject: [PATCH v4] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 src/backend/libpq/crypt.c                     | 19 +++++++++++++++++++
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 +++++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 +++
 src/include/libpq/libpq-be.h                  |  9 +++++++++
 7 files changed, 49 insertions(+)

diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..1f8bce00b16 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -80,6 +84,21 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;
 	}
 
+	/*
+	 * Password OK, but check if rolvaliduntil is less than GUC
+	 * password_expire_warning days to send a warning to the client
+	 */
+	if (!isnull && password_expire_warning > 0)
+	{
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+
+		if ((int) result <= password_expire_warning)
+			MyClientConnectionInfo.warning_message =
+					psprintf("your password will expire in %d days", (int) (result / 86400));
+	}
+
 	return shadow_pass;
 }
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..1f9929e7eaf 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..e92f4f151c9 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4ed61921ccd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#13Gilles Darold
gillesdarold@gmail.com
In reply to: songjinzhou (#11)
Re: Pasword expiration warning

Le 07/01/2026 à 09:37, songjinzhou a écrit :

Hello fellow hackers, I've refined the print information based on the japin code above, but otherwise remained unchanged. The result is as follows:

[postgres@localhost:~/test/bin]$ ./psql -p 5432 -U test_user2 -d postgres
Password for user test_user2:
2026-01-07 00:28:25.999 PST [82198] WARNING:  your password will expire in 7 hours and 31 minutes
WARNING:  your password will expire in 7 hours and 31 minutes
psql (19devel)
Type "help" for help.

postgres=> \q
[postgres@localhost:~/test/bin]$ ./psql -p 5432 -U test_user3 -d postgres
Password for user test_user3:
2026-01-07 00:28:33.998 PST [82282] WARNING:  your password will expire in 2 days and 7 hours
WARNING:  your password will expire in 2 days and 7 hours
psql (19devel)
Type "help" for help.

postgres=> \q
[postgres@localhost:~/test/bin]$

Thanks

songjinzhou
tsinghualucky912@foxmail.com

I'm not in favor of a higher granularity like explained in my previous
answer, I don't see it as a countdown to the second. But why not if
there's more people in favor of a more detailed remaining time, your
improvement will be welcome.

#14songjinzhou
tsinghualucky912@foxmail.com
In reply to: Gilles Darold (#13)
Re: Pasword expiration warning

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple countdown. I did think it would be abrupt for users to see "0 days". I checked v4, and I think it's fine. (PS: Should we add relevant explanations to the SGML?) Thank you.

songjinzhou
tsinghualucky912@foxmail.com

#15Japin Li
japinli@hotmail.com
In reply to: songjinzhou (#14)
Re: Pasword expiration warning

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

#16Gilles Darold
gilles@darold.net
In reply to: Japin Li (#15)
1 attachment(s)
Re: Pasword expiration warning

Le 08/01/2026 à 04:37, Japin Li a écrit :

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

Here is a new version of the patch that adds the documentation for the
new GUC, fix the warning message (days(s) instead of days) and handle
the 'Infinity' value for the VALID UNTIL clause.

--
Gilles Darold
http://hexacluster.ai/

Attachments:

v5-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-patch; charset=UTF-8; name=v5-0001-Add-password_expire_warning-GUC-to-warn-clients.patchDownload
From 3361b5260ab4d432223012d14991742bcc653d70 Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Thu, 8 Jan 2026 06:55:44 +0100
Subject: [PATCH v5] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 19 ++++++++++++++++++-
 src/backend/libpq/crypt.c                     | 19 +++++++++++++++++++
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 +++++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 +++
 src/include/libpq/libpq-be.h                  |  9 +++++++++
 8 files changed, 67 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..3384347a822 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,8 +1106,25 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warnings" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how many days before a role's password expiration a <literal>WARNING</literal>
+        message is sent to the client  upon successful connection. It requires that
+        a <command>VALID UNTIL</command> date is set for the user. A value of <literal>0d</literal>
+        disable this behavior. The default value is <literal>7d</literal>. 
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
-      <term><varname>password_encryption</varname> (<type>enum</type>)
+      <term><varname>password_encryption</varname> (<type>enum</type>
+)
       <indexterm>
        <primary><varname>password_encryption</varname> configuration parameter</primary>
       </indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..47a9d425a71 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -80,6 +84,21 @@ get_role_password(const char *role, const char **logdetail)
 		return NULL;
 	}
 
+	/*
+	 * Password OK, but check if rolvaliduntil is less than GUC
+	 * password_expire_warning days to send a warning to the client
+	 */
+	if (!isnull && password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+	{
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+
+		if ((int) result <= password_expire_warning)
+			MyClientConnectionInfo.warning_message =
+					psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+	}
+
 	return shadow_pass;
 }
 
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..1f9929e7eaf 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..e92f4f151c9 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4ed61921ccd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#17liu xiaohui
liuxh.zj.cn@gmail.com
In reply to: Gilles Darold (#16)
回复: Pasword expiration warning

________________________________
发件人: Gilles Darold <gilles@darold.net>
发送时间: 2026年1月8日 14:04
收件人: Japin Li <japinli@hotmail.com>; songjinzhou <tsinghualucky912@foxmail.com>
抄送: Gilles Darold <gilles@darold.net>; PostgreSQL Hackers <pgsql-hackers@postgresql.org>; Andrew Dunstan <andrew@dunslane.net>; Bossart, Nathan <bossartn@amazon.com>; Tom Lane <tgl@sss.pgh.pa.us>
主题: Re: Pasword expiration warning

Le 08/01/2026 à 04:37, Japin Li a écrit :

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

Here is a new version of the patch that adds the documentation for the
new GUC, fix the warning message (days(s) instead of days) and handle
the 'Infinity' value for the VALID UNTIL clause.

--
Gilles Darold
http://hexacluster.ai/

Dear Gilles,

Thank you for submitting the v5 patch to add the password_expire_warning GUC.
The feature is useful and the implementation is mostly solid.
I reviewed the patch with a particular focus on the comments and documentation,
and I noticed several inconsistencies between the comments, the documentation,
and the actual code that could confuse future maintainers or users.

Here are the main issues I found:

1. /doc/src/sgml/config.sgml
There is an unrelated change in config.sgml around the password_encryption parameter:
the closing parenthesis of the <type> tag was split onto its own line, resulting in an isolated ")".

This appears to be an accidental editing artifact and is not required for the new feature.
It should be reverted to keep the documentation formatting consistent with the rest of the file.

2. Comments referring to "days" while the internal variable uses seconds:

src/backend/libpq/crypt.c:
/* Emit a warning 7 days before password expiration */

These hard-code "7 days" may be no longer accurate once the value becomes configurable.
Better comments would be:

/* Threshold (in seconds) before password expiration to emit a warning at login (0 = disabled; default 7 days) */

3. Minor typo/grammar in src/include/libpq/libpq-be.h
In the comment for warning_message:
"... at enf of InitPostgres(). ... it is use to show the password expiration warning."
Should be: "at the end of InitPostgres()" and "it is used to show".

Overall the implementation works correctly, but aligning all comments and documentation with the actual units (seconds internally, days for users) would greatly improve clarity.

Best regards,
Xiaohui Liu

#18Japin Li
japinli@hotmail.com
In reply to: Gilles Darold (#16)
1 attachment(s)
Re: Pasword expiration warning

On Thu, 08 Jan 2026 at 07:04, Gilles Darold <gilles@darold.net> wrote:

Le 08/01/2026 à 04:37, Japin Li a écrit :

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

Here is a new version of the patch that adds the documentation for the
new GUC, fix the warning message (days(s) instead of days) and handle
the 'Infinity' value for the VALID UNTIL clause.

Thanks for updating the patch.

1.
I noticed a warning when applying the patch.

Applying: Add password_expire_warning GUC to warn clients
.git/rebase-apply/patch:31: trailing whitespace.
disable this behavior. The default value is <literal>7d</literal>.
warning: 1 line adds whitespace errors.

2.
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
-      <term><varname>password_encryption</varname> (<type>enum</type>)
+      <term><varname>password_encryption</varname> (<type>enum</type>
+)

I think this modification isn't necessary.

3.
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+

Perhaps we could use TimestampTz for the result variable and substitute
USECS_PER_SEC for 1000000.0—that would avoid the unnecessary type cast.

4.
+ if ((int) result <= password_expire_warning)

If the result exceeds INT_MAX, it triggers undefined behavior (IIRC).
Therefore, we should probably cast password_expire_warning itself.

5.
With this feature, GetCurrentTimestamp() might end up being called twice.
Perhaps we can avoid that duplication.

Attached is v6 of the patch addressing the issues above. Please take a look.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

Attachments:

v6-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-diffDownload
From 9716ba56b0866e87dee6ab30462df634b4afcd33 Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Thu, 8 Jan 2026 06:55:44 +0100
Subject: [PATCH v6] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 16 +++++++++
 src/backend/libpq/crypt.c                     | 36 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 +++++
 8 files changed, 75 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 601aa3afb8e..9bc1723a2b2 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1104,6 +1104,22 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warnings" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how many days before a role's password expiration a <literal>WARNING</literal>
+        message is sent to the client  upon successful connection. It requires that
+        a <command>VALID UNTIL</command> date is set for the user. A value of <literal>0d</literal>
+        disable this behavior. The default value is <literal>7d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..35a876eea5d 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Emit a warning 7 days before password expiration */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +74,32 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz	now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..1f9929e7eaf 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets the number of days before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..e92f4f151c9 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of days before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4ed61921ccd 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client at end
+	 * of the connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(). For example, it is use to show the
+	 * password expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#19Gilles Darold
gilles@darold.net
In reply to: Japin Li (#18)
1 attachment(s)
Re: Pasword expiration warning

Le 08/01/2026 à 08:43, Japin Li a écrit :

On Thu, 08 Jan 2026 at 07:04, Gilles Darold <gilles@darold.net> wrote:

Le 08/01/2026 à 04:37, Japin Li a écrit :

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

Here is a new version of the patch that adds the documentation for the
new GUC, fix the warning message (days(s) instead of days) and handle
the 'Infinity' value for the VALID UNTIL clause.

Thanks for updating the patch.

1.
I noticed a warning when applying the patch.

Applying: Add password_expire_warning GUC to warn clients
.git/rebase-apply/patch:31: trailing whitespace.
disable this behavior. The default value is <literal>7d</literal>.
warning: 1 line adds whitespace errors.

2.
<varlistentry id="guc-password-encryption" xreflabel="password_encryption">
-      <term><varname>password_encryption</varname> (<type>enum</type>)
+      <term><varname>password_encryption</varname> (<type>enum</type>
+)

I think this modification isn't necessary.

3.
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+

Perhaps we could use TimestampTz for the result variable and substitute
USECS_PER_SEC for 1000000.0—that would avoid the unnecessary type cast.

4.
+ if ((int) result <= password_expire_warning)

If the result exceeds INT_MAX, it triggers undefined behavior (IIRC).
Therefore, we should probably cast password_expire_warning itself.

5.
With this feature, GetCurrentTimestamp() might end up being called twice.
Perhaps we can avoid that duplication.

Attached is v6 of the patch addressing the issues above. Please take a look.

Thanks Japin, the implementation is fully working using the TimestampTz
cast.

I've attached a new patch to fix documentation and comments reported by
liu xiaohui and create a commitfest entry :
https://commitfest.postgresql.org/patch/6381/ Every one involved in the
review should edit the commitfest entry.

--
Gilles Darold
http://hexacluster.ai/

Attachments:

v7-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-patch; charset=UTF-8; name=v7-0001-Add-password_expire_warning-GUC-to-warn-clients.patchDownload
From 81de8df39982df5724ce51ea88bc29a28e0005f1 Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Thu, 8 Jan 2026 09:30:45 +0100
Subject: [PATCH v7] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 16 ++++++++
 src/backend/libpq/crypt.c                     | 39 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 +++++
 8 files changed, 78 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..483aa8f8026 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,22 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warnings" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how many time before a role's password expiration a <literal>WARNING</literal>
+        message is sent to the client upon successful connection. It requires that
+        a <command>VALID UNTIL</command> date is set for the user. A value of <literal>0d</literal>
+        disable this behavior. The default value is <literal>7d</literal> and the max value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..147c8834380 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +77,32 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz	now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..15e0d10e162 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how many time before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#20Japin Li
japinli@hotmail.com
In reply to: Gilles Darold (#19)
Re: Pasword expiration warning

On Thu, 08 Jan 2026 at 09:41, Gilles Darold <gilles@darold.net> wrote:

Le 08/01/2026 à 08:43, Japin Li a écrit :

On Thu, 08 Jan 2026 at 07:04, Gilles Darold <gilles@darold.net> wrote:

Le 08/01/2026 à 04:37, Japin Li a écrit :

On Thu, 08 Jan 2026 at 10:57, "songjinzhou" <tsinghualucky912@foxmail.com> wrote:

Hi, Gilles Darold

First of all, thank you for your reply. This is indeed not a simple
countdown. I did think it would be abrupt for users to see "0 days". I
checked v4, and I think it's fine. (PS: Should we add relevant
explanations to the SGML?) Thank you.

I'd like to hear more opinions on this.

Here is a new version of the patch that adds the documentation for the
new GUC, fix the warning message (days(s) instead of days) and handle
the 'Infinity' value for the VALID UNTIL clause.

Thanks for updating the patch.

1.
I noticed a warning when applying the patch.

Applying: Add password_expire_warning GUC to warn clients
.git/rebase-apply/patch:31: trailing whitespace.
disable this behavior. The default value is <literal>7d</literal>.
warning: 1 line adds whitespace errors.

2.
<varlistentry id="guc-password-encryption" xreflabel="password_encryption">
-      <term><varname>password_encryption</varname> (<type>enum</type>)
+      <term><varname>password_encryption</varname> (<type>enum</type>
+)

I think this modification isn't necessary.

3.
+		float8          result;
+
+		result = ((float8) (vuntil - GetCurrentTimestamp())) / 1000000.0; /* in seconds */
+

Perhaps we could use TimestampTz for the result variable and substitute
USECS_PER_SEC for 1000000.0—that would avoid the unnecessary type cast.

4.
+ if ((int) result <= password_expire_warning)

If the result exceeds INT_MAX, it triggers undefined behavior (IIRC).
Therefore, we should probably cast password_expire_warning itself.

5.
With this feature, GetCurrentTimestamp() might end up being called twice.
Perhaps we can avoid that duplication.

Attached is v6 of the patch addressing the issues above. Please take a look.

Thanks Japin, the implementation is fully working using the
TimestampTz cast.

I've attached a new patch to fix documentation and comments reported
by liu xiaohui and create a commitfest entry :
https://commitfest.postgresql.org/patch/6381/ Every one involved in
the review should edit the commitfest entry.

A minor nitpick:

1.
+     <varlistentry id="guc-password-expire-warnings" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)

We should probably use guc-password-expire-warning as the ID, since the GUC is
named password_expire_warning (singular).

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

#21Gilles Darold
gilles@darold.net
In reply to: Japin Li (#20)
1 attachment(s)
Re: Pasword expiration warning

Le 08/01/2026 à 10:27, Japin Li a écrit :

We should probably use guc-password-expire-warning as the ID, since the GUC is
named password_expire_warning (singular).

Patch updated.

--
Gilles Darold
http://www.darold.net/

Attachments:

v8-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-patch; charset=UTF-8; name=v8-0001-Add-password_expire_warning-GUC-to-warn-clients.patchDownload
From 9b4fe1f5fa7d55799f95be356625fee0400cb77e Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Thu, 8 Jan 2026 13:23:10 +0100
Subject: [PATCH v8] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 16 ++++++++
 src/backend/libpq/crypt.c                     | 39 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 +++++
 8 files changed, 78 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..9d1361dfad3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,22 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warning" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how many time before a role's password expiration a <literal>WARNING</literal>
+        message is sent to the client upon successful connection. It requires that
+        a <command>VALID UNTIL</command> date is set for the user. A value of <literal>0d</literal>
+        disable this behavior. The default value is <literal>7d</literal> and the max value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..147c8834380 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -20,6 +20,7 @@
 #include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
+#include "postmaster/postmaster.h"
 #include "utils/builtins.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
@@ -27,6 +28,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +77,32 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz	now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 52c05a9d1d5..d0d5c87d4ea 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..15e0d10e162 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how many time before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

#22Japin Li
japinli@hotmail.com
In reply to: Gilles Darold (#21)
2 attachment(s)
Re: Pasword expiration warning

On Thu, 08 Jan 2026 at 13:31, Gilles Darold <gilles@darold.net> wrote:

Le 08/01/2026 à 10:27, Japin Li a écrit :

We should probably use guc-password-expire-warning as the ID, since the GUC is
named password_expire_warning (singular).

Patch updated.

Thanks for updating the patch.

I noticed that src/backend/libpq/crypt.c no longer needs "postmaster/postmaster.h",
so I've removed it in v8.

I've also added a TAP test for the new GUC parameter. The updated patch is attached.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

Attachments:

v8-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-diffDownload
From 1bd224fffecc8d7752dd41897126d391ccfdf138 Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Thu, 8 Jan 2026 13:23:10 +0100
Subject: [PATCH v8 1/2] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 16 ++++++++
 src/backend/libpq/crypt.c                     | 38 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 +++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 +++++
 8 files changed, 77 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..9d1361dfad3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,22 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warning" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how many time before a role's password expiration a <literal>WARNING</literal>
+        message is sent to the client upon successful connection. It requires that
+        a <command>VALID UNTIL</command> date is set for the user. A value of <literal>0d</literal>
+        disable this behavior. The default value is <literal>7d</literal> and the max value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..8b6b28fbec3 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,6 +27,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int		password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +76,32 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz	now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..3441c75e54a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..15e0d10e162 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how many time before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

v8-0002-Add-TAP-test-for-password_expire_warning.patchtext/x-diffDownload
From 6e589c4098abe2b198f8db08e2053d6f708e8ecb Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Fri, 9 Jan 2026 10:21:19 +0800
Subject: [PATCH v8 2/2] Add TAP test for password_expire_warning

---
 .../authentication/t/008_password_expire.pl   | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 src/test/authentication/t/008_password_expire.pl

diff --git a/src/test/authentication/t/008_password_expire.pl b/src/test/authentication/t/008_password_expire.pl
new file mode 100644
index 00000000000..83c69a35d61
--- /dev/null
+++ b/src/test/authentication/t/008_password_expire.pl
@@ -0,0 +1,36 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test for authentication password expiration warning message.
+
+use strict;
+use warnings FATAL => 'all';
+use Time::Piece;
+use Time::Seconds;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $dt = localtime;   # Current datetime
+$dt += ONE_DAY;       # Add 1 day
+
+my $valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "password_expire_warning = '1d'");
+$node->start;
+
+$node->safe_psql('postgres',
+	"CREATE USER test_user WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', "local all all scram-sha-256");
+$node->reload;
+
+$ENV{"PGPASSWORD"} = '12345678';
+$node->connect_ok('user=test_user dbname=postgres',
+	qq(test password_expire_warning),
+	expected_stderr =>
+		qr/your password will expire in/);
+
+done_testing();
-- 
2.43.0

#23Yuefei Shi
shiyuefei1004@gmail.com
In reply to: Japin Li (#22)
Re: Pasword expiration warning

A few review comments for V8.

===
1.

+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int)
(result / 86400));
+		}

Please consider localization of the warning message.

2. typo fix

a. `Controls how many time ...` should be `Controls how much time ...`.

b. `Sets how many time before password expire to emit ...` should be
`Sets how much time before password expires to emit ...`

#24Japin Li
japinli@hotmail.com
In reply to: Yuefei Shi (#23)
2 attachment(s)
Re: Pasword expiration warning

On Fri, 09 Jan 2026 at 14:10, Yuefei Shi <shiyuefei1004@gmail.com> wrote:

A few review comments for V8.

===
1.
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+				MyClientConnectionInfo.warning_message =
+						psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+		}
Please consider localization of the warning message.

2. typo fix
a. `Controls how many time ...` should be `Controls how much time ...`.
b. `Sets how many time before password expire to emit ...` should be `Sets how much time before password expires to emit ...`

Nice catch. Updated in v9. Please take to look.

I've also replaced the magic constant 86400 with the SECS_PER_DAY macro and
enclosed the statement in braces since it now spans multiple lines.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

Attachments:

v9-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-diffDownload
From 7a519631dd6df96ed94b58fd77b13fe256e49e6e Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Thu, 8 Jan 2026 13:23:10 +0100
Subject: [PATCH v9 1/2] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 17 ++++++++
 src/backend/libpq/crypt.c                     | 41 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 ++++
 8 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..6760aa3b641 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warning" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how much time (in seconds) before a role's password expiration
+        a <literal>WARNING</literal> message is sent to the client upon successful
+        connection. It requires that a <command>VALID UNTIL</command> date is set
+        for the role. A value of <literal>0d</literal> disable this behavior. The
+        default value is <literal>7d</literal> and the maximum value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..5c00c7775ce 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,6 +27,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int			password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +76,35 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC;	/* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+			{
+				MyClientConnectionInfo.warning_message =
+					psprintf(_("your password will expire in %d day(s)"),
+							 (int) (result / SECS_PER_DAY));
+			}
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..3441c75e54a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..e4f107cc43b 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how much time before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

v9-0002-Add-TAP-test-for-password_expire_warning.patchtext/x-diffDownload
From a3c4981f8c43a356b67eaa15c231fdbdc811af8e Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Fri, 9 Jan 2026 10:21:19 +0800
Subject: [PATCH v9 2/2] Add TAP test for password_expire_warning

---
 .../authentication/t/008_password_expire.pl   | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)
 create mode 100644 src/test/authentication/t/008_password_expire.pl

diff --git a/src/test/authentication/t/008_password_expire.pl b/src/test/authentication/t/008_password_expire.pl
new file mode 100644
index 00000000000..83c69a35d61
--- /dev/null
+++ b/src/test/authentication/t/008_password_expire.pl
@@ -0,0 +1,36 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test for authentication password expiration warning message.
+
+use strict;
+use warnings FATAL => 'all';
+use Time::Piece;
+use Time::Seconds;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $dt = localtime;   # Current datetime
+$dt += ONE_DAY;       # Add 1 day
+
+my $valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "password_expire_warning = '1d'");
+$node->start;
+
+$node->safe_psql('postgres',
+	"CREATE USER test_user WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', "local all all scram-sha-256");
+$node->reload;
+
+$ENV{"PGPASSWORD"} = '12345678';
+$node->connect_ok('user=test_user dbname=postgres',
+	qq(test password_expire_warning),
+	expected_stderr =>
+		qr/your password will expire in/);
+
+done_testing();
-- 
2.43.0

#25Steven Niu
niushiji@gmail.com
In reply to: Japin Li (#24)
Re: Pasword expiration warning

From: Japin Li <japinli@hotmail.com>
Sent: Friday, January 09, 2026 15:12
To: Yuefei Shi <shiyuefei1004@gmail.com>
Cc: Gilles Darold <gilles@darold.net>; songjinzhou <tsinghualucky912@foxmail.com>; PostgreSQL Hackers <pgsql-hackers@postgresql.org>; Andrew Dunstan <andrew@dunslane.net>; Bossart, Nathan <bossartn@amazon.com>; Tom Lane <tgl@sss.pgh.pa.us>; liu xiaohui <liuxh.zj.cn@gmail.com>
Subject: Re: Pasword expiration warning

On Fri, 09 Jan 2026 at 14:10, Yuefei Shi <shiyuefei1004@gmail.com> wrote:

A few review comments for V8.

===
1.
+             if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+             {
+                     TimestampTz result = (vuntil - now) / USECS_PER_SEC; /* in seconds */
+
+                     if (result <= (TimestampTz) password_expire_warning)
+                             MyClientConnectionInfo.warning_message =
+                                             psprintf("your password will expire in %d day(s)", (int) (result / 86400));
+             }
Please consider localization of the warning message.

2. typo fix
a. `Controls how many time ...` should be `Controls how much time ...`.
b. `Sets how many time before password expire to emit ...` should be `Sets how much time before password expires to emit ...`

Nice catch. Updated in v9.  Please take to look.

I've also replaced the magic constant 86400 with the SECS_PER_DAY macro and
enclosed the statement in braces since it now spans multiple lines.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

________________________________________

Hi, Jiapin,

I reviewed the v9-0002-Add-TAP-test-for-password_expire_warning.patch
and here are my comments:

1. I think we should add tow more cases. One case is for the feature is disbaled. And another is for no warning when >1d remaining.
2. The modification to pg_hba.conf is unnecessary as the default pg_hba.conf generated by initdb already allows local connections with appropriate methods.
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf('pg_hba.conf', "local all all scram-sha-256");
3. Make the expected string to be more exact.
qr/your password will expire in/);
-->
qr/your password will expire in 1d/);

Thanks,
Steven

#26Japin Li
japinli@hotmail.com
In reply to: Steven Niu (#25)
2 attachment(s)
Re: Pasword expiration warning

Hi, Steven

Thanks for the review.

On Fri, 09 Jan 2026 at 07:36, Steven Niu <niushiji@gmail.com> wrote:

Hi, Jiapin,

I reviewed the v9-0002-Add-TAP-test-for-password_expire_warning.patch
and here are my comments:

1. I think we should add tow more cases. One case is for the feature is disbaled. And another is for no warning when >1d remaining.

Add in v10.

2. The modification to pg_hba.conf is unnecessary as the default pg_hba.conf generated by initdb already allows local connections with appropriate methods.
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf('pg_hba.conf', "local all all scram-sha-256");

Yes, it allows local connections, but they are always in trust mode, so no
password is required (or used).

3. Make the expected string to be more exact.
qr/your password will expire in/);
-->
qr/your password will expire in 1d/);

Fixed. PFA.

v10-0001 - No changes.
v10-0002 - Address review comments.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.

Attachments:

v10-0001-Add-password_expire_warning-GUC-to-warn-clients.patchtext/x-diffDownload
From e5f0562176b41d77c6b8f2166551d55bac72e8e9 Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Fri, 9 Jan 2026 15:03:52 +0800
Subject: [PATCH v10 1/2] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Author: Gilles Darold <gilles@darold.net>
---
 doc/src/sgml/config.sgml                      | 17 ++++++++
 src/backend/libpq/crypt.c                     | 41 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 ++++
 8 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..6760aa3b641 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warning" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how much time (in seconds) before a role's password expiration
+        a <literal>WARNING</literal> message is sent to the client upon successful
+        connection. It requires that a <command>VALID UNTIL</command> date is set
+        for the role. A value of <literal>0d</literal> disable this behavior. The
+        default value is <literal>7d</literal> and the maximum value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..5c00c7775ce 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,6 +27,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int			password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +76,35 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC;	/* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+			{
+				MyClientConnectionInfo.warning_message =
+					psprintf(_("your password will expire in %d day(s)"),
+							 (int) (result / SECS_PER_DAY));
+			}
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..3441c75e54a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..e4f107cc43b 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how much time before password expire to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

v10-0002-Add-TAP-test-for-password_expire_warning.patchtext/x-diffDownload
From bbe19d8c6a879c54d00daf6f96d5d6be298dad3c Mon Sep 17 00:00:00 2001
From: Japin Li <japinli@hotmail.com>
Date: Fri, 9 Jan 2026 15:04:45 +0800
Subject: [PATCH v10 2/2] Add TAP test for password_expire_warning

---
 .../authentication/t/008_password_expire.pl   | 50 +++++++++++++++++++
 1 file changed, 50 insertions(+)
 create mode 100644 src/test/authentication/t/008_password_expire.pl

diff --git a/src/test/authentication/t/008_password_expire.pl b/src/test/authentication/t/008_password_expire.pl
new file mode 100644
index 00000000000..fdeae1b84d1
--- /dev/null
+++ b/src/test/authentication/t/008_password_expire.pl
@@ -0,0 +1,50 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test for authentication password expiration warning message.
+
+use strict;
+use warnings FATAL => 'all';
+use Time::Piece;
+use Time::Seconds;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "password_expire_warning = '1d'");
+$node->start;
+
+my $dt = localtime;   # Current datetime
+$dt += ONE_DAY;       # Add 1 day
+my $valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+$node->safe_psql('postgres',
+	"CREATE USER test_user1 WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+$dt += ONE_DAY;
+$valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+$node->safe_psql('postgres',
+	"CREATE USER test_user2 WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+# Ensure subsequent connections authenticate with the password.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', "local all all scram-sha-256");
+$node->reload;
+
+$ENV{"PGPASSWORD"} = '12345678';
+
+$node->connect_ok('user=test_user1 dbname=postgres',
+	qq(emit password expiration warning),
+	expected_stderr =>
+		qr/your password will expire in 0 day\(s\)/);
+
+$node->connect_ok('user=test_user2 dbname=postgres',
+	qq(no password expiration warning is emitted));
+
+$node->append_conf('postgresql.conf', "password_expire_warning = '0'");
+$node->reload;
+
+$node->connect_ok('user=test_user1 dbname=postgres',
+	qq(disable password expire warning));
+
+done_testing();
-- 
2.43.0

#27Gilles Darold
gilles@darold.net
In reply to: Japin Li (#26)
2 attachment(s)
Re: Pasword expiration warning

Le 09/01/2026 à 10:04, Japin Li a écrit :

Hi, Steven

Thanks for the review.

On Fri, 09 Jan 2026 at 07:36, Steven Niu <niushiji@gmail.com> wrote:

Hi, Jiapin,

I reviewed the v9-0002-Add-TAP-test-for-password_expire_warning.patch
and here are my comments:

1. I think we should add tow more cases. One case is for the feature is disbaled. And another is for no warning when >1d remaining.

Add in v10.

2. The modification to pg_hba.conf is unnecessary as the default pg_hba.conf generated by initdb already allows local connections with appropriate methods.
unlink($node->data_dir . '/pg_hba.conf');
$node->append_conf('pg_hba.conf', "local all all scram-sha-256");

Yes, it allows local connections, but they are always in trust mode, so no
password is required (or used).

3. Make the expected string to be more exact.
qr/your password will expire in/);
-->
qr/your password will expire in 1d/);

Fixed. PFA.

v10-0001 - No changes.
v10-0002 - Address review comments.

Here is a v11 version of the patch.

v11-0001 - fix a miss on the typo fixes ( s/expire/expires/ in GUC
description ) and add your name in the authors list.

v11-0002 - Add a test with Infinity in VALID UNTIL value.

--
Gilles Darold
http://hexacluster.ai/

Attachments:

v11-0001-Add-password_expire_warning-GUC-to-warn-clie.patchtext/x-patch; charset=UTF-8; name=v11-0001-Add-password_expire_warning-GUC-to-warn-clie.patchDownload
From d80098d595754d58427df5705790db51f794940e Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Fri, 9 Jan 2026 12:13:59 +0100
Subject: [PATCH v11 1/2] Add password_expire_warning GUC to warn clients

Introduce a new server configuration parameter, password_expire_warning,
which controls how many days before a role's password expiration a
warning message is sent to the client upon successful connection.

Authors: Gilles Darold <gilles@darold.net>, Japin Li <japinli@hotmail.com>
---
 doc/src/sgml/config.sgml                      | 17 ++++++++
 src/backend/libpq/crypt.c                     | 41 +++++++++++++++----
 src/backend/utils/init/miscinit.c             |  1 +
 src/backend/utils/init/postinit.c             |  7 ++++
 src/backend/utils/misc/guc_parameters.dat     |  9 ++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     |  3 ++
 src/include/libpq/libpq-be.h                  |  9 ++++
 8 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0fad34da6eb..6760aa3b641 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1106,6 +1106,23 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-password-expire-warning" xreflabel="password_expire_warning">
+      <term><varname>password_expire_warning</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>password_expire_warning</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls how much time (in seconds) before a role's password expiration
+        a <literal>WARNING</literal> message is sent to the client upon successful
+        connection. It requires that a <command>VALID UNTIL</command> date is set
+        for the role. A value of <literal>0d</literal> disable this behavior. The
+        default value is <literal>7d</literal> and the maximum value <literal>30d</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index 4c1052b3d42..5c00c7775ce 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -27,6 +27,12 @@
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/*
+ * Threshold (in seconds) before password expiration to emit a warning
+ * at login (0 = disabled; default 7 days)
+ */
+int			password_expire_warning = 604800;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -70,14 +76,35 @@ get_role_password(const char *role, const char **logdetail)
 
 	ReleaseSysCache(roleTup);
 
-	/*
-	 * Password OK, but check to be sure we are not past rolvaliduntil
-	 */
-	if (!isnull && vuntil < GetCurrentTimestamp())
+	if (!isnull)
 	{
-		*logdetail = psprintf(_("User \"%s\" has an expired password."),
-							  role);
-		return NULL;
+		TimestampTz now = GetCurrentTimestamp();
+
+		/*
+		 * Password OK, but check to be sure we are not past rolvaliduntil
+		 */
+		if (vuntil < now)
+		{
+			*logdetail = psprintf(_("User \"%s\" has an expired password."),
+								  role);
+			return NULL;
+		}
+
+		/*
+		 * Password OK, but check if rolvaliduntil is less than GUC
+		 * password_expire_warning days to send a warning to the client
+		 */
+		if (password_expire_warning > 0 && vuntil < PG_INT64_MAX)
+		{
+			TimestampTz result = (vuntil - now) / USECS_PER_SEC;	/* in seconds */
+
+			if (result <= (TimestampTz) password_expire_warning)
+			{
+				MyClientConnectionInfo.warning_message =
+					psprintf(_("your password will expire in %d day(s)"),
+							 (int) (result / SECS_PER_DAY));
+			}
+		}
 	}
 
 	return shadow_pass;
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..24737c95c28 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1089,6 +1089,7 @@ RestoreClientConnectionInfo(char *conninfo)
 
 	/* Copy the fields back into place */
 	MyClientConnectionInfo.authn_id = NULL;
+	MyClientConnectionInfo.warning_message = NULL;
 	MyClientConnectionInfo.auth_method = serialized.auth_method;
 
 	if (serialized.authn_id_len >= 0)
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..3441c75e54a 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -1229,6 +1229,13 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	if (!bootstrap)
 		pgstat_bestart_final();
 
+	/*
+	 * Emit a warning message to the client when set, for example
+	 * to warn the user that the password will expire.
+	 */
+	if (MyClientConnectionInfo.warning_message)
+		ereport(WARNING, (errmsg("%s", MyClientConnectionInfo.warning_message)));
+
 	/* close the transaction we started above */
 	if (!bootstrap)
 		CommitTransactionCommand();
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..43b3af0cb5c 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2248,6 +2248,15 @@
   options => 'password_encryption_options',
 },
 
+{ name => 'password_expire_warning', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Sets how much time before password expires to emit a warning at client connection. Default is 7 days, 0 means no warning.',
+  flags => 'GUC_UNIT_S',
+  variable => 'password_expire_warning',
+  boot_val => '604800',
+  min => '0',
+  max => '2592000',
+},
+
 { name => 'plan_cache_mode', type => 'enum', context => 'PGC_USERSET', group => 'QUERY_TUNING_OTHER',
   short_desc => 'Controls the planner\'s selection of custom or generic plan.',
   long_desc => 'Prepared statements can have custom and generic plans, and the planner will attempt to choose which is better.  This can be set to override the default behavior.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..ca59b7cc1f6 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -98,6 +98,7 @@
 #scram_iterations = 4096
 #md5_password_warnings = on             # display md5 deprecation warnings?
 #oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+#password_expire_warning = '7d'         # 0-30d time before password expiration to emit a warning
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index f01886e1098..420f8053255 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* number of seconds before emitting a warning for password expiration */
+extern PGDLLIMPORT int password_expire_warning;
+
 /*
  * Types of password hashes or secrets.
  *
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4ff..4dac9f98089 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -103,6 +103,15 @@ typedef struct ClientConnectionInfo
 	 * meaning if authn_id is not NULL; otherwise it's undefined.
 	 */
 	UserAuth	auth_method;
+
+	/*
+	 * Message to send to the client in case of connection success.
+	 * When not NULL a WARNING message is sent to the client after a
+	 * successful connection in src/backend/utils/init/postinit.c at
+	 * enf of InitPostgres(), currently only used to show the password
+	 * expiration warning.
+	 */
+	const char *warning_message;
 } ClientConnectionInfo;
 
 /*
-- 
2.43.0

v11-0002-Add-TAP-test-for-password_expire_warnin.patchtext/x-patch; charset=UTF-8; name=v11-0002-Add-TAP-test-for-password_expire_warnin.patchDownload
From 6774c4bdf83499ddcaabcb784bc29c2f7c4a1e9e Mon Sep 17 00:00:00 2001
From: Gilles Darold <gilles@darold.net>
Date: Fri, 9 Jan 2026 12:18:31 +0100
Subject: [PATCH v11 2/2 2/2] Add TAP test for password_expire_warnin

---
 .../authentication/t/008_password_expire.pl   | 56 +++++++++++++++++++
 1 file changed, 56 insertions(+)
 create mode 100644 src/test/authentication/t/008_password_expire.pl

diff --git a/src/test/authentication/t/008_password_expire.pl b/src/test/authentication/t/008_password_expire.pl
new file mode 100644
index 00000000000..02e4bc9e7b9
--- /dev/null
+++ b/src/test/authentication/t/008_password_expire.pl
@@ -0,0 +1,56 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test for authentication password expiration warning message.
+
+use strict;
+use warnings FATAL => 'all';
+use Time::Piece;
+use Time::Seconds;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "password_expire_warning = '1d'");
+$node->start;
+
+my $dt = localtime;   # Current datetime
+$dt += ONE_DAY;       # Add 1 day
+my $valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+$node->safe_psql('postgres',
+	"CREATE USER test_user1 WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+$dt += ONE_DAY;
+$valid_until = $dt->strftime("%Y-%m-%d %H:%M:%S");
+$node->safe_psql('postgres',
+	"CREATE USER test_user2 WITH VALID UNTIL '$valid_until' PASSWORD '12345678'");
+
+$node->safe_psql('postgres',
+	"CREATE USER test_user3 WITH VALID UNTIL 'Infinity' PASSWORD '12345678'");
+
+# Ensure subsequent connections authenticate with the password.
+unlink($node->data_dir . '/pg_hba.conf');
+$node->append_conf('pg_hba.conf', "local all all scram-sha-256");
+$node->reload;
+
+$ENV{"PGPASSWORD"} = '12345678';
+
+$node->connect_ok('user=test_user1 dbname=postgres',
+	qq(emit password expiration warning),
+	expected_stderr =>
+		qr/your password will expire in 0 day\(s\)/);
+
+$node->connect_ok('user=test_user2 dbname=postgres',
+	qq(no password expiration warning is emitted));
+
+$node->connect_ok('user=test_user3 dbname=postgres',
+	qq(no password expiration warning is emitted for infinity));
+
+$node->append_conf('postgresql.conf', "password_expire_warning = '0'");
+$node->reload;
+
+$node->connect_ok('user=test_user1 dbname=postgres',
+	qq(disable password expire warning));
+
+done_testing();
-- 
2.43.0

#28Japin Li
japinli@hotmail.com
In reply to: Gilles Darold (#27)
Re: Pasword expiration warning

Here is a v11 version of the patch.

v11-0001 - fix a miss on the typo fixes ( s/expire/expires/ in GUC
description ) and add your name in the authors list.

v11-0002 - Add a test with Infinity in VALID UNTIL value.

Thanks for updating the patches. LGTM.

--
Regards,
Japin Li
ChengDu WenWu Information Technology Co., Ltd.